0xacfb…4082

All memos sent from and to 0xacfb…4082.

0xacfba7ce…4082·#25,829,679·0x81e32c0a…279a4e
posted, funded and claimed &mdash; and anything the contract still owes you &mdash; is read from the chain against your address. Nothing about you is stored here.</p>' +'<button class="btn btn-pri" id=doConnect>Connect a wallet</button></div>'; $("doConnect").onclick=connect; return} const m=mine||{posted:[],claimed:[],backing:[]}; /* A CANCELLED BOUNTY OWES YOU MONEY THAT `pendingWithdrawals` CANNOT SEE. The contract moves a funder's stake there only when they call claimRefundFromCancelledOpenBounty; until then it is still held in the bounty. So "nothing owed" was true of the withdrawal balance and false about the money - which is the wrong thing to be precise about. */ const reclaim=(m.backing||[]).filter(x=>statusOf(x.b)==="cancelled"&&x.amt>0n); const reclaimSum=reclaim.reduce((a,x)=>a+x.amt,0n); /* Nothing owed is not a broken button. The control appears when there is something to press it for, and the figure says the state in words when there is not. */ let h='<div class=box><h3>To withdraw</h3>' +'<p class="owed num'+(pending?"":" none")+'">' +(pending?eth(pending)+'<span class=mut style="font-size:15px;font-weight:500">' +usdPara(pending)+"</span>":"Nothing owed right now")+"</p>" +'<p class="mut" style="font-size:13px;margin-top:6px">Everything the contract owes you &mdash; payouts, refunds and cancelled stakes &mdash; waits here until you pull it.</p>' +(reclaimSum?'<p style="font-size:13px;margin-top:10px">You also have <b>'+eth(reclaimSum) +'</b> to reclaim from '+(reclaim.length===1?"a cancelled bounty":reclaim.length+" cancelled bounties") +' &mdash; open '+reclaim.map(x=>'<a href="#b='+x.b.id+'">#'+x.b.id+"</a>").join(", ") +" and claim the refund, and it lands here.</p>":"") +(pending?'<div class=row style="margin-top:14px"><button class="btn btn-pri" id=doWd>Withdraw</button></div>':"") +"</div>"; h+='<div class="box'+(m.posted.length?"":" thin")+'"><h3>Posted ('+m.posted.length+")</h3>" +(m.posted.length?m.posted.map(rowOf).join(""):'<p class=mut>None yet.</p>')+"</div>"; h+='<div class="box'+(m.backing.length?"":" thin")+'"><h3>Funded ('+m.backing.length+")</h3>" +(m.total>m.scanned?'<p class="mut" style="font-size:12px;margin-bottom:8px">Scanned the newest ' +m.scanned+" of "+m.total+" bounties.</p>":"") +(m.backing.length?m.backing.map(x=>'<button class=b data-b="'+x.b.id+'">' +'<span class=b-hd><span class=b-id>#'+x.b.id+'</span><span class=b-t>'+esc(x.b.name) +'</span><span class="b-amt num">'+eth(x.amt)+"</span></span>" +'<span class=b-meta>'+badges(x.b) +(statusOf(x.b)==="cancelled"&&x.amt>0n?'<span class="pill new">refund waiting</span>':"") +"<span>your stake</span></span></button>").join("") :'<p class=mut>You have not funded anyone else&rsquo;s bounty.</p>')+"</div>"; h+='<div class="box'+(m.claimed.length?"":" thin")+'"><h3>Claimed ('+m.claimed.length+")</h3>" +(m.claimed.length?m.claimed.map(c=>'<button class=b data-b="'+c.bountyId+'">' +'<span class=b-hd><span class=b-id>#'+c.id+'</span><span class=b-t>'+esc(c.name)+"</span>" +(c.accepted?'<span class="pill paid">accepted</span>':"")+"</span>" +'<span class=b-meta><span>on bounty #'+c.bountyId+"</span><span>"+ago(c.createdAt) +"</span></span></button>").join(""):'<p class=mut>None yet.</p>')+"</div>"; el.innerHTML=h}; const renderChip=()=>{ const c=$("connect"); c.classList.toggle("on",!!account); c.innerHTML="<b>"+(account?esc(myName||shortAddr(account)):"connect")+"</b>" +(account?'<span class="mut num">'+fmt(ethBal,3)+"</span>":"")}; const render=()=>{ renderChip(); $("hero").classList.toggle("hide",view.t!=="list"); $("pList").classList.toggle("hide",view.t!=="list"); $("pNew").classList.toggle("hide",view.t!=="new"); $("pMine").classList.toggle("hide",view.t!=="mine"); $("pB").classList.toggle("hide",view.t!=="b"); $("tList").classList.toggle("on",view.t==="list"||view.t==="b"); $("tNew").classList.toggle("on",view.t==="new"); $("tMine").classList.toggle("on",view.t==="mine"); if(view.t==="list")renderList(); if(view.t==="b")renderDetail(); if(view.t==="mine")renderMine(); if(view.t==="new"){ $("nMin").textContent="Minimum "+eth(CFG.minBounty)+". A 2.5% fee is taken from the pot when a claim is accepted."; $("kindNote").textContent=newKind==="open" ?"Anyone may add to the pot. Once somebody else has, which claim gets paid is settled by a weighted vote of the funders — you cannot accept one on your own." :"You fund it and you decide. Accept a claim whenever you like, or cancel and take the money back."} for(const el of document.querySelectorAll("[data-b]")) el.onclick=()=>go("b",BigInt(el.dataset.b)); /* "Fund it" said Fund and did Open. It now opens the bounty AND puts the cursor in the field it promised. */ for(const el of document.querySelectorAll("[data-fund]")) el.onclick=()=>go("b",BigInt(el.dataset.fund),"fund"); /* TWO-WAY, AND THAT IS THE WHOLE POINT. This used to only ever ADD `disabled`, so the first transaction that was cancelled or reverted left every control on the page dead until a reload - the page looked broken, and the button that would have explained why was disabled too. A guard that cannot be lifted is not a guard, it is a latch. */ for(const el of document.querySelectorAll(".btn,.b")){ if(busy)el.setAttribute("disabled","");else el.removeAttribute("disabled")} wire()}; /* -------------------------------------------------------------- ACTIONS */ const send=async(label,tx)=>{ if(busy)return false; try{ busy=true;render(); await checkWallet(); setStat(label+": confirm in your wallet…"); const hash=await rpc(S,[Object.assign({from:account},tx)]); setStat(label+" sent "+txLink(hash)); await waitTx(hash); setStat(label+" confirmed "+txLink(hash),"ok"); busy=false; await refresh(); return true} catch(e){ busy=false; const m=String(e&&(e.data&&e.data.message||e.message)||e); setStat(/user rejected|denied/i.test(m)?"Cancelled.":m,"err"); render(); return false} finally{busy=false}}; const need=async()=>{ if(account)return true; await connect(); return !!account}; const to=(data,value)=>value===undefined?{to:POIDH,data}:{to:POIDH,data,value:"0x"+value.toString(16)}; const postBounty=async()=>{ if(!await need())return; const t=$("nTitle").value.trim(),d=$("nDesc").value.trim(); const v=parseAmt($("nAmt").value); if(!t)return setStat("Give the bounty a title.","err"); if(v==null)return setStat("That amount is not a number.","err"); if(v<CFG.minBounty)return setStat("The minimum is "+eth(CFG.minBounty)+".","err"); const open=newKind==="open"; if(await send(open?"Open bounty":"Solo bounty", to(cStr2(open?W_OPEN:W_SOLO,t,d),v))){ $("nTitle").value="";$("nDesc").value="";$("nAmt").value=""; go("b",CFG.count-1n)}}; const joinBounty=async()=>{ if(!await need())return; const v=parseAmt(($("joinAmt")||{}).value); if(v==null)return setStat("That amount is not a number.","err"); if(v<CFG.minContrib)return setStat("The minimum is "+eth(CFG.minContrib)+".","err"); await send("Funding #"+detail.id,to(cUint(W_JOIN,detail.id),v))}; const postClaim=async()=>{ if(!await need())return; const t=$("cTitle").value.trim(),d=$("cDesc").value.trim(),u=$("cUri").value.trim(); if(!t)return setStat("Give the claim a title.","err"); if(!/^(ipfs:\/\/|https:\/\/)\S+$/i.test(u)) return setStat("The picture needs an ipfs:// or https:// link.","err"); await send("Claim on #"+detail.id,to(cStr3(W_MKCLAIM,detail.id,t,d,u)))}; /* A claim's tokenURI points at metadata on somebody else's host, so nothing is fetched until the reader asks for it by name. The URI is the claimant's to choose; only http(s) and ipfs are followed, and the result is used as an image source, never as markup. */ const IPFS="https://ipfs.io/ipfs/"; const httpize=u=>{ u=String(u||"").trim(); if(/^ipfs:\/\//i.test(u))return IPFS+u.replace(/^ipfs:\/\/(ipfs\/)?/i,""); return /^https:\/\//i.test(u)?u:""}; const showPic=async cid=>{ const box=document.querySelector('[data-pw="'+cid+'"]'); if(!box)return; box.innerHTML='<span class="mut" style="font-size:13px">Reading the claim&hellip;</span>'; /* THE tokenURI IS READ HERE, NOT WITH THE BOUNTY. It is needed only by the reader who asks for this one picture, so fetching every claim's URI up front spent calls and bytes on links most visits never follow. */ let uri=""; try{const h=await rpcRead(C,[{to:NFT,data:cUint(R_URI,cid)},L]); uri=decStrAt(h,Number(uword(h,0)))} catch(e){box.innerHTML='<span class="mut" style="font-size:13px">Could not read the claim NFT.</span>';return} const url=httpize(uri); if(!url){box.innerHTML='<span class="mut" style="font-size:13px">' +(uri?"That link is not one this page will follow.":"No picture was attached.")+"</span>";return} box.innerHTML='<span class="mut" style="font-size:13px">Fetching from ' +esc(new URL(url).host)+"&hellip;</span>"; try{ const r=await fetch(url,{headers:{accept:"application/json,image/*"}}); const ct=r.headers.get("content-type")||""; let img=url; if(/json/.test(ct)||!/^image\//.test(ct)){ const j=await r.json(); img=httpize(j&&(j.image||j.image_url||j.imageUrl)||"")} if(!img)throw Error("no image in that metadata"); box.innerHTML='<img src="'+esc(img)+'" alt="claim picture" loading=lazy>'} catch(e){box.innerHTML='<span class="mut" style="font-size:13px">Could not read it: ' +esc(String(e.message||e))+' &mdash; <a href="'+esc(url)+'" target=_blank rel=noreferrer>open it yourself</a>.</span>'}}; const wire=()=>{ const on=(id,fn)=>{const el=$(id);if(el)el.onclick=fn}; echoUsd("joinAmt","joinUsd"); on("doJoin",joinBounty); on("doClaim",postClaim); on("doPull",()=>send("Taking back your stake",to(cUint(W_WDOPEN,detail.id)))); on("doCancel",()=>{ if(!confirm("Cancel bounty #"+detail.id+"? Your own stake is credited back to you; other funders claim theirs."))return; send("Cancelling #"+detail.id,to(cUint(detail.open?W_COPEN:W_CSOLO,detail.id)))}); on("doRefund",()=>send("Refund from #"+detail.id,to(cUint(W_REFUND,detail.id)))); on("doResolve",()=>send("Resolving the vote",to(cUint(W_RESOLVE,detail.id)))); on("doWd",()=>send("Withdrawal",to("0x"+W_WITHDRAW))); for(const el of document.querySelectorAll("[data-vote]")) el.onclick=()=>send("Vote",to("0x"+W_VOTE+encUint(detail.id)+encBool(el.dataset.vote==="1"))); for(const el of document.querySelectorAll("[data-accept]")) el.onclick=()=>send("Accepting claim #"+el.dataset.accept, to(cTwo(W_ACCEPT,detail.id,el.dataset.accept))); for(const el of document.querySelectorAll("[data-submit]")) el.onclick=()=>send("Putting claim #"+el.dataset.submit+" to a vote", to(cTwo(W_SUBMIT,detail.id,el.dataset.submit))); for(const el of document.querySelectorAll("[data-pic]")) el.onclick=()=>{el.disabled=true;showPic(BigInt(el.dataset.pic))}}; /* ------------------------------------------------------------------ NAV */ const go=async(t,id,intent)=>{ if($("stat").className)setStat(""); view={t,id:id==null?null:id}; if(t==="b"){detail=null;render(); try{detail=await readDetail(id); if(account&&!myName)nameRev(account).then(n=>{if(n){myName=n;renderChip()}}).catch(()=>{}); render()} catch(e){setStat(String(e&&e.message||e),"err")}} else render(); if(intent==="fund"){const f=$("joinAmt"); if(f){f.scrollIntoView({block:"center",behavior:"smooth"});f.focus()}} const h=t==="b"?"#b="+id:t==="list"?"":"#"+t; if(location.hash!==h)history.replaceState(null,"",h||location.pathname); if(intent!=="fund")scrollTo({top:0,behavior:"instant"})}; const fromHash=()=>{ const m=/^#(?:b=)?(\d+)$/.exec(location.hash||""); if(m)return go("b",BigInt(m[1])); if(/^#new$/.test(location.hash))return go("new"); if(/^#mine$/.test(location.hash))return go("mine"); return go("list")}; addEventListener("hashchange",fromHash); const navShut=()=>$("navov").classList.add("hide"); $("burger").onclick=()=>$("navov").classList.remove("hide"); $("navx").onclick=navShut; for(const el of $("navov").querySelectorAll("[data-nav]")) el.onclick=e=>{e.preventDefault();navShut();go(el.dataset.nav)}; addEventListener("keydown",e=>{if(e.key==="Escape")navShut()}); $("tList").onclick=()=>go("list"); $("tNew").onclick=()=>go("new"); $("tMine").onclick=()=>go("mine"); $("back").onclick=()=>go("list"); $("home").onclick=e=>{e.preventDefault();go("list")}; $("more").onclick=async()=>{ $("more").disabled=true; try{await loadList(shown+PAGE)}catch(e){setStat(String(e&&e.message||e),"err")} render()}; for(const el of $("kind").querySelectorAll("button")) el.onclick=()=>{newKind=el.dataset.k; for(const o of $("kind").querySelectorAll("button"))o.classList.toggle("on",o===el); render()}; /* The dollar figure follows the field rather than waiting for a submit, so the size of what is being staked is visible while it is being decided. */ const echoUsd=(from,into)=>{ const f=$(from),t=$(into); if(!f||!t)return; const paint=()=>{const v=parseAmt(f.value); t.textContent=v==null||!v?"":"\u2248 "+(usd(v)||"")}; f.oninput=paint;paint()}; echoUsd("nAmt","nUsd"); $("doNew").onclick=postBounty; $("rpX").onclick=()=>$("readPick").classList.add("hide"); $("readPick").onclick=e=>{if(e.target===$("readPick"))$("readPick").classList.add("hide")}; /* --------------------------------------------------- WALLETCONNECT GLUE */ const WC_SIGN=["eth_sendTransaction","eth_signTransaction","personal_sign","eth_sign", "eth_signTypedData","eth_signTypedData_v3","eth_signTypedData_v4"]; const wcToWallet=m=>m.indexOf("wallet_")===0||WC_SIGN.indexOf(m)>=0; const wcRelay=WCCLI(WCU,u=>new WebSocket(u)); let wcRid=0; const wcHttp=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:++wcRid,jsonrpc:"2.0",method,params:params||[]})}); const j=await r.json(); if(j&&j.error)throw Object.assign(Error(j.error.message||"the read node refused"),{code:j.error.code}); return j?j.result:null}; const wcShowQR=uri=>{ $("wcQR").innerHTML=qrSvg(QR8.build(uri)); $("wcCopy").onclick=()=>{try{navigator.clipboard.writeText(uri); $("wcCopy").textContent="copied"}catch{}}; $("wcm").classList.remove("hide"); return new Promise((res,rej)=>{ const bail=()=>{cleanup();rej(Object.assign(Error(""),{code:4001}))}; const key=e=>{if(e.key==="Escape")bail()}; const cleanup=()=>{removeEventListener("keydown",key);$("wcm").onclick=null; $("wcX").onclick=null;wcShowQR.settle=null}; addEventListener("keydown",key); $("wcX").onclick=bail; $("wcm").onclick=e=>{if(e.target===$("wcm"))bail()}; wcShowQR.settle=()=>{cleanup();res()}})}; const wcCloseQR=()=>{if(wcShowQR.settle)wcShowQR.settle(); $("wcm").classList.add("hide");$("wcCopy").textContent="copy link"}; async function wcConnect(){ const meta={name:"POIDH",description:"Bounties on Ethereum — pics or it didn't happen", url:location.origin,icons:[]}; const{uri,done}=await wcRelay.pair(WC_PID,meta); const bail=wcShowQR(uri); setStat("Scan the code with your wallet."); let sess; try{sess=await Promise.race([done,bail])} catch(e){wcRelay.close();throw e} finally{wcCloseQR()} const accts=sess.accounts.map(a=>a.split(":").pop()); if(!accts.length){wcRelay.close();throw Error("That wallet approved no account.")} /* A phone wallet signs; it does not necessarily serve reads. Prefer a node the reader named, fall back to the session only if it offers eth_call, and say plainly what to do when neither is true rather than rendering an empty page. */ const canRead=sess.methods.indexOf("eth_call")>=0; const read=(method,params)=>{ if(readNode())return wcHttp(method,params); if(canRead)return wcRelay.request(sess,method,params||[]); return Promise.reject(Object.assign( Error("This wallet does not answer reads. Name a read node with the globe icon."), {noWallet:true}))}; wcRelay.on(ev=>{if(ev&&ev.t==="closed")location.reload()}); return{request:({method,params})=> method==="eth_requestAccounts"||method==="eth_accounts"?Promise.resolve(accts): method==="eth_chainId"?Promise.resolve("0x1"): wcToWallet(method)?wcRelay.request(sess,method,params||[]):read(method,params), on:()=>{}}} /* ------------------------------------------------------------- CONNECT */ const bind=p=>{ PROV=p; if(!p||!p.on)return; p.on("accountsChanged",a=>{account=(a&&a[0]||"").toLowerCase()||null; myName="";if(!account)PROV=null;render();refresh()}); p.on("chainChanged",()=>location.reload())}; /* An action pressed while disconnected used to open the picker and then throw the action away, so posting a bounty always took two goes: connect, then press again. `connect()` now settles when the picker does, and the caller carries on with what it was asked to do. */ let _connWait=null; const settleConn=()=>{const f=_connWait;_connWait=null;if(f)f()}; const use=async get=>{ $("wpick").classList.add("hide"); try{ const p=typeof get==="function"?await get():get; if(!p)return; bind(p); const accs=await rpc("eth_requestAccounts",[]); account=(accs&&accs[0]||"").toLowerCase()||null; myName=""; let cid=await rpc(I,[]); if(+cid!==CHAIN){ try{await rpc("wallet_switchEthereumChain",[{chainId:"0x1"}]);cid=await rpc(I,[])}catch{} if(+cid!==CHAIN)setStat("POIDH here is on Ethereum mainnet; your wallet is elsewhere.","err")} await refresh(); if(account)nameRev(account).then(n=>{if(n){myName=n;renderChip()}}).catch(()=>{})} catch(e){ PROV=null;account=null;render(); setStat(/rejected/i.test(String(e.message||e))?"Cancelled.":String(e.message||e),"err")} finally{settleConn()}}; const connect=async()=>{ if(account){ account=null;PROV=null;myName="";mine=null;pending=0n;ethBal=0n; render();setStat("Disconnected.");return refresh()} /* ASK AGAIN, EVERY TIME. EIP-6963 providers announce themselves once, and a wallet that was still waking up when the page loaded never made the list - which is what made connecting feel like it worked on the second try. The request is re-broadcast here and given a moment to be answered, so the picker is built from what is present NOW rather than what was present at load. */ try{dispatchEvent(new Event("eip6963:requestProvider"))}catch{} await new Promise(r=>setTimeout(r,80)); const choices=providers.map(p=>({name:p.info.name,icon:p.info.icon,get:()=>p.provider})); if(!choices.length&&window.ethereum) choices.push({name:"Browser wallet",get:()=>window.ethereum}); /* Always offered, injected wallet or not: it is the only route for a phone. */ choices.push({name:"WalletConnect",svg:WC_ICON,get:wcConnect}); const list=$("wpList"); list.innerHTML=choices.map((c,i)=> '<button class="wrow" data-w="'+i+'">' +(c.icon?'<img src="'+esc(c.icon)+'" alt="">':'<span class="wki">'+(c.svg||"")+"</span>") +"<span>"+esc(c.name)+"</span></button>").join(""); $("wpNone").textContent=""; for(const el of list.querySelectorAll("button[data-w]")) el.onclick=()=>use(choices[+el.dataset.w].get); $("wpick").classList.remove("hide"); return new Promise(res=>{_connWait=res})}; const shutPicker=()=>{$("wpick").classList.add("hide");settleConn()}; $("connect").onclick=connect; $("wpX").onclick=shutPicker; $("wpick").onclick=e=>{if(e.target===$("wpick"))shutPicker()}; /* ------------------------------------------------------------- SUCCESSOR A pointer nobody reads moves nobody. This asks the contract serving the page for the newest version in its own lineage - and it can only know which contract that is from the GATEWAY HOSTNAME, because a page that wrote its own address into itself would change the chunks the address is derived from, and no salt solves that fixpoint. So a file opened from disk simply does not get this notice. It SAYS, it does not send: no redirect, no auto-navigation, nothing on screen is rewritten. The bytes stay the bytes that were audited, and leaving them is the reader's decision. Every failure is silent, because a missing notice is a smaller harm than a wrong one. */ const SEL_LATEST="52bfe789"; const selfAddress=()=>{ const m=/^(0x[0-9a-fA-F]{40})\./.exec(location.hostname||""); return m?m[1].toLowerCase():""}; const checkSuccessor=async()=>{ const here=selfAddress(); if(!here)return; try{ const tip=aword(await rpcRead(C,[{to:here,data:"0x"+SEL_LATEST},L],1),0); if(!tip||tip===ZERO||tip.toLowerCase()===here)return; $("newerLink").href=location.href.replace(here,tip.toLowerCase()) .replace(here.replace(/^0x/,""),tip.toLowerCase().replace(/^0x/,"")); $("newerLi").classList.remove("hide")} catch{}}; /* ---------------------------------------------------------------- START */ $("addrLi").innerHTML='bounties <a href="'+SCAN+"address/"+POIDH +'#code" target=_blank rel=noreferrer>'+POIDH.slice(0,10)+"…"+POIDH.slice(-4)+"</a>"; $("pgLi").innerHTML=(()=>{const a=selfAddress(); return a?'page <a href="'+SCAN+"address/"+a+'#code" target=_blank rel=noreferrer>' +a.slice(0,10)+"…"+a.slice(-4)+"</a> · ERC-8244" :"page: opened from a file, not from its contract"})(); /* A wallet that is already connected answers without a prompt; one that is not is never asked on load. With neither a wallet nor a named read node the page has nowhere to read from, and says so rather than reaching a host nobody picked. */ (async()=>{ /* The cache paints before anything is requested, so a return visit has the list on screen while the live read is still in flight. */ if(view.t==="list")paintFromCache(); render(); fromHash(); try{ if(window.ethereum){ const a=await window.ethereum.request({method:"eth_accounts"}).catch(()=>[]); if(a&&a[0]){bind(window.ethereum);account=a[0].toLowerCase(); nameRev(account).then(n=>{if(n){myName=n;renderChip()}}).catch(()=>{})}}} catch{} if(!account&&!window.ethereum&&!readNode()){renderChip();showReadPick();return} await refresh(); checkSuccessor()})(); /* The ballot's clock is the only thing on the page that moves on its own. */ setInterval(()=>{if(view.t==="b"&&detail&&detail.voting)renderDetail()},30000); </script></body></html>
0xacfba7ce…4082·#25,829,678·0xbcb664cc…54b8af
imList(r[6]); b.more=0; /* The rare bounty with more than ten. The getter cannot reach the older ones, so their ids are walked the only way the contract allows and read in a second wave - and the reader is told the depth is bounded rather than being shown a list that quietly stops. */ if(r[7]!=null){ /* The whole id array in one wave, then the claims it named in one more: the ids the fast path already returned are simply skipped. Walking the array in pieces cost a round trip per piece for no gain, since a revert inside `aggregate3` is free and marks the end just as well from anywhere. */ const walk=[]; for(let i=0;i<CLAIM_CAP;i++)walk.push({to:POIDH,data:cTwo(R_BCLAIMS,id,i)}); const w=await mc3(walk); const ids=[]; for(let i=0;i<w.length;i++){if(w[i]==null)break;ids.push(uword(w[i],0))} const have=new Set(b.claims.map(c=>String(c.id))); const missing=ids.filter(x=>!have.has(String(x))); if(missing.length){ const cs=await mc3(missing.map(cid=>({to:POIDH,data:cUint(R_CLAIM,cid)}))); b.claims=b.claims.concat(cs.map(decClaim).filter(Boolean).reverse())} b.more=ids.length>=CLAIM_CAP?ids.length:0} b.mine=account?b.parts.find(p=>isMe(p.addr))||null:null; /* THE WEIGHT COMES FROM THE LIVE PARTICIPANT LIST, NOT FROM `voteWeightSnapshot`. The snapshot getter is public but its ROUND is not - `voteWeightSnapshotRound` is private - so a weight left over from an earlier round reads exactly like a current one, and voting on it dies with `NotActiveParticipant`. The live list is not an approximation of the snapshot, it is equal to it. At `submitClaimForVote` every participant with a positive amount is snapshotted at that amount, and for the whole of the round `joinOpenBounty` and `withdrawFromOpenBounty` both revert with `VotingOngoing` - so the list cannot move while a vote is open. Reading it is exact, and one call cheaper. */ b.myWeight=b.mine?b.mine.amt:0n; /* The issuer does not get a vote to cast: `submitClaimForVote` counts their stake as yes and stamps `lastVotedRound` in the same transaction. That is derivable, so it never needs a log. */ b.iVoted=b.voting&&account&&isMe(b.issuer)?true:null; if(b.voting&&account&&!isMe(b.issuer)&&b.myWeight>0n) b.iVoted=await votedThisRound(b); return b}; /* HAVE I ALREADY VOTED? The only state that knows is `lastVotedRound`, and it is private - so this is the one question in the dapp that no number of `eth_call`s can answer, and the only place a log is load-bearing. It is asked opportunistically and never depended on: a node that will not serve logs returns null, and the page says it could not check rather than hiding a control the reader is entitled to press. The window is the round, not history - a round is at most `votingPeriod` old - so this stays well inside the 10,000-block cap that windowed nodes enforce. */ const T_VOTECAST="0xf86157a5d86bb7712df058863b02b9593d09c70f8264d183bade1d5452053dde"; const votedThisRound=async b=>{ try{ const tip=Number(await rpcRead("eth_blockNumber",[],1)); const started=b.votes.deadline-Number(CFG.period); const age=Math.max(0,Math.floor(Date.now()/1000)-started); /* 12s blocks, plus a wide margin, and never more than one round's worth. */ const span=Math.min(Math.ceil(age/12)+600,Math.ceil(Number(CFG.period)/12)+600); const logs=await rpcRead("eth_getLogs",[{address:POIDH, fromBlock:"0x"+Math.max(0,tip-span).toString(16),toBlock:"0x"+tip.toString(16), topics:[T_VOTECAST,"0x"+encAddr(account),"0x"+encUint(b.id),"0x"+encUint(b.voting)]}],1); /* The round is in the log body, so a vote from a previous round on the same claim - a claim can be put up again after one fails - is not counted. */ return logs.some(l=>uword(l.data,2)===b.round)} catch{return null}}; /* ---------------------------------------------------------------- REFRESH THREE WAVES, NOT SEVEN ROUND TRIPS. Everything here is an `eth_call`, and `aggregate3` will carry as many of them in one request as you like - so the only thing that has to be paid for twice is a genuine DEPENDENCY, where one answer decides what to ask next. There are exactly two of those: the bounty count decides which ids to read, and the account's own arrays decide which bounties and claims belong to it. So three waves, and everything within a wave rides together. 1. the counter (+ the account's pending balance and ETH, when connected) 2. the list, the account's `userBounties`/`userClaims`, and the funder scan across every bounty - one request 3. the rows those arrays named - one request Measured against a public node: 7 requests in 5 sequential waves before, 3 after. */ const refresh=async()=>{ try{ /* ---- wave 1 -------------------------------------------------------- */ const w1=cfgCalls(); const nCfg=w1.length; w1.push({to:FEED,data:"0x"+F_ROUND}); if(account)w1.push({to:POIDH,data:cAddr(R_PEND,account)}, {to:MC3,data:cAddr(SEL_ETHBAL,account)}); const r1=await mc3(w1); cfgTake(r1.slice(0,nCfg)); /* latestRoundData -> (roundId, answer, startedAt, updatedAt, answeredInRound). A negative or stale answer is discarded rather than shown. */ usdPerEth=null; const fr=r1[nCfg]; if(fr){ const answer=uword(fr,1),at=Number(uword(fr,3)); if(answer>0n&&at>0&&Math.floor(Date.now()/1000)-at<FEED_STALE)usdPerEth=answer} pending=account&&r1[nCfg+1]?uword(r1[nCfg+1],0):0n; ethBal=account&&r1[nCfg+2]?uword(r1[nCfg+2],0):0n; if(!account){mine=null} /* ---- wave 2 -------------------------------------------------------- */ const count=Number(CFG.count); const want=Math.min(shown,count); const listIds=[]; for(let i=0;i<want;i++)listIds.push(BigInt(count-1-i)); const w2=sumCalls(listIds); let atProbe=-1,atScan=-1,scanIds=[]; if(account){ atProbe=w2.length; for(let i=0;i<PROBE;i++)w2.push({to:POIDH,data:cAddrId(R_UB,account,i)}); for(let i=0;i<PROBE;i++)w2.push({to:POIDH,data:cAddrId(R_UC,account,i)}); /* FUNDING LEAVES NO PER-USER RECORD. `userBounties` and `userClaims` exist; there is no `userContributions`, and the only way in is `getParticipants` per bounty - the one read here that cannot be paged, because you cannot know which bounties to look at until you have looked at them all. It is affordable because the id space is dense and small, and it costs no extra round trip at all now that it rides in this wave. */ atScan=w2.length; const span=Math.min(count,SCAN_CAP); for(let i=0;i<span;i++){const id=count-1-i;scanIds.push(id); w2.push({to:POIDH,data:cUint(R_PARTS,id)})}} const r2=await mc3(w2); LIST=sumTake(listIds,r2,0); shown=want; markSeen(count); if(!account){render();return} const bids=[],cids=[],hits=[]; for(let i=0;i<PROBE;i++){const v=r2[atProbe+i];if(v==null)break;bids.push(uword(v,0))} for(let i=0;i<PROBE;i++){const v=r2[atProbe+PROBE+i];if(v==null)break;cids.push(uword(v,0))} for(let i=0;i<scanIds.length;i++){ const p=decParts(r2[atScan+i]).find(x=>isMe(x.addr)&&x.amt>0n); if(p)hits.push({id:scanIds[i],amt:p.amt})} /* ---- wave 3 -------------------------------------------------------- */ const postedIds=bids.slice().reverse(); const claimIds=cids.slice().reverse(); const backIds=hits.map(h=>BigInt(h.id)); const w3=sumCalls(postedIds) .concat(claimIds.map(id=>({to:POIDH,data:cUint(R_CLAIM,id)}))) .concat(sumCalls(backIds)); const r3=w3.length?await mc3(w3):[]; const posted=sumTake(postedIds,r3,0); const at2=postedIds.length*3; const claimed=claimIds.map((_,i)=>decClaim(r3[at2+i])).filter(Boolean); const backRows=sumTake(backIds,r3,at2+claimIds.length); const backing=hits.map(h=>({b:backRows.find(r=>Number(r.id)===h.id),amt:h.amt})) .filter(x=>x.b&&!isMe(x.b.issuer)); mine={posted,claimed,backing,scanned:scanIds.length,total:count}; if(view.t==="b"&&view.id!=null)detail=await readDetail(view.id); render()} catch(e){ if(e&&e.noWallet){render();showReadPick();return} setStat(String(e&&e.message||e),"err");render()}}; /* ------------------------------------------------------------ READ NODE A page with no wallet has nowhere to read from. Shipping a default endpoint would make it work out of the box, at the cost of every reader silently handing their IP and their queries to a host they never picked - and on an immutable page that choice can never be revisited. So the node is offered, never assumed: nothing is contacted until somebody taps one, and the tap is remembered. The suggestions differ in what they will serve, which decides whether the history-derived figures appear at all, so they say so. */ /* Four rather than three, because this list can never be edited: if one host goes away the others still work, and the globe icon still takes any URL. Labels are measured, not assumed - each was probed for whether it serves eth_getLogs at all, and over what span. */ const READ_NODES=[ ["publicnode","https://ethereum-rpc.publicnode.com","state only"], ["dRPC","https://eth.drpc.org","full, windowed"], ["MEV Blocker","https://rpc.mevblocker.io","full, windowed"], ["Tenderly","https://gateway.tenderly.co/public/mainnet","full"]]; const showReadPick=()=>{ const box=$("readOpts"); if(!box.childElementCount){ box.innerHTML=READ_NODES.map((n,i)=> '<button class="btn btn-sm" data-n="'+i+'" title="'+esc(n[1])+'">'+esc(n[0]) +' <span class="mut" style="font-weight:400">· '+esc(n[2])+"</span></button>").join(""); for(const el of box.querySelectorAll("button[data-n]")) el.onclick=()=>{const u=READ_NODES[+el.dataset.n][1]; try{localStorage.setItem(NODE_KEY,u)}catch{} $("readPick").classList.add("hide"); setStat("Reading through "+u.replace(/^https:\/\//,"")+"."); refresh()}} $("readPick").classList.remove("hide")}; /* ---------------------------------------------------------------- THEME */ const paint=on=>{document.documentElement.classList.toggle("d",on); $("tc").content=on?"#101725":"#7ba7e8"}; let pref=null; try{pref=localStorage.getItem("poidh:theme")}catch{} /* `matchMedia` is guarded rather than assumed. This page cannot be updated, so an environment without it - an embedded view, a reader, a test harness - must still get a readable page rather than a dead script. */ const sysDark=typeof matchMedia==="function"?matchMedia("(prefers-color-scheme:dark)"):null; paint(pref?pref==="d":!!(sysDark&&sysDark.matches)); if(sysDark&&sysDark.addEventListener)sysDark.addEventListener("change",e=>{if(!pref)paint(e.matches)}); $("theme").onclick=()=>{ pref=document.documentElement.classList.contains("d")?"l":"d"; paint(pref==="d"); try{localStorage.setItem("poidh:theme",pref)}catch{}}; $("node").onclick=()=>{ $("navov").classList.add("hide"); const t=prompt("HTTPS Ethereum RPC for reads when no wallet is connected.\n" +"Leave blank to clear. The page uses your wallet when there is one.",readNode()||""); if(t===null)return; const v=t.trim(); try{if(!v)localStorage.removeItem(NODE_KEY); else if(/^https:\/\/\S+$/i.test(v))localStorage.setItem(NODE_KEY,v); else return setStat("Read node must be an https:// URL.","err")}catch{} setStat(v?"Read node set.":"Read node cleared.","ok"); refresh()}; /* ----------------------------------------------------------------- TABS */ /* --------------------------------------------------------------- RENDER */ const left=sec=>{ const s=Math.max(0,sec-Math.floor(Date.now()/1000)); if(!s)return"ended"; if(s<3600)return Math.ceil(s/60)+"m left"; if(s<172800)return Math.round(s/3600)+"h left"; return Math.round(s/86400)+"d left"}; /* ONLY THE EXCEPTION IS LABELLED. Nearly every bounty is an open one, so an "open" pill on each row carried no information and actively misread beside the status: "open cancelled" is not a state anything can be in. The kind is now said only when it is the unusual one, and the detail view says it in words under Decided by. */ const badges=b=>{ const out=[]; if(!b.open)out.push('<span class=pill>solo</span>'); /* A provisional row knows the kind, which cannot change, and nothing about the state, which can. It says so instead of guessing. */ if(b.prov)return out.join(" "); const st=statusOf(b); if(b.voting&&b.voting>0n)out.push('<span class="pill vote">voting</span>'); else if(st==="open")out.push('<span class="pill live">live</span>'); else if(st==="paid")out.push('<span class="pill paid">paid</span>'); else out.push('<span class="pill dead">cancelled</span>'); return out.join(" ")}; const rowOf=b=>'<button class=b data-b="'+b.id+'">' +'<span class=b-hd><span class=b-id>#'+b.id+'</span>' +'<span class=b-t>'+esc(b.name||"(untitled)")+"</span>" +'<span class="b-amt num'+(b.prov?" prov":b.amount?"":" spent")+'">' +(b.prov?"···":b.amount?eth(b.amount):"&mdash;")+"</span></span>" +'<span class=b-meta>'+(isNew(b.id)?'<span class="pill new">new</span> ':"")+badges(b) +"<span>"+esc(shortAddr(b.issuer))+"</span>" +"<span>"+ago(b.createdAt)+"</span></span></button>"; const renderFeature=()=>{ const b=LIST.find(x=>x.id===FEATURED); const el=$("feature"); if(!b){el.innerHTML="";return} const st=statusOf(b); el.innerHTML='<div class=feat>' +'<div class=lbl>Pinned bounty</div>' +"<h2>"+esc(b.name)+"</h2>" +'<div class="pot num">'+fmt(b.amount,4)+' <span>ETH pooled'+usdPara(b.amount)+'</span></div>' +'<div class=b-meta>'+(isNew(b.id)?'<span class="pill new">new</span>':"")+badges(b)+"</div>" +"<p>"+esc((b.desc||"").slice(0,200).replace(/\s+\S*$/,""))+"&hellip;</p>" +'<div class=row>' +'<button class="btn btn-pri" data-b="'+b.id+'">Read it</button>' +(st==="open"&&b.open&&!b.voting?'<button class="btn" data-fund="'+b.id+'">Fund it</button>':"") +"</div></div>"}; const renderList=()=>{ renderFeature(); const rest=LIST.filter(b=>b.id!==FEATURED); $("list").innerHTML=rest.length?rest.map(rowOf).join("") :'<p class=mut>Nothing read yet.</p>'; const live=LIST.filter(b=>statusOf(b)==="open"&&!b.prov); const pot=live.reduce((a,b)=>a+b.amount,0n); $("heroStats").innerHTML=CFG.count?'<span><b>'+CFG.count+"</b> posted</span>" +(live.length?"<span><b>"+fmt(pot,4)+" ETH</b>"+usdPara(pot)+" in "+live.length+" open pot" +(live.length===1?"":"s")+"</span>":"") +'<span>fee <b>2.5%</b></span>':""; $("more").disabled=false; $("more").classList.toggle("hide",shown>=Number(CFG.count)); $("more").textContent="Load older ("+Math.max(0,Number(CFG.count)-shown)+" more)"}; const claimCard=(b,c)=>{ const won=b.claimId===c.id&&statusOf(b)==="paid"; const voting=b.voting===c.id; const canAccept=account&&isMe(b.issuer)&&statusOf(b)==="open"&&!b.voting &&(!b.open||!b.everExternal); const canSubmit=account&&isMe(b.issuer)&&b.open&&b.everExternal &&statusOf(b)==="open"&&!b.voting&&!c.accepted; return'<div class="claim'+(won?" win":"")+'">' +'<div class=shot data-pw="'+c.id+'">' +'<button class="btn btn-sm" data-pic="'+c.id+'">Show the picture</button>' +"</div>" +'<div class=body><div class=who><b>'+esc(c.name||"claim #"+c.id)+"</b>" +(won?'<span class="pill paid">accepted</span>':voting?'<span class="pill vote">on the ballot</span>':"") +"</div>" +(c.desc?'<div class=cd>'+linkify(c.desc)+"</div>":"") +'<div class=b-meta><span>'+acct(c.issuer)+"</span><span>"+ago(c.createdAt)+"</span>" +'<a href="'+SCAN+"nft/"+NFT+"/"+c.id+'" target=_blank rel=noreferrer>the NFT</a></div>' +((canAccept||canSubmit)?'<div class=row style="margin-top:12px">' +(canAccept?'<button class="btn btn-sm btn-pri" data-accept="'+c.id+'">Accept &amp; pay</button>':"") +(canSubmit?'<button class="btn btn-sm btn-pri" data-submit="'+c.id+'">Put to a vote</button>':"") +"</div>":"") +"</div></div>"}; const renderDetail=()=>{ const b=detail; const el=$("bBody"); if(!b){el.innerHTML='<p class=mut>Reading…</p>';return} const st=statusOf(b); const fee=b.amount*FEE_BPS/BPS; const iAmIssuer=isMe(b.issuer); const live=st==="open"; const idle=live&&!b.voting; let h='<div class=lbl style="margin-top:6px">bounty details</div>' +'<h1 class=t>'+esc(b.name||"(untitled)")+"</h1>" +'<div class=b-meta style="margin-top:12px;justify-content:center">' +(isNew(b.id)?'<span class="pill new">new</span>':"")+badges(b) +"<span>#"+b.id+"</span><span>posted "+ago(b.createdAt)+"</span></div>" +'<div class=desc>'+linkify(b.desc||"")+"</div>" +'<div class="pot-big num">'+fmt(b.amount,4)+" <span>ETH"+usdPara(b.amount)+"</span></div>" +'<div class=byline>posted by '+acct(b.issuer)+(iAmIssuer?'<span class=you>you</span>':"")+"</div>"; h+='<div class=facts>' +'<div class=fact><span>Winner takes</span><b class=num>'+eth(b.amount-fee) +'<span class=mut style="font-weight:500">'+usdPara(b.amount-fee)+"</span></b></div>" +'<div class=fact><span>Protocol fee</span><b class="num mut">'+eth(fee)+" &middot; 2.5%</b></div>" +(st==="paid"?'<div class=fact><span>Paid to</span><b>'+acct(b.claimer)+"</b></div>":"") +'<div class=fact><span>Decided by</span><b>' +(b.open?(b.everExternal?"a vote of its "+b.parts.length+" funders":"the issuer, so far") :"the issuer alone")+"</b></div>" +"</div>"; /* ---- the ballot */ if(b.voting){ const v=b.votes,tot=v.yes+v.no; const pct=tot>0n?Number(v.yes*100n/tot):0; const ended=Math.floor(Date.now()/1000)>=v.deadline; h+='<div class="box hot"><h3>Claim #'+b.voting+" is on the ballot</h3>" +'<div class=bars><i style="width:'+pct+'%"></i><i class=no style="width:'+(100-pct)+'%"></i></div>' +'<div class=b-meta><span class=num>'+eth(v.yes)+" for</span><span class=num>"+eth(v.no) +" against</span><span>"+(ended?"voting closed":left(v.deadline))+"</span></div>" +'<p class="mut" style="margin-top:8px;font-size:12px">Weight is what each funder had in the pot when the vote opened. It passes on more than half of the weight cast; the issuer&rsquo;s stake is counted for it automatically.</p>'; const canVote=account&&!isMe(b.issuer)&&b.myWeight>0n&&!ended&&b.iVoted!==true; h+='<div class=row style="margin-top:10px">' +(canVote?'<button class="btn btn-sm btn-pri" data-vote=1>Vote for ('+eth(b.myWeight)+')</button>' +'<button class="btn btn-sm" data-vote=0>Vote against</button>':"") +(ended?'<button class="btn btn-sm btn-pri" id=doResolve>Resolve &mdash; ' +(votePasses(v)?"it passes":"it fails")+"</button>":"") +"</div>"; if(account&&!ended){ if(isMe(b.issuer)) h+='<p class="mut" style="margin-top:8px;font-size:12px">You opened this vote, so your stake was counted as a yes when you did. There is nothing further to cast.</p>'; else if(b.myWeight===0n) h+='<p class="mut" style="margin-top:8px;font-size:12px">You held no stake when this vote opened, so you cannot vote in it.</p>'; else if(b.iVoted===true) h+='<p class="mut" style="margin-top:8px;font-size:12px">You have already voted in this round.</p>'; else if(b.iVoted===null) h+='<p class="mut" style="margin-top:8px;font-size:12px">This node will not serve logs, so whether you have already voted could not be checked. If you have, the vote will be refused.</p>';} h+="</div>"} /* ---- funders */ if(b.open&&b.parts.length){ h+='<div class=box><h3>Funders</h3><div class=plist>' +b.parts.map(p=>"<div><span>"+acct(p.addr)+(isMe(p.addr)?'<span class=you>you</span>':"") +'</span><span class=num>'+eth(p.amt)+"</span></div>").join("") +"</div></div>"} /* ---- what you can do */ const acts=[]; if(idle&&b.open&&!iAmIssuer)acts.push('<div class=field><span class=lbl>Add to the pot (ETH)</span>' +'<div class=row><input id=joinAmt class=num inputmode=decimal placeholder="'+fmt(CFG.minContrib,6)+'" style="flex:1;min-width:120px">' +'<button class="btn btn-pri" id=doJoin>Fund it</button></div>' +'<p class="mut" id=joinUsd style="margin-top:7px;font-size:13px"></p>' +'<p class="mut" style="margin-top:5px;font-size:13px">Minimum '+eth(CFG.minContrib) +". Funding buys a vote on which claim gets paid, weighted by what you put in.</p></div>"); if(idle&&b.open&&b.mine&&!iAmIssuer) acts.push('<button class="btn" id=doPull>Take back '+eth(b.mine.amt)+"</button>"); if(idle&&iAmIssuer) acts.push('<button class="btn" id=doCancel>Cancel the bounty</button>'); if(st==="cancelled"&&b.open&&b.mine) acts.push('<button class="btn btn-pri" id=doRefund>Claim your refund of '+eth(b.mine.amt)+"</button>"); if(acts.length)h+='<div class=box><h3>You</h3>'+acts.join('<div style="height:8px"></div>')+"</div>"; /* ---- claiming */ if(idle&&account&&!iAmIssuer){ h+='<div class=box><h3>Claim it</h3>' +'<p class="mut" style="font-size:12px">Post the proof. The picture is yours to host &mdash; put it on IPFS and give the link here; the contract mints the claim NFT against it and holds it until the bounty is settled.</p>' +'<div class=field><input id=cTitle maxlength=120 placeholder="Title"></div>' +'<div class=field><textarea id=cDesc style="min-height:70px" placeholder="What the picture shows."></textarea></div>' +'<div class=field><input id=cUri placeholder="ipfs://… or https://…"></div>' +'<button class="btn btn-pri btn-block" id=doClaim>Submit the claim</button></div>'} else if(idle&&!account) h+='<div class=box><h3>Claim it</h3><p class=mut>Connect a wallet to post the picture and claim this bounty.</p></div>'; /* ---- claims */ h+='<div class=box><h3>Claims ('+b.claims.length+")</h3>" +(b.more?'<p class="mut" style="font-size:13px;margin-bottom:10px">Only the newest ' +b.claims.length+" are shown.</p>":"") +(b.claims.length?'<div class=claims>'+b.claims.map(c=>claimCard(b,c)).join("")+"</div>" :'<p class=mut>Nobody has claimed this yet. Pics or it didn&rsquo;t happen.</p>')+"</div>"; h+='<p class="mut" style="margin-top:14px;font-size:12px"><a href="'+SCAN+"address/"+POIDH +'#readContract" target=_blank rel=noreferrer>Read this bounty on Etherscan</a></p>'; el.innerHTML=h}; const renderMine=()=>{ const el=$("mineBody"); /* An empty state is still a state. It said one flat sentence and let the footer close on it; it now says what would be here and offers the one control that fills it. */ if(!account){el.innerHTML='<div class=empty>' +"<p>Nothing to show until a wallet is connected.</p>" +'<p class=sub>What you have
0xacfba7ce…4082·#25,829,677·0x29d73c9f…721832
!==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=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[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
0xacfba7ce…4082·#25,829,676·0x7937695f…99a34e
="latest",I="eth_chainId"; const SCAN="https://etherscan.io/"; const FEE_BPS=250n,BPS=10000n; const PAGE=10; /* The bounty this page was built around. It is pinned, not privileged: it is read the same way as every other one, and nothing here can change it. */ const FEATURED=24n; /* The funder scan reads every bounty that has ever existed, so it is bounded - and the bound is shown to the reader rather than applied behind their back. At the observed rate (25 bounties in the contract's first 103 days) the cap is years away; the batch size keeps any one `aggregate3` a sane size. */ const SCAN_CAP=1000; /* How deep the account's own arrays are walked. They are public arrays with no length getter, so the end is found where the getter reverts. */ const PROBE=20; /* How far a bounty's own claim array is walked past the tenth. */ const CLAIM_CAP=100; const SEL_ETHBAL="4d2301cc"; /* Multicall3.getEthBalance(address) */ /* Selectors, written out rather than computed. The page carries a keccak for name resolution, but a constant that can be checked against the verified source by eye is better than one derived at run time. */ const R_COUNT="a0d59787",R_BOUNTY="dc2f8744",R_CLAIM="a888c2cd",R_BCLAIMS="34414835"; const R_PARTS="c1e3bd3e",R_PART="81fb1fb4",R_PEND="f3f43703",R_TRACK="10b385c2"; const R_CURVOTE="383d6d43",R_EXT="b04f5ebd",R_ROUND="2455a9c6"; const R_PERIOD="02a251a3",R_MINB="d5670525",R_MINC="40650c91",R_URI="c87b56dd"; const R_CLAIMSBY="b03a50c7"; /* Chainlink ETH/USD. The hosted UI takes its dollar figures from a price API; this is the same number with an onchain source, so the page keeps its one rule - it reads contracts, and nothing else. The feed is 8 decimals, and its own `updatedAt` is checked before the figure is shown, so a stalled feed is omitted rather than presented as a live price. */ const FEED="0x5f4ec3df9cbd43714fe2740f5e3616155c5b8419"; const F_ROUND="feaf968c"; const FEED_STALE=172800; const W_SOLO="d4ea4227",W_OPEN="8c1d93c5",W_JOIN="a9b2c6eb",W_WDOPEN="faf0f5e9"; const W_CSOLO="b1b4be97",W_COPEN="d496ec7a",W_REFUND="240b727e",W_MKCLAIM="83ffc9f2"; const W_ACCEPT="1d260f8e",W_SUBMIT="9e9251c8",W_VOTE="b7c8994d",W_RESOLVE="30f4ccac"; const W_WITHDRAW="3ccfd60b"; const R_UB="2b9234b9",R_UC="ca9748ea"; /* -------------------------------------------------------------- ABI I/O */ /* -------------------------------------------------------------- ABI I/O */ const strip0x=h=>String(h==null?"":h).replace(/^0x/,""); const pad32=h=>strip0x(h).padStart(64,"0"); const encUint=v=>BigInt(v).toString(16).padStart(64,"0"); const encAddr=a=>pad32(String(a).toLowerCase()); const encBool=b=>encUint(b?1:0); const encBytes=b=>{let h="";for(const x of b)h+=x.toString(16).padStart(2,"0"); return encUint(b.length)+(h?h.padEnd(Math.ceil(h.length/64)*64,"0"):"")}; const encStr=s=>encBytes(new TextEncoder().encode(s)); const word=(h,i)=>strip0x(h).slice(i*64,(i+1)*64); const uword=(h,i)=>{const w=word(h,i);return w?BigInt("0x"+w):0n}; const aword=(h,i)=>"0x"+word(h,i).slice(24); const hexNum=n=>"0x"+Number(n).toString(16); const decStrAt=(h,byteOff)=>{const s=strip0x(h),i=byteOff*2; const len=Number(BigInt("0x"+s.slice(i,i+64)));if(!len)return""; const raw=s.slice(i+64,i+64+len*2),b=new Uint8Array(len); for(let k=0;k<len;k++)b[k]=parseInt(raw.slice(k*2,k*2+2),16); return new TextDecoder().decode(b)}; const encAgg3=cs=>{let head="",tail="",off=cs.length*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(cs.length)+head+tail}; /* A read that came back as something other than an aggregate3 response - a truncated body, a provider answering an `eth_call` with a status word, a gateway returning a stub - used to surface as "Cannot convert 0x to a BigInt" from deep inside the decoder, which tells a reader nothing and hides which of their nodes is at fault. It is checked at the door instead. */ const decAgg3=hex=>{const h=strip0x(hex),at=i=>h.slice(i*64,(i+1)*64); if(h.length<128||h.length%64)throw Error( "The read node gave a malformed answer to a batched call. Pick another node with the read-node control."); 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))); out.push(ok?"0x"+h.slice((lw+1)*64,(lw+1)*64+len*2):null)} return out}; const mc3=async cs=>cs.length?decAgg3(await rpcRead(C,[{to:MC3,data:encAgg3(cs)},L])):[]; /* ------------------------------------------------------------- NUMBERS */ const fmt=(v,dp=4,dec=18)=>{ if(v==null)return"—"; v=BigInt(v);const neg=v<0n;if(neg)v=-v; const base=10n**BigInt(dec); let s=(v/base).toString(); let frac=(v%base).toString().padStart(dec,"0").slice(0,dp).replace(/0+$/,""); if(v>0n&&v<base&&!frac){dp=Math.min(dec,8);frac=(v%base).toString().padStart(dec,"0").slice(0,dp).replace(/0+$/,"")} s=s.replace(/\B(?=(\d{3})+(?!\d))/g,","); return (neg?"-":"")+s+(frac?"."+frac:"")}; const parseAmt=(t,dec=18)=>{ t=String(t||"").trim().replace(/,/g,""); if(!/^\d*\.?\d*$/.test(t)||t===""||t===".")return null; const[i,f=""]=t.split("."); if(f.length>dec)return null; return BigInt((i||"0")+f.padEnd(dec,"0"))}; const shortAddr=a=>a?a.slice(0,6)+"…"+a.slice(-4):""; const isAddr=a=>/^0x[0-9a-fA-F]{40}$/.test(String(a||"").trim()); const ago=t=>{ if(!t)return""; const s=Math.max(0,Math.floor(Date.now()/1000)-t); if(s<90)return"just now"; if(s<5400)return Math.round(s/60)+"m ago"; if(s<129600)return Math.round(s/3600)+"h ago"; return Math.round(s/86400)+"d ago"}; /* ------------------------------------------------- WALLETCONNECT (PORTED) Lifted verbatim from zSwap v0.2: the ChaCha20-Poly1305 / X25519 / Ed25519 helpers, the relay client, and a QR encoder - about 16 KB of protocol, carried so the page need not fetch a 635 KB bundle from a host at run time. A page that can never be updated cannot depend on a CDN still being there. It is self-contained: it touches no DOM and no state of this page, which is what made it portable at all. */ const WC_PID="1e8390ef1c1d8a185e035912a1409749"; const WC_ICON='<svg viewBox="0 0 400 400" fill="none" aria-hidden="true"><circle cx="200" cy="200" r="200" fill="#3396ff"/><path fill="#fff" d="m122.519 148.965c42.791-41.729 112.171-41.729 154.962 0l5.15 5.022c2.14 2.086 2.14 5.469 0 7.555l-17.617 17.18c-1.07 1.043-2.804 1.043-3.874 0l-7.087-6.911c-29.853-29.111-78.253-29.111-108.106 0l-7.59 7.401c-1.07 1.043-2.804 1.043-3.874 0l-17.617-17.18c-2.14-2.086-2.14-5.469 0-7.555zm191.397 35.529 15.679 15.29c2.14 2.086 2.14 5.469 0 7.555l-70.7 68.944c-2.139 2.087-5.608 2.087-7.748 0l-50.178-48.931c-.535-.522-1.402-.522-1.937 0l-50.178 48.931c-2.139 2.087-5.608 2.087-7.748 0l-70.7015-68.945c-2.1396-2.086-2.1396-5.469 0-7.555l15.6795-15.29c2.1396-2.087 5.6085-2.087 7.7481 0l50.1789 48.932c.535.522 1.402.522 1.937 0l50.177-48.932c2.139-2.087 5.608-2.087 7.748 0l50.179 48.932c.535.522 1.402.522 1.937 0l50.179-48.931c2.139-2.087 5.608-2.087 7.748 0z"/></svg>'; const WCU=(()=>{ const rl=(v,c)=>(v<<c|v>>>(32-c))>>>0; const wds=b=>{const o=[];for(let i=0;i<b.length;i+=4)o.push((b[i]|b[i+1]<<8|b[i+2]<<16|b[i+3]<<24)>>>0);return o}; function cblk(x,k,n,c){ const s=[0x61707865,0x3320646e,0x79622d32,0x6b206574, k[0],k[1],k[2],k[3],k[4],k[5],k[6],k[7],c,n[0],n[1],n[2]]; for(let i=0;i<16;i++)x[i]=s[i]; for(let r=0;r<10;r++){ const q=(a,b,cc,d)=>{ x[a]=(x[a]+x[b])>>>0;x[d]=rl(x[d]^x[a],16); x[cc]=(x[cc]+x[d])>>>0;x[b]=rl(x[b]^x[cc],12); x[a]=(x[a]+x[b])>>>0;x[d]=rl(x[d]^x[a],8); x[cc]=(x[cc]+x[d])>>>0;x[b]=rl(x[b]^x[cc],7)}; q(0,4,8,12);q(1,5,9,13);q(2,6,10,14);q(3,7,11,15); q(0,5,10,15);q(1,6,11,12);q(2,7,8,13);q(3,4,9,14);} for(let i=0;i<16;i++)x[i]=(x[i]+s[i])>>>0;} function chacha(key,nonce,ctr,src){ const k=wds(key),n=wds(nonce),x=new Uint32Array(16),out=new Uint8Array(src.length); for(let p=0;p<src.length;p+=64){cblk(x,k,n,ctr++); for(let i=0;i<64&&p+i<src.length;i++)out[p+i]=src[p+i]^(x[i>>2]>>>((i&3)*8)&255);} return out} function poly(msg,key){ const P=(1n<<130n)-5n; let r=0n,sk=0n; for(let i=15;i>=0;i--)r=(r<<8n)|BigInt(key[i]); r&=0x0ffffffc0ffffffc0ffffffc0fffffffn; for(let i=31;i>=16;i--)sk=(sk<<8n)|BigInt(key[i]); let a=0n; for(let i=0;i<msg.length;i+=16){ const n=Math.min(16,msg.length-i); let b=0n; for(let j=n-1;j>=0;j--)b=(b<<8n)|BigInt(msg[i+j]); a=((a+b+(1n<<BigInt(8*n)))*r)%P;} a=(a+sk)&((1n<<128n)-1n); const o=new Uint8Array(16); for(let i=0;i<16;i++){o[i]=Number(a&255n);a>>=8n} return o} const p16=n=>n%16?16-n%16:0; const le64=n=>{const b=new Uint8Array(8);for(let i=0;i<8;i++){b[i]=n&255;n=Math.floor(n/256)}return b}; function tagOf(ct,key,nonce){ const otk=chacha(key,nonce,0,new Uint8Array(32)); const pad=new Uint8Array(p16(ct.length)); const m=new Uint8Array(ct.length+pad.length+16); m.set(ct);m.set(pad,ct.length); m.set(le64(0),ct.length+pad.length);m.set(le64(ct.length),ct.length+pad.length+8); return poly(m,otk)} const seal=(key,nonce,pt)=>{ const ct=chacha(key,nonce,1,pt),tg=tagOf(ct,key,nonce); const o=new Uint8Array(ct.length+16);o.set(ct);o.set(tg,ct.length);return o}; const open=(key,nonce,box)=>{ if(box.length<16)return null; const ct=box.subarray(0,box.length-16),tg=box.subarray(box.length-16); const w=tagOf(ct,key,nonce); let bad=0;for(let i=0;i<16;i++)bad|=w[i]^tg[i]; return bad?null:chacha(key,nonce,1,ct)}; const hex=b=>{let s="";for(const x of b)s+=x.toString(16).padStart(2,"0");return s}; const unhex=h=>{const b=new Uint8Array(h.length/2); for(let i=0;i<b.length;i++)b[i]=parseInt(h.substr(i*2,2),16);return b}; const bin=b=>{let s="";for(let i=0;i<b.length;i+=4096)s+=String.fromCharCode.apply(null,b.subarray(i,i+4096));return s}; const b64=b=>btoa(bin(b)); const unb64=s=>{const t=atob(s),b=new Uint8Array(t.length); for(let i=0;i<t.length;i++)b[i]=t.charCodeAt(i);return b}; const b64u=b=>b64(b).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,""); const utf8=s=>new TextEncoder().encode(s); const B58="123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; const b58=b=>{let n=0n;for(const x of b)n=n*256n+BigInt(x); let s="";while(n>0n){s=B58[Number(n%58n)]+s;n/=58n} for(const x of b){if(x)break;s="1"+s}return s}; const rnd=n=>crypto.getRandomValues(new Uint8Array(n)); const sha256=async b=>new Uint8Array(await crypto.subtle.digest("SHA-256",b)); const topicOf=async k=>hex(await sha256(k)); const encode=(symKey,payload,senderPub)=>{ const iv=rnd(12),pt=utf8(JSON.stringify(payload)); const sealed=seal(symKey,iv,pt); const t=senderPub?1:0; const head=senderPub?1+32:1; const out=new Uint8Array(head+12+sealed.length); out[0]=t; if(senderPub)out.set(senderPub,1); out.set(iv,head);out.set(sealed,head+12); return b64(out)}; const decode=(symKey,msg)=>{ const b=unb64(msg); if(!b.length)return null; const t=b[0]; if(t!==0&&t!==1)return null; const head=t===1?33:1; if(b.length<head+12+16)return null; const iv=b.subarray(head,head+12),box=b.subarray(head+12); const pt=open(symKey,iv,box); if(!pt)return null; try{return JSON.parse(new TextDecoder().decode(pt))}catch{return null}}; const didKey=pub=>{const b=new Uint8Array(34);b[0]=0xed;b[1]=0x01;b.set(pub,2); return"did:key:z"+b58(b)}; async function relayJwt(aud){ let kp; try{kp=await crypto.subtle.generateKey({name:"Ed25519"},true,["sign","verify"])} catch{throw Error("This browser cannot do Ed25519. WalletConnect needs a newer " +"browser; an extension wallet works here today.")} const pub=new Uint8Array(await crypto.subtle.exportKey("raw",kp.publicKey)); const iss=didKey(pub); const iat=Math.floor(Date.now()/1e3); const h=b64u(utf8(JSON.stringify({alg:"EdDSA",typ:"JWT"}))); const p=b64u(utf8(JSON.stringify({iss,sub:hex(rnd(32)),aud,iat,exp:iat+86400}))); const sig=new Uint8Array(await crypto.subtle.sign({name:"Ed25519"},kp.privateKey,utf8(h+"."+p))); return h+"."+p+"."+b64u(sig)} async function kex(){ let kp; try{kp=await crypto.subtle.generateKey({name:"X25519"},true,["deriveBits"])} catch{throw Error("This browser cannot do X25519. WalletConnect needs a newer " +"browser; an extension wallet works here today.")} const pub=new Uint8Array(await crypto.subtle.exportKey("raw",kp.publicKey)); return{priv:kp.privateKey,pub}} async function sessionKey(priv,peerHex){ const peer=await crypto.subtle.importKey("raw",unhex(peerHex),{name:"X25519"},false,[]); const bits=new Uint8Array(await crypto.subtle.deriveBits({name:"X25519",public:peer},priv,256)); const hk=await crypto.subtle.importKey("raw",bits,"HKDF",false,["deriveBits"]); const out=await crypto.subtle.deriveBits( {name:"HKDF",hash:"SHA-256",salt:new Uint8Array(0),info:new Uint8Array(0)},hk,256); return new Uint8Array(out)} return{seal,open,chacha,poly,hex,unhex,b64,unb64,b64u,b58,didKey,rnd,sha256, topicOf,encode,decode,relayJwt,kex,sessionKey}; })(); const WCCLI=(U,WSC)=>{ const RELAY="wss://relay.walletconnect.org"; const T_PROPOSE=1100,T_APPROVE=1101,T_SETTLE=1102,T_SETTLE_RES=1103,T_REQ=1108,T_RES=1109; const TTL=300,STTL=604800; let sock=null,nextId=1,waiters={},subs={},onEvent=null,alive=false,gone=""; const rpcId=()=>Date.now()*1000+(nextId++%1000); const sendRaw=o=>{ if(!alive)throw Error(gone||"The wallet connection is closed."); sock.send(JSON.stringify(o))}; function drop(why){ if(!alive&&gone)return; alive=false;gone=why; const ws=waiters;waiters={};subs={}; for(const k in ws)ws[k].rej(Error(why)); if(onEvent)onEvent({t:"closed",why})} function relayCall(method,params){ const id=rpcId(); return new Promise((res,rej)=>{ waiters[id]={res,rej}; sendRaw({id,jsonrpc:"2.0",method,params}); setTimeout(()=>{if(waiters[id]){delete waiters[id];rej(Error("relay timeout"))}},15000)})} function handle(raw){ let m;try{m=JSON.parse(raw)}catch{return} if(m.method==="irn_subscription"){ sendRaw({id:m.id,jsonrpc:"2.0",result:true}); const d=m.params&&m.params.data; if(d&&subs[d.topic])subs[d.topic](d.message); return} if(m.id&&waiters[m.id]){ const w=waiters[m.id];delete waiters[m.id]; if(m.error)w.rej(Error(m.error.message||"relay error"));else w.res(m.result)}} async function connect(projectId){ try{sock&&sock.close()}catch{} sock=null;alive=false;gone="";waiters={};subs={}; const jwt=await U.relayJwt(RELAY); const url=`${RELAY}/?auth=${jwt}&projectId=${projectId}&ua=zswap`; const ws=WSC(url); sock=ws; await new Promise((res,rej)=>{ let d=0; const t=setTimeout(()=>{if(!d){d=1;rej(Error("The relay did not answer."))}},15000); const fail=m=>{try{ws.close()}catch{}if(sock===ws)sock=null;rej(Error(m))}; const end=f=>x=>{if(d)return;d=1;clearTimeout(t);f(x)}; sock.onopen=end(()=>{alive=true;res()}); sock.onerror=end(()=>fail("The relay could not be reached.")); sock.onclose=end(e=>fail("The relay closed the connection" +(e&&e.code?" (code "+e.code+(e.reason?": "+e.reason:"")+")":"")+"."))}) sock.onclose=()=>drop("The relay connection dropped. Reconnect your wallet."); sock.onmessage=e=>handle(typeof e.data==="string"?e.data:""); } const subscribe=async(topic,fn)=>{subs[topic]=fn;await relayCall("irn_subscribe",{topic})}; const publish=(topic,message,tag,ttl)=> relayCall("irn_publish",{topic,message,ttl:ttl||TTL,tag,prompt:tag===T_PROPOSE||tag===T_REQ}); async function pair(projectId,meta){ await connect(projectId); const symKey=U.rnd(32); const pTopic=await U.topicOf(symKey); const kp=await U.kex(); const proposeId=rpcId(); let settle=null; const done=new Promise((res,rej)=>{settle={res,rej}}); await subscribe(pTopic,async msg=>{ const m=U.decode(symKey,msg); if(!m||m.id!==proposeId)return; if(m.error)return settle.rej(Error(m.error.message||"rejected")); const rp=m.result&&m.result.responderPublicKey; if(!rp)return; const sKey=await U.sessionKey(kp.priv,rp); const sTopic=await U.topicOf(sKey); await subscribe(sTopic,async smsg=>{ const s=U.decode(sKey,smsg); if(!s)return; if(s.method==="wc_sessionSettle"){ await publish(sTopic,U.encode(sKey,{id:s.id,jsonrpc:"2.0",result:true}),T_SETTLE_RES,STTL); const ns=s.params&&s.params.namespaces||{}; const accts=[]; for(const k in ns)for(const a of ns[k].accounts||[])accts.push(a); settle.res({topic:sTopic,key:sKey,accounts:accts, methods:(ns.eip155&&ns.eip155.methods)||[]}); return} if(s.method==="wc_sessionDelete")return drop("The wallet ended the session."); if(s.method&&onEvent)onEvent({t:"session",m:s}); if(s.id&&waiters["s"+s.id]){ const w=waiters["s"+s.id];delete waiters["s"+s.id]; if(s.error)w.rej(Object.assign(Error(s.error.message||"rejected"),{code:s.error.code})); else w.res(s.result)}}); }); const proposal={id:proposeId,jsonrpc:"2.0",method:"wc_sessionPropose",params:{ relays:[{protocol:"irn"}], proposer:{publicKey:U.hex(kp.pub),metadata:meta}, requiredNamespaces:{}, optionalNamespaces:{eip155:{chains:["eip155:1"], methods:["eth_sendTransaction","personal_sign","eth_signTypedData_v4", "eth_call","eth_chainId","eth_accounts","eth_getBalance", "wallet_switchEthereumChain"], events:["chainChanged","accountsChanged"]}}}}; await publish(pTopic,U.encode(symKey,proposal),T_PROPOSE); const uri=`wc:${pTopic}@2?relay-protocol=irn&symKey=${U.hex(symKey)}`; return{uri,done}} function request(sess,method,params){ const id=rpcId(); const body={id,jsonrpc:"2.0",method:"wc_sessionRequest", params:{request:{method,params:params||[]},chainId:"eip155:1"}}; const p=new Promise((res,rej)=>{ waiters["s"+id]={res,rej}; setTimeout(()=>{if(waiters["s"+id]){delete waiters["s"+id];rej(Error("wallet timed out"))}},300000)}); publish(sess.topic,U.encode(sess.key,body),T_REQ).catch(e=>{ const w=waiters["s"+id];if(!w)return;delete waiters["s"+id];w.rej(e)}); return p} return{pair,request,close:()=>{drop("The wallet connection was closed.");try{sock&&sock.close()}catch{}sock=null}, on:f=>{onEvent=f},_relayCall:relayCall}; }; const QR8=(()=>{ const EXP=new Uint8Array(512),LOG=new Uint8Array(256); (()=>{let x=1;for(let i=0;i<255;i++){EXP[i]=x;LOG[x]=i;x<<=1;if(x&256)x^=285} for(let i=255;i<512;i++)EXP[i]=EXP[i-255]})(); const mul=(a,b)=>a&&b?EXP[LOG[a]+LOG[b]]:0; const gen=n=>{let p=[1]; for(let i=0;i<n;i++){const q=new Array(p.length+1).fill(0); for(let j=0;j<p.length;j++){q[j]^=mul(p[j],EXP[i]);q[j+1]^=p[j]}p=q} return p}; const rs=(data,n)=>{ const g=gen(n),r=new Uint8Array(data.length+n); r.set(data); for(let i=0;i<data.length;i++){ const c=r[i];if(!c)continue; for(let j=0;j<=n;j++)r[i+j]^=mul(g[n-j],c)} return r.subarray(data.length)}; const SZ=49,NBLK=2,DPB=97,ECB=24,ALIGN=[6,24,42]; const bch=(v,g,n)=>{let d=v<<n; const bits=x=>{let c=0;while(x){c++;x>>>=1}return c}; const gl=bits(g); while(bits(d)>=gl)d^=g<<(bits(d)-gl); return d}; const fmtBits=mask=>{ const v=(1<<3)|mask; return ((v<<10)|bch(v,0x537,10))^0x5412}; const verBits=()=>(8<<12)|bch(8,0x1f25,12); function encode(str){ const bytes=new TextEncoder().encode(str); if(bytes.length>NBLK*DPB-2)throw Error("pairing code too long to draw"); const bits=[]; const push=(v,n)=>{for(let i=n-1;i>=0;i--)bits.push((v>>i)&1)}; push(4,4);push(bytes.length,8); for(const b of bytes)push(b,8); const cap=(NBLK*DPB)*8; for(let i=0;i<4&&bits.length<cap;i++)bits.push(0); while(bits.length%8)bits.push(0); const cw=[]; for(let i=0;i<bits.length;i+=8){let v=0;for(let j=0;j<8;j++)v=(v<<1)|bits[i+j];cw.push(v)} const pads=[0xec,0x11]; for(let i=0;cw.length<NBLK*DPB;i++)cw.push(pads[i%2]); const blocks=[],ecs=[]; for(let i=0;i<NBLK;i++){ const d=Uint8Array.from(cw.slice(i*DPB,(i+1)*DPB)); blocks.push(d);ecs.push(rs(d,ECB))} const out=[]; for(let i=0;i<DPB;i++)for(const b of blocks)out.push(b[i]); for(let i=0;i<ECB;i++)for(const e of ecs)out.push(e[i]); return out} function frame(){ const m=[],fn=[]; for(let i=0;i<SZ;i++){m.push(new Array(SZ).fill(0));fn.push(new Array(SZ).fill(0))} const put=(r,c,v)=>{m[r][c]=v;fn[r][c]=1}; const finder=(r,c)=>{ for(let i=-1;i<8;i++)for(let j=-1;j<8;j++){ const rr=r+i,cc=c+j; if(rr<0||cc<0||rr>=SZ||cc>=SZ)continue; const on=(i>=0&&i<=6&&(j===0||j===6))||(j>=0&&j<=6&&(i===0||i===6))||(i>=2&&i<=4&&j>=2&&j<=4); put(rr,cc,on?1:0)}}; finder(0,0);finder(0,SZ-7);finder(SZ-7,0); for(const r of ALIGN)for(const c of ALIGN){ if((r===6&&c===6)||(r===6&&c===ALIGN[ALIGN.length-1])||(r===ALIGN[ALIGN.length-1]&&c===6))continue; for(let i=-2;i<=2;i++)for(let j=-2;j<=2;j++) put(r+i,c+j,(Math.abs(i)===2||Math.abs(j)===2||(i===0&&j===0))?1:0)} for(let i=8;i<SZ-8;i++){put(6,i,i%2?0:1);put(i,6,i%2?0:1)} put(SZ-8,8,1); for(let i=0;i<9;i++){if(!fn[8][i])put(8,i,0);if(!fn[i][8])put(i,8,0)} for(let i=0;i<8;i++){if(!fn[8][SZ-1-i])put(8,SZ-1-i,0);if(!fn[SZ-1-i][8])put(SZ-1-i,8,0)} const vb=verBits(); for(let i=0;i<18;i++){ const b=(vb>>i)&1,r=Math.floor(i/3),c=i%3; put(SZ-11+c,r,b);put(r,SZ-11+c,b)} return{m,fn}} function place(m,fn,cw){ let bit=0,up=1; const get=i=>(cw[i>>3]>>(7-(i&7)))&1; for(let c=SZ-1;c>0;c-=2){ if(c===6)c--; for(let k=0;k<SZ;k++){ const r=up?SZ-1-k:k; for(let j=0;j<2;j++){ const cc=c-j; if(fn[r][cc])continue; m[r][cc]=bit<cw.length*8?get(bit):0;bit++}} up^=1}} const maskFn=(k,r,c)=>[ (r+c)%2===0,r%2===0,c%3===0,(r+c)%3===0, (((r/2)|0)+((c/3)|0))%2===0,((r*c)%2+(r*c)%3)===0, (((r*c)%2+(r*c)%3)%2)===0,(((r+c)%2+(r*c)%3)%2)===0][k]; function penalty(m){ let p=0; for(let i=0;i<SZ;i++){ let rc=1,cc=1; for(let j=1;j<SZ;j++){ rc=m[i][j]===m[i][j-1]?rc+1:1;if(rc===5)p+=3;else if(rc>5)p++; cc=m[j][i]===m[j-1][i]?cc+1:1;if(cc===5)p+=3;else if(cc>5)p++}} for(let i=0;i<SZ-1;i++)for(let j=0;j<SZ-1;j++){ const s=m[i][j]+m[i][j+1]+m[i+1][j]+m[i+1][j+1]; if(s===0||s===4)p+=3} const pat=[1,0,1,1,1,0,1]; const hit=(get)=>{ for(let i=0;i<SZ;i++)for(let j=0;j<SZ-6;j++){ let okk=1;for(let k=0;k<7;k++)if(get(i,j+k)!==pat[k]){okk=0;break} if(!okk)continue; let before=1,after=1; for(let k=j-4;k<j;k++)if(k<0||get(i,k)
0xacfba7ce…4082·#25,829,672·0x95b2af0d…ffc260
<!doctype html><html lang=en><head> <meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1,viewport-fit=cover"> <meta name=theme-color id=tc content="#7ba7e8"> <title>poidh — pics or it didn't happen</title> <meta name=description content="Onchain front end for the POIDH bounty market on Ethereum mainnet. Post a bounty, fund one together, claim it with a picture."> <link rel=icon href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='.9em' font-size='90'%3E%F0%9F%93%B8%3C/text%3E%3C/svg%3E"> <style> /* POIDH — the first poidh wore a cornflower field, coral accents and big friendly type, and centred everything that mattered. That is kept. What is borrowed from zFi is the discipline underneath it: hairlines instead of boxes, no shadows, no gradients, and micro-labels that say what a thing is rather than decorating it. */ :root{ color-scheme:light; --bg:#7ba7e8; /* the cornflower */ --fg:#fff; --mut:rgba(255,255,255,.74); --dim:rgba(255,255,255,.52); --line:rgba(255,255,255,.34); --line2:rgba(255,255,255,.18); --sunk:rgba(255,255,255,.10); --hov:rgba(255,255,255,.16); --accent:#ff5b57; /* the coral */ --on-accent:#fff; --k:#fff;--kf:#5b8fd8; --good:#eafff0;--bad:#fff0ef} .d{ color-scheme:dark; --bg:#101725; --fg:#eef3fb; --mut:rgba(238,243,251,.68); --dim:rgba(238,243,251,.44); --line:rgba(238,243,251,.26); --line2:rgba(238,243,251,.14); --sunk:rgba(238,243,251,.06); --hov:rgba(238,243,251,.12); --accent:#ff6b66; --on-accent:#101725; --k:#7ba7e8;--kf:#0b1020; --good:#9ff0bb;--bad:#ffb4b1} *{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent} :focus-visible{outline:2px solid var(--accent);outline-offset:3px} @media(prefers-reduced-motion:reduce){*{transition:none!important}} body{background:var(--bg);color:var(--fg);line-height:1.5; font:16px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif; -webkit-font-smoothing:antialiased;padding:0 0 90px;overflow-x:hidden} a{color:inherit;text-underline-offset:3px;text-decoration-thickness:1px} a:hover{color:var(--accent)} .wrap{max-width:780px;margin:0 auto;padding:0 20px} .mut{color:var(--mut)}.dim{color:var(--dim)}.hide{display:none!important} .num{font-variant-numeric:tabular-nums} .ctr{text-align:center} .lbl{font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--mut);font-weight:600} /* ------------------------------------------------------------------ bar */ .bar{position:sticky;top:0;z-index:20;background:var(--bg);border-bottom:1px solid var(--line2)} .bar .wrap{display:flex;align-items:center;gap:12px;height:62px} .brand{font-size:19px;font-weight:800;letter-spacing:.2em;text-decoration:none;flex:0 0 auto} .brand:hover{color:inherit} .tagline{font-size:13px;color:var(--mut);flex:1;min-width:0;font-weight:500; overflow:hidden;text-overflow:ellipsis;white-space:nowrap} @media(max-width:620px){.tagline{display:none}.brand{flex:1}} .trail{display:flex;align-items:center;gap:8px} .chip{border:2px solid var(--fg);background:none;color:var(--fg);cursor:pointer; font:inherit;font-size:13px;font-weight:700;padding:7px 13px;border-radius:999px; display:inline-flex;align-items:center;gap:7px;max-width:min(15rem,46vw)} .chip:hover{background:var(--fg);color:var(--bg)} .chip b{font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0} .chip.on{border-color:var(--accent);color:var(--accent)} .chip.on:hover{background:var(--accent);color:var(--on-accent)} /* The burger, and the X it becomes — coral, top right, exactly where the first poidh put them. */ /* The one control that changes nothing but how this looks, so it is the quietest thing in the bar: no border, no fill, half a disc, and it only comes up to full strength under the cursor. */ .themeb{width:30px;height:30px;border:0;background:none;padding:4px;cursor:pointer; opacity:.5;transition:opacity .15s;flex:0 0 auto;line-height:0} .themeb:hover,.themeb:focus-visible{opacity:1} .themeb svg{width:22px;height:22px;display:block} .themeb circle{fill:none;stroke:var(--fg);stroke-width:1.6} .themeb path{fill:var(--fg)} .burger{width:34px;height:34px;border:0;background:none;cursor:pointer;padding:6px; display:flex;flex-direction:column;justify-content:center;gap:5px;flex:0 0 auto} .burger i{display:block;height:3px;background:var(--accent);border-radius:2px} .navov{position:fixed;inset:0;z-index:50;background:var(--bg); display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px; padding:70px 20px} .navov a,.navov button{background:none;border:0;color:var(--fg);font:inherit; font-size:25px;font-weight:500;padding:11px 18px;cursor:pointer;text-decoration:none} .navov a:hover,.navov button:hover{color:var(--accent)} .navx{position:absolute;top:20px;right:22px;width:38px;height:38px;border:0;background:none; color:var(--accent);font-size:32px;line-height:1;cursor:pointer;font-weight:400;padding:0} /* ----------------------------------------------------------------- hero */ .hero{padding:44px 0 30px;text-align:center} h1.big{font-size:40px;line-height:1.06;font-weight:800;letter-spacing:-.02em} @media(min-width:640px){h1.big{font-size:52px}} .hero p{margin:16px auto 0;max-width:34rem;font-size:16px;color:var(--mut)} .hero .stats{margin-top:20px;display:flex;flex-wrap:wrap;gap:8px 26px;justify-content:center; font-size:13px;color:var(--mut);font-weight:600} .hero .stats b{color:var(--fg);font-weight:800} /* ----------------------------------------------------------------- tabs */ .tabs{display:flex;justify-content:center;gap:4px;margin:6px 0 4px} .tabs button{background:none;border:0;color:var(--mut);font:inherit;font-size:13px; font-weight:700;letter-spacing:.14em;text-transform:uppercase;padding:10px 16px; cursor:pointer;border-bottom:3px solid transparent} .tabs button:hover{color:var(--fg)} .tabs button.on{color:var(--fg);border-bottom-color:var(--accent)} /* ----------------------------------------------------------------- bits */ .btn{display:inline-flex;align-items:center;justify-content:center;gap:7px; border:2px solid var(--fg);border-radius:999px;background:none;color:var(--fg);font:inherit; font-size:14px;font-weight:700;padding:10px 20px;cursor:pointer;text-decoration:none; white-space:nowrap} .btn:hover:not(:disabled){background:var(--fg);color:var(--bg)} .btn:disabled{opacity:.4;cursor:not-allowed} .btn-pri{background:var(--accent);border-color:var(--accent);color:var(--on-accent)} .btn-pri:hover:not(:disabled){background:var(--accent);color:var(--on-accent);opacity:.85} .btn-sm{padding:6px 14px;font-size:13px;border-width:2px} .btn-block{width:100%} .row{display:flex;flex-wrap:wrap;gap:10px} .ctr .row,.row.ctr{justify-content:center} input,textarea{background:rgba(255,255,255,.10);color:var(--fg);border:2px solid var(--line); border-radius:10px;font:inherit;font-size:16px;padding:11px 13px;width:100%} .d input,.d textarea{background:rgba(255,255,255,.05)} input::placeholder,textarea::placeholder{color:var(--dim)} input:focus,textarea:focus{border-color:var(--accent);outline:0} textarea{min-height:104px;resize:vertical} /* Inputs at 780px are hard to scan and harder to aim at. The heading stays centred with the rest of the page; the controls get a column. */ .form{max-width:30rem;margin:0 auto;text-align:left} main{min-height:56vh;padding-bottom:26px} .empty{text-align:center;padding:34px 0 46px;max-width:32rem;margin:0 auto} .empty p{font-size:18px;font-weight:600} .owed{font-size:26px;font-weight:800;letter-spacing:-.02em} .owed.none{font-size:17px;font-weight:600;color:var(--mut);letter-spacing:0} .empty .sub{margin:12px 0 22px;font-size:14px;font-weight:400;color:var(--mut)} .field{margin:18px 0} .field .lbl{display:block;margin-bottom:7px} .seg{display:flex;gap:8px} .seg button{flex:1;background:none;border:2px solid var(--line);border-radius:999px; color:var(--mut);font:inherit;font-size:13px;font-weight:700;padding:9px 6px;cursor:pointer} .seg button.on{background:var(--accent);border-color:var(--accent);color:var(--on-accent)} #stat{margin:16px 0 0;font-size:14px;font-weight:600;word-break:break-word} #stat:not(:empty){padding:11px 14px;border:2px solid var(--line);border-radius:10px} #stat.err{border-color:var(--accent);color:var(--bad)} #stat.ok{border-color:#fff;color:var(--good)} /* -------------------------------------------------------------- bounties */ .b{display:block;width:100%;text-align:left;background:none;color:var(--fg);font:inherit; cursor:pointer;border:0;border-bottom:1px solid var(--line2);padding:18px 4px} .b:hover{background:var(--sunk)} .box .b{padding-left:0;padding-right:0} .box .b:last-child{border-bottom:0;padding-bottom:2px} .b-hd{display:flex;gap:12px;align-items:baseline} .b-id{font-size:13px;color:var(--dim);font-weight:700;flex:0 0 auto} .b-t{font-weight:700;font-size:19px;line-height:1.25;flex:1;min-width:0;overflow-wrap:anywhere} .b-amt{font-weight:800;font-size:16px;flex:0 0 auto} .b-meta{margin-top:9px;display:flex;flex-wrap:wrap;gap:7px 12px;align-items:center; font-size:13px;color:var(--mut);font-weight:500} .pill{font-size:11px;letter-spacing:.1em;text-transform:uppercase;font-weight:800; border:2px solid var(--line);border-radius:999px;padding:1px 9px} .pill.live{border-color:var(--fg);color:var(--fg)} .pill.dead{border-color:var(--line2);color:var(--dim)} .pill.paid{background:var(--fg);border-color:var(--fg);color:var(--bg)} .pill.vote,.pill.new{background:var(--accent);border-color:var(--accent);color:var(--on-accent)} .prov{color:var(--dim)} .spent{color:var(--dim);font-weight:600} /* The pinned bounty, given the room the first poidh gave a bounty: centred, large, and saying the one number that matters. */ .feat{border:3px solid var(--accent);border-radius:16px;padding:26px 22px;margin:8px 0 6px; text-align:center} .feat .lbl{color:var(--accent);margin-bottom:12px} .feat h2{font-size:27px;line-height:1.2;font-weight:800;overflow-wrap:anywhere} .feat .pot{margin-top:14px;font-size:32px;font-weight:800;letter-spacing:-.02em} .feat .pot span{font-size:16px;font-weight:600;color:var(--mut)} .feat .b-meta{justify-content:center;margin-top:12px} .feat p{margin-top:14px;color:var(--mut);font-size:15px;max-width:36rem; margin-left:auto;margin-right:auto} .feat .row{margin-top:18px;justify-content:center} /* ---------------------------------------------------------------- detail */ .back{background:none;border:0;color:var(--mut);font:inherit;font-size:13px;font-weight:700; letter-spacing:.1em;text-transform:uppercase;cursor:pointer;padding:18px 0 0} .back:hover{color:var(--accent)} h1.t{font-size:32px;line-height:1.15;font-weight:800;letter-spacing:-.02em;margin:14px 0 0; overflow-wrap:anywhere} /* The column is centred; the prose inside it is not. A bounty description runs to paragraphs, and centred paragraphs give the eye no left edge to return to - the one place the first poidh's look had to give way to reading. */ .desc{margin:20px auto 0;max-width:38rem;white-space:pre-wrap;overflow-wrap:anywhere; color:var(--mut);font-size:15px;line-height:1.7;text-align:left} .desc a{color:var(--fg);text-decoration:underline} .pot-big{margin-top:22px;font-size:42px;font-weight:800;letter-spacing:-.03em;line-height:1} .pot-big span{font-size:17px;font-weight:600;color:var(--mut);letter-spacing:0} .byline{margin-top:10px;font-size:14px;color:var(--mut);font-weight:500} .facts{margin:26px auto 0;max-width:100%} .fact{display:flex;justify-content:space-between;gap:16px;padding:11px 2px; border-bottom:1px solid var(--line2);font-size:14px;text-align:left} .fact:first-child{border-top:1px solid var(--line2)} .fact span{color:var(--mut);flex:0 0 auto;font-weight:500} .fact b{font-weight:700;text-align:right;overflow-wrap:anywhere;min-width:0} .box{border:2px solid var(--line);border-radius:14px;padding:20px;margin-top:22px;text-align:left} .box.hot{border-color:var(--accent)} .box.thin{padding:16px 20px} .box.thin h3{margin-bottom:6px} .box h3{font-size:12px;letter-spacing:.16em;text-transform:uppercase;font-weight:800; margin-bottom:14px;color:var(--mut)} .box.hot h3{color:var(--accent)} .bars{display:flex;height:12px;border-radius:999px;overflow:hidden;background:var(--line2); margin:10px 0 8px} .bars i{display:block;background:var(--accent)} .bars i.no{background:transparent} /* The claim card is the whole point of poidh, so it looks like the one the first one drew: a coral frame, the picture, and who took it. */ .claims{display:grid;gap:16px;grid-template-columns:1fr} @media(min-width:560px){.claims{grid-template-columns:1fr 1fr}} .claim{border:3px solid var(--accent);border-radius:10px;overflow:hidden; display:flex;flex-direction:column} .claim.win{border-color:var(--fg)} .claim .shot{background:var(--sunk);min-height:120px;display:flex;align-items:center; justify-content:center;padding:16px} .claim .shot img{display:block;width:100%;height:auto} .claim .body{padding:13px 15px 15px} .claim .who{display:flex;align-items:center;justify-content:space-between;gap:10px} .claim .who b{font-weight:800;font-size:15px;overflow-wrap:anywhere} .claim .cd{margin-top:8px;font-size:13px;color:var(--mut);white-space:pre-wrap; overflow-wrap:anywhere} .claim .b-meta{margin-top:10px;font-size:12px} .plist{font-size:14px} .plist a,.byline a{text-decoration:none} .plist a:hover,.byline a:hover{text-decoration:underline} .plist div{display:flex;justify-content:space-between;gap:12px;padding:7px 0; border-bottom:1px solid var(--line2)} .plist div:last-child{border-bottom:0} .you{font-size:10px;letter-spacing:.1em;font-weight:800;background:var(--accent); color:var(--on-accent);border-radius:999px;padding:1px 7px;margin-left:7px} /* --------------------------------------------------------------- modals */ .modal{position:fixed;inset:0;z-index:60;background:rgba(16,23,37,.72);display:flex; align-items:center;justify-content:center;padding:20px;backdrop-filter:blur(4px)} .sheet{background:var(--bg);border:3px solid var(--accent);border-radius:16px; max-width:380px;width:100%;padding:22px;max-height:88vh;overflow:auto} .sheet-hd{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px} .sheet-hd h3{font-size:13px;letter-spacing:.16em;text-transform:uppercase;font-weight:800} .x{background:none;border:0;color:var(--accent);font:inherit;font-size:26px;cursor:pointer; line-height:1;padding:0 4px} .wrow{display:flex;align-items:center;gap:12px;width:100%;background:none;color:var(--fg); border:2px solid var(--line);border-radius:12px;margin-top:9px;padding:12px 14px;font:inherit; font-size:15px;font-weight:600;cursor:pointer;text-align:left} .wrow:hover{border-color:var(--accent)} .wrow img,.wki{width:24px;height:24px;flex:0 0 auto;display:block} .wki svg{width:24px;height:24px;display:block} #wcQR{margin:0 auto;max-width:270px;background:#fff;padding:10px;border-radius:10px} #readOpts button{width:100%;justify-content:flex-start;margin-top:9px;text-transform:none; letter-spacing:0} /* --------------------------------------------------------------- footer */ /* `footer.wrap`, NOT `footer`. Every one of these is also a `.wrap`, and `.wrap{margin:0 auto}` sets margin-top to zero at a higher specificity than a bare type selector - so the footer's top margin was being discarded and its rule was drawn hard against whatever the page ended on. */ footer.wrap{margin-top:64px;border-top:1px solid var(--line2);padding-top:22px; font-size:13px;color:var(--mut);text-align:center} footer ul{list-style:none;display:flex;flex-wrap:wrap;gap:6px 18px;justify-content:center} footer a{text-decoration:underline} footer p{margin-top:12px;max-width:36rem;margin-left:auto;margin-right:auto;color:var(--dim)} </style> </head><body> <header class=bar><div class=wrap> <a class=brand href="#" id=home>POIDH</a> <span class=tagline>pics or it didn&rsquo;t happen</span> <div class=trail> <button class=themeb id=theme title="Light or dark" aria-label="Switch between light and dark"> <svg viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="7.5"/><path d="M10 2.5a7.5 7.5 0 0 1 0 15z"/></svg></button> <button class=chip id=connect><b>connect</b></button> <button class=burger id=burger aria-label="Menu"><i></i><i></i><i></i></button> </div></div></header> <!-- The first poidh put everything but the bounty behind a full-screen menu. It keeps the page in front of you and the plumbing out of the way. --> <div class="navov hide" id=navov> <button class=navx id=navx aria-label="Close">&times;</button> <a href="#" data-nav=list>bounties</a> <a href="#new" data-nav=new>post a bounty</a> <a href="#mine" data-nav=mine>my bounties</a> <button id=node>read node</button> <a href="https://etherscan.io/address/0xE731dFadBFf20542E10D09D26Fc71445C70d4232#code" target=_blank rel=noreferrer>contract</a> <a href="https://docs.poidh.xyz" target=_blank rel=noreferrer>how it works</a> <a href="https://poidh.xyz" target=_blank rel=noreferrer>poidh.xyz</a> </div> <main class=wrap> <section class=hero id=hero> <h1 class=big>pics or it didn&rsquo;t happen</h1> <p>Put money on something you want to see done. Whoever does it posts the picture, and the money is theirs.</p> <div class=stats id=heroStats></div> </section> <div id=stat></div> <nav class=tabs> <button id=tList class=on>Bounties</button> <button id=tNew>Post</button> <button id=tMine>Mine</button> </nav> <!-- ------------------------------------------------------------- LIST --> <section id=pList> <div id=feature></div> <div id=list style="margin-top:18px"></div> <div class="row ctr" style="margin:26px 0 4px"><button class="btn btn-sm" id=more>Load older</button></div> </section> <!-- -------------------------------------------------------------- POST --> <section id=pNew class=hide> <h2 class="big ctr" style="font-size:30px;margin:26px 0 6px">post a bounty</h2> <p class="mut ctr" style="max-width:30rem;margin:0 auto 6px;font-size:14px">Say what you want to see, put money behind it, and let whoever does it show you.</p> <div class=form> <div class=field><span class=lbl>Kind</span> <div class=seg id=kind> <button data-k=open class=on>Open &mdash; anyone may fund</button> <button data-k=solo>Solo &mdash; you fund it</button> </div> <p class="mut" style="margin-top:8px;font-size:12px" id=kindNote></p></div> <div class=field><label class=lbl for=nTitle>Title</label> <input id=nTitle maxlength=200 placeholder="What must be proved?"></div> <div class=field><label class=lbl for=nDesc>Description</label> <textarea id=nDesc placeholder="Say exactly what a picture has to show for this to be paid. A link to a longer spec is fine."></textarea></div> <div class=field><label class=lbl for=nAmt>Amount (ETH)</label> <input id=nAmt inputmode=decimal placeholder="0.1" class=num> <p class="mut" style="margin-top:7px;font-size:13px" id=nUsd></p> <p class="mut" style="margin-top:5px;font-size:13px" id=nMin></p></div> <button class="btn btn-pri btn-block" id=doNew>Post bounty</button> <p class="mut" style="margin-top:12px;font-size:13px">Bounties may only be posted from an ordinary account &mdash; the contract refuses contract callers, so a Safe or a smart account cannot issue one.</p> </div> </section> <!-- -------------------------------------------------------------- MINE --> <section id=pMine class=hide> <h2 class="big ctr" style="font-size:30px;margin:26px 0 18px">my bounties</h2> <div id=mineIntro></div> <div id=mineBody></div> </section> <!-- ------------------------------------------------------------ DETAIL --> <section id=pB class=hide> <button class=back id=back>&larr; all bounties</button> <div id=bBody class=ctr></div> </section> </main> <footer class=wrap> <ul> <li id=addrLi></li> <li id=pgLi></li> <li id=newerLi class=hide><a id=newerLink href="#">a newer version exists &rarr;</a></li> <li><a href="https://poidh.xyz" target=_blank rel=noreferrer>poidh.xyz</a></li> <li><a href="https://docs.poidh.xyz" target=_blank rel=noreferrer>docs</a></li> </ul> <p style="margin-top:10px">This page reads the bounty contract directly and talks to nothing else. Claim pictures live on IPFS and are fetched only when you ask for one.</p> </footer> <!-- --------------------------------------------------------------- MODALS --> <div class="modal hide" id=wpick><div class=sheet> <div class=sheet-hd><h3>Connect</h3><button class=x id=wpX>&times;</button></div> <div id=wpList></div><p class="mut" id=wpNone style="font-size:12px"></p> </div></div> <div class="modal hide" id=wcm><div class=sheet> <div class=sheet-hd><h3>WalletConnect</h3><button class=x id=wcX>&times;</button></div> <div id=wcQR></div> <button class="btn btn-sm btn-block" id=wcCopy style="margin-top:14px">copy link</button> </div></div> <div class="modal hide" id=readPick><div class=sheet> <div class=sheet-hd><h3>Read node</h3><button class=x id=rpX>&times;</button></div> <p class="mut" style="font-size:12px;margin-bottom:12px">No wallet is connected, so this page has nowhere to read from. Pick a node &mdash; it is remembered, and the globe icon takes any URL.</p> <div id=readOpts></div> </div></div> <script> "use strict"; /* ------------------------------------------------------------ CONSTANTS */ /* PoidhV3 and its claim NFT, on Ethereum mainnet. Both immutable, both read directly: there is no indexer, no API and no event log in the read path, which is what makes this view possible from a page that can never be updated. */ const POIDH="0xe731dfadbff20542e10d09d26fc71445c70d4232"; const NFT="0x9c5f45d5e1382e4058d334d93c6c01442012a4d9"; const MC3="0xca11bde05977b3631167028862be2a173976ca11"; const ZERO="0x0000000000000000000000000000000000000000"; const CHAIN=1; const C="eth_call",S="eth_sendTransaction",L
0xacfba7ce…4082·#25,798,779·0xc6d1b5df…899270
h.max(0,lo-pad),yHi=hi+pad; const X=b=>((b-x0)/(x1-x0))*300; const Y=v=>78-((v-yLo)/((yHi-yLo)||1))*72; /* Principal only moves when someone deposits or withdraws, so the honest shape is a step, not a curve through empty blocks. */ let d=""; pts.forEach((p,i)=>{const x=X(p.blk),y=Y(Number(p.v)/1e18); d+=i?" L"+x.toFixed(1)+" "+(Y(Number(pts[i-1].v)/1e18)).toFixed(1)+" L"+x.toFixed(1)+" "+y.toFixed(1) :"M"+x.toFixed(1)+" "+y.toFixed(1)}); $("sparkLine").setAttribute("d",d); $("sparkFill").setAttribute("d",d+" L300 80 L0 80 Z"); el.classList.remove("hide")}; const syncBps=()=>{ const sum=splits.reduce((a,s)=>a+(s.bps||0),0); $("bpsLeft").textContent=(sum/100).toFixed(2)+"% designated"; $("bpsBar").style.width=Math.min(100,sum/100)+"%"; $("splitNote").textContent=sum>10000?"Over 100%." :(sum<10000?(10000-sum)/100+"% banks to you.":""); $("doGive").disabled=busy||(!!account&&(!parseAmt($("amt").value)||sum>10000))}; const renderSplits=()=>{ const box=$("splits"); const opts=vault.causes.filter(c=>c.active); if(!opts.length){box.innerHTML='<p class="hint" style="margin:0">No active cause to name yet.</p>';return} if(!splits.length){ box.innerHTML='<p class="hint" style="margin:0">No cause named — the yield banks to you until you name one.</p>';return} box.innerHTML=splits.map((s,i)=> '<div class="split"><select data-i="'+i+'">' +opts.map(c=>'<option value="'+c.id+'"'+(c.id===s.id?" selected":"")+">"+esc(c.name)+"</option>").join("") +'</select><input data-b="'+i+'" type="text" inputmode="decimal" value="'+(s.bps/100)+'">' +'<span class="mut" style="font-size:.8125rem">%</span>' +'<button class="btn btn-sm" data-x="'+i+'" aria-label="Remove">×</button></div>').join(""); for(const el of box.querySelectorAll("select[data-i]")) el.onchange=e=>{splits[+e.target.dataset.i].id=Number(e.target.value)}; for(const el of box.querySelectorAll("input[data-b]")) el.oninput=e=>{const v=parseFloat(e.target.value); splits[+e.target.dataset.b].bps=Number.isFinite(v)?Math.round(v*100):0; syncBps()}; for(const el of box.querySelectorAll("button[data-x]")) el.onclick=e=>{splits.splice(+e.currentTarget.dataset.x,1);render()}}; const addToSplits=id=>{ if(!splits.some(s=>s.id===id)){ const left=Math.max(0,10000-splits.reduce((a,s)=>a+(s.bps||0),0)); splits.push({id,bps:left||0})} showTab("tGive");render(); $("vault").scrollIntoView({behavior:"smooth",block:"start"})}; const renderMarquee=()=>{ const box=$("marq"); if(!vault.causes.length){box.innerHTML="";return} const one=vault.causes.map(c=> '<a class="marq-i" href="#causes"><span style="color:var(--warn,#e0a13a)">★</span>' +"<b>"+esc(c.name)+"</b><span>"+fmt(c.harvested,5)+" rETH raised</span></a>").join(""); box.innerHTML=one+one}; const ACT={ Deposited:["Vault Deposit","i-blue"],Withdrawn:["Withdrawal","i-amber"], WithdrawnReth:["Withdrawal","i-amber"],YieldAllocated:["Donation Sent","i-green"], ClaimedReth:["Claimed","i-green"],ClaimedEther:["Claimed","i-green"], CauseCreated:["Cause Created","i-blue"],BankWithdrawn:["Bank Withdrawal","i-amber"], BankWithdrawnReth:["Bank Withdrawal","i-amber"]}; const HEART='<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"/></svg>'; const DOWN='<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14"/><path d="m19 12-7 7-7-7"/></svg>'; const UP='<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5"/><path d="m5 12 7-7 7 7"/></svg>'; const STAR='<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="m12 2 3 6.6 7 .9-5.1 4.8 1.3 7L12 18l-6.2 3.3 1.3-7L2 9.5l7-.9z"/></svg>'; const renderActivity=()=>{ const box=$("acty"); if(activity==null){box.innerHTML='<p class="pos-empty">Loading…</p>';return} if(!activity.length){box.innerHTML='<p class="pos-empty">No activity readable from this RPC.</p>';return} box.innerHTML=activity.map(e=>{ const[label,tone]=ACT[e.kind]||[e.kind,"i-blue"]; let sub="",amt="",ico=DOWN; const causeName=id=>{const c=vault.causes.find(c=>c.id===id);return c?esc(c.name):"cause #"+id}; if(e.kind==="Deposited"){ sub="From "+shortAddr("0x"+e.t1.slice(26)); amt=fmt(BigInt("0x"+(e.w1||"0")),4)+" Ξ"} else if(e.kind==="Withdrawn"||e.kind==="WithdrawnReth"||e.kind==="BankWithdrawn"||e.kind==="BankWithdrawnReth"){ ico=UP; sub=shortAddr("0x"+e.t1.slice(26)); amt=fmt(BigInt("0x"+(e.w0||"0")),4)+(e.kind==="Withdrawn"||e.kind==="BankWithdrawn"?" Ξ":" rETH")} else if(e.kind==="YieldAllocated"){ ico=HEART; sub=causeName(Number(BigInt(e.t2||"0x0"))); amt=fmt(BigInt("0x"+(e.w0||"0")),4)+" rETH"} else if(e.kind==="ClaimedReth"||e.kind==="ClaimedEther"){ ico=HEART; sub=causeName(Number(BigInt(e.t1||"0x0"))); amt=fmt(BigInt("0x"+(e.w0||"0")),4)+" rETH"} else if(e.kind==="CauseCreated"){ ico=STAR; sub=causeName(Number(BigInt(e.t1||"0x0"))); amt=""} return '<div class="act"><span class="ico '+tone+'">'+ico+"</span>" +'<span class="who"><b>'+label+"</b><span>"+sub+"</span></span>" +'<span class="amt"><b>'+amt+"</b><span>"+ago(tsCache.get(e.blk))+"</span></span>" +'<a href="'+SCAN+"tx/"+e.tx+'" target="_blank" rel="noreferrer" aria-label="View transaction">' +'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg></a></div>'}).join("")}; const renderCauses=()=>{ const box=$("czList"); if(!vault.causes.length){box.innerHTML='<p class="hint">No causes yet.</p>';return} box.innerHTML=vault.causes.map(c=> '<div class="card cz'+(c.active?"":" off")+'">' +'<div class="cz-t"><h3>'+esc(c.name)+'</h3><span class="cz-n">#'+c.id+(c.active?"":" · inactive")+'</span></div>' +'<div class="cz-rows">' +'<div class="rowline"><span>Given to date</span><b>'+fmt(c.harvested,5)+" rETH</b></div>" +'<div class="rowline"><span>Awaiting claim</span><b>'+fmt(c.claimable,5)+" rETH</b></div>" +"</div>" +'<a class="cz-to" href="'+SCAN+"address/"+c.recipient+'" target="_blank" rel="noreferrer">'+shortAddr(c.recipient)+" ↗</a>" +'<div class="acts"><button class="btn btn-sm" data-claim="'+c.id+'"'+(c.claimable>0n?"":" disabled")+">Claim</button>" +'<button class="btn btn-sm" data-add="'+c.id+'"'+(c.active?"":" disabled")+">Designate</button></div>" +"</div>").join(""); for(const el of box.querySelectorAll("button[data-claim]")) el.onclick=()=>send("Claim",{to:VAULT,data:"0x"+F_CLAIMR+encUint(el.dataset.claim)}); for(const el of box.querySelectorAll("button[data-add]")) el.onclick=()=>addToSplits(Number(el.dataset.add))}; /* -------------------------------------------------------------- ACTIONS */ const send=async(label,tx)=>{ if(busy)return false; try{ busy=true;render(); await checkWallet(); setStat(label+": confirm in your wallet…"); const hash=Array.isArray(tx) ?await sendBatch(account,tx) :await rpc(S,[Object.assign({from:account},tx)]); setStat(label+" sent "+txLink(hash)); await waitTx(hash); setStat(label+" confirmed "+txLink(hash),"ok"); busy=false; await refresh(); return true; }catch(e){ busy=false; const m=String(e&&(e.data&&e.data.message||e.message)||e); setStat(/user rejected|denied/i.test(m)?"Cancelled.":m,"err"); render(); return false} finally{busy=false}}; const cleanSplits=()=>{ const out=splits.filter(s=>s.bps>0).map(s=>({id:Number(s.id),bps:Number(s.bps)})); const seen=new Set(); for(const s of out){if(seen.has(s.id))throw Error("The same cause is named twice.");seen.add(s.id)} out.sort((a,b)=>a.id-b.id); if(out.reduce((a,s)=>a+s.bps,0)>10000)throw Error("Designation exceeds 100%."); return out}; const sameAsChain=o=>o.length===me.allocs.length &&o.every((s,i)=>s.id===me.allocs[i].id&&s.bps===me.allocs[i].bps); $("amt").oninput=()=>render(); $("asset").onchange=()=>render(); $("wAsset").onchange=()=>render(); $("max").onclick=()=>{ $("amt").value=$("asset").value==="eth" ?fmt(me.ethBal>10n**16n?me.ethBal-10n**16n:0n,8) :fmt(me.rethBal,8); render()}; $("wMaxBtn").onclick=()=>{ $("wAmt").value=$("wAsset").value==="eth"?fmt(me.withdrawable,10):fmt(me.reth,10)}; $("addSplit").onclick=()=>{ const opts=vault.causes.filter(c=>c.active); if(!opts.length)return setStat("No active cause to name yet.","err"); const free=opts.find(c=>!splits.some(s=>s.id===c.id))||opts[0]; const left=Math.max(0,10000-splits.reduce((a,s)=>a+(s.bps||0),0)); splits.push({id:free.id,bps:left||0});render()}; $("doGive").onclick=async()=>{ try{ if(!account)return connect(); const eth=$("asset").value==="eth"; const amt=parseAmt($("amt").value); if(!amt||amt<=0n)throw Error("Enter an amount."); const o=cleanSplits(); const keep=sameAsChain(o); if(eth){ if(amt>me.ethBal)throw Error("More than your ETH balance."); const data=keep?"0x"+F_DEPOSIT:"0x"+F_DEPDES+pad32("20")+encAllocs(o); return send("Stake",{to:VAULT,data,value:"0x"+amt.toString(16)})} if(amt>me.rethBal)throw Error("More than your rETH balance."); const data=keep?"0x"+F_DEPR+encUint(amt) :"0x"+F_DEPRDES+encUint(amt)+pad32("40")+encAllocs(o); if(me.allowance>=amt)return send("Stake rETH",{to:VAULT,data}); const approve={to:RETH,data:"0x"+F_APPROVE+encAddr(VAULT)+encUint(amt)}; if(await canBatch(account)) return send("Stake rETH",[approve,{to:VAULT,data}]); const okd=await send("Approve rETH",approve); if(!okd)return; return send("Stake rETH",{to:VAULT,data}); }catch(e){setStat(String(e.message||e),"err")}}; $("doWithdraw").onclick=()=>{ try{ const eth=$("wAsset").value==="eth"; const cap=eth?me.withdrawable:me.reth; let amt=parseAmt($("wAmt").value); if(amt==null||amt<=0n)throw Error("Enter an amount."); if(amt>=cap)amt=MAXU; send("Withdraw",{to:VAULT,data:"0x"+(eth?F_WITHDRAW:F_WITHDRAWR)+encUint(amt)}); }catch(e){setStat(String(e.message||e),"err")}}; $("doHarvest").onclick=()=>send("Harvest",{to:VAULT,data:"0x"+F_HARVEST+encAddr(account)}); $("doBank").onclick=()=>send("Bank withdrawal",{to:VAULT,data:"0x"+F_WBANK+encUint(MAXU)}); $("doBankR").onclick=()=>send("Bank withdrawal",{to:VAULT,data:"0x"+F_WBANKR+encUint(MAXU)}); /* A name is only usable once it is shown resolving to an address, so the destination is visible before anything is signed. */ const resolved={}; const showResolved=()=>{ const bits=[]; for(const[id,label]of[["czRecip","Recipient"],["czOwner","Owner"]]){ const v=$(id).value.trim(); if(!v||isAddr(v))continue; bits.push(label+" "+esc(v)+" → "+(resolved[v]===undefined?"resolving…" :resolved[v]==="offchain"?NAME_OFFCHAIN :!resolved[v]?"not registered":shortAddr(resolved[v])))} $("czResolved").innerHTML=bits.join("<br>")}; const wantName=async v=>{ if(!v||isAddr(v)||resolved[v]!==undefined)return; resolved[v]=undefined; const a=await nameFwdCached(v).catch(e=>e&&e.offchain?"offchain":""); resolved[v]=a;showResolved()}; for(const id of["czRecip","czOwner"]) $(id).oninput=()=>{const v=$(id).value.trim();showResolved();wantName(v)}; const asAddress=async v=>{ v=String(v||"").trim(); if(isAddr(v))return v; const a=resolved[v]&&resolved[v]!=="offchain"?resolved[v]:await nameFwdCached(v); if(!a||a==="offchain")throw Error("“"+v+"” does not resolve to an address."); return a}; $("doCreate").onclick=async()=>{ try{ if(!account)return connect(); const name=$("czName").value.trim(); if(!name)throw Error("Give the cause a name — it is fixed at creation."); const recip=await asAddress($("czRecip").value); const owner=$("czOwner").value.trim()?await asAddress($("czOwner").value):account; send("Create cause",{to:VAULT, data:"0x"+F_CREATE+encAddr(owner)+encAddr(recip)+pad32("60")+encStr(name)}); }catch(e){setStat(String(e.message||e),"err")}}; /* --------------------------------------------------- WALLETCONNECT GLUE */ const WC_SIGN=["eth_sendTransaction","eth_signTransaction","personal_sign","eth_sign", "eth_signTypedData","eth_signTypedData_v3","eth_signTypedData_v4"]; const wcToWallet=m=>m.indexOf("wallet_")===0||WC_SIGN.indexOf(m)>=0; const wcRelay=WCCLI(WCU,u=>new WebSocket(u)); let wcRid=0; const wcHttp=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:++wcRid,jsonrpc:"2.0",method,params:params||[]})}); const j=await r.json(); if(j&&j.error)throw Object.assign(Error(j.error.message||"the read node refused"),{code:j.error.code}); return j?j.result:null}; const wcShowQR=uri=>{ $("wcQR").innerHTML=qrSvg(QR8.build(uri)); $("wcCopy").onclick=()=>{try{navigator.clipboard.writeText(uri); $("wcCopy").textContent="copied"}catch{}}; $("wcm").classList.remove("hide"); return new Promise((res,rej)=>{ const bail=()=>{cleanup();rej(Object.assign(Error(""),{code:4001}))}; const key=e=>{if(e.key==="Escape")bail()}; const cleanup=()=>{removeEventListener("keydown",key);$("wcm").onclick=null; $("wcX").onclick=null;wcShowQR.settle=null}; addEventListener("keydown",key); $("wcX").onclick=bail; $("wcm").onclick=e=>{if(e.target===$("wcm"))bail()}; wcShowQR.settle=()=>{cleanup();res()}})}; const wcCloseQR=()=>{if(wcShowQR.settle)wcShowQR.settle(); $("wcm").classList.add("hide");$("wcCopy").textContent="copy link"}; async function wcConnect(){ const meta={name:"Firstfruits",description:"Charitable giving on Ethereum", url:location.origin,icons:[]}; const{uri,done}=await wcRelay.pair(WC_PID,meta); const bail=wcShowQR(uri); setStat("Scan the code with your wallet."); let sess; try{sess=await Promise.race([done,bail])} catch(e){wcRelay.close();throw e} finally{wcCloseQR()} const accts=sess.accounts.map(a=>a.split(":").pop()); if(!accts.length){wcRelay.close();throw Error("That wallet approved no account.")} /* A phone wallet signs; it does not necessarily serve reads. Prefer a node the reader named, fall back to the session only if it offers eth_call, and say plainly what to do when neither is true rather than rendering an empty page. */ const canRead=sess.methods.indexOf("eth_call")>=0; const read=(method,params)=>{ if(readNode())return wcHttp(method,params); if(canRead)return wcRelay.request(sess,method,params||[]); return Promise.reject(Object.assign( Error("This wallet does not answer reads. Name a read node with the globe icon."), {noWallet:true}))}; wcRelay.on(ev=>{if(ev&&ev.t==="closed")location.reload()}); return{request:({method,params})=> method==="eth_requestAccounts"||method==="eth_accounts"?Promise.resolve(accts): method==="eth_chainId"?Promise.resolve("0x1"): wcToWallet(method)?wcRelay.request(sess,method,params||[]):read(method,params), on:()=>{}}} /* ------------------------------------------------------------- CONNECT */ const bind=p=>{ PROV=p; if(!p||!p.on)return; p.on("accountsChanged",a=>{account=(a&&a[0]||"").toLowerCase()||null; myName="";if(!account)PROV=null;render();refresh()}); p.on("chainChanged",()=>location.reload())}; const use=async get=>{ $("wpick").classList.add("hide"); try{ const p=typeof get==="function"?await get():get; if(!p)return; bind(p); const accs=await rpc("eth_requestAccounts",[]); account=(accs&&accs[0]||"").toLowerCase()||null; myName=""; let cid=await rpc(I,[]); if(+cid!==CHAIN){ try{await rpc("wallet_switchEthereumChain",[{chainId:"0x1"}]);cid=await rpc(I,[])}catch{} if(+cid!==CHAIN)setStat("This vault is on Ethereum mainnet; your wallet is elsewhere.","err")} splits=[]; await refresh(); }catch(e){ PROV=null;account=null;render(); setStat(/rejected/i.test(String(e.message||e))?"Cancelled.":String(e.message||e),"err")}}; const connect=()=>{ if(account){ account=null;PROV=null;splits=[];myName="";render();setStat("Disconnected.");return refresh()} const choices=providers.map(p=>({name:p.info.name,icon:p.info.icon,get:()=>p.provider})); if(!choices.length&&window.ethereum) choices.push({name:"Browser wallet",get:()=>window.ethereum}); /* Always offered, injected wallet or not: it is the only route for a phone. */ choices.push({name:"WalletConnect",svg:WC_ICON,get:wcConnect}); const list=$("wpList"); list.innerHTML=choices.map((c,i)=> '<button class="wrow" data-w="'+i+'">' +(c.icon?'<img src="'+esc(c.icon)+'" alt="">':'<span class="wki">'+(c.svg||"")+"</span>") +"<span>"+esc(c.name)+"</span></button>").join(""); $("wpNone").textContent=""; for(const el of list.querySelectorAll("button[data-w]")) el.onclick=()=>use(choices[+el.dataset.w].get); $("wpick").classList.remove("hide")}; $("connect").onclick=connect; $("connect2").onclick=()=>{if(!account)connect();else $("vault").scrollIntoView({behavior:"smooth"})}; $("wpX").onclick=()=>$("wpick").classList.add("hide"); $("wpick").onclick=e=>{if(e.target===$("wpick"))$("wpick").classList.add("hide")}; /* ------------------------------------------------------------- SUCCESSOR A pointer nobody reads moves nobody. This asks the contract serving the page for the newest version in its own lineage - and it can only know which contract that is from the GATEWAY HOSTNAME, because a page that wrote its own address into itself would change the chunks the address is derived from, and no salt solves that fixpoint. So a file opened from disk simply does not get this notice. It SAYS, it does not send: no redirect, no auto-navigation, nothing on screen is rewritten. The bytes stay the bytes that were audited, and leaving them is the reader's decision. Every failure is silent, because a missing notice is a smaller harm than a wrong one. */ const SEL_LATEST="52bfe789"; const selfAddress=()=>{ const m=/^(0x[0-9a-fA-F]{40})\./.exec(location.hostname||""); return m?m[1].toLowerCase():""}; const checkSuccessor=async()=>{ const here=selfAddress(); if(!here)return; try{ const tip=aword(await rpcRead(C,[{to:here,data:"0x"+SEL_LATEST},L],1),0); if(!tip||tip===ZERO||tip.toLowerCase()===here)return; $("newerLink").href=location.href.replace(here,tip.toLowerCase()) .replace(here.replace(/^0x/,""),tip.toLowerCase().replace(/^0x/,"")); $("newerLi").classList.remove("hide")} catch{}}; /* ---------------------------------------------------------------- START */ $("pageWhere").textContent=/\.w4eth\.|\.w3link\.|\.eth\.limo$/.test(location.host) ?"Served from Ethereum":"Served from "+(location.host||"a file"); render(); (async()=>{ const p=providers.length===1?providers[0].provider:window.ethereum; if(p){ try{bind(p);const a=await p.request({method:"eth_accounts"}); if(a&&a[0])account=a[0].toLowerCase();else PROV=p} catch{PROV=p}} refresh(); checkSuccessor()})(); setInterval(()=>{if(!busy&&!document.hidden)refresh()},30000); </script>
0xacfba7ce…4082·#25,798,778·0xba1ea8ee…b0d226
>=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)))||""}; /* ------------------------------------------------------------- EIP-5792 rETH has no permit - checked: DOMAIN_SEPARATOR() and nonces() both revert - so staking it always needs an approval first. Where the wallet can batch, that approval and the deposit become one confirmation; where it cannot, they stay two transactions and nothing else changes. */ const capsFor=(c,id)=>{ for(const key of Object.keys(c||{})){ try{if(BigInt(key)===id)return c[key]}catch{}} return undefined}; const canBatch=async owner=>{ try{const c=await rpc("wallet_getCapabilities",[owner]); const k=capsFor(c,BigInt(CHAIN))!==undefined?capsFor(c,BigInt(CHAIN)):capsFor(c,0n); return!!(k&&k.atomic&&(k.atomic.status==="supported"||k.atomic.status==="ready"))} catch{return false}}; const sendBatch=async(owner,calls)=>{ const res=await rpc("wallet_sendCalls",[{version:"2.0.0",chainId:"0x1", 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"); }catch(e){if(/^batch /.test(e.message))throw e} await new Promise(r=>setTimeout(r,1000))} throw Error("batch timed out")}; /* ---------------------------------------------------------------- STATE */ let vault={causes:[],rate:0n,principal:0n,reth:0n,claim:0n}; let me={reth:0n,principal:0n,bank:0n,withdrawable:0n,pendReth:0n,pendEth:0n,allocs:[], ethBal:0n,rethBal:0n,allowance:0n}; let usdPerEth=null,apy=null,patrons=null,series=null,delta30=null,activity=null; let myName=""; let splits=[]; let busy=false; const $=id=>document.getElementById(id); const esc=s=>String(s).replace(/[&<>"]/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c])); const setStat=(msg,cls)=>{const el=$("stat");el.className=cls||"";el.innerHTML=msg||""}; const txLink=h=>'<a href="'+SCAN+'tx/'+h+'" target="_blank" rel="noreferrer">'+h.slice(0,10)+"…</a>"; const usd=v=>{ if(usdPerEth==null||v==null)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}))+")"}; /* ------------------------------------------------------------ READ NODE A page with no wallet has nowhere to read from. Shipping a default endpoint would make it work out of the box, at the cost of every reader silently handing their IP and their queries to a host they never picked - and on an immutable page that choice can never be revisited. So the node is offered, never assumed: nothing is contacted until somebody taps one, and the tap is remembered. The suggestions differ in what they will serve, which decides whether the history-derived figures appear at all, so they say so. */ /* Four rather than three, because this list can never be edited: if one host goes away the others still work, and the globe icon still takes any URL. Labels are measured, not assumed - each was probed for whether it serves eth_getLogs at all, and over what span. */ const READ_NODES=[ ["publicnode","https://ethereum-rpc.publicnode.com","state only"], ["dRPC","https://eth.drpc.org","full, windowed"], ["MEV Blocker","https://rpc.mevblocker.io","full, windowed"], ["Tenderly","https://gateway.tenderly.co/public/mainnet","full"]]; const showReadPick=()=>{ const box=$("readOpts"); if(!box.childElementCount){ box.innerHTML=READ_NODES.map((n,i)=> '<button class="btn btn-sm" data-n="'+i+'" title="'+esc(n[1])+'">'+esc(n[0]) +' <span class="mut" style="font-weight:400">· '+esc(n[2])+"</span></button>").join(""); for(const el of box.querySelectorAll("button[data-n]")) el.onclick=()=>{const u=READ_NODES[+el.dataset.n][1]; try{localStorage.setItem(NODE_KEY,u)}catch{} $("readPick").classList.add("hide"); setStat("Reading through "+u.replace(/^https:\/\//,"")+"."); refresh()}} $("readPick").classList.remove("hide")}; /* ---------------------------------------------------------------- THEME */ const paint=on=>{document.documentElement.classList.toggle("d",on); $("tc").content=on?"#212121":"#fcfcfc"}; let pref=null; try{pref=localStorage.getItem("firstfruits:theme")}catch{} const sysDark=matchMedia("(prefers-color-scheme:dark)"); paint(pref?pref==="d":sysDark.matches); sysDark.addEventListener("change",e=>{if(!pref)paint(e.matches)}); $("theme").onclick=()=>{ pref=document.documentElement.classList.contains("d")?"l":"d"; paint(pref==="d"); try{localStorage.setItem("firstfruits:theme",pref)}catch{}}; $("node").onclick=()=>{ const t=prompt("HTTPS Ethereum RPC for reads when no wallet is connected.\n" +"Leave blank to clear. The page uses your wallet when there is one.",readNode()||""); if(t===null)return; const v=t.trim(); try{if(!v)localStorage.removeItem(NODE_KEY); else if(/^https:\/\/\S+$/i.test(v))localStorage.setItem(NODE_KEY,v); else return setStat("Read node must be an https:// URL.","err")}catch{} setStat(v?"Read node set.":"Read node cleared.","ok"); refresh()}; /* ----------------------------------------------------------------- TABS */ const TABS=[["tGive","pGive"],["tWd","pWd"],["tNew","pNew"]]; const showTab=id=>{for(const[t,p]of TABS){ $(t).classList.toggle("on",t===id);$(p).classList.toggle("hide",t!==id)}}; for(const[t]of TABS)$(t).onclick=()=>showTab(t); /* ---------------------------------------------------------------- READS */ const loadVault=async()=>{ const base=await mc3([ {to:VAULT,data:"0x"+F_NEXTID},{to:VAULT,data:"0x"+F_TPRINC}, {to:VAULT,data:"0x"+F_TRETH},{to:VAULT,data:"0x"+F_TCLAIM}, {to:RETH,data:"0x"+F_RATE},{to:FEED,data:"0x"+F_ROUND}]); const next=base[0]?Number(uword(base[0],0)):1; vault.principal=base[1]?uword(base[1],0):0n; vault.reth=base[2]?uword(base[2],0):0n; vault.claim=base[3]?uword(base[3],0):0n; vault.rate=base[4]?uword(base[4],0):0n; usdPerEth=null; if(base[5]){ const answer=uword(base[5],1),updatedAt=Number(uword(base[5],3)); const age=Math.floor(Date.now()/1000)-updatedAt; if(answer>0n&&updatedAt>0&&age<FEED_STALE)usdPerEth=answer} const ids=[];for(let i=1;i<next;i++)ids.push(i); const raw=await mc3(ids.map(i=>({to:VAULT,data:"0x"+F_CAUSES+encUint(i)}))); vault.causes=ids.map((id,k)=>{ const h=raw[k];if(!h)return null; return{id,owner:aword(h,0),pending:aword(h,1),recipient:aword(h,2), active:uword(h,3)===1n,name:decStrAt(h,Number(uword(h,4))), claimable:uword(h,5),harvested:uword(h,6)}}).filter(Boolean)}; const loadMe=async()=>{ if(!account){me={reth:0n,principal:0n,bank:0n,withdrawable:0n,pendReth:0n,pendEth:0n, allocs:[],ethBal:0n,rethBal:0n,allowance:0n};return} const[r,bal]=await Promise.all([ mc3([{to:VAULT,data:"0x"+F_PATRONS+encAddr(account)}, {to:VAULT,data:"0x"+F_WABLE+encAddr(account)}, {to:VAULT,data:"0x"+F_PEND+encAddr(account)}, {to:VAULT,data:"0x"+F_ALLOCS+encAddr(account)}, {to:RETH,data:"0x"+F_BALOF+encAddr(account)}, {to:RETH,data:"0x"+F_ALLOW+encAddr(account)+encAddr(VAULT)}]), rpcRead("eth_getBalance",[account,L]).catch(()=>"0x0")]); me.reth=r[0]?uword(r[0],0):0n; me.principal=r[0]?uword(r[0],1):0n; me.bank=r[0]?uword(r[0],2):0n; me.withdrawable=r[1]?uword(r[1],0):0n; me.pendReth=r[2]?uword(r[2],0):0n; me.pendEth=r[2]?uword(r[2],1):0n; me.rethBal=r[4]?uword(r[4],0):0n; me.allowance=r[5]?uword(r[5],0):0n; me.ethBal=BigInt(bal||0); me.allocs=[]; if(r[3]){const n=Number(uword(r[3],1)); for(let i=0;i<n;i++)me.allocs.push({id:Number(uword(r[3],2+i*2)),bps:Number(uword(r[3],3+i*2))})}}; /* The history the overview card needs, all of it derived from the vault's own logs: who has ever staked, what the principal was at each block, and the feed of what happened. */ const loadHistory=async()=>{ logSpend=0; const tip=Number(BigInt(await rpcRead("eth_blockNumber",[]))); chainTip=tip; $("blockAt").textContent="block "+tip; const[evs,rate]=await Promise.all([ scanVault(tip), readApy(tip).catch(()=>null)]); apy=rate; const seen=new Set(); for(const e of evs)if(e.kind==="Deposited")seen.add(e.t1); patrons=seen.size; /* Walk backwards from the principal the contract reports now, undoing each event, so the curve ends on the live number rather than near it. */ const pts=[{blk:tip,v:vault.principal}]; let v=vault.principal; for(let i=evs.length-1;i>=0;i--){ const e=evs[i]; if(e.kind==="Deposited")v-=BigInt("0x"+(e.w1||"0")); else if(e.kind==="Withdrawn")v+=BigInt("0x"+(e.w0||"0")); else if(e.kind==="WithdrawnReth")v+=BigInt("0x"+(e.w1||"0")); else continue; if(v<0n)v=0n; pts.push({blk:e.blk,v})} pts.push({blk:FROM_BLOCK,v:0n}); pts.reverse(); series=pts; /* A 30-day change needs 30 days of vault. This one is younger than that, so the window is whatever history exists and the label says which - a shorter span named honestly beats a "30D" that silently measures 26. */ const cut=tip-MONTH_BLOCKS; let base=null; for(const p of pts){if(p.blk<=cut&&p.v>0n)base=p;else if(p.blk>cut)break} if(!base)base=pts.find(p=>p.v>0n)||null; if(base&&base.v>0n){ const days=Math.max(1,Math.round((tip-base.blk)*12/86400)); delta30={pct:Number((vault.principal-base.v)*10000n/base.v)/100, label:days>=29?"30D":days+"D"}} else delta30=null; const kinds={Deposited:1,Withdrawn:1,WithdrawnReth:1,YieldAllocated:1, ClaimedReth:1,ClaimedEther:1,CauseCreated:1,BankWithdrawn:1,BankWithdrawnReth:1}; const feed=evs.filter(e=>kinds[e.kind]).slice(-7).reverse(); await blockTimes(feed.map(e=>e.blk)); activity=feed; $("scanState").textContent=evs.length+" events indexed in your browser" +(logsCapped?", in windows":"")}; let refreshing=false; const refresh=async()=>{ if(refreshing)return;refreshing=true; try{ await loadVault(); await loadMe(); if(!splits.length&&me.allocs.length)splits=me.allocs.map(a=>({id:a.id,bps:a.bps})); render(); if(account&&myName==="")nameRev(account).then(n=>{if(n){myName=n;render()}}).catch(()=>{}); try{await loadHistory();render()} catch(e){ /* The chain-derived half is allowed to fail on its own. An RPC that will not serve logs costs the reader four numbers, not the page. */ patrons=null;apy=null;series=null;activity=[]; render(); /* Name the reason. A reader whose node will not serve logs can point the page at one that will, but only if the page says what went wrong. */ $("scanState").textContent="No log history: " +String(e&&e.message||e).slice(0,80)} }catch(e){ if(e&&e.noWallet){ $("sTvl").textContent="—"; showReadPick(); }else setStat(String(e.message||e),"err"); }finally{refreshing=false}}; /* --------------------------------------------------------------- RENDER */ const rethToEth=r=>vault.rate?r*vault.rate/(10n**18n):0n; const ethToReth=e=>vault.rate?e*(10n**18n)/vault.rate:0n; const render=()=>{ if(vault.rate)$("readPick").classList.add("hide"); $("sTvl").textContent=fmt(vault.principal,4); $("sTvlUsd").textContent=usd(vault.principal); $("sCauses").textContent=vault.causes.length; $("sPatrons").textContent=patrons==null?"—":patrons; $("sApy").textContent=apy==null?"—":apy.toFixed(2)+"%"; const given=vault.causes.reduce((a,c)=>a+c.harvested,0n); $("sGiven").firstChild.textContent=fmt(given,4)+" rETH "; $("sGivenUsd").textContent=usd(rethToEth(given)); const d=$("sDelta"); if(delta30==null)d.classList.add("hide"); else{d.classList.remove("hide"); d.className="delta"+(delta30.pct>0.005?"":delta30.pct<-0.005?" dn":" flat"); d.textContent=(delta30.pct>0?"+":"")+delta30.pct.toFixed(2)+"% ("+delta30.label+")"} drawSpark(); const link=(id,addr)=>{const a=$(id); a.href=SCAN+"address/"+addr;a.textContent=shortAddr(addr)+" ↗"}; link("lnkVault",VAULT);link("lnkReth",RETH);link("lnkFeed",FEED); link("lnkNb",NETBAL);link("lnkMc",MC3); $("lnkAll").href=SCAN+"address/"+VAULT; $("chipAddr").textContent=account?(myName||shortAddr(account)):"Connect Wallet"; $("connect").title=account||"Connect a wallet"; $("chipBal").textContent=account?fmt(me.ethBal,4)+" ETH":""; $("posBody").classList.toggle("hide",!account); $("posEmpty").classList.toggle("hide",!!account); const eth=$("asset").value==="eth"; $("balLbl").textContent=account?(eth?fmt(me.ethBal,4)+" ETH":fmt(me.rethBal,4)+" rETH"):""; const amt=parseAmt($("amt").value); $("amtNote").textContent=amt&&vault.rate ?(eth?"≈ "+fmt(ethToReth(amt),4)+" rETH at the current rate "+usd(amt) :"≈ "+fmt(rethToEth(amt),4)+" ETH of principal "+usd(rethToEth(amt))):""; renderSplits(); syncBps(); $("doGive").textContent=!account?"Connect wallet" :(amt&&amt>0n?"Stake "+fmt(amt,4)+" "+(eth?"ETH":"rETH"):"Enter an amount"); $("pPrincipal").textContent=fmt(me.withdrawable,5)+" ETH"; $("pReth").textContent=fmt(me.reth,5)+" rETH"; $("pPending").textContent=fmt(me.pendReth,6)+" rETH"; $("pBank").textContent=fmt(me.bank,6)+" rETH"; $("wMax").textContent=$("wAsset").value==="eth"?fmt(me.withdrawable,5)+" ETH":fmt(me.reth,5)+" rETH"; $("curSplitSum").textContent=me.allocs.length ?(me.allocs.reduce((a,x)=>a+x.bps,0)/100).toFixed(2)+"%":"none"; $("curSplits").innerHTML=me.allocs.length ?me.allocs.map(a=>{const c=vault.causes.find(c=>c.id===a.id); return (a.bps/100).toFixed(2)+"% → "+(c?esc(c.name):"cause #"+a.id)}).join("<br>") :"Undesignated — all yield accrues to your bank."; $("doHarvest").disabled=busy||!account||me.reth===0n; $("doBank").disabled=$("doBankR").disabled=busy||!account||me.bank===0n; $("doWithdraw").disabled=busy||!account||me.reth===0n; renderCauses();renderMarquee();renderActivity()}; const drawSpark=()=>{ const el=$("spark"); if(!series||series.length<2){el.classList.add("hide");return} const cut=chainTip-MONTH_BLOCKS; let pts=series.filter(p=>p.blk>=cut); if(pts.length<2)pts=series.slice(-2); if(pts[0].blk>cut)pts=[{blk:cut,v:pts[0].v}].concat(pts); const xs=pts.map(p=>p.blk),ys=pts.map(p=>Number(p.v)/1e18); const x0=Math.min(...xs),x1=Math.max(...xs); const lo=Math.min(...ys),hi=Math.max(...ys); if(x1===x0){el.classList.add("hide");return} const pad=(hi-lo)*.25||Math.max(hi*.05,1e-9); const yLo=Mat
0xacfba7ce…4082·#25,798,777·0x3426be40…f69cc4
Id=()=>Date.now()*1000+(nextId++%1000); const sendRaw=o=>{ if(!alive)throw Error(gone||"The wallet connection is closed."); sock.send(JSON.stringify(o))}; function drop(why){ if(!alive&&gone)return; alive=false;gone=why; const ws=waiters;waiters={};subs={}; for(const k in ws)ws[k].rej(Error(why)); if(onEvent)onEvent({t:"closed",why})} function relayCall(method,params){ const id=rpcId(); return new Promise((res,rej)=>{ waiters[id]={res,rej}; sendRaw({id,jsonrpc:"2.0",method,params}); setTimeout(()=>{if(waiters[id]){delete waiters[id];rej(Error("relay timeout"))}},15000)})} function handle(raw){ let m;try{m=JSON.parse(raw)}catch{return} if(m.method==="irn_subscription"){ sendRaw({id:m.id,jsonrpc:"2.0",result:true}); const d=m.params&&m.params.data; if(d&&subs[d.topic])subs[d.topic](d.message); return} if(m.id&&waiters[m.id]){ const w=waiters[m.id];delete waiters[m.id]; if(m.error)w.rej(Error(m.error.message||"relay error"));else w.res(m.result)}} async function connect(projectId){ try{sock&&sock.close()}catch{} sock=null;alive=false;gone="";waiters={};subs={}; const jwt=await U.relayJwt(RELAY); const url=`${RELAY}/?auth=${jwt}&projectId=${projectId}&ua=zswap`; const ws=WSC(url); sock=ws; await new Promise((res,rej)=>{ let d=0; const t=setTimeout(()=>{if(!d){d=1;rej(Error("The relay did not answer."))}},15000); const fail=m=>{try{ws.close()}catch{}if(sock===ws)sock=null;rej(Error(m))}; const end=f=>x=>{if(d)return;d=1;clearTimeout(t);f(x)}; sock.onopen=end(()=>{alive=true;res()}); sock.onerror=end(()=>fail("The relay could not be reached.")); sock.onclose=end(e=>fail("The relay closed the connection" +(e&&e.code?" (code "+e.code+(e.reason?": "+e.reason:"")+")":"")+"."))}) sock.onclose=()=>drop("The relay connection dropped. Reconnect your wallet."); sock.onmessage=e=>handle(typeof e.data==="string"?e.data:""); } const subscribe=async(topic,fn)=>{subs[topic]=fn;await relayCall("irn_subscribe",{topic})}; const publish=(topic,message,tag,ttl)=> relayCall("irn_publish",{topic,message,ttl:ttl||TTL,tag,prompt:tag===T_PROPOSE||tag===T_REQ}); async function pair(projectId,meta){ await connect(projectId); const symKey=U.rnd(32); const pTopic=await U.topicOf(symKey); const kp=await U.kex(); const proposeId=rpcId(); let settle=null; const done=new Promise((res,rej)=>{settle={res,rej}}); await subscribe(pTopic,async msg=>{ const m=U.decode(symKey,msg); if(!m||m.id!==proposeId)return; if(m.error)return settle.rej(Error(m.error.message||"rejected")); const rp=m.result&&m.result.responderPublicKey; if(!rp)return; const sKey=await U.sessionKey(kp.priv,rp); const sTopic=await U.topicOf(sKey); await subscribe(sTopic,async smsg=>{ const s=U.decode(sKey,smsg); if(!s)return; if(s.method==="wc_sessionSettle"){ await publish(sTopic,U.encode(sKey,{id:s.id,jsonrpc:"2.0",result:true}),T_SETTLE_RES,STTL); const ns=s.params&&s.params.namespaces||{}; const accts=[]; for(const k in ns)for(const a of ns[k].accounts||[])accts.push(a); settle.res({topic:sTopic,key:sKey,accounts:accts, methods:(ns.eip155&&ns.eip155.methods)||[]}); return} if(s.method==="wc_sessionDelete")return drop("The wallet ended the session."); if(s.method&&onEvent)onEvent({t:"session",m:s}); if(s.id&&waiters["s"+s.id]){ const w=waiters["s"+s.id];delete waiters["s"+s.id]; if(s.error)w.rej(Object.assign(Error(s.error.message||"rejected"),{code:s.error.code})); else w.res(s.result)}}); }); const proposal={id:proposeId,jsonrpc:"2.0",method:"wc_sessionPropose",params:{ relays:[{protocol:"irn"}], proposer:{publicKey:U.hex(kp.pub),metadata:meta}, requiredNamespaces:{}, optionalNamespaces:{eip155:{chains:["eip155:1"], methods:["eth_sendTransaction","personal_sign","eth_signTypedData_v4", "eth_call","eth_chainId","eth_accounts","eth_getBalance", "wallet_switchEthereumChain"], events:["chainChanged","accountsChanged"]}}}}; await publish(pTopic,U.encode(symKey,proposal),T_PROPOSE); const uri=`wc:${pTopic}@2?relay-protocol=irn&symKey=${U.hex(symKey)}`; return{uri,done}} function request(sess,method,params){ const id=rpcId(); const body={id,jsonrpc:"2.0",method:"wc_sessionRequest", params:{request:{method,params:params||[]},chainId:"eip155:1"}}; const p=new Promise((res,rej)=>{ waiters["s"+id]={res,rej}; setTimeout(()=>{if(waiters["s"+id]){delete waiters["s"+id];rej(Error("wallet timed out"))}},300000)}); publish(sess.topic,U.encode(sess.key,body),T_REQ).catch(e=>{ const w=waiters["s"+id];if(!w)return;delete waiters["s"+id];w.rej(e)}); return p} return{pair,request,close:()=>{drop("The wallet connection was closed.");try{sock&&sock.close()}catch{}sock=null}, on:f=>{onEvent=f},_relayCall:relayCall}; }; const QR8=(()=>{ const EXP=new Uint8Array(512),LOG=new Uint8Array(256); (()=>{let x=1;for(let i=0;i<255;i++){EXP[i]=x;LOG[x]=i;x<<=1;if(x&256)x^=285} for(let i=255;i<512;i++)EXP[i]=EXP[i-255]})(); const mul=(a,b)=>a&&b?EXP[LOG[a]+LOG[b]]:0; const gen=n=>{let p=[1]; for(let i=0;i<n;i++){const q=new Array(p.length+1).fill(0); for(let j=0;j<p.length;j++){q[j]^=mul(p[j],EXP[i]);q[j+1]^=p[j]}p=q} return p}; const rs=(data,n)=>{ const g=gen(n),r=new Uint8Array(data.length+n); r.set(data); for(let i=0;i<data.length;i++){ const c=r[i];if(!c)continue; for(let j=0;j<=n;j++)r[i+j]^=mul(g[n-j],c)} return r.subarray(data.length)}; const SZ=49,NBLK=2,DPB=97,ECB=24,ALIGN=[6,24,42]; const bch=(v,g,n)=>{let d=v<<n; const bits=x=>{let c=0;while(x){c++;x>>>=1}return c}; const gl=bits(g); while(bits(d)>=gl)d^=g<<(bits(d)-gl); return d}; const fmtBits=mask=>{ const v=(1<<3)|mask; return ((v<<10)|bch(v,0x537,10))^0x5412}; const verBits=()=>(8<<12)|bch(8,0x1f25,12); function encode(str){ const bytes=new TextEncoder().encode(str); if(bytes.length>NBLK*DPB-2)throw Error("pairing code too long to draw"); const bits=[]; const push=(v,n)=>{for(let i=n-1;i>=0;i--)bits.push((v>>i)&1)}; push(4,4);push(bytes.length,8); for(const b of bytes)push(b,8); const cap=(NBLK*DPB)*8; for(let i=0;i<4&&bits.length<cap;i++)bits.push(0); while(bits.length%8)bits.push(0); const cw=[]; for(let i=0;i<bits.length;i+=8){let v=0;for(let j=0;j<8;j++)v=(v<<1)|bits[i+j];cw.push(v)} const pads=[0xec,0x11]; for(let i=0;cw.length<NBLK*DPB;i++)cw.push(pads[i%2]); const blocks=[],ecs=[]; for(let i=0;i<NBLK;i++){ const d=Uint8Array.from(cw.slice(i*DPB,(i+1)*DPB)); blocks.push(d);ecs.push(rs(d,ECB))} const out=[]; for(let i=0;i<DPB;i++)for(const b of blocks)out.push(b[i]); for(let i=0;i<ECB;i++)for(const e of ecs)out.push(e[i]); return out} function frame(){ const m=[],fn=[]; for(let i=0;i<SZ;i++){m.push(new Array(SZ).fill(0));fn.push(new Array(SZ).fill(0))} const put=(r,c,v)=>{m[r][c]=v;fn[r][c]=1}; const finder=(r,c)=>{ for(let i=-1;i<8;i++)for(let j=-1;j<8;j++){ const rr=r+i,cc=c+j; if(rr<0||cc<0||rr>=SZ||cc>=SZ)continue; const on=(i>=0&&i<=6&&(j===0||j===6))||(j>=0&&j<=6&&(i===0||i===6))||(i>=2&&i<=4&&j>=2&&j<=4); put(rr,cc,on?1:0)}}; finder(0,0);finder(0,SZ-7);finder(SZ-7,0); for(const r of ALIGN)for(const c of ALIGN){ if((r===6&&c===6)||(r===6&&c===ALIGN[ALIGN.length-1])||(r===ALIGN[ALIGN.length-1]&&c===6))continue; for(let i=-2;i<=2;i++)for(let j=-2;j<=2;j++) put(r+i,c+j,(Math.abs(i)===2||Math.abs(j)===2||(i===0&&j===0))?1:0)} for(let i=8;i<SZ-8;i++){put(6,i,i%2?0:1);put(i,6,i%2?0:1)} put(SZ-8,8,1); for(let i=0;i<9;i++){if(!fn[8][i])put(8,i,0);if(!fn[i][8])put(i,8,0)} for(let i=0;i<8;i++){if(!fn[8][SZ-1-i])put(8,SZ-1-i,0);if(!fn[SZ-1-i][8])put(SZ-1-i,8,0)} const vb=verBits(); for(let i=0;i<18;i++){ const b=(vb>>i)&1,r=Math.floor(i/3),c=i%3; put(SZ-11+c,r,b);put(r,SZ-11+c,b)} return{m,fn}} function place(m,fn,cw){ let bit=0,up=1; const get=i=>(cw[i>>3]>>(7-(i&7)))&1; for(let c=SZ-1;c>0;c-=2){ if(c===6)c--; for(let k=0;k<SZ;k++){ const r=up?SZ-1-k:k; for(let j=0;j<2;j++){ const cc=c-j; if(fn[r][cc])continue; m[r][cc]=bit<cw.length*8?get(bit):0;bit++}} up^=1}} const maskFn=(k,r,c)=>[ (r+c)%2===0,r%2===0,c%3===0,(r+c)%3===0, (((r/2)|0)+((c/3)|0))%2===0,((r*c)%2+(r*c)%3)===0, (((r*c)%2+(r*c)%3)%2)===0,(((r+c)%2+(r*c)%3)%2)===0][k]; function penalty(m){ let p=0; for(let i=0;i<SZ;i++){ let rc=1,cc=1; for(let j=1;j<SZ;j++){ rc=m[i][j]===m[i][j-1]?rc+1:1;if(rc===5)p+=3;else if(rc>5)p++; cc=m[j][i]===m[j-1][i]?cc+1:1;if(cc===5)p+=3;else if(cc>5)p++}} for(let i=0;i<SZ-1;i++)for(let j=0;j<SZ-1;j++){ const s=m[i][j]+m[i][j+1]+m[i+1][j]+m[i+1][j+1]; if(s===0||s===4)p+=3} const pat=[1,0,1,1,1,0,1]; const hit=(get)=>{ for(let i=0;i<SZ;i++)for(let j=0;j<SZ-6;j++){ let okk=1;for(let k=0;k<7;k++)if(get(i,j+k)!==pat[k]){okk=0;break} if(!okk)continue; let before=1,after=1; for(let k=j-4;k<j;k++)if(k<0||get(i,k)!==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="firstfruits: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{} /* ----------------------------------------------------------------- LOGS */ /* Every RPC caps eth_getLogs differently - some by block span, some by result count, some not at all - and the page cannot know which one the reader's wallet is pointed at. So it asks for the whole range and halves on refusal until the answer fits, which lands on any cap without being told it. */ /* Every RPC caps eth_getLogs differently - some by block span, some by result count, some not at all - and the page cannot know which one the reader's wallet is pointed at. So it asks for the whole range, and on refusal walks down a ladder of window sizes until one is accepted. SEQUENTIALLY, and on a budget. Two earlier shapes were measured against real endpoints and both were wrong: recursive halving with no floor turned an archive-gated node's single refusal into 4,920 requests, and fanning the windows out in parallel tripped rate limits that read as further refusals, so a merely range-capped node looked broken. One window at a time, with a hard ceiling on how many, behaves on all three kinds of node. */ const LADDER=[0,45000,9000,2000],BUDGET=140; let logsCapped=false,logSpend=0; const oneCall=async(address,topics,from,to)=>{ if(logSpend>=BUDGET)throw Error("log scan budget spent"); logSpend++; return(await rpcRead("eth_getLogs", [{address,topics:topics||[],fromBlock:hexNum(from),toBlock:hexNum(to)}],1))||[]}; const getLogsWide=async(address,topics,from,to)=>{ if(from>to)return[]; let last; for(const w of LADDER){ try{ if(!w)return await oneCall(address,topics,from,to); const out=[]; for(let a=from;a<=to;a+=w)out.push(...await oneCall(address,topics,a,Math.min(to,a+w-1))); logsCapped=true; return out; }catch(e){ if(e&&e.noWallet)throw e; last=e; if(logSpend>=BUDGET)break}} throw last||Error("eth_getLogs unavailable")}; /* The reader's own index, in their own browser. A first visit scans the vault's whole life - 57 logs at the time of writing - and every visit after it scans only the blocks since. No server ever holds this. */ const IDX_KEY="firstfruits:index:v1"; const loadIdx=()=>{try{const j=JSON.parse(localStorage.getItem(IDX_KEY)||"null"); return j&&Array.isArray(j.ev)&&j.to>=FROM_BLOCK?j:null}catch{return null}}; const saveIdx=j=>{try{localStorage.setItem(IDX_KEY,JSON.stringify(j))}catch{}}; const decodeEv=l=>{ const kind=EV[l.topics[0]]; if(!kind)return null; const d=l.data||"0x"; return{blk:parseInt(l.blockNumber,16),ix:parseInt(l.logIndex||"0x0",16),kind,tx:l.transactionHash, t1:l.topics[1]||"",t2:l.topics[2]||"", w0:word(d,0),w1:word(d,1),w2:word(d,2)}}; let chainTip=0; const scanVault=async latest=>{ const cache=loadIdx(); let evs=cache?cache.ev:[]; const from=cache?Math.max(FROM_BLOCK,cache.to+1):FROM_BLOCK; if(from<=latest){ const fresh=(await getLogsWide(VAULT,null,from,latest)).map(decodeEv).filter(Boolean); /* A reorg can only rewrite the tail, so the tail is what gets dropped. */ evs=evs.filter(e=>e.blk<from).concat(fresh); saveIdx({to:latest,ev:evs.slice(-600)})} evs.sort((a,b)=>a.blk-b.blk||a.ix-b.ix); return evs}; /* Two NARROW windows rather than one wide one: the oldest rate update about a month back, and the newest. A 10,000-block cap serves both without ever being asked for the 216,000 blocks between them. */ const readApy=async latest=>{ const w=9000; const[oldLogs,newLogs]=await Promise.all([ getLogsWide(NETBAL,[T_BALANCES],latest-MONTH_BLOCKS,latest-MONTH_BLOCKS+w), getLogsWide(NETBAL,[T_BALANCES],latest-w,latest)]); const dec=l=>{const d=l.data; const total=uword(d,1),supply=uword(d,3),t=Number(uword(d,4)); if(supply===0n||!t)return null; return{rate:Number(total*(10n**18n)/supply)/1e18,t}}; const a=(oldLogs.map(dec).filter(Boolean))[0]; const b=(newLogs.map(dec).filter(Boolean)).pop(); if(!a||!b||b.t<=a.t||a.rate<=0)return null; const years=(365*86400)/(b.t-a.t); const apy=(Math.pow(b.rate/a.rate,years)-1)*100; return isFinite(apy)&&apy>-50&&apy<100?apy:null}; const tsCache=new Map(); const blockTimes=async blocks=>{ const want=[...new Set(blocks)].filter(b=>!tsCache.has(b)); await Promise.all(want.map(async b=>{ try{const h=await rpcRead("eth_getBlockByNumber",[hexNum(b),false],2); if(h&&h.timestamp)tsCache.set(b,Number(BigInt(h.timestamp)))}catch{}})); return tsCache}; /* --------------------------------------------------------------- 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>
0xacfba7ce…4082·#25,798,772·0x85e87bec…2df133
o your chosen causes.</p></div></div> <div class="arrow"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg></div> <div class="step"><span class="ico"><svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 7V4a1 1 0 0 0-1-1H5a2 2 0 0 0 0 4h15a1 1 0 0 1 1 1v4"/><path d="M3 5v14a2 2 0 0 0 2 2h15a1 1 0 0 0 1-1v-4"/><path d="M18 12a2 2 0 0 0 0 4h4v-4Z"/></svg></span> <div><p>5. Withdraw</p><p>Your principal is always yours and withdrawable.</p></div></div> </div> </div> <div class="card pane"> <div class="ov-hd" style="padding-bottom:1rem;margin-bottom:.25rem"> <h2>Recent Activity</h2> <a id="lnkAll" target="_blank" rel="noreferrer" style="font-size:.8125rem;color:var(--pri);text-decoration:none;display:inline-flex;align-items:center;min-height:1.75rem;padding:0 .2rem">View All</a> </div> <div class="acty" id="acty"><p class="pos-empty">Loading…</p></div> </div> </div></div></section> <!-- ---------------------------------------------------------------- CAUSES --> <section id="causes"><div class="wrap"> <h2 class="sec-h">Causes</h2> <p class="sec-sub">Permissionless: anyone may create one. Claiming pays the recipient, whoever calls it.</p> <div class="cz-grid" id="czList"></div> </div></section> <!-- ---------------------------------------------------------- TRANSPARENCY --> <section><div class="wrap"><div class="card" style="padding:1.75rem"> <h2 style="font-size:1.1875rem;font-weight:600">Protocol Transparency</h2> <p class="sec-sub">Firstfruits is non-custodial and open source. Every number on this page is read from one of these.</p> <div class="kv"> <div><span>Vault Contract</span><a id="lnkVault" target="_blank" rel="noreferrer"></a></div> <div><span>rETH Token</span><a id="lnkReth" target="_blank" rel="noreferrer"></a></div> <div><span>Chainlink ETH/USD</span><a id="lnkFeed" target="_blank" rel="noreferrer"></a></div> <div><span>Rocket Pool Balances</span><a id="lnkNb" target="_blank" rel="noreferrer"></a></div> <div><span>Multicall3</span><a id="lnkMc" target="_blank" rel="noreferrer"></a></div> </div> <p style="margin-top:1.25rem"><a href="https://github.com/unattended-backpack/firstfruits" target="_blank" rel="noreferrer" style="font-size:.875rem;color:var(--pri);text-decoration:none;display:inline-flex;align-items:center;min-height:1.75rem">View source on GitHub ↗</a></p> </div></div></section> <div class="band"><div class="wrap"> <div><b>Start giving what grows.</b><p class="mut">Stake ETH. Earn yield. Change the world.</p></div> <button class="btn btn-pri btn-lg" id="connect2">Connect Wallet →</button> </div></div> </main> <footer><div class="wrap"> <div class="foot-grid"> <div> <span class="brand"><svg viewBox="0 0 24 24" aria-hidden="true"><g fill="currentColor"><path d="M20.5 2.5C21.6 11 17.5 18 11 21.5 10 13 14 6 20.5 2.5Z"/><path d="M2.2 8.5C7.6 9.8 10.6 14.4 10.9 21.4 5.6 20.1 2.5 15.5 2.2 8.5Z"/></g></svg><span>Firstfruits</span></span> <p class="sec-sub" style="margin-top:.75rem;max-width:26rem">A charitable-giving vault built with rETH. Stake ETH, donate the yield, keep your principal.</p> </div> <div> <h2>Resources</h2> <ul> <li><a href="https://docs.rocketpool.net/liquid-staking/overview" target="_blank" rel="noreferrer">RocketPool Docs</a></li> <li><a href="https://github.com/unattended-backpack/firstfruits" target="_blank" rel="noreferrer">Contracts</a></li> <li><a href="https://firstfruits-ui.vercel.app/" target="_blank" rel="noreferrer">Hosted UI</a></li> </ul> </div> <div> <h2>This page</h2> <ul> <li><span class="mut" id="pageWhere">—</span></li> <li><span class="mut">No server, no indexer, no API</span></li> <li><span class="mut" id="scanState">—</span></li> <li id="newerLi" class="hide"><a id="newerLink" href="#">A newer version exists ↗</a></li> </ul> </div> </div> <div class="foot-end"> <span>VPL Copyleft 2026 Firstfruits. Non-custodial and open source.</span> <span style="font-style:italic">“Honour the Lord with thy substance, and with the firstfruits of all thine increase.”</span> </div> </div></footer> <div id="wcm" class="modal hide"><div class="card"> <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem"> <b>WalletConnect</b><button class="btn btn-sm" id="wcX">Close</button></div> <div id="wcQR"></div> <p class="hint" style="text-align:center">Scan with a mobile wallet, or <button class="btn btn-sm" id="wcCopy" style="margin-left:.35rem">copy link</button></p> </div></div> <div id="wpick" class="modal hide"><div class="card"> <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem"> <b>Connect a wallet</b><button class="btn btn-sm" id="wpX">Close</button></div> <div id="wpList"></div><p class="hint" id="wpNone"></p> </div></div> <script> "use strict"; /* ------------------------------------------------------------ CONSTANTS */ const VAULT="0xff0000000674ffb069f73038b0a54c7937552088"; const RETH="0xae78736cd615f374d3085123a210448e74fc6393"; const MC3="0xca11bde05977b3631167028862be2a173976ca11"; /* Chainlink ETH/USD. The hosted UI takes its dollar figures from a price API; this is the same number with an onchain source, so the page keeps its one rule - it reads contracts, and nothing else. */ const FEED="0x5f4ec3df9cbd43714fe2740f5e3616155c5b8419"; const FEED_STALE=172800; /* RocketNetworkBalances. Its BalancesUpdated carries totalEth and rethSupply, whose ratio IS the rETH exchange rate at that block - so the rate's history is readable from logs, with no archive node and no API. That is where the APY on the overview card comes from. */ const NETBAL="0x1d9f14c6bfd8358b589964bad8665add248e9473"; const T_BALANCES="0xdd27295717c4fbd48b1840f846e18be6f0b7bd6b55608e697e53b15848cecdf9"; /* The vault's first log - its OwnershipTransferred, at construction. Nothing about the vault exists before this block, so no scan needs to look earlier. */ const FROM_BLOCK=25606349; const MONTH_BLOCKS=216000; const ZERO="0x0000000000000000000000000000000000000000"; const MAXU=(1n<<256n)-1n; const CHAIN=1; const C="eth_call",S="eth_sendTransaction",L="latest",I="eth_chainId"; const SCAN="https://etherscan.io/"; const F_NEXTID="2ee997b3",F_TPRINC="6dcf1bb2",F_TRETH="fb470ae9",F_TCLAIM="30b7262b"; const F_CAUSES="538a942a",F_PATRONS="2ef46320",F_WABLE="c6f27781",F_PEND="c7bf1980",F_ALLOCS="c3d38897"; const F_DEPOSIT="d0e30db0",F_DEPDES="8f18037a",F_DEPR="3f683618",F_DEPRDES="fc64748f"; const F_HARVEST="0e5c011e",F_WITHDRAW="2e1a7d4d",F_WITHDRAWR="545bbfab"; const F_WBANK="be2ff4a9",F_WBANKR="3ea428fc",F_CREATE="0d50288f",F_CLAIMR="fbf0c32c"; const F_RATE="e6aa216c",F_BALOF="70a08231",F_ALLOW="dd62ed3e",F_APPROVE="095ea7b3"; const F_ROUND="feaf968c"; /* Vault event topics, in the order the contract declares them. */ const EV={ "0x73a19dd210f1a7f902193214c0ee91dd35ee5b4d920cba8d519eca65a7b488ca":"Deposited", "0x92ccf450a286a957af52509bc1c9939d1a6a481783e142e41e2499f0bb66ebc6":"Withdrawn", "0x83d60b29d1376b78e2812b4cc06063732827c1a52185be1924c2da49bddbb7cb":"WithdrawnReth", "0xb128f8ed07afeffa77252a341109d9b8385f636972cebf4cc805c719f62dc34b":"Designated", "0x121c5042302bae5fc561fbc64368f297ca60a880878e1e3a7f7e9380377260bf":"Harvested", "0x43ea0a5f330260fa4368797b43381bb76160b10aeaadc04b44a4ede0eaeb5405":"YieldAllocated", "0xc2e35536eefd471aca99dde2e590c28ef94138fbbde462a6435a974ef13c3d1b":"YieldBanked", "0x45100ac04e0460052b03b152802b617a82911c0f9e2cda3cca3ce2beabe084a5":"BankWithdrawn", "0xbb86fa658458b7cfb6d419368d2a53ea0de9d8471bc613b7c0056dbe4274063d":"BankWithdrawnReth", "0x70c09a3e66772b4e5784521a55d55e52685a4fe55e1ba0e75a12b00fcefece58":"ClaimedReth", "0x47d233d25e187b6f6551b80ef65950d72ec02e429466b784516231e6042d134f":"ClaimedEther", "0x03836492d478204a7f42dac4e381ea2786322654273cd6babf5cfe823fd586cc":"CauseCreated"}; /* -------------------------------------------------------------- ABI I/O */ const strip0x=h=>String(h==null?"":h).replace(/^0x/,""); const pad32=h=>strip0x(h).padStart(64,"0"); const encUint=v=>BigInt(v).toString(16).padStart(64,"0"); const encAddr=a=>pad32(String(a).toLowerCase()); const encBool=b=>encUint(b?1:0); const encBytes=b=>{let h="";for(const x of b)h+=x.toString(16).padStart(2,"0"); return encUint(b.length)+(h?h.padEnd(Math.ceil(h.length/64)*64,"0"):"")}; const encStr=s=>encBytes(new TextEncoder().encode(s)); const encAllocs=a=>encUint(a.length)+a.map(x=>encUint(x.id)+encUint(x.bps)).join(""); const word=(h,i)=>strip0x(h).slice(i*64,(i+1)*64); const uword=(h,i)=>{const w=word(h,i);return w?BigInt("0x"+w):0n}; const aword=(h,i)=>"0x"+word(h,i).slice(24); const hexNum=n=>"0x"+Number(n).toString(16); const decStrAt=(h,byteOff)=>{const s=strip0x(h),i=byteOff*2; const len=Number(BigInt("0x"+s.slice(i,i+64)));if(!len)return""; const raw=s.slice(i+64,i+64+len*2),b=new Uint8Array(len); for(let k=0;k<len;k++)b[k]=parseInt(raw.slice(k*2,k*2+2),16); return new TextDecoder().decode(b)}; const encAgg3=cs=>{let head="",tail="",off=cs.length*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(cs.length)+head+tail}; 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))); out.push(ok?"0x"+h.slice((lw+1)*64,(lw+1)*64+len*2):null)} return out}; const mc3=async cs=>cs.length?decAgg3(await rpcRead(C,[{to:MC3,data:encAgg3(cs)},L])):[]; /* ------------------------------------------------------------- NUMBERS */ const fmt=(v,dp=4,dec=18)=>{ if(v==null)return"—"; v=BigInt(v);const neg=v<0n;if(neg)v=-v; const base=10n**BigInt(dec); let s=(v/base).toString(); let frac=(v%base).toString().padStart(dec,"0").slice(0,dp).replace(/0+$/,""); if(v>0n&&v<base&&!frac){dp=Math.min(dec,8);frac=(v%base).toString().padStart(dec,"0").slice(0,dp).replace(/0+$/,"")} s=s.replace(/\B(?=(\d{3})+(?!\d))/g,","); return (neg?"-":"")+s+(frac?"."+frac:"")}; const parseAmt=(t,dec=18)=>{ t=String(t||"").trim().replace(/,/g,""); if(!/^\d*\.?\d*$/.test(t)||t===""||t===".")return null; const[i,f=""]=t.split("."); if(f.length>dec)return null; return BigInt((i||"0")+f.padEnd(dec,"0"))}; const shortAddr=a=>a?a.slice(0,6)+"…"+a.slice(-4):""; const isAddr=a=>/^0x[0-9a-fA-F]{40}$/.test(String(a||"").trim()); const ago=t=>{ if(!t)return""; const s=Math.max(0,Math.floor(Date.now()/1000)-t); if(s<90)return"just now"; if(s<5400)return Math.round(s/60)+"m ago"; if(s<172800)return Math.round(s/3600)+"h ago"; return Math.round(s/86400)+"d ago"}; /* ------------------------------------------------- WALLETCONNECT (PORTED) Lifted verbatim from zSwap v0.2: the ChaCha20-Poly1305 / X25519 / Ed25519 helpers, the relay client, and a QR encoder - about 16 KB of protocol, carried so the page need not fetch a 635 KB bundle from a host at run time. A page that can never be updated cannot depend on a CDN still being there. It is self-contained: it touches no DOM and no state of this page, which is what made it portable at all. */ const WC_PID="1e8390ef1c1d8a185e035912a1409749"; const WC_ICON='<svg viewBox="0 0 400 400" fill="none" aria-hidden="true"><circle cx="200" cy="200" r="200" fill="#3396ff"/><path fill="#fff" d="m122.519 148.965c42.791-41.729 112.171-41.729 154.962 0l5.15 5.022c2.14 2.086 2.14 5.469 0 7.555l-17.617 17.18c-1.07 1.043-2.804 1.043-3.874 0l-7.087-6.911c-29.853-29.111-78.253-29.111-108.106 0l-7.59 7.401c-1.07 1.043-2.804 1.043-3.874 0l-17.617-17.18c-2.14-2.086-2.14-5.469 0-7.555zm191.397 35.529 15.679 15.29c2.14 2.086 2.14 5.469 0 7.555l-70.7 68.944c-2.139 2.087-5.608 2.087-7.748 0l-50.178-48.931c-.535-.522-1.402-.522-1.937 0l-50.178 48.931c-2.139 2.087-5.608 2.087-7.748 0l-70.7015-68.945c-2.1396-2.086-2.1396-5.469 0-7.555l15.6795-15.29c2.1396-2.087 5.6085-2.087 7.7481 0l50.1789 48.932c.535.522 1.402.522 1.937 0l50.177-48.932c2.139-2.087 5.608-2.087 7.748 0l50.179 48.932c.535.522 1.402.522 1.937 0l50.179-48.931c2.139-2.087 5.608-2.087 7.748 0z"/></svg>'; const WCU=(()=>{ const rl=(v,c)=>(v<<c|v>>>(32-c))>>>0; const wds=b=>{const o=[];for(let i=0;i<b.length;i+=4)o.push((b[i]|b[i+1]<<8|b[i+2]<<16|b[i+3]<<24)>>>0);return o}; function cblk(x,k,n,c){ const s=[0x61707865,0x3320646e,0x79622d32,0x6b206574, k[0],k[1],k[2],k[3],k[4],k[5],k[6],k[7],c,n[0],n[1],n[2]]; for(let i=0;i<16;i++)x[i]=s[i]; for(let r=0;r<10;r++){ const q=(a,b,cc,d)=>{ x[a]=(x[a]+x[b])>>>0;x[d]=rl(x[d]^x[a],16); x[cc]=(x[cc]+x[d])>>>0;x[b]=rl(x[b]^x[cc],12); x[a]=(x[a]+x[b])>>>0;x[d]=rl(x[d]^x[a],8); x[cc]=(x[cc]+x[d])>>>0;x[b]=rl(x[b]^x[cc],7)}; q(0,4,8,12);q(1,5,9,13);q(2,6,10,14);q(3,7,11,15); q(0,5,10,15);q(1,6,11,12);q(2,7,8,13);q(3,4,9,14);} for(let i=0;i<16;i++)x[i]=(x[i]+s[i])>>>0;} function chacha(key,nonce,ctr,src){ const k=wds(key),n=wds(nonce),x=new Uint32Array(16),out=new Uint8Array(src.length); for(let p=0;p<src.length;p+=64){cblk(x,k,n,ctr++); for(let i=0;i<64&&p+i<src.length;i++)out[p+i]=src[p+i]^(x[i>>2]>>>((i&3)*8)&255);} return out} function poly(msg,key){ const P=(1n<<130n)-5n; let r=0n,sk=0n; for(let i=15;i>=0;i--)r=(r<<8n)|BigInt(key[i]); r&=0x0ffffffc0ffffffc0ffffffc0fffffffn; for(let i=31;i>=16;i--)sk=(sk<<8n)|BigInt(key[i]); let a=0n; for(let i=0;i<msg.length;i+=16){ const n=Math.min(16,msg.length-i); let b=0n; for(let j=n-1;j>=0;j--)b=(b<<8n)|BigInt(msg[i+j]); a=((a+b+(1n<<BigInt(8*n)))*r)%P;} a=(a+sk)&((1n<<128n)-1n); const o=new Uint8Array(16); for(let i=0;i<16;i++){o[i]=Number(a&255n);a>>=8n} return o} const p16=n=>n%16?16-n%16:0; const le64=n=>{const b=new Uint8Array(8);for(let i=0;i<8;i++){b[i]=n&255;n=Math.floor(n/256)}return b}; function tagOf(ct,key,nonce){ const otk=chacha(key,nonce,0,new Uint8Array(32)); const pad=new Uint8Array(p16(ct.length)); const m=new Uint8Array(ct.length+pad.length+16); m.set(ct);m.set(pad,ct.length); m.set(le64(0),ct.length+pad.length);m.set(le64(ct.length),ct.length+pad.length+8); return poly(m,otk)} const seal=(key,nonce,pt)=>{ const ct=chacha(key,nonce,1,pt),tg=tagOf(ct,key,nonce); const o=new Uint8Array(ct.length+16);o.set(ct);o.set(tg,ct.length);return o}; const open=(key,nonce,box)=>{ if(box.length<16)return null; const ct=box.subarray(0,box.length-16),tg=box.subarray(box.length-16); const w=tagOf(ct,key,nonce); let bad=0;for(let i=0;i<16;i++)bad|=w[i]^tg[i]; return bad?null:chacha(key,nonce,1,ct)}; const hex=b=>{let s="";for(const x of b)s+=x.toString(16).padStart(2,"0");return s}; const unhex=h=>{const b=new Uint8Array(h.length/2); for(let i=0;i<b.length;i++)b[i]=parseInt(h.substr(i*2,2),16);return b}; const bin=b=>{let s="";for(let i=0;i<b.length;i+=4096)s+=String.fromCharCode.apply(null,b.subarray(i,i+4096));return s}; const b64=b=>btoa(bin(b)); const unb64=s=>{const t=atob(s),b=new Uint8Array(t.length); for(let i=0;i<t.length;i++)b[i]=t.charCodeAt(i);return b}; const b64u=b=>b64(b).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,""); const utf8=s=>new TextEncoder().encode(s); const B58="123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; const b58=b=>{let n=0n;for(const x of b)n=n*256n+BigInt(x); let s="";while(n>0n){s=B58[Number(n%58n)]+s;n/=58n} for(const x of b){if(x)break;s="1"+s}return s}; const rnd=n=>crypto.getRandomValues(new Uint8Array(n)); const sha256=async b=>new Uint8Array(await crypto.subtle.digest("SHA-256",b)); const topicOf=async k=>hex(await sha256(k)); const encode=(symKey,payload,senderPub)=>{ const iv=rnd(12),pt=utf8(JSON.stringify(payload)); const sealed=seal(symKey,iv,pt); const t=senderPub?1:0; const head=senderPub?1+32:1; const out=new Uint8Array(head+12+sealed.length); out[0]=t; if(senderPub)out.set(senderPub,1); out.set(iv,head);out.set(sealed,head+12); return b64(out)}; const decode=(symKey,msg)=>{ const b=unb64(msg); if(!b.length)return null; const t=b[0]; if(t!==0&&t!==1)return null; const head=t===1?33:1; if(b.length<head+12+16)return null; const iv=b.subarray(head,head+12),box=b.subarray(head+12); const pt=open(symKey,iv,box); if(!pt)return null; try{return JSON.parse(new TextDecoder().decode(pt))}catch{return null}}; const didKey=pub=>{const b=new Uint8Array(34);b[0]=0xed;b[1]=0x01;b.set(pub,2); return"did:key:z"+b58(b)}; async function relayJwt(aud){ let kp; try{kp=await crypto.subtle.generateKey({name:"Ed25519"},true,["sign","verify"])} catch{throw Error("This browser cannot do Ed25519. WalletConnect needs a newer " +"browser; an extension wallet works here today.")} const pub=new Uint8Array(await crypto.subtle.exportKey("raw",kp.publicKey)); const iss=didKey(pub); const iat=Math.floor(Date.now()/1e3); const h=b64u(utf8(JSON.stringify({alg:"EdDSA",typ:"JWT"}))); const p=b64u(utf8(JSON.stringify({iss,sub:hex(rnd(32)),aud,iat,exp:iat+86400}))); const sig=new Uint8Array(await crypto.subtle.sign({name:"Ed25519"},kp.privateKey,utf8(h+"."+p))); return h+"."+p+"."+b64u(sig)} async function kex(){ let kp; try{kp=await crypto.subtle.generateKey({name:"X25519"},true,["deriveBits"])} catch{throw Error("This browser cannot do X25519. WalletConnect needs a newer " +"browser; an extension wallet works here today.")} const pub=new Uint8Array(await crypto.subtle.exportKey("raw",kp.publicKey)); return{priv:kp.privateKey,pub}} async function sessionKey(priv,peerHex){ const peer=await crypto.subtle.importKey("raw",unhex(peerHex),{name:"X25519"},false,[]); const bits=new Uint8Array(await crypto.subtle.deriveBits({name:"X25519",public:peer},priv,256)); const hk=await crypto.subtle.importKey("raw",bits,"HKDF",false,["deriveBits"]); const out=await crypto.subtle.deriveBits( {name:"HKDF",hash:"SHA-256",salt:new Uint8Array(0),info:new Uint8Array(0)},hk,256); return new Uint8Array(out)} return{seal,open,chacha,poly,hex,unhex,b64,unb64,b64u,b58,didKey,rnd,sha256, topicOf,encode,decode,relayJwt,kex,sessionKey}; })(); const WCCLI=(U,WSC)=>{ const RELAY="wss://relay.walletconnect.org"; const T_PROPOSE=1100,T_APPROVE=1101,T_SETTLE=1102,T_SETTLE_RES=1103,T_REQ=1108,T_RES=1109; const TTL=300,STTL=604800; let sock=null,nextId=1,waiters={},subs={},onEvent=null,alive=false,gone=""; const rpc
0xacfba7ce…4082·#25,798,770·0x6d6eccaa…2cf6bf
ecoration:none;transition:border-color .15s} .marq-i:hover{border-color:var(--pri)} .marq-i b{font-weight:600} .marq-i span{color:var(--mut);font-variant-numeric:tabular-nums} /* -------------------------------------------------------------- activity */ .acty{display:flex;flex-direction:column} .act{display:flex;align-items:center;gap:.85rem;padding:.8rem 0;border-bottom:1px solid var(--line)} .act:last-child{border-bottom:0} .act .ico{display:flex;align-items:center;justify-content:center;width:2.25rem;height:2.25rem; flex:0 0 auto;border-radius:.6rem} .act .who{min-width:0;flex:1} .act .who b{display:block;font-size:.875rem;font-weight:600} .act .who span{display:block;font-size:.75rem;color:var(--mut);overflow:hidden; text-overflow:ellipsis;white-space:nowrap} .act .amt{text-align:right;flex:0 0 auto} .act .amt b{display:block;font-size:.8125rem;font-weight:600;font-variant-numeric:tabular-nums} .act .amt span{display:block;font-size:.75rem;color:var(--mut)} .act a{color:var(--mut);flex:0 0 auto;display:inline-flex;align-items:center; justify-content:center;width:1.75rem;height:1.75rem;margin-right:-.35rem} .act a:hover{color:var(--pri)} .split-2{display:grid;gap:1.25rem;margin-top:1.75rem} @media(min-width:1024px){.split-2{grid-template-columns:2fr 1fr;gap:1.5rem;align-items:start}} .stale{font-size:.75rem;color:var(--mut)} /* ---------------------------------------------------------------- modal */ .modal{position:fixed;inset:0;z-index:40;background:#0000008c;display:flex; align-items:center;justify-content:center;padding:1rem} .modal .card{width:100%;max-width:22rem;padding:1.5rem} .wrow{display:flex;align-items:center;gap:.75rem;width:100%;padding:.75rem .85rem;margin:.35rem 0; border:1px solid var(--line);border-radius:var(--r);background:transparent;color:var(--fg); font:inherit;font-weight:600;cursor:pointer;transition:background .15s} .wrow:hover{background:var(--sunk)} .wrow img{width:1.5rem;height:1.5rem;border-radius:.35rem} .wrow .wki{width:1.5rem;height:1.5rem;flex:0 0 auto;line-height:0} .wrow .wki svg{width:100%;height:100%} #wcQR svg{width:100%;height:auto;background:#fff;border-radius:.6rem;display:block} </style> <header class="bar"><div class="wrap"> <a class="brand" href="#top"> <svg viewBox="0 0 24 24" aria-hidden="true"><g fill="currentColor"><path d="M20.5 2.5C21.6 11 17.5 18 11 21.5 10 13 14 6 20.5 2.5Z"/><path d="M2.2 8.5C7.6 9.8 10.6 14.4 10.9 21.4 5.6 20.1 2.5 15.5 2.2 8.5Z"/></g></svg> <span>Firstfruits</span> </a> <nav class="nav"> <a href="#vault">Dashboard</a> <a href="#causes">Causes</a> <a href="#how">How it works</a> </nav> <div class="trail"> <button class="chip" id="connect"><span class="bal" id="chipBal"></span><b id="chipAddr">Connect Wallet</b></button> <button class="icon-btn" id="node" title="Read node" aria-label="Read node"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c2.5 2.7 2.5 15.3 0 18M12 3c-2.5 2.7-2.5 15.3 0 18"/></svg></button> <button class="icon-btn" id="theme" title="Switch theme" aria-label="Switch theme"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/></svg></button> </div> </div></header> <main id="top"> <!-- ------------------------------------------------------------------ HERO --> <div class="hero"><div class="wrap"><div class="hero-grid"> <div> <span class="badge"> <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 160"><defs><style> .rp-c1 { mask: url(#rp-mask); } .rp-c2 { fill: url(#rp-linear-gradient-3); } .rp-c3 { fill: url(#rp-linear-gradient-2); } .rp-c4 { fill: url(#rp-linear-gradient); } .rp-c5 { fill: #fff; } </style><linearGradient id="rp-linear-gradient" x1="1023.49" y1="-10427.81" x2="1134.36" y2="-10538.68" gradientTransform="translate(1158.93 -10403.25) rotate(-180)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f2681d"/><stop offset="1" stop-color="#f89863"/></linearGradient><linearGradient id="rp-linear-gradient-2" x1="-1320.91" y1="-116.53" x2="-1180.26" y2="-116.53" gradientTransform="translate(1046.7 -721.9) rotate(-45)" xlink:href="#linear-gradient"/><linearGradient id="rp-linear-gradient-3" x1="-13.5" y1="174.98" x2="163.35" y2="-1.86" gradientTransform="translate(83.15 -27.63) rotate(45)" gradientUnits="userSpaceOnUse"><stop offset=".11" stop-color="#000"/><stop offset=".13" stop-color="#050505"/><stop offset=".15" stop-color="#131313"/><stop offset=".18" stop-color="#2c2c2c"/><stop offset=".21" stop-color="#4e4e4e"/><stop offset=".24" stop-color="#7a7a7a"/><stop offset=".27" stop-color="#afafaf"/><stop offset=".3" stop-color="#ededed"/><stop offset=".31" stop-color="#fff"/><stop offset=".41" stop-color="#fff"/><stop offset=".75" stop-color="#fff"/><stop offset=".81" stop-color="#fff"/><stop offset=".82" stop-color="#f7f7f7"/><stop offset=".83" stop-color="#e3e3e3"/><stop offset=".85" stop-color="#c1c1c1"/><stop offset=".87" stop-color="#939393"/><stop offset=".88" stop-color="#585858"/><stop offset=".9" stop-color="#101010"/><stop offset=".91" stop-color="#000"/></linearGradient><mask id="rp-mask" x="-220.31" y="-220.31" width="600.61" height="600.61" maskUnits="userSpaceOnUse"><g><rect class="rp-c2" x="-50.12" y="31.18" width="250.09" height="110.77" transform="translate(-39.26 78.33) rotate(-45)"/><g><path class="rp-c5" d="M80,265.31c-102.18,0-185.31-83.13-185.31-185.31S-22.18-105.31,80-105.31,265.31-22.18,265.31,80s-83.13,185.31-185.31,185.31Z"/><path d="M80,9.69c38.83,0,70.31,31.48,70.31,70.31s-31.48,70.31-70.31,70.31S9.69,118.83,9.69,80,41.17,9.69,80,9.69M80-220.31c-40.44,0-79.79,7.97-116.96,23.69-35.79,15.14-67.88,36.76-95.39,64.26-27.51,27.51-49.13,59.6-64.26,95.39-15.72,37.17-23.69,76.52-23.69,116.96s7.97,79.79,23.69,116.96c15.14,35.79,36.76,67.88,64.26,95.39,27.51,27.51,59.6,49.13,95.39,64.26,37.17,15.72,76.52,23.69,116.96,23.69s79.79-7.97,116.96-23.69c35.79-15.14,67.88-36.76,95.39-64.26s49.13-59.6,64.26-95.39c15.72-37.17,23.69-76.52,23.69-116.96s-7.97-79.79-23.69-116.96c-15.14-35.79-36.76-67.88-64.26-95.39-27.51-27.51-59.6-49.13-95.39-64.26-37.17-15.72-76.52-23.69-116.96-23.69h0Z"/></g></g></mask></defs><g id="rp-Logomark"><g id="rp-Logo_BG"><circle class="rp-c4" cx="80" cy="80" r="78.4"/><path id="rp-bg" class="rp-c3" d="M80,9.67c-38.84,0-70.33,31.48-70.33,70.33,0,35.42,26.19,64.73,60.28,69.6,1.32.2,2.66.35,4.02.46,1.33.12,2.67.19,4.02.23.67.02,1.34.03,2.01.03s1.34,0,2.01-.03c1.35-.03,2.69-.11,4.02-.23,1.35-.11,2.69-.26,4.02-.45,34.08-4.87,60.28-34.18,60.28-69.61,0-38.85-31.49-70.33-70.33-70.33Z"/></g><g class="rp-c1"><path id="rp-Logo_Rocket" class="rp-c5" d="M59.68,108.85l-24.96,24.96c1.03.88,2.09,1.72,3.16,2.52l24.64-24.64-2.84-2.84ZM48.31,97.48l-24.63,24.63c.79,1.07,1.64,2.13,2.52,3.17l24.96-24.95-2.84-2.84ZM59.59,97.57l-30.72,30.72c.46.49.93.97,1.4,1.44s.96.94,1.44,1.4l30.72-30.72-2.84-2.84ZM115.23,44.78c-4.53-1.24-10.26-.38-16.2,2.3-5.25,2.36-10.66,6.14-15.54,11.15-2.2,2.25-4.15,4.61-5.83,7l-9.59,1.17-10.69,13.83,10.05,2.6c-.57,2.29-.84,4.5-.8,6.55l-2.53,3.29,3.25,3.25,3.28-2.52c2.06.04,4.26-.23,6.55-.8l2.6,10.05,13.81-10.68,1.16-9.6c2.4-1.69,4.77-3.64,7.03-5.85,5.01-4.89,8.8-10.31,11.16-15.57,2.67-5.93,3.52-11.66,2.29-16.18ZM96.09,72.78c-3.02,3.02-7.45,3.49-9.9,1.04-2.44-2.44-1.98-6.88,1.04-9.9,3.01-3.01,7.45-3.48,9.89-1.03,2.44,2.44,1.98,6.88-1.03,9.89Z"/></g></g></svg> <span style="font-size:1.0625rem">Built with rETH</span> </span> <h1 class="big">Stake ETH.<br><span class="accent">Give what grows.</span></h1> <p class="lead">Firstfruits is a charitable-giving vault. Stake ETH, earn staking yield via rETH, and the yield is routed to causes you care about. Your principal stays yours and is withdrawable anytime.</p> <div class="ctas"> <a href="#vault" class="btn btn-lg">Stake ETH Now →</a> <a href="#causes" class="btn btn-lg">Explore Causes</a> </div> <div class="feats"> <div class="feat"><span class="ico i-blue"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10Z"/></svg></span> <div><p>Principal always yours</p><p>Withdraw your ETH anytime.</p></div></div> <div class="feat"><span class="ico i-green"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22a7 7 0 0 0 7-7c0-2-1-3.9-3-5.5s-3.5-4-4-6.5c-.5 2.5-2 4.5-4 6.5C6 11.1 5 13 5 15a7 7 0 0 0 7 7Z"/></svg></span> <div><p>Powered by rETH</p><p>Secure liquid staking.</p></div></div> <div class="feat"><span class="ico i-amber"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"/></svg></span> <div><p>Give automatically</p><p>Yield routed based on your splits.</p></div></div> </div> </div> <div class="card ov"> <div class="ov-hd"><h2>Vault Overview</h2><span class="mut" style="font-size:.75rem" id="blockAt"></span></div> <div class="tvl"> <div style="display:flex;justify-content:space-between;align-items:baseline;gap:.75rem"> <p class="mut" style="font-size:.875rem">Total Value Locked</p> <span class="delta hide" id="sDelta"></span> </div> <div class="big"><b id="sTvl">…</b><span class="sym">Ξ</span><span class="usd" id="sTvlUsd"></span></div> <div id="readPick" class="hide"> <p class="hint" style="margin-top:.75rem">No wallet found. This page reads Ethereum directly — pick a node to read from, or set your own with the globe icon. Nothing is contacted until you choose. A read node sees your IP address and what you look up; it never sees a key, and nothing you sign passes through it.</p> <div class="acts" id="readOpts"></div> </div> <svg class="spark hide" id="spark" viewBox="0 0 300 80" preserveAspectRatio="none" aria-hidden="true"> <defs><linearGradient id="sparkfill" x1="0" y1="0" x2="0" y2="1"> <stop offset="0%" stop-color="currentColor" stop-opacity=".28"/> <stop offset="100%" stop-color="currentColor" stop-opacity="0"/> </linearGradient></defs> <path class="fl" id="sparkFill" d=""/><path class="ln" id="sparkLine" d=""/> </svg> </div> <div class="tiles"> <div class="tile"><span>Total Causes</span><b id="sCauses">…</b></div> <div class="tile"><span>Total Patrons</span><b id="sPatrons">…</b></div> <div class="tile"><span>Total Yield Donated</span><b id="sGiven">…<span class="usd" id="sGivenUsd"></span></b></div> <div class="tile"><span>Current rETH APY</span><b id="sApy">…</b></div> </div> <a href="#vault" class="btn btn-block btn-lg" style="margin-top:1.5rem">Manage Vault</a> </div> </div></div></div> <!-- --------------------------------------------------------------- MARQUEE --> <div class="marq"><div class="marq-in" id="marq"></div></div> <!-- ----------------------------------------------------------- MANAGE VAULT --> <section id="vault"><div class="wrap"> <h2 class="sec-h">Manage Vault</h2> <p class="sec-sub">Reads and writes the vault contract directly, from your own wallet.</p> <div class="manage"> <div class="card pane"> <div class="tabs"> <button id="tGive" class="on">Stake</button> <button id="tWd">Withdraw</button> <button id="tNew">New cause</button> </div> <div id="pGive"> <div class="field"> <div class="field-hd"><span class="lbl">Stake</span><span id="balLbl"></span></div> <div class="field-row"> <input id="amt" type="text" inputmode="decimal" placeholder="0" autocomplete="off"> <select id="asset"><option value="eth">ETH</option><option value="reth">rETH</option></select> <button class="btn btn-sm" id="max">MAX</button> </div> <p class="hint" id="amtNote"></p> </div> <div class="field"> <div class="field-hd"><span class="lbl">Designate the yield</span><span id="bpsLeft"></span></div> <div id="splits"></div> <div class="bar-track"><i id="bpsBar" style="width:0"></i></div> <div class="field-row"><button class="btn btn-sm" id="addSplit">+ Cause</button> <span class="hint" id="splitNote" style="margin:0"></span></div> </div> <button class="btn btn-pri btn-block btn-lg" id="doGive" style="margin-top:1rem">Connect wallet</button> <p class="hint">Your principal stays withdrawable. Only the rETH appreciation above your principal is routed to the causes you name; anything left undesignated accrues to your bank.</p> </div> <div id="pWd" class="hide"> <div class="field"> <div class="field-hd"><span class="lbl">Withdraw principal</span><span id="wMax"></span></div> <div class="field-row"> <input id="wAmt" type="text" inputmode="decimal" placeholder="0" autocomplete="off"> <select id="wAsset"><option value="eth">as ETH</option><option value="reth">as rETH</option></select> <button class="btn btn-sm" id="wMaxBtn">MAX</button> </div> </div> <button class="btn btn-pri btn-block btn-lg" id="doWithdraw" style="margin-top:1rem">Withdraw</button> <p class="hint">Withdrawing settles your pending yield first, so your causes are paid before your principal leaves. MAX asks the contract for its own maximum rather than the number shown, so a harvest landing in between cannot make it stale.</p> </div> <div id="pNew" class="hide"> <input class="tin" id="czName" placeholder="Name" maxlength="120" autocomplete="off"> <input class="tin" id="czRecip" placeholder="Recipient — 0x address, or a .wei / .gwei / .eth name" style="margin-top:.6rem" autocomplete="off"> <input class="tin" id="czOwner" placeholder="Owner — address or name. Blank = you" style="margin-top:.6rem" autocomplete="off"> <p class="hint" id="czResolved"></p> <button class="btn btn-pri btn-block btn-lg" id="doCreate" style="margin-top:1rem">Create cause</button> <p class="hint">Causes are permissionless: anyone may create one, and the name is fixed at creation. The owner manages the recipient and the active flag; claimed yield only ever goes to the recipient.</p> </div> <p id="stat"></p> </div> <div class="card pane"> <div class="ov-hd" style="padding-bottom:1rem;margin-bottom:.5rem"><h2>Your Position</h2></div> <div id="posBody"> <div class="rowline"><span>Withdrawable principal</span><b id="pPrincipal">—</b></div> <div class="rowline"><span>rETH held for you</span><b id="pReth">—</b></div> <div class="rowline"><span>Pending yield</span><b id="pPending">—</b></div> <div class="rowline"><span>Banked, undesignated</span><b id="pBank">—</b></div> <div class="rowline"><span>Your designation</span><b id="curSplitSum">—</b></div> <p class="hint" id="curSplits"></p> <div class="acts"> <button class="btn btn-sm" id="doHarvest">Harvest</button> <button class="btn btn-sm" id="doBank">Bank → ETH</button> <button class="btn btn-sm" id="doBankR">Bank → rETH</button> </div> <p class="hint">Harvest settles yield accrued so far and routes it by your designation. It is permissionless — anyone may harvest for anyone, and it pays the same causes either way.</p> </div> <p class="pos-empty" id="posEmpty">Connect a wallet to see your position.</p> </div> </div> </div></section> <!-- ------------------------------------------------- HOW IT WORKS + ACTIVITY --> <section><div class="wrap"><div class="split-2"> <div class="card pane" id="how"> <h2 class="sec-h">How Firstfruits Works</h2> <div class="steps"> <div class="step"><span class="ico"><svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3h12l4 6-10 12L2 9Z"/><path d="M11 3 8 9l4 12 4-12-3-6"/><path d="M2 9h20"/></svg></span> <div><p>1. Deposit</p><p>Deposit ETH or rETH into the vault.</p></div></div> <div class="arrow"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg></div> <div class="step"><span class="ico"><svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22a7 7 0 0 0 7-7c0-2-1-3.9-3-5.5s-3.5-4-4-6.5c-.5 2.5-2 4.5-4 6.5C6 11.1 5 13 5 15a7 7 0 0 0 7 7Z"/></svg></span> <div><p>2. Earn</p><p>ETH is staked, which returns rETH to the vault.</p></div></div> <div class="arrow"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg></div> <div class="step"><span class="ico"><svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 7 13.5 15.5 8.5 10.5 2 17"/><polyline points="16 7 22 7 22 13"/></svg></span> <div><p>3. Yield</p><p>rETH appreciates over time, generating staking yield.</p></div></div> <div class="arrow"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg></div> <div class="step"><span class="ico"><svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"/></svg></span> <div><p>4. Give</p><p>Yield is routed t
0xacfba7ce…4082·#25,798,769·0x257c0e2f…b9fa17
<!doctype html> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>Firstfruits — Charitable Giving on Ethereum</title> <meta name="description" content="Charitable giving on Ethereum. Stake ETH or rETH into the vault — your principal stays withdrawable, and the Rocket Pool staking yield it earns is routed to causes you choose."> <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cg fill='%2338a83c'%3E%3Cpath d='M20.5 2.5C21.6 11 17.5 18 11 21.5 10 13 14 6 20.5 2.5Z'/%3E%3Cpath d='M2.2 8.5C7.6 9.8 10.6 14.4 10.9 21.4 5.6 20.1 2.5 15.5 2.2 8.5Z'/%3E%3C/g%3E%3C/svg%3E" type="image/svg+xml"> <meta name="theme-color" id="tc" content="#fcfcfc"> <style> /* Palette lifted from the production build's Skeleton "cerberus" theme: neutral surfaces at chroma 0, a single blue primary at hue 258. */ :root{ color-scheme:light; --s50:oklch(99% 0 0);--s100:oklch(91% 0 0);--s200:oklch(81% 0 0);--s300:oklch(72% 0 0); --s800:oklch(32% 0 0);--s900:oklch(25% 0 0);--s950:oklch(18% 0 0); --p400:oklch(63% .19 255.71);--p500:oklch(57% .21 258.29); --bg:oklch(96.5% 0 0);--card:var(--s50);--sunk:oklch(93.5% 0 0);--line:oklch(87% 0 0); --fg:oklch(18% 0 0);--mut:oklch(45% 0 0);--pri:var(--p500); --k:oklch(18% 0 0);--kf:#fff;--kh:oklch(30% 0 0); --good:oklch(52% .16 149);--bad:oklch(55% .21 25); --shadow:0 1px 2px #0000000a,0 4px 16px #0000000d; --r:.75rem;--r-lg:1rem; } .d{ color-scheme:dark; --bg:oklch(13% 0 0);--card:oklch(17.5% 0 0);--sunk:oklch(22.5% 0 0);--line:oklch(29% 0 0); --fg:oklch(97% 0 0);--mut:oklch(70% 0 0);--pri:var(--p400); --k:oklch(97% 0 0);--kf:oklch(13% 0 0);--kh:oklch(85% 0 0); --good:oklch(75% .16 149);--bad:oklch(70% .18 25); --shadow:0 1px 2px #00000059,0 12px 32px #0000004d; } *{box-sizing:border-box} :focus-visible{outline:2px solid var(--pri);outline-offset:2px} @media(prefers-reduced-motion:reduce){*{transition:none!important;scroll-behavior:auto!important}} html{scroll-behavior:smooth} body{margin:0;background:var(--bg);color:var(--fg); font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans",Arial,sans-serif; line-height:1.5;-webkit-font-smoothing:antialiased} a{color:inherit} h1,h2,h3,p{margin:0} /* Full bleed with generous gutters, as the hosted build does (lg:px-18). A centred max-width column was the single thing that made this page read cramped next to it: the hero has to be able to breathe. */ .wrap{padding:0 1rem} @media(min-width:640px){.wrap{padding:0 1.5rem}} @media(min-width:1024px){.wrap{padding:0 4.5rem}} .mut{color:var(--mut)} .hide{display:none!important} /* --------------------------------------------------------------- app bar */ .bar{position:sticky;top:0;z-index:20;background:color-mix(in oklab,var(--sunk) 90%,transparent); backdrop-filter:blur(12px);border-bottom:1px solid var(--line)} .bar .wrap{display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:1rem;height:4.25rem} .brand{display:flex;align-items:center;gap:.55rem;text-decoration:none;font-size:1.125rem;font-weight:600} .brand svg{width:2rem;height:2rem;flex:0 0 auto} .nav{display:none;justify-content:center;gap:1.75rem} @media(min-width:768px){.nav{display:flex}} .nav a{font-size:.875rem;text-decoration:none;opacity:.75;padding:.4rem 0; display:inline-flex;align-items:center;min-height:1.75rem; border-bottom:2px solid transparent;transition:opacity .15s,border-color .15s} .nav a:hover{opacity:1;border-bottom-color:var(--pri)} .trail{display:flex;align-items:center;gap:.5rem;justify-self:end;min-width:0} .chip{display:inline-flex;align-items:center;gap:.5rem;background:var(--card); border:1px solid var(--line);border-radius:var(--r);padding:.5rem .85rem;font:inherit; font-size:.875rem;cursor:pointer;color:var(--fg);min-width:0;max-width:min(16rem,42vw); transition:border-color .15s} .chip:hover{border-color:var(--pri)} .chip b{font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .chip .bal{color:var(--mut);font-variant-numeric:tabular-nums} @media(max-width:560px){.chip .bal{display:none}} /* A name in the chip is as long as somebody chose to make it. Below this the bar has no room to spare, so the controls shrink and the name truncates rather than pushing the page sideways. */ @media(max-width:430px){ .bar .wrap{gap:.45rem;height:3.85rem} .chip{padding:.4rem .6rem;font-size:.8125rem} .icon-btn{width:2.35rem;height:2.35rem} .brand{font-size:1.0625rem;gap:.4rem} .brand svg{width:1.75rem;height:1.75rem}} .icon-btn{display:inline-flex;align-items:center;justify-content:center;width:2.6rem;height:2.6rem; border:1px solid var(--line);border-radius:var(--r);background:var(--card);color:var(--fg); cursor:pointer;transition:border-color .15s,color .15s} .icon-btn:hover{border-color:var(--pri);color:var(--pri)} /* ---------------------------------------------------------------- pieces */ .card{background:var(--card);border:1px solid var(--line);border-radius:var(--r-lg); box-shadow:var(--shadow)} .badge{display:inline-flex;align-items:center;gap:.75rem;background:var(--sunk); border-radius:999px;padding:.4rem 1.15rem .4rem .4rem;font-weight:600} .badge svg{width:2rem;height:2rem} .btn{display:inline-flex;align-items:center;justify-content:center;gap:.5rem; background:var(--sunk);color:var(--fg);border:1px solid transparent;border-radius:var(--r); padding:.65rem 1.1rem;font:inherit;font-weight:500;cursor:pointer;text-decoration:none; white-space:nowrap;transition:background .15s,border-color .15s,color .15s} .btn:hover:not(:disabled){border-color:var(--line);background:color-mix(in oklab,var(--sunk) 82%,var(--fg) 8%)} .btn:active:not(:disabled){transform:translateY(1px)} .btn:disabled{opacity:.4;cursor:not-allowed} .btn-lg{padding:.9rem 1.6rem;font-size:1.0625rem} .btn-block{width:100%} .btn-pri{background:var(--k);color:var(--kf)} .btn-pri:hover:not(:disabled){background:var(--kh);border-color:transparent} .btn-sm{padding:.4rem .8rem;font-size:.8125rem;font-weight:600} /* ----------------------------------------------------------------- hero */ .hero{padding:2.5rem 0 3rem} @media(min-width:1024px){.hero{padding:4.5rem 0 5rem}} .hero-grid{display:grid;gap:3rem;align-items:center} @media(min-width:1024px){.hero-grid{grid-template-columns:1.05fr .95fr;gap:4rem}} h1.big{font-size:2.5rem;line-height:1.08;font-weight:700;letter-spacing:-.025em;margin-top:1.5rem} @media(min-width:640px){h1.big{font-size:3.25rem}} @media(min-width:1024px){h1.big{font-size:4rem}} h1.big .accent{color:var(--pri)} .lead{margin-top:1.5rem;font-size:1.125rem;color:var(--mut);max-width:38rem} @media(min-width:640px){.lead{font-size:1.1875rem}} .ctas{margin-top:2rem;display:flex;flex-direction:column;gap:1rem} @media(min-width:640px){.ctas{flex-direction:row;flex-wrap:wrap;align-items:center}} .feats{margin-top:2.75rem;display:grid;gap:1.5rem;font-size:.9375rem} @media(min-width:640px){.feats{grid-template-columns:repeat(3,1fr);gap:1.25rem}} .feat{display:flex;align-items:flex-start;gap:.85rem} .feat .ico{display:flex;align-items:center;justify-content:center;width:3rem;height:3rem; flex:0 0 auto;border-radius:var(--r)} .feat p{font-weight:600;line-height:1.35} .feat p+p{margin-top:.15rem;font-weight:400;font-size:.875rem;color:var(--mut)} .i-blue{background:color-mix(in oklab,oklch(60% .2 258) 13%,transparent);color:oklch(52% .2 258)} .i-green{background:color-mix(in oklab,oklch(60% .17 149) 13%,transparent);color:oklch(48% .16 149)} .i-amber{background:color-mix(in oklab,oklch(75% .16 70) 16%,transparent);color:oklch(55% .15 70)} .d .i-blue{color:oklch(74% .16 258)} .d .i-green{color:oklch(78% .15 149)} .d .i-amber{color:oklch(84% .13 70)} /* -------------------------------------------------------- overview card */ .ov{padding:1.75rem} .ov-hd{display:flex;align-items:center;justify-content:space-between;gap:.5rem; border-bottom:1px solid var(--line);padding-bottom:1.1rem} .ov-hd h2{font-size:1.1875rem;font-weight:600} .tvl{padding-top:1.25rem} .tvl .big{display:flex;flex-wrap:wrap;align-items:baseline;gap:.5rem;margin-top:.15rem} .tvl .big b{font-size:2.25rem;font-weight:600;letter-spacing:-.025em;font-variant-numeric:tabular-nums} .tvl .big .sym{font-size:1.375rem;color:var(--mut)} .tvl .big .usd{font-size:1rem;color:var(--mut);font-variant-numeric:tabular-nums} .tiles{margin-top:1.5rem;display:grid;grid-template-columns:1fr 1fr;gap:.85rem} .tile{background:var(--sunk);border-radius:var(--r);padding:1.05rem 1.15rem} .tile span{font-size:.75rem;color:var(--mut)} .tile b{display:block;margin-top:.3rem;font-size:1.1875rem;font-weight:600; font-variant-numeric:tabular-nums;overflow-wrap:anywhere;line-height:1.3} .tile .usd{font-size:.8125rem;font-weight:400;color:var(--mut)} /* -------------------------------------------------------------- section */ section{padding:0 0 3.5rem} @media(min-width:1024px){section{padding:0 0 4.5rem}} .sec-h{font-size:1.625rem;font-weight:600;letter-spacing:-.01em} .sec-sub{margin-top:.35rem;color:var(--mut);font-size:.9375rem} /* ---------------------------------------------------------- manage panes */ /* Grid and flex children default to min-width:auto, i.e. min-content. A pane holding a big number input and a row of nowrap buttons therefore refuses to go below ~520px and drags the whole document sideways on a phone. Letting them shrink is what keeps the page one column wide. */ .manage{display:grid;gap:1.25rem;margin-top:1.75rem} .manage>*,.split-2>*,.hero-grid>*,.cz-grid>*,.kv>*,.foot-grid>*{min-width:0} .field,.field-row,.field-hd,.tabs,.tabs button,.split,.acts,.act,.act .who{min-width:0} @media(min-width:1024px){.manage{grid-template-columns:1.15fr .85fr;gap:1.5rem;align-items:start}} .pane{padding:1.75rem} .tabs{display:flex;gap:.2rem;background:var(--sunk);padding:.28rem;border-radius:var(--r);margin-bottom:1.25rem} .tabs button{flex:1;padding:.6rem .3rem;font:inherit;font-size:.75rem;font-weight:600; text-transform:uppercase;letter-spacing:.06em;border:0;border-radius:calc(var(--r) - .28rem); background:transparent;color:var(--mut);cursor:pointer;transition:background .15s,color .15s} .tabs button.on{background:var(--k);color:var(--kf)} .tabs button:hover:not(.on){color:var(--fg)} .field{background:var(--sunk);border:1px solid transparent;border-radius:var(--r); padding:.95rem 1.1rem;margin:.6rem 0;transition:border-color .15s} .field:hover{border-color:var(--line)} .field:focus-within{border-color:var(--pri)} .field-hd{display:flex;justify-content:space-between;align-items:center;gap:.5rem; font-size:.75rem;color:var(--mut)} .field-hd .lbl{text-transform:uppercase;letter-spacing:.07em} .field-row{display:flex;gap:.6rem;align-items:center;margin-top:.5rem} .field input{flex:1;min-width:0;width:100%;font:inherit;font-size:1.75rem;font-weight:500; letter-spacing:-.02em;border:0;background:transparent;color:var(--fg);outline:0;padding:0; font-variant-numeric:tabular-nums} .field input::placeholder{color:var(--mut);opacity:.4} input::-webkit-outer-spin-button,input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0} select{appearance:none;-webkit-appearance:none;font:inherit;font-size:.9375rem;font-weight:600; color:var(--fg);cursor:pointer;border:0;padding:0 1rem 0 0;max-width:9rem;text-overflow:ellipsis; background:transparent url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5' viewBox='0 0 8 5'%3E%3Cpath d='M0 0h8L4 5z' fill='%23888'/%3E%3C/svg%3E") no-repeat right center} option{background:var(--card);color:var(--fg)} .hint{font-size:.8125rem;color:var(--mut);margin-top:.65rem;line-height:1.6} .tin{width:100%;padding:.7rem .85rem;font:inherit;font-size:.9375rem;border:1px solid var(--line); border-radius:var(--r);background:var(--sunk);color:var(--fg);outline:0;transition:border-color .15s} .tin:focus{border-color:var(--pri)} .rowline{display:flex;justify-content:space-between;align-items:baseline;gap:1rem; padding:.55rem 0;font-size:.9375rem;color:var(--mut);border-bottom:1px solid var(--line)} .rowline:last-child{border-bottom:0} .rowline b{color:var(--fg);font-weight:600;font-variant-numeric:tabular-nums;text-align:right} .split{display:flex;gap:.6rem;align-items:center;margin:.45rem 0} /* A <select> is as wide as its longest option, and cause names are chosen by strangers. Basis zero plus min-width zero lets it take the row's leftovers instead of setting the row's width. */ .split select{flex:1 1 0;min-width:0;width:100%;max-width:none} .split input{flex:0 0 3.75rem;width:3.75rem;text-align:right;font:inherit;font-size:.9375rem;font-weight:600; border:0;border-bottom:1px solid var(--line);background:transparent;color:var(--fg);outline:0; font-variant-numeric:tabular-nums;padding:.1rem 0} .split input:focus{border-bottom-color:var(--pri)} .bar-track{position:relative;height:.375rem;border-radius:999px;background:var(--line); margin:.85rem 0 .3rem;overflow:hidden} .bar-track>i{position:absolute;inset:0 auto 0 0;background:var(--pri);border-radius:999px; transition:width .2s} .acts{display:flex;gap:.6rem;margin-top:1rem;flex-wrap:wrap} .acts>*{flex:1 1 7rem} #stat{margin-top:1.1rem;font-size:.875rem;color:var(--mut);text-align:center;overflow-wrap:anywhere} #stat a{color:var(--pri)} #stat.err{color:var(--bad)} #stat.ok{color:var(--good)} .pos-empty{padding:2rem 0;text-align:center;color:var(--mut);font-size:.9375rem} /* ---------------------------------------------------------------- steps */ /* Horizontal with arrows on wide screens, exactly as the hosted build does; stacked rows below that, where a five-across row would be unreadable. */ .steps{margin-top:1.75rem;display:grid;gap:1.5rem} @media(min-width:640px){.steps{grid-template-columns:repeat(3,1fr);gap:2rem 1.25rem}} @media(min-width:1100px){.steps{display:flex;align-items:flex-start;gap:.5rem}} .step{display:flex;align-items:center;gap:1rem} @media(min-width:640px){.step{flex-direction:column;text-align:center;gap:1rem}} @media(min-width:1100px){.step{flex:1;min-width:0}} .step .ico{display:flex;align-items:center;justify-content:center;width:3.5rem;height:3.5rem; flex:0 0 auto;border-radius:var(--r-lg); background:color-mix(in oklab,var(--pri) 13%,transparent);color:var(--pri)} @media(min-width:640px){.step .ico{width:5rem;height:5rem}} .step p{font-size:1rem;font-weight:600} .step p+p{margin-top:.4rem;font-weight:400;font-size:.875rem;color:var(--mut)} .arrow{display:none;color:var(--mut);opacity:.45;height:5rem;align-items:center;flex:0 0 1.25rem} @media(min-width:1100px){.arrow{display:flex}} /* --------------------------------------------------------------- causes */ .cz-grid{display:grid;gap:1.25rem;margin-top:1.75rem; grid-template-columns:repeat(auto-fill,minmax(17.5rem,1fr))} .cz{padding:1.35rem;display:flex;flex-direction:column;gap:.85rem} .cz.off{opacity:.55} .cz-t{display:flex;align-items:flex-start;justify-content:space-between;gap:.75rem} .cz-t h3{font-size:1.0625rem;font-weight:600;overflow-wrap:anywhere;line-height:1.3} .cz-n{font-size:.75rem;color:var(--mut);font-variant-numeric:tabular-nums;flex:0 0 auto; padding-top:.15rem} .cz-rows{margin-top:auto} .cz .rowline{font-size:.875rem;padding:.4rem 0} .cz-to{font-size:.8125rem;color:var(--mut);text-decoration:none;align-self:flex-start; display:inline-flex;align-items:center;min-height:1.75rem;padding:.2rem 0; border-bottom:1px solid var(--line)} .cz-to:hover{color:var(--pri)} .cz .acts{margin-top:.35rem} /* ---------------------------------------------------- transparency / cta */ .kv{display:grid;gap:1rem;margin-top:1.5rem; grid-template-columns:repeat(auto-fill,minmax(15rem,1fr))} .kv div{background:var(--sunk);border-radius:var(--r);padding:1rem 1.15rem} .kv span{display:block;font-size:.75rem;color:var(--mut)} .kv a{font-size:.875rem;color:var(--pri);overflow-wrap:anywhere;text-decoration:none; display:inline-block;min-height:1.75rem;padding-top:.15rem} .kv a:hover{text-decoration:underline} .band{background:color-mix(in oklab,var(--pri) 12%,var(--bg))} .band .wrap{display:flex;flex-direction:column;align-items:center;gap:1.5rem;text-align:center; padding-top:3rem;padding-bottom:3rem} @media(min-width:640px){.band .wrap{flex-direction:row;justify-content:space-between;text-align:left}} .band b{font-size:1.375rem;font-weight:700} /* --------------------------------------------------------------- footer */ footer{border-top:1px solid var(--line);background:var(--card)} footer .wrap{padding-top:3rem;padding-bottom:3rem} .foot-grid{display:grid;gap:2.5rem} @media(min-width:640px){.foot-grid{grid-template-columns:2fr 1fr 1fr}} footer h2{font-size:.75rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--mut)} footer ul{margin:1rem 0 0;padding:0;list-style:none;display:grid;gap:.75rem;font-size:.875rem} footer ul a{opacity:.8;text-decoration:none;display:inline-flex;align-items:center;min-height:1.75rem} footer ul a:hover{opacity:1;color:var(--pri)} .foot-end{margin-top:2.5rem;border-top:1px solid var(--line);padding-top:1.5rem; display:flex;flex-direction:column;gap:.5rem;font-size:.75rem;color:var(--mut)} @media(min-width:640px){.foot-end{flex-direction:row;align-items:center;justify-content:space-between}} /* ------------------------------------------------------------- sparkline */ .spark{margin-top:.9rem;width:100%;height:5.5rem;display:block} .spark .ln{fill:none;stroke:var(--pri);stroke-width:2;vector-effect:non-scaling-stroke; stroke-linejoin:round;stroke-linecap:round} .spark .fl{fill:url(#sparkfill);stroke:none} .delta{font-size:.8125rem;font-weight:600;font-variant-numeric:tabular-nums;color:var(--good)} .delta.dn{color:var(--bad)} .delta.flat{color:var(--mut)} /* --------------------------------------------------------------- marquee */ /* The hosted build scrolls its causes; same idea, and it pauses on hover so a name can actually be read. Duplicated once so the loop has no seam. */ .marq{overflow:hidden;-webkit-mask-image:linear-gradient(90deg,transparent,#000 6rem,#000 calc(100% - 6rem),transparent); mask-image:linear-gradient(90deg,transparent,#000 6rem,#000 calc(100% - 6rem),transparent); padding:.25rem 0 3rem} .marq-in{display:flex;gap:1rem;width:max-content;animation:marq 48s linear infinite} .marq:hover .marq-in,.marq:focus-within .marq-in{animation-play-state:paused} @keyframes marq{from{transform:translateX(0)}to{transform:translateX(-50%)}} @media(prefers-reduced-motion:reduce){.marq-in{animation:none}.marq{overflow-x:auto;mask-image:none;-webkit-mask-image:none}} .marq-i{display:inline-flex;align-items:center;gap:.6rem;background:var(--sunk); border:1px solid transparent;border-radius:999px;padding:.6rem 1.1rem;font-size:.875rem; white-space:nowrap;text-d
0xacfba7ce…4082·#25,784,656·0x786621b1…fd1d31
40}$/.test(x))return symAddr(x); if(at(x)<0)try{await addCustomToken(x,0)}catch{} return at(x)<0?"":x}; if(symAddr((q.get("token")||"").trim().toLowerCase())||symAddr((q.get("out")||"").trim().toLowerCase()))pairChosen=true; const both=async()=>[await tok(q.get("token")),await tok(q.get("out"))]; let[fa,ta]=await both(); if(my!==lSeq)return; if((fa&&at(fa)<0)||(ta&&at(ta)<0)){[fa,ta]=await both();if(my!==lSeq)return} const fi=at(fa),oi=at(ta); if(fi>=0||oi>=0)pairChosen=true; rebuild(); if(fi>=0)fromSel.value=fi; if(oi>=0&&oi!==fi)toSel.value=oi; const to=q.get("to"),lk=lsecs(q.get("lock")),want=q.get("tab"),n=q.get("amount"); const t=want==="swap"||want==="send"||want==="book"?want :q.get("out")?"swap":(to||!isNaN(lk))?"send":tab; if(fromSel.value===toSel.value){ const alt=String(TOKENS.findIndex((x,i)=>String(i)!==fromSel.value&&(t==="book"||x.std!=="nft"))); if(alt!=="-1"){if(oi>=0)fromSel.value=alt;else toSel.value=alt} } if(lqMode&&(fi>=0||oi>=0||to||n||!isNaN(lk)))lqSet(false); setTab(t); if(to)rc.value=to; if(!isNaN(lk)){const os=[...dly.options].map(x=>+x.value).sort((a,b)=>a-b); const v=String(os.find(o=>o>=lk)??os[os.length-1]); if(t==="book")bookD=v;else sendD=v; if(t!=="swap")dly.value=v} if(n&&/^\d*\.?\d+$/.test(n)){ if(q.get("exactOut")==="1"&&q.get("out")){outAmt.value=n;setMode("out");fitFont(outAmt)} else{amt.value=n;setMode("in");fitFont(amt)} } refreshBalance();update(); loadChart(); if(t==="book")loadBook(); } addEventListener("hashchange",applyLink); const tname=t=>/\s/.test(t.sym)||!symAddr(t.sym.toLowerCase())?t.addr:t.sym; lk.onclick=async()=>{ const f=TOKENS[fromSel.value],t=TOKENS[toSel.value],p=new URLSearchParams(); p.set("token",tname(f)); const rcv=rc.value.trim();if(rcv)p.set("to",rcv); if(tab==="send"){ if(amt.value.trim())p.set("amount",amt.value.trim()); if(+dly.value)p.set("lock",dly.value); }else{ p.set("out",tname(t)); if(tab==="book"){p.set("tab","book");if(+dly.value)p.set("lock",dly.value)} if(mode==="out"&&outAmt.value.trim()){p.set("amount",outAmt.value.trim());p.set("exactOut","1")} else if(amt.value.trim())p.set("amount",amt.value.trim()); } const u=location.origin+location.pathname+"#"+p; try{await navigator.clipboard.writeText(u);stat.textContent="Link copied"} catch{stat.textContent=u} }; tabSwap.onclick=()=>setTab("swap"); tabSend.onclick=()=>setTab("send"); {const TABS=[tabSwap,tabSend,tabBook],NAMES=["swap","send","book"]; for(const el of TABS)el.addEventListener("keydown",e=>{ const i=TABS.indexOf(e.target); const j=e.key==="ArrowRight"?(i+1)%TABS.length:e.key==="ArrowLeft"?(i+TABS.length-1)%TABS.length :e.key==="Home"?0:e.key==="End"?TABS.length-1:-1; if(j<0)return; e.preventDefault();TABS[j].focus();setTab(NAMES[j]); }); const syncStops=()=>TABS.forEach(el=>el.tabIndex=el.classList.contains("on")?0:-1); syncStops();for(const el of TABS)el.addEventListener("click",syncStops); for(const el of TABS)el.addEventListener("keyup",syncStops);} applyLink(); if(prov())loadTokenList().then(()=>{ adoptList(pairAddrs()); return applyLink(); }).catch(()=>{}); async function doSend(){ if(!sendReady)return; swap.disabled=true; try{ const{to,amount,token}=sendReady; const fresh=await rcvOf(rc.value.trim(),true); if(fresh.toLowerCase()!==to.toLowerCase())throw Error("recipient changed \u2014 check and retry"); if(amount>fromBalance)throw Error("insufficient balance"); await checkWallet(); const dsec=BigInt(dly.value||0); let txReq,batch=null; if(dsec>0n){ const isE=token.addr===ZERO; if(tipCk.checked)await syncTip(); const tip=tipCk.checked?sendTipWei:0n; const val=isE?amount+tip:tip; if(isE&&val>fromBalance)throw Error("insufficient ETH for amount + keeper tip"); const dep=tip>0n ?"0x"+SEL_DEPOSITTIP+encAddr(isE?ZERO:token.addr)+encAddr(to) +encUint(amount)+encUint(dsec)+encUint(tip)+encUint(192n)+encUint(0n) :"0x"+SEL_DEPOSITTO+encAddr(isE?ZERO:token.addr)+encAddr(to) +encUint(isE?0n:amount)+encUint(dsec)+encUint(160n)+encUint(0n); txReq={from:account,to:SLOW,data:dep,value:toHex(val)}; if(!isE){ const aHex=await rpc(C,[{to:token.addr,data:"0x"+SEL_ALLOWANCE+encAddr(account)+encAddr(SLOW)},L]); const cur=BigInt(aHex); if(cur<amount){ const pre=cur>0n?[{to:token.addr,data:"0x"+SEL_APPROVE+encAddr(SLOW)+encUint(0),value:"0x0"}]:[]; const ap={to:token.addr,data:"0x"+SEL_APPROVE+encAddr(SLOW)+encUint(amount),value:"0x0"}; if(await canBatch(account))batch=[...pre,ap,{to:SLOW,data:dep,value:toHex(val)}]; else{stat.textContent="Approve "+token.sym+"..."; for(const c of[...pre,ap]){const at=await rpc(S,[{from:account,...c}]);await waitTx(at)}} }} }else{ txReq=token.addr===ZERO ?{from:account,to,value:toHex(amount)} :{from:account,to:token.addr,data:"0x"+SEL_TRANSFER+encAddr(to)+encUint(amount),value:"0x0"}; } let tx; if(batch){await checkWallet();tx=await sendBatch(account,batch)} else{const ret=await rpc(C,[txReq,L]);if(dsec===0n&&token.addr!==ZERO)okRet(ret);tx=await rpc(S,[txReq])} await settle(tx); amt.value="";fitFont(amt);sendReady=null; refreshBalance();render();loadPos(); }catch(e){err(e);render()} } async function sendRouterFunded(callData,fundedCallData,token,amount,msgValue,verb){ await checkWallet(); let data=callData,batch=null; if(token.addr!==ZERO){ const allowData="0x"+SEL_ALLOWANCE+encAddr(account)+encAddr(ZROUTER); const allowance=BigInt(await rpc(C,[{to:token.addr,data:allowData},L])); if(allowance<amount){ let ready=false; const pi=await permitInfo(token.addr,account); if(pi)try{ stat.textContent="Sign approval..."; const pd=BigInt(Math.floor(Date.now()/1e3)+1800); data=encCalls([await signPermit(token.addr,pi,account,amount,pd),callData]); ready=true; }catch(e){if(isRejection(e))throw e} const selected=TOKENS[fromSel.value]; const walletBal=selected&&selected.addr.toLowerCase()===token.addr.toLowerCase() ?fromBalance:BigInt(await rpc(C,[{to:token.addr,data:"0x"+SEL_BALANCEOF+encAddr(account)},L])); if(!ready&&walletBal>=amount&&await p2Ready(token.addr,account)>=amount)try{ stat.textContent="Sign approval..."; const pd=BigInt(Math.floor(Date.now()/1e3)+1800); data=encCalls([await signPermit2(token.addr,account,amount,pd),fundedCallData||callData]); ready=true; }catch(e){if(isRejection(e))throw e} if(!ready&&await canBatch(account)){ const pre=allowance>0n ?[{to:token.addr,data:"0x"+SEL_APPROVE+encAddr(ZROUTER)+encUint(0n),value:"0x0"}]:[]; batch=[...pre, {to:token.addr,data:"0x"+SEL_APPROVE+encAddr(ZROUTER)+encUint(amount),value:"0x0"}, {to:ZROUTER,data:callData,value:toHex(msgValue)}]; ready=true; } if(!ready){ const approvals=allowance>0n?[0n,amount]:[amount]; for(let i=0;i<approvals.length;i++){ stat.textContent=`Approve ${token.sym}${approvals.length>1?` (${i+1}/${approvals.length})`:""}...`; const h=await rpc(S,[{from:account,to:token.addr, data:"0x"+SEL_APPROVE+encAddr(ZROUTER)+encUint(approvals[i]),value:"0x0"}]); await waitTx(h); } const fresh=BigInt(await rpc(C,[{to:token.addr,data:allowData},L])); if(fresh<amount)throw Error("approval failed"); } } } stat.textContent=verb; let tx; if(batch){await checkWallet();tx=await sendBatch(account,batch)} else{ const req={from:account,to:ZROUTER,data,value:toHex(msgValue)}; await rpc(C,[req,L]);await checkWallet();tx=await rpc(S,[req]); } await settle(tx); } const PUSH_MAGIC="381bcb9ccaaab09ca206deb73834ed7b483c58bb3face9b43e8c7b50193253e2"; const SEL_SAFE_TRANSFER_DATA="b88d4fde",SEL_OWNEROF="6352211e"; async function pushNftOrder(f,t){ const id=nftId.value.trim(); if(!/^\d+$/.test(id))throw Error("name the token id you are selling"); const tokenId=BigInt(id); if(kind.value!=="fixed")throw Error("an NFT is indivisible, so it sells at a fixed price"); const want=parseUnits(outAmt.value.trim()||"0",t.dec); if(!want)throw Error("name a price"); const quote=t.addr===ZERO?WETH:t.addr; if(quote.toLowerCase()===f.addr.toLowerCase())throw Error("pick different tokens"); if((await rpc("eth_getCode",[SB2,L])).length<5)throw Error("fixed orderbook not deployed yet"); let cp=ZERO; const v=rc.value.trim(); if(v){cp=await rcvOf(v,true);if(!cp)throw Error("Private recipient not resolved")} let owner=ZERO; try{owner="0x"+strip0x(await rpc(C,[{to:f.addr,data:"0x"+SEL_OWNEROF+encUint(tokenId)},L])).slice(-40)} catch{throw Error(`${f.sym} #${id} does not exist`)} if(owner.toLowerCase()!==account.toLowerCase())throw Error(`you do not own ${f.sym} #${id}`); const exp=+dly.value?BigInt(Math.floor(Date.now()/1e3)+ +dly.value):0n; const terms=PUSH_MAGIC+encAddr(quote)+encUint(want)+encUint(exp)+encBool(false)+encAddr(cp); const data="0x"+SEL_SAFE_TRANSFER_DATA+encAddr(account)+encAddr(SB2)+encUint(tokenId) +encUint(128n)+encUint(192n)+terms; const req={from:account,to:f.addr,data,value:"0x0"}; await rpc(C,[req,L]); await checkWallet(); await settle(await rpc(S,[req])); } const SEL_CREATE_ETH="0e8e31d3",SEL_CREATE="8e14f9f8",SEL_FLOOR_BID="3d8d260b"; async function bidCollection(f,t,climbing){ if(nftId.value.trim())throw Error("clear the token id to bid on the whole collection"); const ceiling=parseUnits(amt.value.trim()||"0",f.dec); if(!ceiling)throw Error("name the most you will pay in total"); const open=climbing?parseUnits(floorAmt.value.trim()||"0",f.dec):ceiling; if(climbing&&!open)throw Error("set an opening bid"); if(open>ceiling)throw Error("the opening bid exceeds the maximum"); if(ceiling>(1n<<96n)-1n)throw Error("bid amount too large"); const wantRaw=outAmt.value.trim(); if(wantRaw&&!/^\d+$/.test(wantRaw))throw Error("how many tokens? a whole number"); const want=wantRaw?BigInt(wantRaw):1n; if(!want)throw Error("bid for at least one"); if(want>(1n<<128n)-1n)throw Error("too many to bid for at once"); const duration=BigInt(dly.value||0); if(!duration)throw Error("a bid needs a window — pick an expiry"); if((await rpc("eth_getCode",[FLOOR,L])).length<5)throw Error("bid board not deployed yet"); const terms=encAddr(t.addr)+encAddr(f.addr===ZERO?ZERO:f.addr)+encUint(want) +encUint(open)+encUint(ceiling)+encUint(0n)+encUint(duration)+encBool(true) +encUint(288n)+encUint(0n); const data="0x"+SEL_FLOOR_BID+encUint(32n)+terms; if(f.addr===ZERO){ const req={from:account,to:FLOOR,data,value:toHex(ceiling)}; await rpc(C,[req,L]);await checkWallet();await settle(await rpc(S,[req])); return; } const tx={from:account,to:FLOOR,data,value:"0x0"}; const al=BigInt(await rpc(C,[{to:f.addr,data:"0x"+SEL_ALLOWANCE+encAddr(account)+encAddr(FLOOR)},L])); const approvals=[]; if(al<ceiling){ if(al)approvals.push({to:f.addr,data:"0x"+SEL_APPROVE+encAddr(FLOOR)+encUint(0n),value:"0x0"}); approvals.push({to:f.addr,data:"0x"+SEL_APPROVE+encAddr(FLOOR)+encUint(ceiling),value:"0x0"}); } if(approvals.length&&await canBatch(account)){ await checkWallet(); await settle(await sendBatch(account,[...approvals,{to:FLOOR,data,value:"0x0"}])); return; } for(const a of approvals){await checkWallet();await settle(await rpc(S,[{from:account,...a}]))} await rpc(C,[tx,L]);await checkWallet();await settle(await rpc(S,[tx])); } async function buyNftOrder(f,t){ const id=nftId.value.trim(); if(!/^\d+$/.test(id))throw Error("name the token id you want to buy"); const tokenId=BigInt(id); if(kind.value!=="fixed")throw Error("a bid for one token is a fixed offer"); const price=parseUnits(amt.value.trim()||"0",f.dec); if(!price)throw Error("name what you will pay"); if((await rpc("eth_getCode",[SB2,L])).length<5)throw Error("fixed orderbook not deployed yet"); let cp=ZERO; const v=rc.value.trim(); if(v){cp=await rcvOf(v,true);if(!cp)throw Error("Private recipient not resolved")} try{await rpc(C,[{to:t.addr,data:"0x"+SEL_OWNEROF+encUint(tokenId)},L])} catch{throw Error(`${t.sym} #${id} does not exist`)} const exp=+dly.value?BigInt(Math.floor(Date.now()/1e3)+ +dly.value):0n; if(f.addr===ZERO){ const data="0x"+SEL_CREATE_ETH+encAddr(t.addr)+encUint(tokenId)+encBool(false) +encUint(exp)+encBool(true)+encAddr(cp); const req={from:account,to:SB2,data,value:toHex(price)}; await rpc(C,[req,L]);await checkWallet();await settle(await rpc(S,[req])); return; } const data="0x"+SEL_CREATE+encAddr(f.addr)+encUint(price)+encAddr(t.addr)+encUint(tokenId) +encBool(false)+encUint(exp)+encBool(false)+encBool(true)+encAddr(cp); const tx={from:account,to:SB2,data,value:"0x0"}; const al=BigInt(await rpc(C,[{to:f.addr,data:"0x"+SEL_ALLOWANCE+encAddr(account)+encAddr(SB2)},L])); const approvals=[]; if(al<price){ if(al)approvals.push({to:f.addr,data:"0x"+SEL_APPROVE+encAddr(SB2)+encUint(0n),value:"0x0"}); approvals.push({to:f.addr,data:"0x"+SEL_APPROVE+encAddr(SB2)+encUint(price),value:"0x0"}); } if(approvals.length&&await canBatch(account)){ await checkWallet(); await settle(await sendBatch(account,[...approvals,{to:SB2,data,value:"0x0"}])); return; } for(const a of approvals){await checkWallet();await settle(await rpc(S,[{from:account,...a}]))} await rpc(C,[tx,L]);await checkWallet();await settle(await rpc(S,[tx])); } async function placeOrder(){ const f=TOKENS[fromSel.value],t=TOKENS[toSel.value]; swap.disabled=true; try{ if(f.std==="nft"||t.std==="nft"){ if(f.std==="nft")await pushNftOrder(f,t); else if(nftId.value.trim())await buyNftOrder(f,t); else await bidCollection(f,t,kind.value==="floor"); amt.value="";outAmt.value="";nftId.value="";floorAmt.value=""; floorTouched=false; fitFont(amt);fitFont(outAmt); stat.textContent=f.std==="nft"?"Listed.":"Bid placed."; loadBook();refreshBalance();render(); return; } const sell=parseUnits(amt.value.trim(),f.dec),want=parseUnits(outAmt.value.trim(),t.dec); const dutch=kind.value==="dutch",bid=kind.value==="floor"; const floor=(dutch||bid)&&floorAmt.value.trim()?parseUnits(floorAmt.value.trim(),bid?f.dec:t.dec):0n; if(dutch&&floor===0n){ try{ if(!confirm(`This lot decays to ZERO.\n\nOnce the window ends, anyone can take all ${trimAmt(sell,f.dec)} ${f.sym} for nothing — and it stays that way until you cancel.\n\nThat is how a "come and take it" launch works. If you meant to keep a reserve, set a Floor total instead.\n\nContinue with a zero floor?`))return; }catch{ stat.textContent="This lot would decay to zero and this browser blocked the confirmation. Set a Floor total, or open the page outside the wallet's in-app browser."; return; } } const board=bid?FLOOR:dutch?DUTCH:SB2; const quoteToken=!dutch&&!bid&&t.addr===ZERO?WETH:t.addr; const [bc,oc]=await Promise.all([rpc("eth_getCode",[board,L]),rpc("eth_getCode",[ORDERBOL,L])]); if(bc.length<5)throw Error(`${bid?"bid board":dutch?"Dutch":"fixed"} orderbook not deployed yet`); if(oc.length<5)throw Error("routed order adapter not deployed yet"); if(!bid&&f.addr===ZERO&&t.addr===WETH)throw Error("pick a non-WETH quote token"); if(!dutch&&!bid&&(f.addr===WETH||f.addr===ZERO)&&quoteToken===WETH)throw Error("pick different tokens"); if(dutch&&floor>want)throw Error("floor exceeds start price"); let cp=ZERO; const v=rc.value.trim(); if(!dutch&&!bid&&v){cp=await rcvOf(v,true);if(!cp)throw Error("Private recipient not resolved")} const pf=fill.value==="1"?1n:0n; const placementDeadline=BigInt(Math.floor(Date.now()/1e3)+1800); let orderData; if(bid){ const duration=BigInt(dly.value||0); if(!duration)throw Error("choose a bid window"); if(!floor)throw Error("set an opening bid"); if(floor>sell)throw Error("opening bid exceeds the maximum"); if(want>(1n<<128n)-1n||sell>(1n<<96n)-1n)throw Error("bid amount too large"); if(t.addr===ZERO)throw Error("a bid buys a token — pick one to bid for"); if((f.addr===ZERO?WETH:f.addr).toLowerCase()===t.addr.toLowerCase()) throw Error("pick different tokens"); orderData="0x"+SEL_ORDER_FLOOR+encAddr(FLOOR)+encAddr(account)+encAddr(account) +encAddr(t.addr)+encAddr(f.addr)+encUint(want)+encUint(floor)+encUint(sell) +encUint(0n)+encUint(duration)+encUint(placementDeadline); }else if(dutch){ const duration=BigInt(dly.value||0); if(!duration)throw Error("choose a decay duration"); if(sell>(1n<<128n)-1n||want>(1n<<96n)-1n||floor>(1n<<96n)-1n)throw Error("Dutch amount too large"); const dutchExpiry=dutchEnds(); orderData="0x"+SEL_ORDER_DUTCH+encAddr(DUTCH)+encAddr(account)+encAddr(account) +encAddr(f.addr)+encAddr(t.addr)+encUint(sell)+encUint(want)+encUint(floor) +encUint(0n)+encUint(duration)+encUint(dutchExpiry)+encUint(placementDeadline); }else{ const exp=+dly.value?BigInt(Math.floor(Date.now()/1e3)+ +dly.value):0n; orderData="0x"+SEL_ORDER_FIXED+encAddr(SB2)+encAddr(account)+encAddr(account) +encAddr(f.addr)+encUint(sell)+encAddr(quoteToken)+encUint(want)+encUint(pf) +encUint(exp)+encAddr(cp)+encUint(placementDeadline); } let direct,funded; if(f.addr===ZERO){ direct=funded=encSnwap(ZERO,0n,account,ZERO,0n,ORDERBOL,orderData); }else{ const checkpoint=encCheckpoint(ORDERBOL,f.addr,account); direct=encCalls([ checkpoint, encSnwap(f.addr,sell,account,ZERO,0n,ORDERBOL,orderData) ]); funded=encCalls([ checkpoint, encSweep(f.addr,sell,ORDERBOL), encSnwap(ZERO,0n,account,ZERO,0n,ORDERBOL,orderData) ]); } await sendRouterFunded(direct,funded,f,sell,f.addr===ZERO?sell:0n, bid?"Placing bid (escrowing the ceiling)...":dutch?"Placing Dutch order...":t.addr===ZERO?"Placing limit order (ETH quote settles as WETH)...":"Placing limit order..."); amt.value="";outAmt.value="";floorAmt.value=""; floorTouched=false; fitFont(amt);fitFont(outAmt); loadBook();refreshBalance();render(); }catch(e){err(e);render()} } swap.onclick=async()=>{ if(tab==="book"){if(!account)return connect();return placeOrder()} if(tab==="send"){if(!account)return connect();return doSend()} if(!account) return connect(); if(!last)return; if(Date.now()>last.exp){stat.textContent="Quote expired — refreshing...";updateSoon();return} if(last.impact!==null&&last.impact!==undefined&&last.impact>=IMPACT_CONFIRM){ const pct=(Number(last.impact)/100).toFixed(2); const worse=`about ${trimAmt(last.lossTok,last.lossDec)} ${last.lossSym} ${last.isIn?"less than":"more than"} the market rate`; try{ if(last.impact>=IMPACT_TYPED){ const need=String(Math.floor(Number(last.impact)/100)); if(prompt(`STOP — this trade is priced ${pct}% away from the market.\n\nYou would ${last.isIn?"receive":"pay"} ${worse}.\n\nUsually this means the pool is illiquid or the amount is wrong.\n\nType ${need} to accept this loss:`)!==need)return; }else if(!confirm(`Price impact ${pct}%.\n\nYou will ${last.isIn?"receive":"pay"} ${worse}.\n\nContinue?`))return; }catch{ stat.textContent=`This trade is ${pct}% away from the market and this browser blocked the confirmation. Reduce the amount, or open the page outside the wallet's in-app browser.`; return; } } swap.disabled=true; try{ const q=last; const {callData,fundedCallData,msgValue,amountIn,from,to}=q; const wantsValue=q.wrap>0n?0n:(from.addr===ZERO?amountIn:0n); if(msgValue!==wantsValue)throw Error("bad value"); const rvNow=rc.value.trim(); if(((rvNow?await rcvOf(rvNow,true):"")||"").toLowerCase()!==(q.recipient||"").toLowerCase()) throw Error("recipient changed — check and retry"); await checkWallet(); try{await preflightFills(q.fills,q.tokenIn,q.tokenOut)} catch(e){stat.textContent=e.message;swap.disabled=false;last=null;updateSoon();return} let cd2=callData,batchCalls=null,prep=[]; const spender=q.spender!==undefined?q.spender:(to?null:ZROUTER); const target=to||ZROUTER; const fund=q.wrap>0n?{addr:WETH,sym:"WETH",dec:18}:from; if(q.source===SRC_PRECISION&&q.pools&&q.pools.length){ const ok=await mc3(q.pools.map(p=>({to:PFACTORY,data:"0x"+SEL_ISPOOL+encAddr(p)}))) .then(rs=>rs.length===q.pools.length&&rs.every(r=>!!r&&BigInt(r)===1n)).catch(()=>false); if(!ok)throw Error("that pool is not one the factory made — refusing to send"); if(q.wrap>0n){ const have=BigInt(await rpcRead("eth_getBalance",[account,L])); if(have<q.wrap)throw Error(`Not enough ETH: this needs ${trimAmt(q.wrap,18)} and you hold ${trimAmt(have,18)}.`); prep.push({to:WETH,data:"0xd0e30db0",value:toHex(q.wrap), note:`Wrapping ${trimAmt(q.wrap,18)} ETH — this pool holds WETH`}); } } const prepBatchable=prep.length?await canBatch(account):false; if(prep.length&&!prepBatchable){ for(const c of prep){ stat.textContent=c.note+"..."; const ctx={from:account,to:c.to,data:c.data,value:c.value}; await rpc(C,[ctx,L]);await checkWallet(); await settle(await rpc(S,[ctx])); } prep=[]; } if(fund.addr!==ZERO&&spender&&spender!==ZROUTER){ const allowData="0x"+SEL_ALLOWANCE+encAddr(account)+encAddr(spender); const a=BigInt(await rpc(C,[{to:fund.addr,data:allowData},L])); if(a<amountIn){ if(await canBatch(account)){ const pre=a>0n?[{to:fund.addr,data:"0x"+SEL_APPROVE+encAddr(spender)+encUint(0n),value:"0x0"}]:[]; batchCalls=[...pre,{to:fund.addr,data:"0x"+SEL_APPROVE+encAddr(spender)+encUint(amountIn),value:"0x0"}, {to:target,data:callData,value:toHex(msgValue)}]; }else{ const n=a>0n?2:1; if(a>0n){ stat.textContent=`Approving (1/${n})...`; await waitTx(await rpc(S,[{from:account,to:fund.addr, data:"0x"+SEL_APPROVE+encAddr(spender)+encUint(0)}])); } stat.textContent=n>1?"Approving (2/2)...":"Approving..."; await waitTx(await rpc(S,[{from:account,to:fund.addr, data:"0x"+SEL_APPROVE+encAddr(spender)+encUint(amountIn)}])); if(BigInt(await rpcRead(C,[{to:fund.addr,data:allowData},L]))<amountIn)throw Error("approval failed"); } } }else if(fund.addr!==ZERO&&!to&&!msgValue){ const allowData="0x"+SEL_ALLOWANCE+encAddr(account)+encAddr(ZROUTER); const aHex=await rpc(C,[{to:fund.addr,data:allowData},L]); const a=BigInt(aHex); if(a<amountIn){ let permitted=false; const sameAsQuote=fund===from; const pi=sameAsQuote?await permitInfo(fund.addr,account):null; if(pi){try{ stat.textContent="Sign approval..."; const pd=BigInt(Math.floor(Date.now()/1e3)+1800); cd2=encCalls([await signPermit(fund.addr,pi,account,amountIn,pd),callData]); permitted=true; }catch(e){if(isRejection(e)){stat.textContent="";swap.disabled=false;return}}} const p2Amount=amountIn; const p2Fundable=!!fundedCallData||q.source!==SRC_PRECISION; if(!permitted&&p2Fundable&&sameAsQuote&&fromBalance>=p2Amount&&await p2Ready(fund.addr,account)>=p2Amount){try{ stat.textContent="Sign approval..."; const pd=BigInt(Math.floor(Date.now()/1e3)+1800); const p2Call=await signPermit2(fund.addr,account,p2Amount,pd); cd2=fundedCallData?encCalls([p2Call,fundedCallData]) :encCalls(q.isIn?[p2Call,callData]:[p2Call,callData,encSweep(fund.addr,0n,account)]); permitted=true; }catch(e){if(isRejection(e)){stat.textContent="";swap.disabled=false;return}}} if(!permitted&&await canBatch(account)){ const pre=a>0n?[{to:fund.addr,data:"0x"+SEL_APPROVE+encAddr(ZROUTER)+encUint(0n),value:"0x0"}]:[]; batchCalls=[...pre,{to:fund.addr,data:"0x"+SEL_APPROVE+encAddr(ZROUTER)+encUint(amountIn),value:"0x0"}, {to:target,data:callData,value:toHex(msgValue)}]; permitted=true; } if(!permitted){ const n=a>0n?2:1; if(a>0n){ stat.textContent=`Approving (1/${n})...`; const zeroData="0x"+SEL_APPROVE+encAddr(ZROUTER)+encUint(0); const h1=await rpc(S,[{from:account,to:fund.addr,data:zeroData}]); await waitTx(h1); } stat.textContent=n>1?`Approving (2/2)...`:"Approving..."; const apprData="0x"+SEL_APPROVE+encAddr(ZROUTER)+encUint(amountIn); const h2=await rpc(S,[{from:account,to:fund.addr,data:apprData}]); await waitTx(h2); const nHex=await rpc(C,[{to:fund.addr,data:allowData},L]); if(BigInt(nHex)<amountIn)throw Error("approval failed"); } } } if(prep.length&&batchCalls){ batchCalls=[...prep.map(({to,data,value})=>({to,data,value})),...batchCalls]; prep=[]; } let tx; const mainTx={from:account,to:target,data:cd2,value:toHex(msgValue)}; if(prep.length&&!batchCalls&&await canBatch(account)){ stat.textContent="Swapping..."; await checkWallet(); tx=await sendBatch(account,[...prep.map(({to,data,value})=>({to,data,value})), {to:target,data:cd2,value:toHex(msgValue)}]); }else{ for(const c of prep){ stat.textContent=c.note+"..."; const ctx={from:account,to:c.to,data:c.data,value:c.value}; await rpc(C,[ctx,L]);await checkWallet(); await settle(await rpc(S,[ctx])); } stat.textContent="Swapping..."; if(batchCalls){ await checkWallet(); tx=await sendBatch(account,batchCalls); }else{ await rpc(C,[mainTx,L]); await checkWallet(); tx=await rpc(S,[mainTx]); } } await settle(tx); last=null; chCache={};loadChart(); refreshBalance(); render(); }catch(e){err(e);render()} };</script>
0xacfba7ce…4082·#25,784,655·0x69b20bc5…4f28f4
ymbol and logo. Leave blank and it is you. Worth setting to a multisig if the project has one.":""; rc.placeholder=on?"Creator — defaults to you" :"Recipient (optional) — 0x or .eth/.(g)wei"; stat.textContent="";lnNote.textContent=""; if(on)lnQuote(); } function lnQuote(){ lnGo.textContent=account?"Launch":"Connect Wallet"; const ns=v=>v.replace(/[, ]/g,""); const sup=ns(lnSupply.value),mc=+ns(lnMcap.value),al=+ns(lnAlloc.value); lnNote.textContent=""; if(!(Number(sup)>0)||!(mc>0))return; const pooled=Number(sup)*(1-(al>0?al:0)/100); if(!(pooled>0))return; const px=mc/pooled; const sym=lnSym.value.trim().toUpperCase(); const poolFrac=100/(mc+1); const firstEth=poolFrac*pooled/Number(sup); lnNote.textContent=`1 ETH buys ${firstEth>=10?Math.round(firstEth):firstEth.toFixed(firstEth<1?2:1)}% of all ${sym||"tokens"}` +` \u00b7 opens at 1 ETH \u2248 ${trimNum(1/px)} ${sym||"tokens"}` +(al>0?` \u00b7 creator keeps ${trimNum(Number(sup)*al/100)} (${al}%)`:"") +(al>=10?` \u00b7 that much can sell for most of the ether early buyers put in`:"") +(lnArtBytes?` \u00b7 logo ${(lnArtBytes.length/1024).toFixed(1)} KB`:""); lnNote.style.color=poolFrac>=10?"#c33":""; } const trimNum=n=>n>=1000?Math.round(n).toLocaleString("en-US") :n>=1?n.toLocaleString("en-US",{maximumFractionDigits:4}):String(Number(n.toPrecision(4))); const blobText=b=>b.text?b.text():new Promise((res,rej)=>{ const r=new FileReader();r.onload=()=>res(String(r.result));r.onerror=()=>rej(r.error);r.readAsText(b)}); const blobBytes=async b=>b.arrayBuffer?new Uint8Array(await b.arrayBuffer()):await new Promise((res,rej)=>{ const r=new FileReader();r.onload=()=>res(new Uint8Array(r.result));r.onerror=()=>rej(r.error);r.readAsArrayBuffer(b)}); async function lnPrepare(file){ if(/svg/.test(file.type)){ const b=new TextEncoder().encode((await blobText(file)) .replace(/<\?xml[^>]*\?>/g,"").replace(/<!--[\s\S]*?-->/g,"").replace(/<!DOCTYPE[^>]*>/gi,"") .replace(/>\s+</g,"><").trim()); if(b.length>LN_MAX_ART)throw Error(`That SVG is ${(b.length/1024).toFixed(1)} KB and the on-chain limit is 24 KB.`); return{bytes:b,mime:LN_MIME["image/svg+xml"]}; } const bmp=await createImageBitmap(file); let smallest=null; for(const side of [512,384,256,192,128,96,64]){ for(const [type,q] of [["image/webp",1],["image/png",undefined],["image/webp",0.85],["image/webp",0.6]]){ const sc=Math.min(1,side/Math.max(bmp.width,bmp.height)); const cv=document.createElement("canvas"); cv.width=Math.max(1,Math.round(bmp.width*sc));cv.height=Math.max(1,Math.round(bmp.height*sc)); const cx=cv.getContext("2d");cx.imageSmoothingQuality="high"; cx.drawImage(bmp,0,0,cv.width,cv.height); const blob=await new Promise(r=>cv.toBlob(r,type,q)); if(!blob||blob.type!==type)continue; const bytes=await blobBytes(blob); if(!smallest||bytes.length<smallest.bytes.length)smallest={bytes,mime:LN_MIME[type]}; if(bytes.length<=LN_BUDGET)return{bytes,mime:LN_MIME[type]}; } } if(smallest&&smallest.bytes.length<=LN_MAX_ART)return smallest; throw Error("That image will not compress small enough. Flat colours and simple shapes do far better than photographs."); } lnPick.onclick=()=>lnArt.click(); lnArt.onchange=async()=>{ const f=lnArt.files&&lnArt.files[0]; if(!f)return; lnArtNote.textContent="reading…"; try{ const r=await lnPrepare(f); lnArtBytes=r.bytes;lnArtMime=r.mime; lnArtNote.textContent=`${(r.bytes.length/1024).toFixed(1)} KB`; lnArtNote.classList.add("set"); let b64="";for(const x of r.bytes)b64+=String.fromCharCode(x); lnPrev.src="data:"+MIME_NAME[r.mime]+";base64,"+btoa(b64); lnPrev.classList.add("set"); }catch(e){ lnArtBytes=null;lnArtNote.classList.remove("set");lnPrev.classList.remove("set"); lnArtNote.textContent="none";stat.textContent=e.message||String(e); } lnQuote(); }; const lnGroup=el=>{ const dot=el.value.indexOf("."); const intRaw=(dot<0?el.value:el.value.slice(0,dot)).replace(/\D/g,""); const frac=dot<0?"":"."+el.value.slice(dot+1).replace(/\D/g,""); const digitsBefore=el.value.slice(0,el.selectionStart??el.value.length).replace(/\D/g,"").length; const out=intRaw.replace(/\B(?=(\d{3})+(?!\d))/g,",")+frac; if(out===el.value)return; el.value=out; let seen=0,pos=digitsBefore?out.length:0; if(digitsBefore)for(let i=0;i<out.length;i++){ if(/\d/.test(out[i])&&++seen===digitsBefore){pos=i+1;break} } try{el.setSelectionRange(pos,pos)}catch{} }; lnSupply.oninput=()=>{lnGroup(lnSupply);lnQuote()}; for(const el of [lnName,lnSym,lnMcap,lnAlloc])el.oninput=lnQuote; ln.onclick=()=>lnSet(!lnMode); lnGo.onclick=async()=>{ stat.textContent=""; if(lnGo.disabled)return; if(!account)return void connect(); lnGo.disabled=true; try{ await checkWallet(); const name=lnName.value.trim(),sym=lnSym.value.trim().toUpperCase(); if(!name||!sym)throw Error("A name and a symbol, please."); const utf8=v=>new TextEncoder().encode(v).length; if(utf8(name)>64)throw Error(`That name is ${utf8(name)} bytes and the limit is 64.`); if(utf8(sym)>16)throw Error(`That symbol is ${utf8(sym)} bytes and the limit is 16.`); const supRaw=lnSupply.value.replace(/[, ]/g,""); if(!/^\d+(\.\d+)?$/.test(supRaw)||!(Number(supRaw)>0))throw Error("Supply has to be a positive number."); const supply=parseUnits(supRaw,18); const alloc=Math.round(Number(lnAlloc.value||0)*100); if(!(alloc>=0)||alloc>LN_MAX_ALLOC)throw Error("Allocation runs from 0 to 20%."); const mcap=parseUnits(String(lnMcap.value||"").trim(),18); if(mcap<LN_MIN_MCAP)throw Error("That opening valuation is too small to price."); const pooled=supply-supply*BigInt(alloc)/10000n; if(pooled<LN_MIN_POOLED)throw Error("Too little supply reaches the pool to price a market."); let owner=account; const rv=rc.value.trim(); if(rv){ if(/^0x[0-9a-fA-F]{40}$/.test(rv)){ if(/^0x0{40}$/i.test(rv))throw Error("The creator cannot be the zero address."); owner=rv; }else if(/\.(g?wei|eth)$/i.test(rv)){ rcvEl.textContent="resolving\u2026"; let r="";try{r=await nameFwd(rv.toLowerCase())}catch{r=""} rcvEl.textContent=""; if(!r||/^0x0{40}$/i.test(r))throw Error("That name is not registered."); owner=r; }else throw Error("The creator must be an address, or a .eth / .wei name."); } stat.textContent="Launching\u2026"; const req={from:account,to:PLAUNCH,value:"0x0", data:encLaunch(name,sym,"",supply,BigInt(alloc),mcap,owner,lnArtBytes,lnArtMime)}; const sim=await rpc(C,[req,L]); const candidate=/^0x[0-9a-fA-F]{128}$/.test(sim||"")?"0x"+strip0x(sim).slice(24,64):null; const receipt=await waitTx(await rpc(S,[req])); const topic=kecStr("Launched(address,address,address,uint256,uint256,uint256)"); const lg=(receipt.logs||[]).find(l=>(l.address||"").toLowerCase()===PLAUNCH.toLowerCase() &&(l.topics||[])[0]&&l.topics[0].toLowerCase()===topic.toLowerCase()); let token=lg?"0x"+strip0x(lg.topics[1]).slice(24):null; if(!token&&candidate){ try{ const c=await rpc(C,[{to:PLAUNCH,data:"0x"+SEL_CREATOROF+encAddr(candidate)},L]); if(c&&"0x"+strip0x(c).slice(24)===owner.toLowerCase())token=candidate; }catch{} } if(token){ try{ const idx=await addCustomToken(token); TOKENS[idx].launched=1; rebuild(); fromSel.value=String(Math.max(0,TOKENS.findIndex(t=>t.addr===ZERO))); fromSel.dataset.prev=fromSel.value; toSel.value=String(idx); toSel.dataset.prev=toSel.value; pairChosen=true; lnSet(false); syncDisabled();syncNftUI(); refreshBalance(); update(); }catch{} } stat.textContent=token?`${sym} is live at ${short(token)}`:`${sym} is live`; lnArtBytes=null;lnArtNote.textContent="none";lnArtNote.classList.remove("set"); lnPrev.classList.remove("set");lnArt.value=""; }catch(e){err(e)} finally{lnGo.disabled=false} }; const SEL_MARKETS="29c21083",SEL_TAPE="29a65241"; const SEL_BYCREATOR="7d78be2b",SEL_TOKEN1="d21220a7",SEL_BYCREATOR_N="aa5e6b5b",SEL_RESERVE0="443cb4bc"; const LAUNCHED=[]; const LAUNCH_SHOW=24; const LAUNCH_SCAN=256; const FINE=300,COARSE=14400,TFS=[["5m",300],["1h",3600],["4h",14400],["1d",86400]]; const CH_POOLS=8; const srcFor=tf=>tf>=COARSE?COARSE:FINE; let chFloor=0,chSupply=0,chFloorKey=""; let chKind=LS.ck==="line"?"line":"candle",chInv=false,chInvKey="",chPeriod=300,chBars=null,chSeq=0,chPools=null,chOpen=LS.ch==="1",chCache={}; let chDraw=null; const unf=p=>(p&0xffffff)*2**(p>>>24); const decBar=w=>{if(!w)return null;const f=s=>Number((w>>BigInt(s))&0xffffffffn); return{b:Number(w&0xffffffffn),o:unf(f(32)),h:unf(f(64)),l:unf(f(96)),c:unf(f(128)),v:unf(f(160)),n:Number((w>>192n)&0xffffn)}}; const rollUp=(bars,src,dst)=>{if(dst<=src)return bars.filter(Boolean); const out=[];let g=null,k=null; for(let i=bars.length-1;i>=0;i--){const b=bars[i];if(!b)continue; const key=Math.floor(b.b*src/dst); if(key!==k){if(g)out.push(g);k=key;g={b:key,o:b.o,h:b.h,l:b.l,c:b.c,v:b.v,n:b.n}} else{g.h=Math.max(g.h,b.h);g.l=Math.min(g.l,b.l);g.c=b.c;g.v+=b.v;g.n+=b.n}} if(g)out.push(g);return out.reverse()}; const CH_WICK_SHARE=.01; const mergeTapes=tapes=>{const m=new Map(); for(const bars of tapes)for(const b of bars||[]){if(!b)continue; let e=m.get(b.b); if(!e)m.set(b.b,e={b:b.b,v:0,n:0,ov:0,cv:0,parts:[]}); e.v+=b.v;e.n+=b.n;e.ov+=b.o*b.v;e.cv+=b.c*b.v;e.parts.push(b)} const out=[...m.values()].sort((x,y)=>y.b-x.b); for(const e of out){ const big=e.v>0?e.parts.filter(x=>x.v>=e.v*CH_WICK_SHARE):[]; const src=big.length?big:e.parts; let h=-Infinity,l=Infinity; for(const x of src){if(x.h>h)h=x.h;if(x.l<l)l=x.l} e.h=h;e.l=l; if(e.v>0){e.o=e.ov/e.v;e.c=e.cv/e.v}else{e.o=src[0].o;e.c=src[0].c} delete e.ov;delete e.cv;delete e.parts} return out}; const chPair=()=>{const f=TOKENS[fromSel.value],t=TOKENS[toSel.value]; if(!f||!t||f.addr===t.addr)return null; return f.addr.toLowerCase()<t.addr.toLowerCase()?[f,t]:[t,f]}; let chLoading=""; async function loadChart(){ const p0=chPair(); const k0=chPairKey(p0); if(k0&&chLoading===k0)return; chBars=null;chPools=null; const p=p0; if(tab!=="swap"||!p){syncChartUI();return} const my=++chSeq,key=k0; const hit=chCache[key]; if(hit&&Date.now()-hit.at<30000){chBars=hit.bars;chPools=hit.pools;syncChartUI();return} chLoading=key; try{ if(PPLENS===ZERO||(await rpc("eth_getCode",[PPLENS,L])).length<5)return syncChartUI(); const marketsFor=async pair=>{ const [raw]=await mc3([{to:PPLENS,data:"0x"+SEL_MARKETS+encAddr(pair[0].addr)+encAddr(pair[1].addr) +encAddr(ZERO)+encUint(0)+encUint(LQ_SCAN)+encUint(0)}]); const found=[]; if(raw){const h=strip0x(raw),n=Number(BigInt("0x"+h.slice(64,128))),base=128; for(let i=0;i<n&&i<LQ_SCAN;i++){const at=k=>BigInt("0x"+h.slice(base+(i*PI_WORDS+k)*64,base+(i*PI_WORDS+k+1)*64)); if(at(PI_HOOK)!==0n||at(PI_LIQ)===0n)continue; found.push({a:"0x"+h.slice(base+(i*PI_WORDS+PI_POOL)*64+24,base+(i*PI_WORDS+PI_POOL+1)*64),liq:at(PI_LIQ)})}} found.sort((x,y)=>y.liq>x.liq?1:y.liq<x.liq?-1:0); return found.slice(0,CH_POOLS).map(x=>x.a); }; let eff=p,pools=await marketsFor(p); if(my!==chSeq)return; if(!pools.length){ const alt=chOtherForm(p); if(alt){ const altPools=await marketsFor(alt); if(my!==chSeq)return; if(altPools.length){eff=alt;pools=altPools} } } const [t0,t1]=eff; if(!pools.length){chCache[key]={at:Date.now(),bars:null,pools:null,pair:null};return syncChartUI()} const reqs=[]; for(const a of pools)reqs.push({to:a,data:"0x"+SEL_TAPE+encUint(FINE)+encUint(256)}, {to:a,data:"0x"+SEL_TAPE+encUint(COARSE)+encUint(256)}); const tapes=await mc3Deep(reqs); if(my!==chSeq)return; const dec1Tape=hex=>{if(!hex)return null; const h=strip0x(hex),n=Number(BigInt("0x"+h.slice(64,128))),bars=[]; for(let j=0;j<n;j++)bars.push(decBar(BigInt("0x"+h.slice(128+j*64,128+(j+1)*64)))); return bars}; const decoded=tapes.map(dec1Tape); const fine=[],coarse=[]; for(let i=0;i<pools.length;i++){fine.push(decoded[i*2]);coarse.push(decoded[i*2+1])} const sc=10**(t0.dec-t1.dec)/1e18; const build=set=>{const m=mergeTapes(set); for(const b of m){b.o*=sc;b.h*=sc;b.l*=sc;b.c*=sc}return m.length?m:null}; const f=build(fine); let c=build(coarse); if(f&&c){const lead=rollUp(f,FINE,COARSE).filter(b=>b.b>c[0].b); if(lead.length)c=lead.concat(c)} chBars=f||c?{[FINE]:f,[COARSE]:c}:null; const live=set=>set.filter(d=>d&&d.some(Boolean)).length; chPools=chBars?{[FINE]:live(fine),[COARSE]:live(coarse)}:null; chCache[key]={at:Date.now(),bars:chBars,pools:chPools,pair:eff===p?null:eff}; }catch{chBars=null;chPools=null} finally{if(chLoading===k0)chLoading=""} if(my===chSeq)syncChartUI(); } const chPoolsAt=step=>chPools&&chPools[step]||0; const chLabel=p=>chInv?`${p[0].sym} per ${p[1].sym}`:`${p[1].sym} per ${p[0].sym}`; async function chFloorFor(p){ const key=chPairKey(p); if(key===chFloorKey)return; chFloorKey=key;chFloor=0;chSupply=0; const tok=p&&p[1]&&p[1].launched?p[1].addr:""; if(!tok)return; try{ const [fp,sup]=await mc3Deep([{to:PLAUNCH,data:"0x"+SEL_FLOORPRICE+encAddr(tok)}, {to:tok,data:"0x"+SEL_TOTALSUPPLY}]); if(key!==chFloorKey)return; chFloor=fp?+formatUnits(BigInt(fp),18):0; chSupply=sup?+formatUnits(BigInt(sup),18):0; }catch{} } function syncChartUI(){ const on=tab==="swap"&&!!chBars; chTog.classList.toggle("hide",!on); chBox.classList.toggle("hide",!on||!chOpen); chTog.setAttribute("aria-expanded",on&&chOpen?"true":"false"); if(!on||!chOpen)return; const p=chEff(); chNote.textContent=p?chLabel(p):""; drawChart();syncInv(); } const NUMERAIRES=new Set([ "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "0xdac17f958d2ee523a2206206994597c13d831ec7", "0x6b175474e89094c44da98b954eedeac495271d0f", ]); const moneyRank=a=>{a=(a||"").toLowerCase(); return NUMERAIRES.has(a)?2:(a===ZERO||a===WETH.toLowerCase())?1:0}; const chPairKey=p=>p?p[0].addr.toLowerCase()+":"+p[1].addr.toLowerCase():""; const chNaturalInv=p=>!!p&&moneyRank(p[0].addr)>moneyRank(p[1].addr); const chOtherForm=p=>{ const eth=p.filter(x=>isEthLike(x.addr)); if(eth.length!==1)return null; const cur=eth[0],rest=p[0]===cur?p[1]:p[0]; const sub=TOKENS.find(x=>x.addr.toLowerCase()===(cur.addr===ZERO?WETH:ZERO).toLowerCase()); if(!sub)return null; return sub.addr.toLowerCase()<rest.addr.toLowerCase()?[sub,rest]:[rest,sub]; }; const chEff=()=>{const p=chPair();if(!p)return p; const hit=chCache[chPairKey(p)]; return (hit&&hit.pair)||p;}; const CH_SLOTS=Math.floor((340-2-42)/3); const chFinest=()=>chBars&&chBars[FINE]?FINE:COARSE; const chSeries=secs=>{ if(!chBars)return null; const src=srcFor(secs),step=chBars[src]?src:chFinest(),base=chBars[step]; if(!base||!base.length)return null; const per=Math.max(secs,step); let pts=rollUp(base,step,per).slice().reverse(); pts=pts.map(b=>{const{o,c}=b; if(!(o>0)||!(c>0)||!isFinite(o)||!isFinite(c))return null; const h=b.h>0&&isFinite(b.h)?b.h:0,l=b.l>0&&isFinite(b.l)?b.l:Infinity; return{...b,h:Math.max(h,o,c),l:Math.min(l,o,c)}}).filter(Boolean); return pts.length?{pts,per,step}:null; }; const chAutoPeriod=()=>{ if(!chBars)return null; const fit=secs=>{const s=chSeries(secs); if(!s||s.per!==secs)return null; const span=s.pts[s.pts.length-1].b-s.pts[0].b+1; return{n:s.pts.length,ok:s.pts.length>=6&&span<=CH_SLOTS}}; let best=null,bestN=-1; for(const[,secs] of TFS){const f=fit(secs); if(!f)continue; if(f.ok)return secs; if(f.n>bestN){best=secs;bestN=f.n}} return best==null?chFinest():best; }; let chTfKey=""; const chSyncPeriod=()=>{ const k=chPairKey(chEff()); if(k===chTfKey)return; const auto=chAutoPeriod(); if(auto==null)return; chTfKey=k;chPeriod=auto; }; const chSyncTfs=()=>{ const finest=chFinest(); if(chPeriod<finest)chPeriod=finest; for(const c of chTf.children){if(!c.dataset.secs)continue; const secs=Number(c.dataset.secs); c.disabled=!!chBars&&secs<finest; c.title=c.disabled?"This pair has no five-minute tape to draw":""; c.classList.toggle("on",secs===chPeriod)} }; const chSyncOrientation=()=>{ const p=chEff(),k=chPairKey(p); if(k===chInvKey)return; chInvKey=k;chInv=chNaturalInv(p); }; const hudFor=i=>{ if(!chDraw)return; const{pts,num,UP,DN,ch,per,hd}=chDraw; if(!hd)return; if(i<0){const b=pts[pts.length-1]; let note="",flip=""; if(chInv&&isFinite(ch)&&ch>-100){ const other=(1/(1+ch/100)-1)*100; const pr=chEff&&chEff(); const base=pr&&pr[1]?pr[1].sym:"the token"; note=` title="${base} is ${other>=0?"+":""}${other.toFixed(2)}% over this window. ` +`This chart prices the pair the other way up, so its line moves the opposite way."`; flip=` <span class="chflip">${safeSym(base)} ${other>=0?"+":""}${other.toFixed(2)}%</span>`; } hd.innerHTML=`<b>${num(b.c)}</b> <span style="color:${ch>=0?UP:DN}"${note}>${ch>=0?"+":""}${ch.toFixed(2)}%</span>${flip}`; return} const b=pts[i],up=b.c>=b.o,d=new Date(b.b*per*1e3); const when=per>=86400 ?d.toLocaleDateString("en-US",{month:"short",day:"numeric"}) :d.toLocaleTimeString("en-US",{hour:"2-digit",minute:"2-digit",hour12:false}); hd.innerHTML=`<b style="color:${up?UP:DN}">${num(b.c)}</b> <span>${when}</span>` +`<span class="ohlc">H ${num(b.h)} L ${num(b.l)} · ${b.n} trade${b.n===1?"":"s"}</span>`; }; const chPoint=e=>{ if(!chDraw)return; const r=chArt.getBoundingClientRect(); if(!r.width)return; const{pts,W,PL,st,b0}=chDraw; const x=(e.clientX-r.left)/r.width*W; const at=b0+Math.round((x-PL)/st-.5); let i=0,near=Infinity; for(let j=0;j<pts.length;j++){const d=Math.abs(pts[j].b-at); if(d<near){near=d;i=j}} hudFor(i); if(!chDraw.svg)return; let el=chDraw.cross; if(!el){ el=document.createElementNS("http://www.w3.org/2000/svg","line"); el.setAttribute("class","cross"); el.setAttribute("y1","0");el.setAttribute("y2","150"); el.setAttribute("stroke","currentColor");el.setAttribute("stroke-opacity",".35"); el.setAttribute("stroke-width",".8"); chDraw.svg.appendChild(el);chDraw.cross=el; } const cx=chDraw.X(i).toFixed(1); el.setAttribute("x1",cx);el.setAttribute("x2",cx); }; chArt.addEventListener("pointermove",chPoint); chArt.addEventListener("pointerdown",chPoint); chArt.addEventListener("pointerleave",()=>{ if(chDraw&&chDraw.cross){chDraw.cross.remove();chDraw.cross=null} hudFor(-1)}); function drawChart(){ if(!chBars)return; const W=340,H=150,PL=2,PR=42,PT=10,PB=14,VH=22; const pw=W-PL-PR,ph=H-PT-PB-VH; chSyncPeriod();chSyncOrientation();chSyncTfs(); const ser=chSeries(chPeriod); if(!ser){chArt.innerHTML='<div class="msg">No trades in this window.</div>';chDraw=null;return} const per=ser.per; let pts=ser.pts; const newest=pts[pts.length-1].b,cap=Math.floor(pw/3); const kept=pts.filter(b=>newest-b.b<cap); const clipped=kept.length<pts.length; pts=kept; if(chInv)pts=pts.map(b=>({...b,o:1/b.o,h:1/b.l,l:1/b.h,c:1/b.c})); if(!pts.length){chArt.innerHTML='<div class="msg">No trades in this window.</div>';chDraw=null;return} let hi=-Infinity,lo=Infinity,vm=0; for(const b of pts){if(b.v>vm)vm=b.v} if(chKind==="line"){ for(const b of pts){if(b.c>hi)hi=b.c;if(b.c<lo)lo=b.c} }else{ let bh=-Infinity,bl=Infinity; for(const b of pts){const t=Math.max(b.o,b.c),u=Math.min(b.o,b.c); if(t>bh)bh=t;if(u<bl)bl=u} const span=bh>bl?bh-bl:bh*.1; const cutHi=Math.min(bh+span*CH_WICK_CAP,bh*2), cutLo=Math.max(bl-span*CH_WICK_CAP,bl/2); for(const b of pts){ const h=Math.min(b.h,cutHi),l=Math.max(b.l,cutLo); if(h>hi)hi=h;if(l<lo)lo=l} } if(hi===lo){hi*=1.001;lo*=0.999} const rawLo=lo; let sp=hi-lo;hi+=sp*.09; lo-=Math.min(sp*.09,rawLo>0?rawLo*.1:0); if(lo<=0)lo=rawLo>0?rawLo*.9:0; sp=hi-lo; const n=pts.length,b0=pts[0].b,slots=pts[n-1].b-b0+1; const st=pw/slots,bw=Math.max(1,Math.min(9,st*.68)); const X=i=>PL+st*(pts[i].b-b0+.5),Y=p=>PT+ph*(1-(p-lo)/sp),vt=PT+ph+6,VY=v=>vt+(VH-6)*(1-(vm?v/vm:0)); const num=v=>v>=1000?v.toLocaleString("en-US",{maximumFractionDigits:2}):v>=1?v.toFixed(3):fmtPx(v); const UP="#16a34a",DN="#c33"; const p0=chEff(),lbl=p0?chLabel(p0):"Price"; let s=`<svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="none" role="img" aria-label="${lbl}, ${n} bars, ${num(pts[n-1].c)} now, range ${num(lo)} to ${num(hi)}">`; for(let g=0;g<=3;g++){const p=lo+sp*g/3,y=Y(p).toFixed(1); s+=`<line x1="${PL}" y1="${y}" x2="${W-PR}" y2="${y}" stroke="currentColor" stroke-opacity=".1"/>` +`<text x="${W-PR+4}" y="${(Y(p)+2.5).toFixed(1)}" font-size="7" fill="currentColor" fill-opacity=".5">${p>0?num(p):"0"}</text>`} for(let i=0;i<n;i++){const b=pts[i],y=VY(b.v); s+=`<rect x="${(X(i)-bw/2).toFixed(1)}" y="${y.toFixed(1)}" width="${bw.toFixed(1)}" height="${Math.max(.4,vt+VH-y).toFixed(1)}" fill="currentColor" fill-opacity=".18"/>`} const fl=chFloor>0?(chInv?chFloor:1/chFloor):0; if(fl>0&&fl>=lo&&fl<=hi){ const fy=Y(fl).toFixed(1); s+=`<line x1="${PL}" y1="${fy}" x2="${W-PR}" y2="${fy}" stroke="currentColor" stroke-opacity=".45" stroke-width=".7" stroke-dasharray="3 2"/>` +`<text x="${PL+3}" y="${(Y(fl)-2.5).toFixed(1)}" font-size="6.5" fill="currentColor" fill-opacity=".6">floor</text>`; } const rise=pts[n-1].c>=pts[0].o; if(chKind==="line"){ const d=pts.map((b,i)=>`${i?"L":"M"}${X(i).toFixed(1)} ${Y(b.c).toFixed(1)}`).join(" "); const col=rise?UP:DN; s+=`<path d="${d} L${X(n-1).toFixed(1)} ${(PT+ph).toFixed(1)} L${X(0).toFixed(1)} ${(PT+ph).toFixed(1)} Z" fill="${col}" fill-opacity=".1"/>` +`<path d="${d}" fill="none" stroke="${col}" stroke-width="1.2" stroke-linejoin="round"/>` +`<circle cx="${X(n-1).toFixed(1)}" cy="${Y(pts[n-1].c).toFixed(1)}" r="1.8" fill="${col}"/>`; }else{ for(let i=0;i<n;i++){const b=pts[i],col=b.c>=b.o?UP:DN,x=X(i); const t=Y(Math.max(b.o,b.c)),bo=Y(Math.min(b.o,b.c)); s+=`<line x1="${x.toFixed(1)}" y1="${Y(b.h).toFixed(1)}" x2="${x.toFixed(1)}" y2="${Y(b.l).toFixed(1)}" stroke="${col}" stroke-width=".8"/>` +`<rect x="${(x-bw/2).toFixed(1)}" y="${t.toFixed(1)}" width="${bw.toFixed(1)}" height="${Math.max(.8,bo-t).toFixed(1)}" fill="${col}"/>`} } s+="</svg>"; const last=pts[n-1],first=pts[0],ch=((last.c-first.o)/first.o)*100; chArt.innerHTML=s+'<div class="hd"></div>'; chDraw={pts,X,W,PL,st,b0,num,UP,DN,ch,per, hd:chArt.querySelector(".hd"),svg:chArt.querySelector("svg"),cross:null}; hudFor(-1); const took=rel(Math.max(per,(slots-1)*per)); const np=chPoolsAt(ser.step); const fine=(chBars&&chBars[chFinest()])||null; const newestBar=fine?fine.find(b=>b&&b.c>0):null; const latest=newestBar?newestBar.c:pts[n-1].c; const lastPx=pts[n-1].c; const canon=latest>0?1/latest:0; const mcap=chSupply>0&&canon>0?chSupply*canon:0; chNote.textContent=p0?[chLabel(p0), mcap>0?`${mcap>=1000?Math.round(mcap).toLocaleString("en-US"):mcap.toFixed(mcap>=1?1:3)} ETH mcap${ ethUsd>0?` (${fmtUsd(mcap*ethUsd)})`:""}`:"", `${np} pool${np===1?"":"s"}`,`${clipped?"last ":""}${took}`].filter(Boolean).join(" · "):""; if(p0){const was=chFloor+":"+chSupply; chFloorFor(p0).then(()=>{if(chFloor+":"+chSupply!==was)drawChart()});} } for(const[label,secs]of TFS){const b=document.createElement("button"); b.textContent=label;b.dataset.secs=String(secs);if(secs===chPeriod)b.className="on"; b.onclick=()=>{chPeriod=secs;chTfKey=chPairKey(chEff());drawChart()}; chTf.appendChild(b)} const kindBtn=document.createElement("button"); kindBtn.className="chk"; const syncKind=()=>{kindBtn.textContent=chKind==="candle"?"Line":"Candle"; kindBtn.title=`Switch to ${kindBtn.textContent.toLowerCase()} chart`}; syncKind(); kindBtn.onclick=()=>{chKind=chKind==="candle"?"line":"candle";LS.ck=chKind;syncKind();drawChart()}; chTf.appendChild(kindBtn); const invBtn=document.createElement("button"); invBtn.className="chk"; invBtn.textContent="⇅"; const syncInv=()=>{const p=chEff(); invBtn.title=p?`Price ${chInv?p[1].sym:p[0].sym} instead — currently ${chInv?p[0].sym:p[1].sym} per ${chInv?p[1].sym:p[0].sym}`:"Invert the price"; invBtn.setAttribute("aria-pressed",chInv?"true":"false")}; syncInv(); invBtn.onclick=()=>{chInv=!chInv;syncInv();drawChart()}; chTf.appendChild(invBtn); const syncChart=()=>{chSyncOrientation();syncInv();loadChart()}; chTog.onclick=()=>{chOpen=!chOpen;LS.ch=chOpen?"1":"0";syncChartUI()}; const lsecs=v=>{const m=/^(\d+)([mhdw]?)$/.exec((v||"").trim()); return m?+m[1]*(m[2]=="d"?86400:m[2]=="h"?3600:m[2]=="w"?604800:m[2]=="m"?60:1):NaN}; const symAddr=x=>{const m=TOKENS.filter(t=>t.sym.toLowerCase()===x); return m.length===1?m[0].addr.toLowerCase():""}; let lSeq=0; async function applyLink(){ const q=new URLSearchParams(location.hash.slice(1)); if(![...q.keys()].length)return; const my=++lSeq; const at=a=>a?TOKENS.findIndex(t=>t.addr.toLowerCase()===a):-1; const tok=async v=>{if(!v)return "";const x=v.trim().toLowerCase(); if(!/^0x[0-9a-f]{
0xacfba7ce…4082·#25,784,652·0x41f41867…815aeb
[hasCode(SBVIEW,L),DUTCH!==ZERO?hasCode(DUTCH,L):false]); if(!sv)return null; const specs=[...BOARDS]; if(dv)specs.push({a:DUTCH,dutch:1}); const pages=await Promise.all(specs.map(async b=>{ let cursor=0n,out=[],turns=0; do{ const data="0x"+(b.dutch?SEL_RECENT_DUTCH:SEL_RECENT)+encAddr(b.a) +(b.dutch?"":encBool(b.v2))+encUint(cursor)+encUint(80)+encUint(256); const p=decViewPage(await rpc(C,[{to:SBVIEW,data},L])); out.push(...p.rows.map(r=>({...r,v2:b.v2||0,dutch:b.dutch||0}))); cursor=p.next; }while(cursor&&out.length<160&&++turns<64&&my===bookSeq); return out; })); const rows=pages.flat(); if(FLOOR!==ZERO&&FLOORVIEW!==ZERO){ await floorRoutable(L); try{ const raw=await rpc(C,[{to:FLOORVIEW, data:"0x"+SEL_RECENT_FLOOR+encAddr(FLOOR)+encUint(0)+encUint(80)+encUint(256)},L]); for(const b of decBidRows(raw)){ if(b.remaining<=0n)continue; rows.push({ id:b.id,board:FLOOR,floor:1,v2:0,dutch:0, maker:b.bidder,pf:b.isNFT?0:1,exp:b.expiry, nA:0,nB:0,cp:ZERO, tA:b.quote,aA:b.proceeds,sA:b.qSym,dA:b.qDec, tB:b.token,aB:b.remaining,sB:b.tSym,dB:b.isNFT?0:b.tDec, price:b.price,initial:b.initial,anyId:b.anyId,isNFT:b.isNFT?1:0, }); } }catch{} } return rows; } const loadBook=()=>loadBookRun().catch(()=>{}); async function loadBookRun(){ if(tab!=="book"){book.innerHTML="";bookRows=[];syncBook(0);return} const my=++bookSeq; const live=(await Promise.all(BOARDS.map(b=> rpc("eth_getCode",[b.a,L]).then(c=>(c||"").length>4?b:null,()=>null)))).filter(Boolean); if(my!==bookSeq)return; if(!live.length){ bookRows=[];bookLegs=[]; book.innerHTML='<h6><span>Orderbook</span></h6><div class="o"><span class="l"><i>No Swapboard deployed on this network yet.</i></span></div>'; syncBook(0);book.classList.remove("hide");return} let rows=null; try{rows=await lensBookRows(my)}catch{rows=null} if(my!==bookSeq)return; if(!rows)rows=[]; for(const b of rows.length?[]:live){ let total=0; try{total=Number(BigInt(await rpc(C,[{to:b.a,data:"0x"+SEL_NEXTID},L])))}catch{continue} if(!total)continue; const hi=total,lo=Math.max(0,hi-40); const ids=[];for(let i=hi-1;i>=lo;--i)ids.push(i); let raw=""; try{raw=await rpc(C,[{to:b.a,data:"0x"+SEL_GETORDERS+encUint(32)+encUint(ids.length)+ids.map(i=>encUint(i)).join("")},L])}catch{continue} if(my!==bookSeq)return; const h=strip0x(raw);if(h.length<128)continue; const w=WORDS(b.v2),words=(h.length-128)/64; let n=Number(BigInt("0x"+h.slice(64,128))); if(!Number.isSafeInteger(n)||n*w>words)n=Math.floor(words/w); const at=(r,k)=>BigInt("0x"+h.slice(128+(r*w+k)*64,128+(r*w+k+1)*64)); const ad=(r,k)=>"0x"+h.slice(128+(r*w+k)*64+24,128+(r*w+k+1)*64); for(let r=0;r<n&&r<ids.length;++r){ if(at(r,1)!==1n)continue; const o=b.v2 ?{id:ids[r],board:b.a,v2:1,maker:ad(r,0),pf:at(r,2)===1n,exp:at(r,3), nA:at(r,4)===1n,nB:at(r,5)===1n,cp:ad(r,6), tA:ad(r,7),aA:at(r,8),tB:ad(r,9),aB:at(r,10)} :{id:ids[r],board:b.a,v2:0,maker:ad(r,0),pf:0,exp:0n,nA:0,nB:0, cp:ZERO,tA:ad(r,2),aA:at(r,3),tB:ad(r,4),aB:at(r,5)}; if(o.exp&&BigInt(Math.floor(Date.now()/1e3))>o.exp &&o.maker.toLowerCase()!==me())continue; if(o.cp!==ZERO &&o.cp.toLowerCase()!==me() &&o.maker.toLowerCase()!==me())continue; rows.push(o); } } if(my!==bookSeq)return; const cutoff=BigInt(Math.floor(Date.now()/1e3)); rows=rows.filter(o=>{ const mine=o.maker.toLowerCase()===me(); if(o.exp&&cutoff>o.exp&&!mine)return false; return o.cp===ZERO ||o.cp.toLowerCase()===me() ||o.maker.toLowerCase()===me(); }); const dutchRows=rows.filter(r=>r.dutch); const bidRows=rows.filter(r=>r.floor); const fillRows=rows.filter(r=>!r.dutch&&!r.floor&&r.v2&&r.pf&&!r.nA&&!r.nB); if(dutchRows.length||bidRows.length||fillRows.length){ const reqs=[ ...dutchRows.map(r=>({to:DUTCH,data:"0x"+SEL_DUTCH_LISTING+encUint(r.id)})), ...bidRows.map(r=>({to:FLOOR,data:"0x"+SEL_BIDS+encUint(r.id)})), ...fillRows.map(r=>({to:r.board,data:"0x"+SEL_INITIAL_B+encUint(r.id)})), ]; const raw=await mc3Deep(reqs); if(my!==bookSeq)return; let k=0; for(const r of dutchRows){const x=raw[k++];if(!x)continue; const h=strip0x(x),w=n=>BigInt("0x"+h.slice(n*64,(n+1)*64)); if(h.length>=640)Object.assign(r,{ start:w(2),duration:w(3),startPrice:w(5),endPrice:w(7),initial:w(8),remaining:w(9)}); } for(const r of bidRows){const x=raw[k++];if(!x)continue; const h=strip0x(x),w=n=>BigInt("0x"+h.slice(n*64,(n+1)*64)); if(h.length>=11*64)Object.assign(r,{start:w(2),duration:w(3),startPrice:w(5),endPrice:w(7)}); } for(const r of fillRows){const x=raw[k++];if(!x)continue; const h=strip0x(x);if(h.length<64)continue; const init=BigInt("0x"+h.slice(0,64)); if(init>r.aB)r.filled=(init-r.aB)*10000n/init; } } if(!rows.length){bookRows=[];bookLegs=[];book.innerHTML="";syncBook(0);return} await loadLogos(rows.flatMap(r=>[r.tA,r.tB])); if(my!==bookSeq)return; const legs=await Promise.all(rows.map(r=>Promise.all([ leg(r.tA,r.aA,r.nA,r.sA!=null&&r.dA!=null?{sym:r.sA,dec:r.dA}:null), leg(r.tB,r.aB,r.nB,r.sB!=null&&r.dB!=null?{sym:r.sB,dec:r.dB}:null) ]))); if(my!==bookSeq)return; bookRows=rows;bookLegs=legs; paintBook(); } let bookPairOnly=LS.bkp!=="0",bookType="all",bookLegs=[]; const sameAsset=(a,b)=>{ const w=x=>{x=(x||"").toLowerCase();return x===ZERO.toLowerCase()?WETH.toLowerCase():x}; return w(a)===w(b); }; const rowInPair=r=>{ const f=TOKENS[fromSel.value]?.addr,t=TOKENS[toSel.value]?.addr; if(!f||!t)return true; return (sameAsset(r.tA,f)&&sameAsset(r.tB,t))||(sameAsset(r.tA,t)&&sameAsset(r.tB,f)); }; const rowIsType=r=>bookType==="all" ||(bookType==="dutch"&&r.dutch) ||(bookType==="bid"&&r.floor) ||(bookType==="nft"&&(r.nA||r.nB||r.isNFT)) ||(bookType==="limit"&&!r.dutch&&!r.floor); function paintBook(){ const rows=bookRows,legs=bookLegs; if(!rows.length){book.innerHTML="";syncBook(0);return} let own="",rest="",nOwn=0,nRest=0,hidden=0; const now=BigInt(Math.floor(Date.now()/1e3)); rows.forEach((r,i)=>{ const[a,bg]=legs[i]||[]; if(!a||!bg)return; const mine=r.maker.toLowerCase()===me(); const expired=!!(r.exp&&now>r.exp); const tags=(expired?'<span class="tg w" title="This order has lapsed and can no longer be filled, but its escrow is still held by the board. Reclaim returns it to you.">expired</span>':"") +(r.nA||r.nB||r.isNFT?'<span class="tg">NFT</span>':"") +(r.v2&&!r.pf&&!r.nA&&!r.nB?'<span class="tg">AON</span>':"") +(r.dutch?'<span class="tg">Dutch</span>':"") +(r.floor?'<span class="tg">Bid</span>':"") +(r.floor&&r.isNFT&&r.anyId?'<span class="tg">any id</span>':"") +(r.filled!==undefined&&r.filled>0n?`<span class="tg">${(Number(r.filled)/100).toFixed(0)}% filled</span>`:"") +(r.cp!==ZERO?'<span class="tg">private</span>':"") +(!a.ok||!bg.ok?'<a class="tg w" href="'+ZLIST_URL+'" target="_blank" rel="noopener"' +' title="Not on the staked token list. Its symbol and decimals are read straight from the contract' +' and nothing has vouched for it - check the address before filling. Stake on zList to get a real' +' token listed.">unverified</a>':""); let when=expired?" · lapsed "+rel(Number(now-r.exp))+" ago — escrow still held" :r.exp?" · "+rel(Number(r.exp-now))+" left":""; if(!expired&&r.dutch&&r.duration){ const end=r.start+r.duration; const floor=r.initial?(r.endPrice*r.remaining+r.initial-1n)/r.initial:0n; when=now>=end?` · floor reached: ${trimAmt(floor,r.dB)} ${r.sB}` :` · floor ${trimAmt(floor,r.dB)} ${r.sB} in ${rel(Number(end-now))}`; } if(!expired&&r.floor&&r.duration){ const end=r.start+r.duration; when=now>=end?" · window closed" :` · climbs to ${trimAmt(r.initial?r.endPrice*r.aB/r.initial:r.endPrice,r.dA)} ${r.sA} in ${rel(Number(end-now))}`; } const dset=`data-i="${r.id}" data-b="${r.board}" data-v="${r.v2}" data-d="${r.dutch||0}" data-f="${r.floor||0}"`; const takeable=!r.floor||(!r.isNFT&&floorReady); const act=mine?`<button data-x="c" ${dset}>${expired?"Reclaim":"Cancel"}</button>` :takeable?`<button data-x="f" ${dset}>${r.floor?"Sell into":"Fill"}</button>` :`<button disabled>${r.isNFT?"NFT bid":"Not routable"}</button>`; const card=r.board.toLowerCase()!==SB1.toLowerCase(); const row=`<div class="ow"><div class="o${card?" ins":""}"${card?` data-k="${r.board}:${r.id}"`:""}>` +`<span class="l">${a.ic}${bg.ic}<span><b>${a.txt} → ${bg.txt}</b>` +`<i>${a.link}${tags}${when}</i></span></span>${act}</div><div class="ic"></div></div>`; if(mine){own+=row;++nOwn;return} if(!(bookPairOnly?rowInPair(r):true)||!rowIsType(r)){++hidden;return} rest+=row;++nRest; }); let html=""; if(nOwn)html+=`<h6><span>Your orders</span><span>${nOwn}</span></h6>`+own; html+=bookFilters(hidden); if(nRest)html+=rest; else html+=`<div class="lqempty">${emptyBook()}${hidden?` ${hidden} hidden by the filters.`:""}</div>`; book.innerHTML=html; syncBook(nOwn+nRest,rows.length); } const cardCache={}; async function loadCard(board,id){ const k=board.toLowerCase()+":"+id; if(cardCache[k])return cardCache[k]; const raw=await rpc(C,[{to:board,data:"0x"+SEL_TOKENURI+encUint(id)},L]); const uri=decodeString(raw); const i=uri.indexOf("base64,"); if(i<0)throw Error("card is not inline"); const j=JSON.parse(atob(uri.slice(i+7))); const out={attrs:Array.isArray(j.attributes)?j.attributes:[],img:safeUrl(j.image), name:safeSym(j.name).slice(0,40)}; cardCache[k]=out;return out; } function cardHtml(c){ const rows=c.attrs.slice(0,10).map(a=>{ const t=safeSym(a&&a.trait_type).slice(0,20); let v=a&&a.value; if(a&&a.display_type==="date"&&+v)v=new Date(+v*1e3).toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"}); else if(a&&a.display_type==="number")v=String(v); return`<dt>${t}</dt><dd>${safeSym(v).slice(0,28)}</dd>`}).join(""); return(rows?`<dl>${rows}</dl>`:"")+(c.img?`<img src="${c.img}" alt="" loading="lazy">`:""); } async function inspect(el,k){ const box=el.parentNode.querySelector(".ic"); if(!box)return; if(box.innerHTML){box.innerHTML="";return} const at=k.lastIndexOf(":"),board=k.slice(0,at),id=BigInt(k.slice(at+1)); box.textContent="Reading the receipt\u2026"; try{box.innerHTML=cardHtml(await loadCard(board,id))||"No card for this order."} catch{box.textContent="This receipt could not be read."} } const BOOK_TYPES=[["all","All"],["limit","Limit"],["dutch","Dutch"],["bid","Bids"],["nft","NFT"]]; const emptyBook=()=>{ const ty=BOOK_TYPES.find(([v])=>v===bookType); const what=bookType==="all"?"orders":`${ty?ty[1]:bookType} orders`; const where=bookPairOnly ?` on ${TOKENS[fromSel.value]?.sym||"?"}/${TOKENS[toSel.value]?.sym||"?"}`:""; return `No ${what.toLowerCase()}${where} from anyone else.`; }; function bookFilters(hidden){ const f=TOKENS[fromSel.value]?.sym||"?",t=TOKENS[toSel.value]?.sym||"?"; const chip=(on,attr,val,label,title)=>`<button class="bkc${on?" on":""}" ` +`data-${attr}="${val}"${title?` title="${title}"`:""}>${label}</button>`; return `<h6><span>Orderbook</span><span>${hidden?hidden+" hidden":""}</span></h6>` +`<div class="bkf">` +chip(bookPairOnly,"bf","1",`${f}/${t}`,"Only orders on the pair selected above") +chip(!bookPairOnly,"bf","0","All pairs","Every order on every board") +`</div><div class="bkf">` +BOOK_TYPES.map(([v,l])=>chip(bookType===v,"bt",v,l)).join("") +`</div>`; } let bookShown=0; function syncBook(n,total){ bookShown=n; total=total===undefined?n:total; const on=tab==="book"&&total>0; bkTog.classList.toggle("hide",!on); bkTog.firstChild.nodeValue=total ?`Orderbook (${n<total?`${n} of ${total}`:total}) `:"Orderbook "; bkTog.setAttribute("aria-expanded",on&&bkOpen?"true":"false"); book.classList.toggle("hide",!on||!bkOpen); } bkTog.onclick=()=>{bkOpen=!bkOpen;LS.bk=bkOpen?"1":"0";syncBook(bookShown,bookRows.length)}; book.addEventListener("click",async e=>{ const fc=e.target.closest("[data-bf]"),tc=e.target.closest("[data-bt]"); if(fc){bookPairOnly=fc.dataset.bf==="1";LS.bkp=bookPairOnly?"1":"0";paintBook();return} if(tc){bookType=tc.dataset.bt;paintBook();return} const b=e.target.closest("button[data-i]"); if(!b){ const row=e.target.closest(".o.ins"); if(row&&!e.target.closest("a"))inspect(row,row.dataset.k); return; } b.disabled=true; ++busy; try{ await checkWallet(); const id=BigInt(b.dataset.i),to=b.dataset.b,dl=BigInt(Math.floor(Date.now()/1e3)+1800); const row=bookRows.find(x=>String(x.id)===b.dataset.i&&x.board.toLowerCase()===to.toLowerCase()); if(!row)throw Error("order changed — refresh and retry"); if(b.dataset.x==="c"){ const native=!row.nA&&row.tA.toLowerCase()===WETH.toLowerCase()&&wantsUnwrap(); const floorish=b.dataset.d==="1"||b.dataset.f==="1"; const sel=floorish ?(native?SEL_DUTCH_CANCEL_UNWRAP:SEL_DUTCH_CANCEL) :(native&&b.dataset.v==="1"?SEL_CANCEL_UNWRAP:SEL_CANCELORD); const data="0x"+sel+encUint(id); const lapsed=row.exp&&BigInt(Math.floor(Date.now()/1e3))>row.exp; stat.textContent=`${lapsed?"Reclaiming":"Cancelling"} — the ${b.dataset.f==="1"?"unspent escrow":"unfilled remainder"} returns as ${native&&(floorish||b.dataset.v==="1")?"ETH":"its escrow token"}`; const tx={from:account,to,data,value:"0x0"}; await rpc(C,[tx,L]); await settle(await rpc(S,[tx])); loadBook();refreshBalance();return; } const r=row; let payB=r.aB,getA=r.aA; if(r.pf&&!r.nA&&!r.nB&&!r.dutch&&!r.floor&&r.v2&&r.maker.toLowerCase()!==account.toLowerCase()){ const dB=r.dB??18,dA=r.dA??18,sB=r.sB||"tokens",sA=r.sA||"tokens"; let have=0n; try{ have=r.tB===ZERO||r.tB.toLowerCase()===WETH.toLowerCase() ?BigInt(await rpc("eth_getBalance",[account,L])) +(r.tB===ZERO?0n:BigInt(await rpc(C,[{to:WETH,data:"0x"+SEL_BALANCEOF+encAddr(account)},L]))) :BigInt(await rpc(C,[{to:r.tB,data:"0x"+SEL_BALANCEOF+encAddr(account)},L])); }catch{} const suggest=have&&have<r.aB?have:r.aB; const answer=prompt( `Fill how much of this order?\n\nIt sells ${trimAmt(r.aA,dA)} ${sA} for ${trimAmt(r.aB,dB)} ${sB} in total, ` +`and can be filled in part.\n\nEnter the ${sB} you want to spend:`, trimAmt(suggest,dB)); if(answer===null){b.disabled=false;return} let asked; try{asked=parseUnits(String(answer).trim(),dB)}catch{throw Error("that is not an amount")} if(!asked)throw Error("enter an amount above zero"); if(asked>r.aB)throw Error(`that is more than the order needs — it costs ${trimAmt(r.aB,dB)} ${sB} in full`); if(asked<r.aB){ const q=await rpc(C,[{to:r.board,data:"0x"+SEL_QUOTEFILL+encUint(id)+encUint(asked)},L]); const h=strip0x(q); if(h.length<128)throw Error("the board would not quote that amount"); getA=BigInt("0x"+h.slice(0,64)); payB=BigInt("0x"+h.slice(64,128)); if(!getA)throw Error("that amount buys nothing — try a larger one"); stat.textContent=`Filling ${trimAmt(payB,dB)} ${sB} of ${trimAmt(r.aB,dB)} for ${trimAmt(getA,dA)} ${sA}...`; } } const nativeOut=!r.nA&&r.tA.toLowerCase()===WETH.toLowerCase()&&wantsUnwrap(); const wethLeg=!r.nB&&r.tB.toLowerCase()===WETH.toLowerCase()&&payB>0n; const plan=wethLeg?await planWethPayment(payB):null; if(plan&&plan.rail==="short") throw Error(`Not enough ether to fill: it costs ${trimAmt(payB,18)} and you hold ` +`${trimAmt(plan.w,18)} WETH plus ${trimAmt(plan.e,18)} ETH. ` +(r.pf ?`This button takes the whole order — to buy part of it, use the Swap tab, which fills this order alongside the pools.` :`This order is all-or-nothing, so it cannot be filled in smaller pieces.`)); if(plan&&plan.wrap>0n){ stat.textContent=`Wrapping ${trimAmt(plan.wrap,18)} ETH to cover the rest...`; const wtx={from:account,to:WETH,data:"0xd0e30db0",value:toHex(plan.wrap)}; await rpc(C,[wtx,L]);await checkWallet();await settle(await rpc(S,[wtx])); } const nativeIn=plan?.rail==="eth"; const routeIn=nativeIn?ZERO:r.tB; const routeOut=nativeOut?ZERO:r.tA; const canonicalNative=routeIn.toLowerCase()===routeOut.toLowerCase() ||(routeOut===ZERO&&routeIn.toLowerCase()===WETH.toLowerCase()); if(!r.nB)await assertFunded(routeIn,payB, known(routeIn)||(routeIn===ZERO?{sym:"ETH",dec:18}:{sym:r.sB||"TOKEN",dec:r.dB??18})); if(!r.nA&&!r.nB&&r.cp===ZERO&&!canonicalNative){ if((await rpc("eth_getCode",[SWAPBOL,L])).length<5)throw Error("orderbook executor not deployed yet"); await preflightLeg(r,routeIn,routeOut,payB); const pay=payB,fillPlan=encFillPlan([ {id:r.id,board:r.board,pay,get:getA,part:false} ],routeIn,routeOut,account,account,dl); const planCall=encSnwap(pay?routeIn:ZERO,pay,account,routeOut,getA,SWAPBOL,fillPlan); const checkpoint=routeIn===ZERO?null:encCheckpoint(SWAPBOL,routeIn,account); const direct=checkpoint?encCalls([checkpoint,planCall]):planCall; const funded=routeIn===ZERO||!pay?direct:encCalls([ checkpoint, encSweep(routeIn,pay,SWAPBOL), encSnwap(ZERO,0n,account,routeOut,getA,SWAPBOL,fillPlan) ]); const meta=known(routeIn)||{addr:routeIn,sym:r.sB||"TOKEN",dec:r.dB??18}; await sendRouterFunded(direct,funded,meta,pay,routeIn===ZERO?pay:0n,"Filling order..."); loadBook();refreshBalance();return; } const data=r.dutch ?"0x"+SEL_DUTCH_FILL+encUint(id)+encUint(r.nA?0n:getA)+encAddr(account)+encUint(payB) :r.v2 ?"0x"+(nativeIn?SEL_FILL2_ETH:nativeOut?SEL_FILL2_UNWRAP:SEL_FILL2) +encUint(id)+encUint(dl)+(nativeIn?"":encUint(payB))+encUint(r.nA?0n:getA)+encAddr(ZERO) :"0x"+SEL_FILL1+encUint(id)+encUint(dl); const payNative=(nativeIn&&!r.dutch&&r.v2)||(r.dutch&&r.tB===ZERO); const wrap=nativeIn&&!payNative&&payB ?{to:WETH,data:"0xd0e30db0",value:toHex(payB)}:null; const tx={from:account,to,data,value:payNative?toHex(payB):"0x0"}; const approvals=[]; if(r.nB){ let ap=ZERO; try{ap="0x"+strip0x(await rpc(C,[{to:r.tB,data:"0x081812fc"+encUint(payB)},L])).slice(-40)}catch{} if(ap.toLowerCase()!==to.toLowerCase()){ approvals.push({to:r.tB,data:"0x"+SEL_APPROVE+encAddr(to)+encUint(payB),value:"0x0"}); } }else if(r.tB!==ZERO&&payB&&!(nativeIn&&!r.dutch&&r.v2)){ const al=BigInt(await rpc(C,[{to:r.tB,data:"0x"+SEL_ALLOWANCE+encAddr(account)+encAddr(to)},L])); if(al<payB){ if(al)approvals.push({to:r.tB,data:"0x"+SEL_APPROVE+encAddr(to)+encUint(0n),value:"0x0"}); approvals.push({to:r.tB,data:"0x"+SEL_APPROVE+encAddr(to)+encUint(payB),value:"0x0"}); } } const revoke=r.dutch&&!r.nB&&r.tB!==ZERO ?{to:r.tB,data:"0x"+SEL_APPROVE+encAddr(to)+encUint(0n),value:"0x0"}:null; if((wrap||approvals.length||revoke)&&await canBatch(account)){ stat.textContent="Preparing and filling atomically..."; await settle(await sendBatch(account,[ ...(wrap?[wrap]:[]),...approvals,{to,data,value:tx.value},...(revoke?[revoke]:[]) ])); loadBook();refreshBalance();return; } if(wrap){ stat.textContent="Wrapping ETH..."; await waitTx(await rpc(S,[{from:account,...wrap}])); } for(let i=0;i<approvals.length;i++){ stat.textContent=`Approve${r.nB?" NFT":""}${approvals.length>1?` (${i+1}/${approvals.length})`:""}...`; await waitTx(await rpc(S,[{from:account,...approvals[i]}])); } await rpc(C,[tx,L]); await settle(await rpc(S,[tx])); if(revoke)try{ stat.textContent="Filled · revoking quote allowance..."; await waitTx(await rpc(S,[{from:account,...revoke}])); stat.textContent="Done"; }catch{stat.textContent="Filled · quote allowance remains; revoke it in your wallet"} loadBook();refreshBalance(); }catch(x){err(x);b.disabled=false} finally{--busy} }); const SLOW="0x000000000000888741B254d37e1b27128AfEAaBC"; const SEL_DEPOSITTO="94eeaec9",SEL_CLAIM="379607f5",SEL_REVERSE="97d15425",SEL_WITHDRAWFROM="d4fdc309"; const SEL_UNLOCK="6198e339",SEL_CLAWBACK="fcc36bc9",SEL_GUARDIAN="0633b14a"; const GRACE=2592000n; const SLOW_GATE="0xb8B546b93a82f4Aa6f0345142dF5679B659ef3D4"; const SEL_DEPOSITTIP="75f92e42",SEL_TIPS="a5c68c59",SEL_REFUNDTIP="d27e1e72"; const TIP_GAS=180000n; let sendTipWei=0n,tipSeq=0; async function syncTip(){ const my=++tipSeq; const on=tab==="send"&&+dly.value>0; tipL.classList.toggle("hide",!on); if(!on||!tipCk.checked){sendTipWei=0n;tipNote.textContent="";return} tipNote.textContent="estimating…"; let wei=0n; try{wei=BigInt(await rpc("eth_gasPrice",[]))*TIP_GAS}catch{} if(my!==tipSeq)return; sendTipWei=wei>0n?wei:10n**15n/2n; tipNote.textContent="tip ≈ "+trimAmt(sendTipWei,18)+" ETH"; render(); } const SEL_OUT="d40d4bc6",SEL_IN="e3993ee7",SEL_PENDING="6577b86a"; const idTok=i=>"0x"+(i&((1n<<160n)-1n)).toString(16).padStart(40,"0"); const idDelay=i=>i>>160n; const w=(h,n)=>BigInt("0x"+(strip0x(h).slice(n*64,n*64+64)||"0")); const metaCache=new Map(); async function tokMeta(a){ a=a.toLowerCase(); if(a===ZERO)return{sym:"ETH",dec:18}; const k=TOKENS.find(t=>t.addr.toLowerCase()===a); if(k)return k; if(metaCache.has(a))return metaCache.get(a); let m={sym:a.slice(0,6)+"…",dec:18}; try{const[sr,dr]=await Promise.all([rpc(C,[{to:a,data:"0x95d89b41"},L]),rpc(C,[{to:a,data:"0x313ce567"},L])]); const d=Number(BigInt(dr)); if(Number.isInteger(d)&&d>=0&&d<=36)m={sym:decodeString(sr).trim().replace(/[<>&"'`]/g,"").slice(0,10)||m.sym,dec:d}; }catch{} metaCache.set(a,m);return m; } const rel=s=>{const a=Math.abs(s),u=[[86400,"d"],[3600,"h"],[60,"m"]]; for(const[n,l]of u)if(a>=n)return Math.floor(a/n)+l;return a+"s"}; let posSeq=0,posShown=25,slowGuard=false; const POSPAGE=25; async function loadPos(){ if(!account){pos.innerHTML="";tabSend.textContent="Send";slowGuard=false;return} const my=++posSeq; let outIds=[],inIds=[]; try{ const[oh,ih,gd]=await Promise.all([ rpc(C,[{to:SLOW,data:"0x"+SEL_OUT+encAddr(account)},L]), rpc(C,[{to:SLOW,data:"0x"+SEL_IN+encAddr(account)},L]), rpc(C,[{to:SLOW,data:"0x"+SEL_GUARDIAN+encAddr(account)},L]).catch(()=>"0x")]); outIds=decUintArr(oh);inIds=decUintArr(ih); if(my===posSeq)slowGuard=w(gd,0)!==0n; }catch{return} if(my!==posSeq)return; const dirs=new Map(); for(const[t,d]of[...outIds.map(i=>[i,"out"]),...inIds.map(i=>[i,"in"])]){ const k=t.toString();dirs.set(k,(dirs.get(k)||"")+d)} const allKeys=[...dirs.keys()].sort((a,b)=>a.length-b.length||(a<b?1:-1)); const keys=allKeys.slice(0,posShown); const more=allKeys.length-keys.length; const ps=[]; for(let i=0;i<keys.length;i+=10){ ps.push(...await Promise.all(keys.slice(i,i+10).map(k=> rpc(C,[{to:SLOW,data:"0x"+SEL_PENDING+encUint(BigInt(k))},L]).catch(()=>0)))); if(my!==posSeq)return; } if(my!==posSeq)return; const rows=[],settled=[]; keys.forEach((k,i)=>{const p=ps[i];if(!p)return; const ts=w(p,0); if(ts===0n){if(dirs.get(k).includes("out"))settled.push(k);return} rows.push({tid:BigInt(k),dir:dirs.get(k),ts,id:w(p,3),amount:w(p,4)})}); const stale=[]; for(let i=0;i<settled.length;i+=10){ const ts=await Promise.all(settled.slice(i,i+10).map(k=> rpc(C,[{to:SLOW_GATE,data:"0x"+SEL_TIPS+encUint(BigInt(k))},L]).catch(()=>0))); if(my!==posSeq)return; ts.forEach((t,j)=>{if(t&&w(t,0)>0n&&idTok(w(t,1))===account.toLowerCase()) stale.push({tid:settled[i+j],amount:w(t,0)})}); } const now=BigInt(Math.floor(Date.now()/1e3)); const ready=rows.filter(r=>r.dir.includes("in")&&now>=r.ts+idDelay(r.id)).length; tabSend.textContent=ready?`Send (${ready}${more?"+":""})`:"Send"; if(tab!=="send"||!(rows.length||stale.length)){pos.innerHTML="";return} rows.sort((a,b)=>Number((a.ts+idDelay(a.id))-(b.ts+idDelay(b.id)))); const metas=await Promise.all(rows.map(r=>tokMeta(idTok(r.id)))); if(my!==posSeq)return; let html=`<div class="ph">Time-locked</div>`; rows.forEach((r,n)=>{ const m=metas[n],exp=r.ts+idDelay(r.id),left=Number(exp-now); const ripe=now>=exp; const amt=`${trimAmt(r.amount,m.dec)} ${m.sym}`; const io=r.dir.includes("in"),oo=r.dir.includes("out"); const held=`data-t="${r.tid}" data-i="${r.id}" data-m="${r.amount}"`; const cb=oo&&ripe&&now>=exp+GRACE; let act,note; if(cb)note="Unclaimed past grace · recoverable"; else if(ripe)note=io?"Ready to claim":"Matured · recipient can claim, you in "+rel(Number(exp+GRACE-now)); else note=(io&&!oo?"Arrives in ":"Reversible · sends in ")+rel(left); if(io&&ripe)act=`<button data-a="c" data-t="${r.tid}">${slowGuard?"Unlock":"Claim"}</button>`; else if(oo&&!ripe)act=`<button data-a="r" ${held}>Reverse</button>`; else if(cb)act=`<button data-a="k" ${held}>Clawback</button>`; else act=`<button disabled>${io?"In":"Out"}</button>`; html+=`<div class="p"><span><b>${oo&&io?"↺":oo?"→":"←"} ${amt}</b><i>${note}</i></span>${act}</div>`; }); stale.forEach(s=>{html+=`<div class="p"><span><b>↩ ${trimAmt(s.amount,18)} ETH</b>` +`<i>Keeper tip on a settled transfer · unspent</i></span>` +`<button data-a="t" data-t="${s.tid}">Reclaim tip</button></div>`}); if(more)html+=`<div class="p" style="justify-content:center"><button data-a="m">Show ${Math.min(more,POSPAGE)} mo
0xacfba7ce…4082·#25,784,651·0x0d82ba1f…2118d0
v;syncDisabled(); stat.textContent="This browser blocked the address prompt — add the token with a #token=0x… link instead."; return} if(!a){sel.value=prev;update();return} stat.textContent="Loading token..."; try{ const idx=await addCustomToken(a); rebuild(); const otherSel=sel===fromSel?toSel:fromSel; if(String(idx)===otherSel.value){otherSel.value=prev;otherSel.dataset.prev=prev} sel.value=String(idx); sel.dataset.prev=sel.value; pairChosen=true; stat.textContent=""; syncDisabled(sel);syncNftUI(); refreshBalance(); update(); }catch(e){ const msg="Couldn't load token: "+(e.message||e); sel.value=prev; syncDisabled();syncNftUI();refreshBalance();update(); stat.textContent=msg; } }); } flip.onclick=()=>{pairChosen=true;const a=fromSel.value,b=toSel.value,v=outAmt.value.replace(/,/g,"");fromSel.value=b;toSel.value=a;if(v&&!isNaN(+v))amt.value=v;setMode("in");fitFont(amt);syncDisabled();syncNftUI();refreshBalance();loadChart();update()}; const sameAcct=(a,b)=>!!a&&!!b&&a.toLowerCase()===b.toLowerCase(); const wkBind=eth=>{ if(!eth||eth.__zb)return; eth.__zb=1; eth.on?.("chainChanged",cid=>{if(connecting||(+cid===1&&account))return;location.reload()}); eth.on?.("accountsChanged",accs=>{if(connecting||sameAcct(accs?.[0],account))return;location.reload()}); }; const wireWallet=eth=>{ if(!eth||wireWallet.done)return; wireWallet.done=true; wkBind(eth); (async()=>{ if(sessionStorage.getItem("dc"))return; try{ const accs=await rpc("eth_accounts",[]); const cid=await rpc(I,[]); if(accs[0]&&+cid===1){ account=accs[0]; showAddr(); const keep2=pairAddrs(); await loadTokenList(); adoptList(keep2); refreshBalance();update();loadPos();syncChart(); } }catch{} })(); (async()=>{ const self=selfFromUrl()||ZSWAP_SELF; if(!self)return; try{ if(+(await rpc(I,[]))!==CHAIN_ID)return; const ret=strip0x(await rpc(C,[{to:self,data:"0x"+SEL_LATEST},L])||""); if(ret.length!==64)return; let tip="0x"+ret.slice(24); const me=self.toLowerCase(); if(BigInt(tip)===0n||tip===me)return; const rd=async(to,sel)=>{const h=strip0x(await rpc(C,[{to,data:"0x"+sel},L])||"");return h.length===64?h:""}; const now=Date.now()/1000; for(let i=0;i<4&&tip!==me;i++){ const pv=await rd(tip,SEL_PREV); if(!pv)break; const prev="0x"+pv.slice(24); if(BigInt(prev)===0n)break; const at=await rd(prev,SEL_SUCCAT); const born=at?Number(BigInt("0x"+at)):0; if(!born||now>=born+MATURITY)break; tip=prev; } if(tip===me)return; const h=(location.hostname||"").split("."); const url=/^0x[0-9a-fA-F]{40}$/.test(h[0]||"") ?`${location.protocol}//${[tip,...h.slice(1)].join(".")}/` :`https://${tip}.w4eth.io/`; footAddr.innerHTML+=` · <a href="${url}" target="_blank" rel="noopener" ` +`title="A newer version exists on chain. This page is unchanged and always will be; the link opens the successor.">newer →</a>`; }catch{} })(); }; wireWallet(prov()); if(!prov())window.addEventListener("ethereum#initialized",()=>wireWallet(prov()),{once:true}); setTimeout(burnTotal,0); setTimeout(loadEthUsd,0); const SEL_TRANSFER="a9059cbb"; const SB2="0x000000dA7bb4B2A9E3e80e9A4D4157E26CA6189b"; const SB1="0x000000fF3D7A2d373615141d7489Ca66683DbecF"; const SBVIEW="0x000000E0b25449F32f7D9259aC449bA88E78dFCE"; const SWAPBOL="0x00000087A6dc5071779Ed1F8274A39230768B976"; const DUTCH="0x000000a213b430D14Bae6062c176289B05e04489"; const ORDERBOL="0x000000e6c7a12C80525ee74e7434aAb919447D95"; const FLOOR="0x00000080198137F790DA4C52bb902cf87c276748"; const FLOORVIEW="0x0000004E376e9dB5D9EC28E6711E1a64997C6ba7"; const SEL_NEXTID="2a58b330",SEL_GETORDERS="03652027",SEL_CANCELORD="514fcac7"; const SEL_FILL1="c37dfc5b",SEL_FILL2="9d136c7f",SEL_FILL2_UNWRAP="3987baf4",SEL_FILL2_ETH="6f608bab"; const SEL_RECENT="6a9849c1",SEL_RECENT_DUTCH="98035c9a",SEL_DUTCH_FILL="ae7a8260",SEL_DUTCH_CANCEL="40e58ee5"; const SEL_CANCEL_UNWRAP="21dd76f9",SEL_DUTCH_CANCEL_UNWRAP="8382de65"; const SEL_DUTCH_LISTING="de74e57b"; const SEL_CANDS="5f452988",SEL_DUTCH_CANDS="eb33e466",SEL_FILLPLAN="c277f67c"; const SEL_FILLPLAN_SWAP="9090c8e5",SEL_SNWAP="5f3bd1c8"; const SEL_COLL_BIDS="16bb24eb"; const SEL_FLOOR_CANDS="c587d927",SEL_RECENT_FLOOR="a5edd13d"; const SEL_ORDER_FLOOR="23e93357"; const SEL_BOL_FLOOR="b732d224"; let floorReady=null; async function floorRoutable(block){ if(FLOOR===ZERO||FLOORVIEW===ZERO)return false; if(floorReady!==null)return floorReady; try{ const h=await rpc(C,[{to:SWAPBOL,data:"0x"+SEL_BOL_FLOOR},block||L]); floorReady=("0x"+strip0x(h).slice(-40)).toLowerCase()===FLOOR.toLowerCase(); }catch{return false} return floorReady; } const SEL_BIDS="4423c5f1"; const SEL_INITIAL_B="878ea250",SEL_TOKENURI="c87b56dd",SEL_LOGOOF="6ce273ac"; const SEL_QUOTEFILL="f325beda"; async function preflightLeg(row,routeIn,routeOut,pay){ if(!row.floor)return preflightAsk(row); if(!await floorRoutable(L))throw Error("this executor cannot route bids yet"); const want=routeIn===ZERO?WETH:routeIn,gets=routeOut===ZERO?WETH:routeOut; let h; try{h=await rpc(C,[{to:FLOOR,data:"0x"+SEL_BIDS+encUint(row.id)},L])} catch{throw Error("bid could not be read — refresh and retry")} const at=i=>strip0x(h).slice(i*64,(i+1)*64); if(strip0x(h).length<11*64)throw Error("bid could not be read — refresh and retry"); const addr=i=>"0x"+at(i).slice(24); if(/^0x0{40}$/i.test(addr(0)))throw Error("bid is closed — refresh and retry"); if(BigInt("0x"+at(1)))throw Error("NFT bids are not routable from here"); if(addr(4).toLowerCase()!==want.toLowerCase() ||addr(6).toLowerCase()!==gets.toLowerCase()) throw Error("bid changed — refresh and retry"); if(BigInt("0x"+at(10))<pay)throw Error("bid partly taken — refresh and retry"); } async function preflightFills(fills,tokenIn,tokenOut){ if(!fills||!fills.length)return; const inA=(tokenIn===ZERO?WETH:tokenIn).toLowerCase(); const outA=(tokenOut===ZERO?WETH:tokenOut).toLowerCase(); const reads=fills.map(f=>f.board.toLowerCase()===FLOOR.toLowerCase() ?{to:FLOOR,data:"0x"+SEL_BIDS+encUint(f.id)} :f.board.toLowerCase()===DUTCH.toLowerCase() ?{to:DUTCH,data:"0x"+SEL_DUTCH_LISTING+encUint(f.id)} :{to:f.board,data:"0x"+SEL_GETORDERS+encUint(32)+encUint(1)+encUint(f.id)}); const res=await mc3Deep(reads); const stale=()=>{throw Error("an order in this route just changed — refreshing")}; for(let i=0;i<fills.length;i++){ const f=fills[i],raw=res[i]; if(!raw)stale(); const s=strip0x(raw),at=k=>BigInt("0x"+s.slice(k*64,(k+1)*64)); if(f.board.toLowerCase()===FLOOR.toLowerCase()){ if(s.length<11*64)stale(); const ad=k=>"0x"+s.slice(k*64+24,(k+1)*64); if(/^0x0{40}$/i.test(ad(0))||at(1))stale(); if(ad(4).toLowerCase()!==inA||ad(6).toLowerCase()!==outA)stale(); if(at(10)<f.pay)stale(); }else if(f.board.toLowerCase()===DUTCH.toLowerCase()){ if(s.length<640||at(9)===0n)stale(); }else{ const wds=WORDS(f.board.toLowerCase()===SB2.toLowerCase()?1:0); if(s.length<(2+wds)*64)stale(); const w=k=>at(2+k),ad=k=>"0x"+s.slice((2+k)*64+24,(3+k)*64); if(w(1)!==1n)stale(); const v2=wds===11,iA=v2?7:2,iB=v2?9:4; if(ad(iA).toLowerCase()!==outA||ad(iB).toLowerCase()!==inA)stale(); if(w(iB+1)<f.pay)stale(); } } } async function preflightAsk(row){ if(row.dutch){ let h; try{h=await rpc(C,[{to:DUTCH,data:"0x"+SEL_DUTCH_LISTING+encUint(row.id)},L])} catch{throw Error("listing could not be read — refresh and retry")} const s=strip0x(h); if(s.length<640)throw Error("listing could not be read — refresh and retry"); if(BigInt("0x"+s.slice(9*64,10*64))===0n)throw Error("listing is closed — refresh and retry"); return; } let h; try{h=await rpc(C,[{to:row.board, data:"0x"+SEL_GETORDERS+encUint(32)+encUint(1)+encUint(row.id)},L])} catch{throw Error("order could not be read — refresh and retry")} const s=strip0x(h),wds=WORDS(row.v2); if(s.length<(2+wds)*64)throw Error("order could not be read — refresh and retry"); const at=k=>BigInt("0x"+s.slice(128+k*64,128+(k+1)*64)); const ad=k=>"0x"+s.slice(128+k*64+24,128+(k+1)*64); if(at(1)!==1n)throw Error("order is no longer active — refresh and retry"); const iA=row.v2?7:2,iB=row.v2?9:4; if(ad(iA).toLowerCase()!==row.tA.toLowerCase() ||ad(iB).toLowerCase()!==row.tB.toLowerCase()) throw Error("order changed — refresh and retry"); if(at(iA+1)!==row.aA||at(iB+1)!==row.aB)throw Error("order changed — refresh and retry"); } const SEL_ORDER_FIXED="bcdb7936",SEL_ORDER_DUTCH="bbd94e1a",SEL_CHECKPOINT="a972985e"; const BOARDS=[{a:SB2,v2:1},{a:SB1,v2:0}]; const WORDS=b=>b?11:6; let bookSeq=0,bookRows=[],candCache={},bkOpen=LS.bk!=="0"; let busy=0; if(LS.wm==="weth"||LS.wm==="auto")ethMode.value=LS.wm; ethMode.onchange=()=>{LS.wm=ethMode.value}; const wantsUnwrap=()=>ethMode.value!=="weth"; const planWethPayment=async need=>{ let w=0n,e=0n; try{ const[wh,eh]=await Promise.all([ rpc(C,[{to:WETH,data:"0x"+SEL_BALANCEOF+encAddr(account)},L]), rpc("eth_getBalance",[account,L])]); w=BigInt(wh);e=BigInt(eh); }catch{return{rail:"weth",wrap:0n,w,e}} if(w>=need)return{rail:"weth",wrap:0n,w,e}; if(e>=need)return{rail:"eth",wrap:0n,w,e}; let gas=0n; try{gas=BigInt(await rpc("eth_gasPrice",[]))*400000n}catch{} const spendable=e>gas?e-gas:0n; if(w+spendable>=need)return{rail:"weth",wrap:need-w,w,e}; return{rail:"short",wrap:0n,w,e}; }; const assertFunded=async(token,need,meta)=>{ if(!need)return; let have; try{ have=token===ZERO ?BigInt(await rpc("eth_getBalance",[account,L])) :BigInt(await rpc(C,[{to:token,data:"0x"+SEL_BALANCEOF+encAddr(account)},L])); }catch{return} if(have>=need)return; const m=meta||known(token)||{sym:token===ZERO?"ETH":"tokens",dec:18}; throw Error(`Not enough ${m.sym} to fill: it costs ${trimAmt(need,m.dec)} ` +`and you hold ${trimAmt(have,m.dec)}.`); }; const known=a=>TOKENS.find(t=>t.addr.toLowerCase()===a.toLowerCase()); const sm=i=>i.replace('width="20" height="20"','width="15" height="15"'); const escan=(a,id)=>`<a href="https://etherscan.io/${id!=null?`nft/${a}/${id}`:`token/${a}`}" target="_blank" rel="noreferrer">${a.slice(0,6)}…${a.slice(-4)} ↗</a>`; const SEL_CONTRACTURI="e8a3d485"; const okDataImg=u=>typeof u==="string"&&u.length<=60000 &&/^data:image\/[a-z.+-]{2,20};base64,[A-Za-z0-9+/=]+$/i.test(u); const imgFromContractURI=raw=>{ const P="data:application/json;base64,"; const u=String(raw||"").trim(); if(!u.startsWith(P)||u.length>400000)return ""; let txt; try{ const bin=atob(u.slice(P.length)); txt=new TextDecoder().decode(Uint8Array.from(bin,c=>c.charCodeAt(0))); }catch{return ""} let j;try{j=JSON.parse(txt)}catch{return ""} return okDataImg(j&&j.image)?j.image:""; }; const logoCache={}; async function loadLogos(addrs){ const want=[...new Set(addrs.map(a=>a.toLowerCase()))] .filter(a=>a!==ZERO&&!known(a)&&logoCache[a]===undefined); if(!want.length)return; let raw=[]; if(TOKENLIST!==ZERO)raw=await mc3Deep(want.map(a=>({to:TOKENLIST,data:"0x"+SEL_LOGOOF+encAddr(a)}))).catch(()=>[]); want.forEach((a,i)=>{let u="";try{if(raw[i])u=safeUrl(decodeString(raw[i]))}catch{}logoCache[a]=u}); const self=want.filter(a=>!logoCache[a]); if(!self.length)return; const cu=await mc3Deep(self.map(a=>({to:a,data:"0x"+SEL_CONTRACTURI}))).catch(()=>[]); self.forEach((a,i)=>{try{ const u=imgFromContractURI(cu[i]?decodeString(cu[i]):""); if(u)logoCache[a]=u; }catch{}}); } async function leg(addr,amt,isNft,meta){ const k=known(addr); if(isNft)return{ic:genIcon("#"),txt:`#${amt}`,ok:0,link:escan(addr,amt)}; const raw=meta&&meta.sym?meta:await tokMeta(addr),m={...raw,sym:safeSym(raw.sym)}; const lg=k?"":logoCache[addr.toLowerCase()]; const ic=k?sm(k.icon):lg?`<img src="${lg}" width="15" height="15">`:genIcon(m.sym); return{ic,txt:`${trimAmt(amt,m.dec)} ${k?safeSym(k.sym):'“'+m.sym+'”'}`,ok:!!k,link:addr===ZERO?"ETH":escan(addr)}; } let floorSeq=0; nftId.addEventListener("input",()=>{syncOrderType();showFloor();render()}); async function showFloor(){ const el=document.getElementById("floorNote");if(!el)return; const side=isNft(fromSel.value)?fromSel.value:isNft(toSel.value)?toSel.value:null; const live=tab==="book"&&side!==null&&!nftId.value.trim(); if(!live){el.classList.add("hide");el.textContent="";return} const seq=++floorSeq,t=TOKENS[side]; el.classList.remove("hide");el.textContent="Checking floor bids…"; const rows=await collectionFloor(t.addr).catch(()=>[]); if(seq!==floorSeq)return; if(!rows.length){el.textContent="No standing bids on this collection.";return} const best=rows[0],q=known(best.quote); el.textContent=`${rows.length} collection bid${rows.length>1?"s":""} · best pays ` +`${trimAmt(best.proceeds,q?q.dec:18)} ${q?q.sym:"?"} for any ${t.sym}`; } function decBidRows(hex){ const out=[]; try{ const h=strip0x(hex),bytes=h.length/2; const word=b=>{if(b<0||b+32>bytes)throw 0;return BigInt("0x"+h.slice(b*2,(b+32)*2))}; const num=b=>{const v=word(b);if(v>0xffffffffffffffffn)throw 0;return Number(v)}; const arr=num(0); if(arr<64||arr+32>bytes)throw 0; const n=num(arr),base=arr+32; if(n>256||base+n*32>bytes)throw 0; for(let i=0;i<n;i++){ try{ const row=base+num(base+i*32); if(row+17*32>bytes)throw 0; const f=k=>word(row+k*32); const txt=k=>{const p=row+num(row+k*32),len=num(p); if(p+32+len>bytes)throw 0; return safeSym(new TextDecoder().decode(hexToBytes(h.slice((p+32)*2,(p+32+Math.min(len,1024))*2))))}; const dec=k=>{const v=num(row+k*32);return v>0&&v<=37?v-1:null}; out.push({ id:f(0), bidder:"0x"+h.slice((row+1*32+12)*2,(row+2*32)*2), token:"0x"+h.slice((row+2*32+12)*2,(row+3*32)*2), quote:"0x"+h.slice((row+3*32+12)*2,(row+4*32)*2), isNFT:f(4)!==0n, anyId:f(5)!==0n, remaining:f(7), initial:f(8), price:f(9), proceeds:f(10), expiry:f(12), tDec:dec(13),qDec:dec(14), tSym:txt(15),qSym:txt(16), }); }catch{continue} } return out; }catch{return out} } async function collectionFloor(collection){ if(FLOORVIEW===ZERO||FLOOR===ZERO)return[]; const data="0x"+SEL_COLL_BIDS+encAddr(FLOOR)+encAddr(collection)+encAddr(ZERO) +encUint(0)+encUint(16)+encUint(256); const raw=await rpc(C,[{to:FLOORVIEW,data},L]).catch(()=>null); if(!raw)return[]; const rows=decBidRows(raw); return rows.filter(r=>r.anyId&&r.remaining>0n) .sort((a,b)=>{const l=b.proceeds*a.remaining,r=a.proceeds*b.remaining;return l>r?1:l<r?-1:0}); } async function floorCandidates(tokenIn,tokenOut,block){ const bin=tokenIn===ZERO?WETH:tokenIn,bout=tokenOut===ZERO?WETH:tokenOut; const data="0x"+SEL_FLOOR_CANDS+encAddr(FLOOR)+encAddr(bin)+encAddr(bout) +encUint(0)+encUint(64)+encUint(512); const raw=await rpc(C,[{to:FLOORVIEW,data},block]).catch(()=>null); if(!raw)return[]; const out=[]; for(const b of decBidRows(raw)){ if(b.isNFT||b.remaining<=0n||b.initial<=0n||b.price<=0n||b.proceeds<=0n)continue; if(b.token.toLowerCase()!==bin.toLowerCase())continue; if(b.quote.toLowerCase()!==bout.toLowerCase())continue; out.push({ id:b.id,board:FLOOR,floor:1,v2:0,dutch:0, maker:b.bidder,pf:1,exp:b.expiry,nA:0,nB:0,cp:ZERO, tA:b.quote,aA:b.proceeds,sA:b.qSym,dA:b.qDec, tB:b.token,aB:b.remaining,sB:b.tSym,dB:b.tDec, price:b.price,initial:b.initial, }); } return out; } function floorGet(o,pay){return o.price*pay/o.initial} function floorPay(o,want){ if(want<=0n)return 0n; const g=(want*o.initial+o.price-1n)/o.price; return g>o.aB?o.aB:g; } function rowGet(o,pay){return o.floor?floorGet(o,pay):o.aA*pay/o.aB} function decViewPage(hex){ const h=strip0x(hex),bytes=h.length/2; const word=b=>{if(b<0||b+32>bytes)throw Error("bad lens page");return BigInt("0x"+h.slice(b*2,(b+32)*2))}; const addr=(b)=>"0x"+h.slice((b+12)*2,(b+32)*2); const arr=Number(word(0)),next=word(32); if(!Number.isSafeInteger(arr)||arr<64||arr+32>bytes)throw Error("bad lens offset"); const n=Number(word(arr)),head=arr+32; if(!Number.isSafeInteger(n)||n>256||head+n*32>bytes)throw Error("bad lens length"); const text=(base,k)=>{const off=Number(word(base+k*32)),p=base+off,len=Number(word(p)); if(!Number.isSafeInteger(off)||!Number.isSafeInteger(len)||p+32+len>bytes)throw Error("bad lens string"); const cap=Math.min(len,1024),a=new Uint8Array(cap); for(let i=0;i<cap;i++)a[i]=parseInt(h.slice((p+32+i)*2,(p+33+i)*2),16); return new TextDecoder().decode(a)}; const rows=[]; const dec=v=>{const d=Number(v);return Number.isInteger(d)&&d>=0&&d<=36?d:null}; for(let i=0;i<n;i++){ try{ const off=Number(word(head+i*32)),b=head+off; if(!Number.isSafeInteger(off)||b+16*32>bytes)throw Error("bad lens row"); rows.push({id:word(b),maker:addr(b+32),pf:word(b+64)===1n,exp:word(b+96), nA:word(b+128)===1n,nB:word(b+160)===1n,cp:addr(b+192), tA:addr(b+224),aA:word(b+256),sA:safeSym(text(b,9)),dA:dec(word(b+320)), tB:addr(b+352),aB:word(b+384),sB:safeSym(text(b,13)),dB:dec(word(b+448)),board:addr(b+480)}); }catch{continue} } return{rows,next}; } const betterRate=(a,b)=>a.aA*b.aB>b.aA*a.aB; const MAX_BOOK_LEGS=32,AON_PAIR_SEEDS=24; function planBookExactIn(rows,amountIn,ammOut,ammIn){ if(!amountIn||!ammIn)return null; let c=rows.filter(o=>!o.nA&&!o.nB&&o.aA&&(o.aB||o.dutch)&&(o.pf||o.aB<=amountIn) &&(!o.floor||(o.price>0n&&o.initial>0n)) &&!(account&&o.maker&&o.maker.toLowerCase()===account.toLowerCase())); const candidateCapped=c.length>256; const aonCount=c.reduce((n,o)=>n+!o.pf,0); const aonBounded=aonCount>=3; c.sort((a,b)=>betterRate(a,b)?-1:betterRate(b,a)?1:0); if(c.length>256)c.length=256; const build=(seed=-1,seed2=-1)=>{ let rem=amountIn,bin=0n,bout=0n,fills=[],legCapped=false; const take=o=>{ if(o.aA*ammIn<=ammOut*o.aB)return; if(!o.pf&&o.aB>rem)return; if(fills.length>=MAX_BOOK_LEGS){legCapped=true;return} if(o.aB===0n){ fills.push({id:o.id,board:o.board,pay:0n,get:o.aA,part:false}); bout+=o.aA;return; } const pay=o.aB<rem?o.aB:rem; if(!pay)return; const get=o.pf?(pay===o.aB?o.aA:rowGet(o,pay)):o.aA; if(!get)return; fills.push({id:o.id,board:o.board,pay,get,part:o.pf&&pay<o.aB}); rem-=pay;bin+=pay;bout+=get; }; if(seed>=0)take(c[seed]); if(seed2>=0)take(c[seed2]); for(let i=0;i<c.length;i++)if(i!==seed&&i!==seed2)take(c[i]); return{fills,bookIn:bin,bookOut:bout,ammIn:rem,score:bout+rem*ammOut/ammIn, bounded:candidateCapped||aonBounded||legCapped}; }; let best=build(-1); for(let i=0;i<c.length;i++)if(!c[i].pf){ const p=build(i);if(p.score>best.score)best=p; } const aon=[];for(let i=0;i<c.length&&aon.length<AON_PAIR_SEEDS;i++)if(!c[i].pf)aon.push(i); for(let i=0;i<aon.length;i++)for(let j=i+1;j<aon.length;j++){ const p=build(aon[i],aon[j]);if(p.score>best.score)best=p; } return best.fills.length?best:null; } const cheaperRate=(a,b)=>a.aB*b.aA<b.aB*a.aA; function planBookExactOut(rows,amountOut,ammIn,ammOut){ if(!amountOut||!ammOut)return null; let c=rows.filter(o=>!o.nA&&!o.nB&&o.aA&&(o.aB||o.dutch)&&(o.pf||o.aA<=amountOut) &&(!o.floor||(o.price>0n&&o.initial>0n)) &&!(account&&o.maker&&o.maker.toLowerCase()===account.toLowerCase())); const candidateCapped=c.length>256; const aonCount=c.reduce((n,o)=>n+!o.pf,0); const aonBounded=aonCount>=3; c.sort((a,b)=>cheaperRate(a,b)?-1:cheaperRate(b,a)?1:0); if(c.length>256)c.length=256; const build=(seed=-1,seed2=-1)=>{ let left=amountOut,bin=0n,bout=0n,fills=[],legCapped=false; const take=o=>{ if(o.aB*ammOut>=ammIn*o.aA)return; if(!o.pf&&o.aA>left)return; if(fills.length>=MAX_BOOK_LEGS){legCapped=true;return} const want=o.aA<left?o.aA:left; let pay,get; if(o.aB===0n){pay=0n;get=want} else if(want===o.aA){pay=o.aB;get=o.aA} else if(o.floor){ pay=floorPay(o,want); get=floorGet(o,pay); } else{ pay=(want*o.aB+o.aA-1n)/o.aA; if(pay>o.aB)pay=o.aB; get=pay*o.aA/o.aB; } if(!get)return; fills.push({id:o.id,board:o.board,pay,get,part:want!==o.aA}); bin+=pay;bout+=get;left-=get<left?get:left; }; if(seed>=0)take(c[seed]); if(seed2>=0)take(c[seed2]); for(let i=0;i<c.length&&left;i++)if(i!==seed&&i!==seed2)take(c[i]); return{fills,bookIn:bin,bookOut:bout,ammOut:left,score:bin+left*ammIn/ammOut, bounded:candidateCapped||aonBounded||legCapped}; }; let best=build(-1); for(let i=0;i<c.length;i++)if(!c[i].pf){ const p=build(i);if(p.score<best.score)best=p; } const aon=[];for(let i=0;i<c.length&&aon.length<AON_PAIR_SEEDS;i++)if(!c[i].pf)aon.push(i); for(let i=0;i<aon.length;i++)for(let j=i+1;j<aon.length;j++){ const p=build(aon[i],aon[j]);if(p.score<best.score)best=p; } return best.fills.length?best:null; } function encFillPlan(fills,tokenIn,tokenOut,recipient,refundTo,deadline){ let tail=encUint(fills.length); for(const f of fills)tail+=encUint(f.id)+encAddr(f.board)+encUint(f.pay)+encUint(f.get)+encBool(f.part); return"0x"+SEL_FILLPLAN+encAddr(tokenIn)+encAddr(tokenOut)+encAddr(recipient)+encAddr(refundTo) +encUint(deadline)+encUint(192)+tail; } function encFillPlanAndSwap(fills,tokenIn,tokenOut,recipient,refundTo,deadline,ammIn,ammData){ let ft=encUint(fills.length); for(const f of fills)ft+=encUint(f.id)+encAddr(f.board)+encUint(f.pay)+encUint(f.get)+encBool(f.part); const d=strip0x(ammData||"0x"),dt=encUint(d.length/2)+d.padEnd(Math.ceil(d.length/64)*64,"0"); return"0x"+SEL_FILLPLAN_SWAP+encAddr(tokenIn)+encAddr(tokenOut)+encAddr(recipient)+encAddr(refundTo) +encUint(deadline)+encUint(256)+encUint(ammIn)+encUint(256+ft.length/2)+ft+dt; } function encSnwap(tokenIn,amountIn,recipient,tokenOut,minOut,executor,data){ const d=strip0x(data),body=encUint(d.length/2)+d.padEnd(Math.ceil(d.length/64)*64,"0"); return"0x"+SEL_SNWAP+encAddr(tokenIn)+encUint(amountIn)+encAddr(recipient)+encAddr(tokenOut) +encUint(minOut)+encAddr(executor)+encUint(224)+body; } const encCheckpoint=(executor,token,recipient)=>encSnwap( ZERO,0n,recipient,ZERO,0n,executor,"0x"+SEL_CHECKPOINT+encAddr(token) ); const encSweep=(token,amount,to)=>"0x"+SEL_SWEEP+encAddr(token)+encUint(0)+encUint(amount)+encAddr(to); function encPermit2Hybrid(ammData,fills,tokenIn,tokenOut,recipient,refundTo,deadline,bookIn,bookOut){ const bookData=encFillPlan(fills,tokenIn,tokenOut,recipient,refundTo,deadline); const steps=[encCheckpoint(SWAPBOL,tokenIn,refundTo)]; if(bookIn)steps.push(encSweep(tokenIn,bookIn,SWAPBOL)); steps.push(encSnwap(ZERO,0n,recipient,tokenOut,bookOut,SWAPBOL,bookData)); if(ammData&&ammData!=="0x")steps.push(ammData); steps.push(encSweep(tokenIn,0n,refundTo)); return encCalls(steps); } async function swapCandidates(tokenIn,tokenOut,block){ const key=tokenIn.toLowerCase()+":"+tokenOut.toLowerCase(),old=candCache[key]; if(old&&Date.now()-old.at<15000&&old.block===block){ old.rows.incomplete=old.incomplete;old.rows.unavailable=old.unavailable; return old.rows; } const snapshotAt=Date.now(); const boardIn=tokenIn===ZERO?WETH:tokenIn; const boardOut=tokenOut===ZERO?WETH:tokenOut; const [lc,le,ld]=await Promise.all([ hasCode(SBVIEW,block),hasCode(SWAPBOL,block), DUTCH!==ZERO?hasCode(DUTCH,block):false ]); if(!lc||!le){ const rows=[];rows.unavailable=true;return rows; } const specs=[...BOARDS]; if(ld){ specs.push({a:DUTCH,dutch:1,ti:tokenIn}); if(tokenIn===ZERO)specs.push({a:DUTCH,dutch:1,ti:WETH}); } const groups=await Promise.all(specs.map(async b=>{ let cursor=0n,out=[],turns=0,incomplete=false,failed=false; try{do{ const ti=b.dutch?b.ti:boardIn,to=boardOut; const data="0x"+(b.dutch?SEL_DUTCH_CANDS:SEL_CANDS)+encAddr(b.a) +(b.dutch?"":encBool(b.v2))+encAddr(ti)+encAddr(to)+encAddr(SWAPBOL) +encUint(cursor)+encUint(512); const p=decViewPage(await rpc(C,[{to:SBVIEW,data},block])); out.push(...p.rows.map(r=>({...r,v2:b.v2||0,dutch:b.dutch||0}))); cursor=p.next; ++turns; }while(cursor&&turns<64)}catch{failed=true;incomplete=true} incomplete=incomplete||!!cursor; return{out,incomplete,failed}; })); let rows=groups.flatMap(g=>g.out); if(tokenIn===ZERO&&tokenOut.toLowerCase()===WETH.toLowerCase()){ rows=rows.filter(r=>r.dutch&&r.tA.toLowerCase()===WETH.toLowerCase()&&r.tB===ZERO); } else if(await floorRoutable(block)){ rows=rows.concat(await floorCandidates(tokenIn,tokenOut,block).catch(()=>[])); } const incomplete=groups.some(g=>g.incomplete); const unavailable=groups.every(g=>g.failed&&!g.out.length); rows.incomplete=incomplete; rows.unavailable=unavailable; const current=candCache[key],bn=BigInt(block),cn=current?BigInt(current.block):-1n; if(!current||bn>cn||(bn===cn&&snapshotAt>=current.at)){ candCache[key]={at:snapshotAt,block,rows,incomplete,unavailable}; } for(const k in candCache)if(k!==key&&snapshotAt-candCache[k].at>60000)delete candCache[k]; return rows; } async function lensBookRows(my){ const [sv,dv]=await Promise.all(
0xacfba7ce…4082·#25,784,650·0x21e5a9ea…63d1d8
ethod,params||[]): read(method,params), on:()=>{}}} let wcRead=true; const wkWrap=document.getElementById("wkWrap"),wkList=document.getElementById("wkList"); const wkChoices=()=>W6963.map(d=>({name:d.info.name,icon:d.info.icon,get:()=>d.provider})) .concat([{name:"WalletConnect",svg:WC_ICON,get:wcConnect}]); const wkPick=cs=>new Promise(res=>{ wkList.textContent=""; let done=false; const end=v=>{if(done)return;done=true; wkWrap.classList.add("hide");removeEventListener("keydown",esc);res(v)}; const esc=e=>{if(e.key==="Escape")end(null)}; cs.forEach(c=>{ const b=document.createElement("button"); b.type="button";b.className="tkr";b.setAttribute("role","option"); if(c.icon){const im=document.createElement("img");im.src=c.icon;im.alt="";b.appendChild(im)} else if(c.svg){const sp=document.createElement("span");sp.className="wki";sp.innerHTML=c.svg;b.appendChild(sp)} b.appendChild(document.createTextNode(c.name)); b.onclick=()=>end(c); wkList.appendChild(b)}); wkWrap.onclick=e=>{if(e.target===wkWrap)end(null)}; addEventListener("keydown",esc); wkWrap.classList.remove("hide"); wkList.firstChild?.focus()}); async function wkResolve(){ if(PROV)return true; wkScan(); const inj=window.ethereum; if(W6963.length<2&&inj)return true; const cs=wkChoices(); const c=await wkPick(cs); if(!c)return false; stat.textContent="Connecting…"; try{PROV=await c.get()} catch(e){if(!isRejection(e))err(e);return false} wkBind(PROV); return true} async function connect(){ if(connecting)return; connecting=true; try{ if(!await wkResolve())return; if(!prov()){stat.textContent=NO_WALLET;return} swap.textContent="Connecting…";swap.disabled=true; lnGo.textContent="Connecting…";lnGo.disabled=true; let accs; try{accs=await rpc("eth_requestAccounts",[])} catch(e){err(e);return} if(!accs[0])return; let chainId=await rpc(I,[]); if(+chainId!==1){ try{await rpc("wallet_switchEthereumChain",[{chainId:"0x1"}])} catch(e){stat.textContent=isRejection(e)?"":"Switch to Ethereum mainnet.";return} chainId=await rpc(I,[]); if(+chainId!==1){stat.textContent="Switch to Ethereum mainnet.";return} } sessionStorage.removeItem("dc"); account=accs[0]; const keepPair=pairAddrs(); await loadTokenList(); adoptList(keepPair); showAddr(); refreshBalance(); update(); fcMine(); loadPos(); syncChart(); if(lnMode)lnQuote(); }finally{connecting=false;swap.disabled=false; lnGo.disabled=false;lnGo.textContent=account?"Launch":"Connect Wallet";render()} } const lqReserveGas=()=>lqList.querySelector(".lqnew")?LQ_CREATE_GAS+300000n:400000n; const lqFillMax=(tok,max)=>{ const box=lqList.querySelector(".lqadd:not(.hide)"); const pair=chPair(); if(!box||!pair)return false; const side=pair[0].addr.toLowerCase()===tok.addr.toLowerCase()?"0":"1"; const el=box.querySelector(`.lqin[data-side="${side}"]`); if(!el)return false; el.value=formatUnits(max,tok.dec); el.dispatchEvent(new Event("input",{bubbles:true})); return true; }; bal1.addEventListener("click",async e=>{ if(e.target.tagName!=="A")return; e.preventDefault(); const t=TOKENS[toSel.value]; if(!t)return; let reserve=0n; if(t.addr===ZERO){ try{reserve=BigInt(await rpc("eth_gasPrice",[]))*lqReserveGas()}catch{} } const max=toBalance>reserve?toBalance-reserve:0n; if(lqFillMax(t,max))return; outAmt.value=formatUnits(max,t.dec); fitFont(outAmt); lqPreviewSoon(); }); bal.addEventListener("click",async e=>{ if(e.target.tagName!=="A")return; e.preventDefault(); const f=TOKENS[fromSel.value]; if(!f)return; let reserve=0n; if(f.addr===ZERO){ try{reserve=BigInt(await rpc("eth_gasPrice",[])) *(lqMode?lqReserveGas():tab==="swap"?900000n:tab==="book"?350000n :+dly.value?(tipCk.checked?200000n+TIP_GAS:200000n):30000n)}catch{} } const max=fromBalance>reserve?fromBalance-reserve:0n; if(lqMode&&lqFillMax(f,max))return; amt.value=formatUnits(max,f.dec); fitFont(amt); if(lqMode){lqPreviewSoon();return} setMode("in"); update(); }); let seq=0; const REVERTS={ "1ab7da6b":"That took too long and the deadline passed, so nothing was placed. Try again — you were not charged for an order.", "98c2c368":"The order was refused as malformed. Check the amounts and the expiry.", "7939f424":"The token transfer was refused — usually a missing or spent approval. Try again and approve first.", "90b8ec18":"The token transfer was refused by the token itself.", "3e3f8f73":"The approval was refused by the token.", "b12d13eb":"Ether could not be sent to the recipient — a contract there may not accept it.", "ab143c06":"That call re-entered and was stopped.", }; const explain=e=>{ if(e?.noWallet)return NO_WALLET; const s=String(e?.data?.data||e?.data||e?.message||e||""); const m=/0x([0-9a-fA-F]{8})\b/.exec(s); const hit=m&&REVERTS[m[1].toLowerCase()]; return hit||("Error: "+s.slice(0,110)); }; const err=e=>stat.textContent=isRejection(e)?"":explain(e); const settle=async tx=>{ if(!tx){stat.textContent="Done";return} if(!/^0x[0-9a-f]{64}$/i.test(tx))throw Error("bad tx"); stat.innerHTML=txLink(tx,"Sent");await waitTx(tx);stat.innerHTML=txLink(tx,"Done")}; const txLink=(h,label)=>`${label} <a href="https://etherscan.io/tx/${h}" target="_blank" rel="noreferrer">${h.slice(0,10)}…${h.slice(-6)}</a>`; let autoQ=0; async function update(auto){ if(!auto)autoQ=0; syncIcons(); fcSync(); if(lqMode)return loadLq(); if(tab==="book")return render(); if(tab==="send")return updateSend(); const my=++seq; quoting=false; last=null; rate.textContent=""; if(!lqMode&&!lnMode)rcvHdr.textContent="You receive"; render(); const f=TOKENS[fromSel.value], t=TOKENS[toSel.value]; if(!f||!t)return; if(f.addr===t.addr){stat.textContent="Pick different tokens.";return} stat.textContent=""; const isIn=mode==="in"; const inEl=isIn?amt:outAmt, outEl=isIn?outAmt:amt, inTok=isIn?f:t; outEl.value=""; fitFont(outEl); if(!inEl.value.trim())return; let swapAmt; try{swapAmt=parseUnits(inEl.value.trim(),inTok.dec)} catch(e){const q=inEl.value.trim(); if(q&&!/\.$/.test(q))stat.textContent=/decimals/.test(e.message)?`${inTok.sym} has ${inTok.dec} decimals`:"Invalid amount"; return} if(swapAmt<=0n)return; const rv=rc.value.trim(); let rcv=""; if(rv){ const fail=m=>{rc.classList.add("bad");rcvEl.textContent=""; outEl.value="";fitFont(outEl);stat.textContent=m}; const bad=rcErr(rv); if(bad){fail(bad);return} if(isAddr(rv))rcv=rv; else{ rcvEl.textContent="resolving..."; try{rcv=await rcvOf(rv)} catch(e){if(my!==seq)return;fail(e&&e.offchain?e.message:"Name not registered");return} if(my!==seq)return; if(!rcv){fail("Name not registered");return} } rcvEl.textContent=rcv; }else rcvEl.textContent=""; rc.classList.remove("bad"); if(!rv&&f.addr===WETH&&t.addr===ZERO){ outEl.value=inEl.value;fitFont(outEl); rate.textContent=`1 ${f.sym} = 1 ${t.sym}`; last={callData:"0x2e1a7d4d"+encUint(swapAmt),msgValue:0n,amountIn:swapAmt,from:f,to:WETH,recipient:"",exp:Date.now()+QUOTE_TTL}; render();return; } if(!rv&&f.addr===ZERO&&t.addr===WETH){ outEl.value=inEl.value;fitFont(outEl); rate.textContent=`1 ${f.sym} = 1 ${t.sym}`; last={callData:"0xd0e30db0",msgValue:swapAmt,amountIn:swapAmt,from:f,to:WETH,recipient:"",exp:Date.now()+QUOTE_TTL}; render();return; } const pct=Math.min(10,Math.max(.01,+slip.value||.5)); const slipBps=BigInt(Math.round(pct*100)); const deadline=BigInt(Math.floor(Date.now()/1e3)+1800); outEl.value="...";fitFont(outEl);quoting=true;render(); const acct=account; const gaveUp0=mcGaveUp,ok0=mcOk,failed0=mcFailed; try{ const a=encAddr(rcv||acct||ZERO),rf=encAddr(acct||ZERO),tl=encBool(!isIn)+encAddr(f.addr)+encAddr(t.addr)+encUint(swapAmt)+encUint(slipBps)+encUint(deadline); const _rq=[]; const Q=(x,d,u,v,S,sb,mv)=>{_rq.push({to:ZQUOTER,data:"0x"+x+d,u,v,S,sb,mv});return _rq.length-1}; let r=null; const candKey=f.addr.toLowerCase()+":"+t.addr.toLowerCase(); const cachedCandidates=candCache[candKey]; const quoteBlock=cachedCandidates&&Date.now()-cachedCandidates.at<15000 ?cachedCandidates.block:await blockNow(); const candJob=acct?swapCandidates(f.addr,t.addr,quoteBlock).catch(()=>{ const x=[];x.unavailable=true;return x }):Promise.resolve([]); const gasPriceJob=acct?rpc("eth_gasPrice",[]).then(BigInt).catch(()=>0n):Promise.resolve(0n); const v4Job=isIn?quoteV4Pools(f,t,swapAmt,quoteBlock).catch(()=>null):Promise.resolve(null); const preJob=isIn?quotePrecision(f,t,swapAmt,PROUTE,quoteBlock).catch(()=>null):Promise.resolve(null); const SRC_CURVE=5; let curveOnlyOut=false; const exactOutSafe=y=>{if(isIn||!y.sources||!y.sources.includes(SRC_CURVE))return true;curveOnlyOut=true;return false}; const pick=y=>{if(!y)return;if(!exactOutSafe(y))return;const v=isIn?y.best.amountOut:y.best.amountIn;if(!v)return; if(!r||(isIn?v>r.best.amountOut:v<r.best.amountIn))r=y}; const tl0=encBool(!isIn)+encAddr(f.addr)+encAddr(t.addr)+encUint(swapAmt)+encUint(0n)+encUint(deadline); const jobs=[ [Q("e7798987",a+tl,0,4,0,0,6)], [Q("e453166e",a+rf+tl0,4,9),Q("e453166e",a+rf+tl,4,9)], [Q("4c464f59",a+tl0,8,13),Q("4c464f59",a+tl,8,13)]]; let splitSlip=0n; if(isIn){ const w=slipBps*3n,SS=w<150n?150n:w>500n?500n:w; splitSlip=SS; const ps=encAddr(f.addr)+encAddr(t.addr)+encUint(swapAmt)+encUint(SS)+encUint(deadline); for(const x of["892af013","85f86a90"])jobs.push([Q(x,a+ps,4,8,1,SS)]); } const tailStep=swapAmt/100n||1n,tailAmt=swapAmt>tailStep?swapAmt-tailStep:0n; const tailJobs=[]; if(tailAmt){ const z=encBool(!isIn)+encAddr(f.addr)+encAddr(t.addr)+encUint(tailAmt)+encUint(0n)+encUint(deadline); tailJobs.push(Q("e453166e",a+rf+z,4,9),Q("4c464f59",a+z,8,13)); if(isIn)for(const x of["892af013","85f86a90"]){ const ps=encAddr(f.addr)+encAddr(t.addr)+encUint(tailAmt)+encUint(splitSlip)+encUint(deadline); tailJobs.push(Q(x,a+ps,4,8,1,splitSlip)); } } const refAmt=swapAmt/REF_N; const iRef=refAmt>0n ?Q("e453166e",a+rf+encBool(!isIn)+encAddr(f.addr)+encAddr(t.addr)+encUint(refAmt)+encUint(0n)+encUint(deadline),4,9) :-1; const mcRaw=(await Promise.all(mc3Split(_rq,QPAIR) .map(g=>mc3Deep(g.map(q=>({to:q.to,data:q.data})),quoteBlock)))).flat(); if(my!==seq)return; const dec=_rq.map((q,i)=>{try{return mcRaw[i]?decQ(mcRaw[i],q.sb||slipBps,!isIn,q.u,q.v,q.S,q.mv):null}catch{return null}}); for(const g of jobs)pick(g.length>1?merge(dec[g[0]],dec[g[1]]):dec[g[0]]); if(isIn&&!(dec[jobs[1][0]]||dec[jobs[1][1]]||dec[jobs[2][0]]||dec[jobs[2][1]])){ const hq=await hubRoute(f,t,swapAmt,slipBps,deadline,rcv||acct||ZERO,acct||ZERO,quoteBlock).catch(()=>null); if(my!==seq)return; if(hq)pick(hq); } let tail=null; for(const i of tailJobs){const y=dec[i];if(!y)continue; const v=isIn?y.best.amountOut:y.best.amountIn; if(v&&(!tail||(isIn?v>tail.best.amountOut:v<tail.best.amountIn)))tail=y; } if(my!==seq)return; const v4q=await v4Job; let v4c=null; if(v4q&&v4q.amountOut>0n){ const v4Min=v4q.amountOut-(v4q.amountOut*slipBps)/10000n; const p=v4q.pool; v4c={best:{source:4,amountIn:v4q.amountIn,amountOut:v4q.amountOut,feeBps:BigInt(p.fee)/100n}, callData:"0x"+SEL_V4SWAP+encPoolKey(p)+encBool(p.zeroForOne)+encUint(v4q.amountIn) +encUint(v4Min)+encAddr(rcv||acct||ZERO)+encUint(deadline), fundedCallData:null,amountLimit:v4Min,msgValue:f.addr===ZERO?swapAmt:0n, to:V4PORT,spender:f.addr===ZERO?null:V4PORT}; if(!r)r=v4c; } const pq=await preJob; if(my!==seq)return; let pc=null; if(pq&&pq.out>0n){ const pMin=pq.out-(pq.out*slipBps)/10000n; const pNative=pq.tokenIn===ZERO; const pWethIn=pNative&&(pq.unwrap||0n)>0n; const pTo=rcv||acct||ZERO; const pFund=pWethIn?WETH:pq.tokenIn; const routeData=pWethIn ?encPRouteWeth(pq.pools,pq.tokenOut,swapAmt,pMin,pTo) :encPRoute(pq.pools,pq.tokenIn,pq.tokenOut,swapAmt,pMin,pTo); const routedLeg=encSnwap(pFund,swapAmt,pTo,pq.tokenOut,pMin,PROUTE,routeData); const checkpointLeg=encSnwap(ZERO,0n,acct||ZERO,ZERO,0n,PROUTE, "0x"+SEL_PCHECKPOINT+encAddr(pFund)+strip0x(keccak(hexToBytes(routeData)))+encAddr(acct||ZERO)); const pCall=(pNative&&!pWethIn)?routedLeg:encCalls([checkpointLeg,routedLeg]); const pFunded=(pNative&&!pWethIn)?null:encCalls([ checkpointLeg, encSweep(pFund,swapAmt,PROUTE), encSnwap(ZERO,0n,pTo,pq.tokenOut,pMin,PROUTE,routeData), ]); pc={best:{source:SRC_PRECISION,amountIn:swapAmt,amountOut:pq.out,feeBps:pq.fee/100n}, callData:pCall, fundedCallData:pFunded,amountLimit:pMin,msgValue:(pNative&&!pWethIn)?swapAmt:0n, wrap:pq.wrap||0n,outDiffers:!!pq.outDiffers,poolOut:pq.tokenOut, to:null,spender:(pNative&&!pWethIn)?null:ZROUTER,pools:pq.pools}; if(!r)r=pc; } if(!r&&curveOnlyOut)throw Error(`exact-output is unavailable for this pair — enter an amount to PAY instead`); if(!r)throw Error("bad quote"); if(r.best.amountIn<=0n||r.best.amountOut<=0n||(!isIn&&r.amountLimit<r.best.amountIn))throw Error("bad quote"); let hybrid=null,bookIncomplete=false,bookUnavailable=false,plannerBounded=false; if(acct){ const candidates=await candJob; bookIncomplete=!!candidates.incomplete; bookUnavailable=!!candidates.unavailable; if(my!==seq)return; const quoteRem=async(x,eo)=>{ const qs=[],groups=[],add=(sel,data,u,v,S,sb)=>{ qs.push({data:"0x"+sel+data,u,v,S,sb});return qs.length-1}; const z0=encBool(eo)+encAddr(f.addr)+encAddr(t.addr)+encUint(x)+encUint(0n)+encUint(deadline); const zs=encBool(eo)+encAddr(f.addr)+encAddr(t.addr)+encUint(x)+encUint(slipBps)+encUint(deadline); groups.push([add("e453166e",a+rf+z0,4,9),add("e453166e",a+rf+zs,4,9)]); groups.push([add("4c464f59",a+z0,8,13),add("4c464f59",a+zs,8,13)]); if(!eo){ const w=slipBps*3n,ss=w<150n?150n:w>500n?500n:w; const ps=encAddr(f.addr)+encAddr(t.addr)+encUint(x)+encUint(ss)+encUint(deadline); for(const sel of["892af013","85f86a90"])groups.push([add(sel,a+ps,4,8,1,ss)]); } const raw=await mc3Deep(qs.map(q=>({to:ZQUOTER,data:q.data})),quoteBlock); const ds=qs.map((q,i)=>{try{return raw[i]?decQ(raw[i],q.sb||slipBps,eo,q.u,q.v,q.S):null}catch{return null}}); let best=null; for(const g of groups){ const y=g.length>1?merge(ds[g[0]],ds[g[1]]):ds[g[0]]; if(!y)continue; if(eo&&y.sources&&y.sources.includes(SRC_CURVE))continue; const v=eo?y.best.amountIn:y.best.amountOut; if(v&&(!best||(eo?v<best.best.amountIn:v>best.best.amountOut)))best=y; } return best; }; const gasValue=async wei=>{ const token=isIn?t.addr:f.addr; if(token===ZERO||token.toLowerCase()===WETH.toLowerCase())return wei; const eo=!isIn,ti=isIn?ZERO:token,to=isIn?token:ZERO; const z=encBool(eo)+encAddr(ti)+encAddr(to)+encUint(wei)+encUint(0n)+encUint(deadline); try{ const raw=await rpc(C,[{to:ZQUOTER,data:"0xe453166e"+a+rf+z},quoteBlock]); const q=decQ(raw,0n,eo,4,9); return isIn?q.best.amountOut:q.best.amountIn; }catch{return null} }; const tailDelta=swapAmt-tailAmt; const seedOut=isIn&&tail&&r.best.amountOut>tail.best.amountOut ?r.best.amountOut-tail.best.amountOut:r.best.amountOut; const seedIn=!isIn&&tail&&r.best.amountIn>tail.best.amountIn ?r.best.amountIn-tail.best.amountIn:r.best.amountIn; let p=isIn ?planBookExactIn(candidates,swapAmt,seedOut,tail&&seedOut!==r.best.amountOut?tailDelta:r.best.amountIn) :planBookExactOut(candidates,swapAmt,seedIn,tail&&seedIn!==r.best.amountIn?tailDelta:r.best.amountOut); plannerBounded=!!(p&&p.bounded); let amm=null,ammFor=null; for(let round=0;p&&(isIn?p.ammIn:p.ammOut)&&round<2;round++){ const rem=isIn?p.ammIn:p.ammOut; amm=await quoteRem(rem,!isIn).catch(()=>null);ammFor=rem; if(!amm)break; const next=isIn ?planBookExactIn(candidates,swapAmt,amm.best.amountOut,rem) :planBookExactOut(candidates,swapAmt,amm.best.amountIn,rem); if(!next){p=null;break} const stable=(isIn?next.ammIn===p.ammIn:next.ammOut===p.ammOut); p=next;plannerBounded=plannerBounded||!!p.bounded;if(stable)break; } if(p){ const rem=isIn?p.ammIn:p.ammOut; if(!rem)amm=null; else if(!(amm&&ammFor===rem))amm=await quoteRem(rem,!isIn).catch(()=>null); if(!rem||amm){ const recipient=rcv||acct; const ammBudget=amm?(isIn?rem:amm.amountLimit):0n; const totalBudget=p.bookIn+ammBudget; const expectedIn=p.bookIn+(amm?amm.best.amountIn:0n); const expectedOut=p.bookOut+(amm?amm.best.amountOut:0n); const saving=isIn ?(expectedOut>r.best.amountOut?expectedOut-r.best.amountOut:0n) :(expectedIn<r.best.amountIn?r.best.amountIn-expectedIn:0n); let gasFloor=null; if(saving){ const gasPrice=await gasPriceJob; if(gasPrice){ const extraWei=gasPrice*(120000n+105000n*BigInt(p.fills.length)); gasFloor=await gasValue(extraWei); } } const edgeBps=5n+2n*BigInt(p.fills.length); const fallbackWin=isIn ?expectedOut*10000n>r.best.amountOut*(10000n+edgeBps) :expectedIn*(10000n+edgeBps)<r.best.amountIn*10000n; const wins=saving>0n&&(gasFloor===null?fallbackWin:saving*10n>gasFloor*12n); if(my!==seq)return; if(wins){ const fp=encFillPlanAndSwap( p.fills,f.addr,t.addr,recipient,acct,deadline,ammBudget,amm?amm.callData:"0x" ); const minOut=isIn?p.bookOut+(amm?amm.amountLimit:0n):swapAmt; const snwapIn=totalBudget===0n?ZERO:f.addr; const planCall=encSnwap(snwapIn,totalBudget,recipient,t.addr,minOut,SWAPBOL,fp); const callData=f.addr===ZERO?planCall:encCalls([ encCheckpoint(SWAPBOL,f.addr,acct),planCall ]); let fundedCallData=null; if(f.addr!==ZERO){ fundedCallData=encPermit2Hybrid( amm?amm.callData:"0x",p.fills,f.addr,t.addr,recipient,acct,deadline,p.bookIn,p.bookOut ); } r={...r,best:{...r.best,amountIn:expectedIn,amountOut:expectedOut},callData, fills:p.fills, fundedCallData,amountLimit:isIn?minOut:totalBudget,msgValue:f.addr===ZERO?totalBudget:0n, slipBps:amm&&amm.slipBps, to:null,spender:f.addr===ZERO?null:ZROUTER, pool:null,wrap:0n,outDiffers:false,poolOut:undefined}; hybrid=isIn ?{bookIn:p.bookIn,bookOut:p.bookOut,ammIn:p.ammIn} :{bookIn:p.bookIn,bookOut:p.bookOut,ammOut:p.ammOut}; plannerBounded=plannerBounded||!!p.bounded; } } } } for(const c of [v4c,pc]) if(c&&c!==r&&c.best.amountOut>r.best.amountOut){r=c;hybrid=null} const finalIn=r.best.amountIn; const finalOut=r.best.amountOut; if(isIn){outAmt.value=trimAmt(finalOut,t.dec);fitFont(outAmt)} else{amt.value=trimAmt(finalIn,f.dec);fitFont(amt)} const rx=(+formatUnits(finalOut,t.dec))/(+formatUnits(finalIn,f.dec)); const s=r.best.source,raw=SOURCES[s]||"?"; const src=hybrid?"Orderbook + AMM":(s===3||s===4||s===SRC_PRECISION)&&r.best.feeBps?`${raw}\u00a0${Number(r.best.feeBps)/100}%`:raw; rate.textContent=isFinite(rx)?`1 ${f.sym} ≈ ${rx>=1?rx.toLocaleString("en-US",{maximumFractionDigits:4}):rx.toPrecision(4)} ${t.sym} · ${src}`:""; const paysSym=r.poolOut===ZERO?"ETH":"WETH"; rcvHdr.textContent=r.outDiffers?`You receive \u2014 as ${paysSym}`:"You receive"; if(r.outDiffers&&rate.textContent)rate.textContent+=` \u00b7 pays ${paysSym}`; rate.title=""; if(isIn)rate.textContent+=` · Min ${trimAmt(r.amountLimit,t.dec)} ${t.sym}`; else{ const exactMax=formatUnits(r.amountLimit,f.dec); rate.textContent+=` · Max ${maxAmt(r.amountLimit,f.dec)} ${f.sym}`; rate.title=`Exact maximum: ${exactMax} ${f.sym}`; } if(r.slipBps>slipBps)rate.textContent+=` · Split — bound widened to ${Number(r.slipBps)/100}%`; if(bookIncomplete)rate.textContent+=" · Book scan capped"; if(plannerBounded)rate.textContent+=" · Planner heuristic/capped"; if(bookUnavailable)rate.textContent+=" · AMM only (books offline)"; if(pq&&pq.capped)rate.textContent+=` · Precision scan capped (${LQ_SCAN})`; if(isIn&&f.launched&&(t.addr===ZERO||t.addr.toLowerCase()===WETH.toLowerCase())){ const floorOut=await quoteFloor(f.addr,swapAmt); if(my!==seq)return; if(floorOut>0n){ if(floorOut>finalOut){ rate.textContent+=` \u00b7 Floor pays more: ${trimAmt(floorOut,18)} ETH`; rate.title=`This coin can be burned back to the launcher for its share of the ether ` +`locked in its pool - ${trimAmt(floorOut,18)} ETH, against ${trimAmt(finalOut,18)} from selling. ` +`Redeem through the launcher at ${PLAUNCH} to take the larger.`; }else{ rate.textContent+=` \u00b7 Floor ${trimAmt(floorOut,18)} ETH`; rate.title=`The ether backing this amount if it were redeemed instead of sold. ` +`Selling pays more right now.`; } } } let refQ=iRef>=0&&dec[iRef]?dec[iRef].best:null; let refPair=null; if(!refQ&&isIn&&pq&&pq.out>0n&&refAmt>0n){ try{ const small=await quotePrecision(f,t,refAmt,PROUTE,quoteBlock); if(my!==seq)return; if(small&&small.out>0n)refPair={amountOut:small.out}; }catch{} } if(my!==seq)return; const impSrc=refQ||refPair; const imp=impSrc?impactBps(isIn?finalOut:finalIn,isIn?impSrc.amountOut:impSrc.amountIn,!isIn):null; const mkt=impSrc?(isIn?impSrc.amountOut:impSrc.amountIn)*REF_N:0n; const lossTok=imp!==null&&mkt?(isIn?(mkt>finalOut?mkt-finalOut:0n):(finalIn>mkt?finalIn-mkt:0n)):0n; if(imp!==null&&imp>=IMPACT_HIDE){ const pctTxt=(Number(imp)/100).toFixed(2); rate.textContent+=` · Impact ${pctTxt}%`; if(imp>=IMPACT_WARN)stat.textContent=`High price impact: ${pctTxt}% — about ${trimAmt(lossTok,isIn?t.dec:f.dec)} ${isIn?t.sym:f.sym} worse than the market rate.`; } const inApprove=isIn?swapAmt:r.msgValue||r.amountLimit; if(acct)last={callData:r.callData,fundedCallData:r.fundedCallData,msgValue:r.msgValue, to:r.to,spender:r.spender, amountIn:inApprove,from:f,recipient:rcv||"",exp:Date.now()+QUOTE_TTL,impact:imp, lossTok,lossSym:isIn?t.sym:f.sym,lossDec:isIn?t.dec:f.dec, fills:r.fills||null,tokenIn:f.addr,tokenOut:t.addr, source:r.best.source, wrap:r.wrap||0n,pools:r.pools||null, mkt,got:isIn?finalOut:finalIn,isIn}; quoting=false; render(); }catch(e){if(my===seq){ quoting=false; if(advancePastDeadPair()){loadChart();update();return} outEl.value="";fitFont(outEl);render(); stat.textContent=e?.noWallet?NO_WALLET :mcGaveUp>gaveUp0||(mcFailed>failed0&&mcOk===ok0) ?"Your RPC could not complete the quote — some venues were never asked. Try again, or switch networks in your wallet and back." :"No route: "+(e.message||e)}} } let debounceT=0; const showAddr=async()=>{const a=account,short=a.slice(0,6)+"..."+a.slice(-4); addr.textContent=short;addr.title=a+" - click to disconnect";addr.classList.add("on"); addr.setAttribute("role","button");addr.tabIndex=0; const n=await nameRev(a);if(n&&a===account)addr.textContent=n}; async function disconnect(){ sessionStorage.setItem("dc","1"); try{await rpc("wallet_revokePermissions",[{eth_accounts:{}}])}catch{} location.reload(); } addr.onclick=()=>{if(account)disconnect()}; addr.onkeydown=e=>{if(account&&(e.key==="Enter"||e.key===" ")){e.preventDefault();disconnect()}}; const AUTO_Q_MAX=5; const updateSoon=()=>{clearTimeout(debounceT);debounceT=setTimeout(()=>{debounceT=0;update()},250)}; const setMode=m=>{mode=m;const o=m==="in"?outAmt:amt;o.value="";fitFont(o);last=null;sendReady=null;render()}; amt.addEventListener("input",()=>{ if(lqMode){fitFont(amt);lqPreviewSoon();return} if(tab==="book"){fitFont(amt);render();return}setMode("in");fitFont(amt);updateSoon()}); outAmt.addEventListener("input",()=>{ if(lqMode){fitFont(outAmt);lqPreviewSoon();return} if(tab==="book"){fitFont(outAmt);seedFloor();render();return}setMode("out");fitFont(outAmt);updateSoon()}); const slipWarn=()=>slip.classList.toggle("warn",+slip.value>=3); if(LS.slip&&+LS.slip>=.01&&+LS.slip<=10)slip.value=LS.slip; slipWarn(); slip.addEventListener("input",()=>{last=null;slipWarn();render();updateSoon()}); rc.addEventListener("input",()=>{ if(lnMode)return; if(lqMode){clearTimeout(lqToT);lqToT=setTimeout(lqShowTo,300);return} last=null;sendReady=null;render();updateSoon()}); let lqToT; const onEnter=e=>{ if(e.key!=="Enter")return; const go=lnMode?lnGo:swap; if(!go||go.classList.contains("hide"))return; e.preventDefault(); if(debounceT){clearTimeout(debounceT);debounceT=0;update();return} if(!go.disabled)go.click(); }; for(const el of[amt,outAmt,rc,slip,floorAmt,nftId,lnName,lnSym,lnSupply,lnMcap,lnAlloc]) if(el)el.addEventListener("keydown",onEnter); slip.addEventListener("change",()=>{slip.value=Math.min(10,Math.max(.01,+slip.value||.5));slipWarn();LS.slip=slip.value}); for(const sel of [fromSel,toSel]){ sel.dataset.prev=sel.value; sel.addEventListener("change",async()=>{ if(sel.value!=="__custom"){ sel.dataset.prev=sel.value; sel.dataset.addr=TOKENS[sel.value]?.addr||""; pairChosen=true; syncDisabled(sel); syncNftUI(); if(sel===fromSel)refreshBalance();else refreshBalance1(); loadChart(); if(tab==="book"&&bookRows.length)paintBook(); update(); return; } last=null;render(); const prev=sel.dataset.prev; let a; try{a=(prompt("Paste token address:")||"").trim()} catch{sel.value=pre
0xacfba7ce…4082·#25,784,649·0xb818da07…5313b8
rn} if(!sell||!want){swap.textContent=bid?"Place bid":"Place order";swap.disabled=true;return} if(bid){ if(t.addr===ZERO){swap.textContent="Pick a token to bid for";swap.disabled=true;return} if((f.addr===ZERO?WETH:f.addr).toLowerCase()===t.addr.toLowerCase()){ swap.textContent="Pick different token";swap.disabled=true;return} if(!floor){swap.textContent="Set an opening bid";swap.disabled=true;return} if(floor>sell){swap.textContent="Opening exceeds maximum";swap.disabled=true;return} if(+dly.value===0){swap.textContent="Choose a bid window";swap.disabled=true;return} if(sell>fromBalance){swap.textContent="Insufficient balance";swap.disabled=true;return} swap.textContent=`Bid ${trimAmt(floor,f.dec)} → ${trimAmt(sell,f.dec)} ${f.sym} for ${trimAmt(want,t.dec)} ${t.sym}`; swap.disabled=false;return; } if(f.addr===ZERO&&t.addr===WETH){swap.textContent="Pick different token";swap.disabled=true;return} if(!dutch&&f.addr===WETH&&t.addr===ZERO){swap.textContent="Pick different token";swap.disabled=true;return} if(dutch&&floor>want){swap.textContent="Floor exceeds start";swap.disabled=true;return} if(dutch&&+dly.value===0){swap.textContent="Choose decay duration";swap.disabled=true;return} if(sell>fromBalance){swap.textContent="Insufficient balance";swap.disabled=true;return} swap.textContent=dutch ?`Dutch ${trimAmt(sell,f.dec)} ${f.sym} · ${trimAmt(want,t.dec)} → ${trimAmt(floor,t.dec)} ${t.sym}` :`Place ${fill.value==="1"?"":"all-or-nothing "}order \u2014 ${trimAmt(sell,f.dec)} ${f.sym} \u2192 ${trimAmt(want,t.dec)} ${t.addr===ZERO?"WETH":t.sym}`; { const qs=t.addr===ZERO?"ETH":t.sym; rate.textContent=dutch ?`Falls from ${trimAmt(want,t.dec)} to ${trimAmt(floor,t.dec)} ${qs} over ${rel(+dly.value)}, then rests at ${trimAmt(floor,t.dec)} ${qs} ` +(+dRest.value?`for ${rel(+dRest.value)} and returns to you`:`until you cancel`) :`${fill.value==="1"?"Fills in any amount":"Fills all at once"} at ${trimAmt(want,t.dec)} ${t.addr===ZERO?"WETH":t.sym} for ${trimAmt(sell,f.dec)} ${f.sym}` +(+dly.value?`, expiring in ${rel(+dly.value)}`:", and never expires"); } swap.disabled=false;return; } if(tab==="send"){ if(!sendReady){ let n=0n;try{n=parseUnits(amt.value.trim(),TOKENS[fromSel.value].dec)}catch{} swap.textContent=n>0n?"Name a recipient":"Enter an amount"; swap.disabled=true;return} if(sendReady.amount>fromBalance){swap.textContent="Insufficient balance";swap.disabled=true;return} const a=`${trimAmt(sendReady.amount,sendReady.token.dec)} ${sendReady.token.sym}`,dv=+dly.value; if(dv&&sendTipWei>0n&&sendReady.token.addr===ZERO &&sendReady.amount+sendTipWei>fromBalance){ swap.textContent="Insufficient ETH for amount + keeper tip";swap.disabled=true;return} swap.textContent=(dv?`Lock ${a} for ${rel(dv)}`:`Send ${a}`)+` \u2192 ${sendReady.label}`; swap.disabled=false;return; } if(quoting){swap.textContent="Quoting…";swap.disabled=true;return} if(!last){swap.textContent="Swap";swap.disabled=true;return} if(last.amountIn>fromBalance){swap.textContent="Insufficient balance";swap.disabled=true;return} swap.textContent="Swap";swap.disabled=false; }; syncIcons(); render(); const isRejection=e=>e?.code===4001||/user\s+reject|user\s+denied/i.test(e?.message||""); const W6963=[]; addEventListener("eip6963:announceProvider",e=>{ const d=e.detail; if(!d||!d.provider||!d.info||W6963.some(x=>x.info.uuid===d.info.uuid))return; W6963.push(d)}); const wkScan=()=>{try{dispatchEvent(new Event("eip6963:requestProvider"))}catch{}}; wkScan(); const WC_PID="1e8390ef1c1d8a185e035912a1409749"; const WC_ICON='<svg viewBox="0 0 400 400" fill="none" aria-hidden="true"><circle cx="200" cy="200" r="200" fill="#3396ff"/><path fill="#fff" d="m122.519 148.965c42.791-41.729 112.171-41.729 154.962 0l5.15 5.022c2.14 2.086 2.14 5.469 0 7.555l-17.617 17.18c-1.07 1.043-2.804 1.043-3.874 0l-7.087-6.911c-29.853-29.111-78.253-29.111-108.106 0l-7.59 7.401c-1.07 1.043-2.804 1.043-3.874 0l-17.617-17.18c-2.14-2.086-2.14-5.469 0-7.555zm191.397 35.529 15.679 15.29c2.14 2.086 2.14 5.469 0 7.555l-70.7 68.944c-2.139 2.087-5.608 2.087-7.748 0l-50.178-48.931c-.535-.522-1.402-.522-1.937 0l-50.178 48.931c-2.139 2.087-5.608 2.087-7.748 0l-70.7015-68.945c-2.1396-2.086-2.1396-5.469 0-7.555l15.6795-15.29c2.1396-2.087 5.6085-2.087 7.7481 0l50.1789 48.932c.535.522 1.402.522 1.937 0l50.177-48.932c2.139-2.087 5.608-2.087 7.748 0l50.179 48.932c.535.522 1.402.522 1.937 0l50.179-48.931c2.139-2.087 5.608-2.087 7.748 0z"/></svg>'; const WCU=(()=>{ const rl=(v,c)=>(v<<c|v>>>(32-c))>>>0; const wds=b=>{const o=[];for(let i=0;i<b.length;i+=4)o.push((b[i]|b[i+1]<<8|b[i+2]<<16|b[i+3]<<24)>>>0);return o}; function cblk(x,k,n,c){ const s=[0x61707865,0x3320646e,0x79622d32,0x6b206574, k[0],k[1],k[2],k[3],k[4],k[5],k[6],k[7],c,n[0],n[1],n[2]]; for(let i=0;i<16;i++)x[i]=s[i]; for(let r=0;r<10;r++){ const q=(a,b,cc,d)=>{ x[a]=(x[a]+x[b])>>>0;x[d]=rl(x[d]^x[a],16); x[cc]=(x[cc]+x[d])>>>0;x[b]=rl(x[b]^x[cc],12); x[a]=(x[a]+x[b])>>>0;x[d]=rl(x[d]^x[a],8); x[cc]=(x[cc]+x[d])>>>0;x[b]=rl(x[b]^x[cc],7)}; q(0,4,8,12);q(1,5,9,13);q(2,6,10,14);q(3,7,11,15); q(0,5,10,15);q(1,6,11,12);q(2,7,8,13);q(3,4,9,14);} for(let i=0;i<16;i++)x[i]=(x[i]+s[i])>>>0;} function chacha(key,nonce,ctr,src){ const k=wds(key),n=wds(nonce),x=new Uint32Array(16),out=new Uint8Array(src.length); for(let p=0;p<src.length;p+=64){cblk(x,k,n,ctr++); for(let i=0;i<64&&p+i<src.length;i++)out[p+i]=src[p+i]^(x[i>>2]>>>((i&3)*8)&255);} return out} function poly(msg,key){ const P=(1n<<130n)-5n; let r=0n,sk=0n; for(let i=15;i>=0;i--)r=(r<<8n)|BigInt(key[i]); r&=0x0ffffffc0ffffffc0ffffffc0fffffffn; for(let i=31;i>=16;i--)sk=(sk<<8n)|BigInt(key[i]); let a=0n; for(let i=0;i<msg.length;i+=16){ const n=Math.min(16,msg.length-i); let b=0n; for(let j=n-1;j>=0;j--)b=(b<<8n)|BigInt(msg[i+j]); a=((a+b+(1n<<BigInt(8*n)))*r)%P;} a=(a+sk)&((1n<<128n)-1n); const o=new Uint8Array(16); for(let i=0;i<16;i++){o[i]=Number(a&255n);a>>=8n} return o} const p16=n=>n%16?16-n%16:0; const le64=n=>{const b=new Uint8Array(8);for(let i=0;i<8;i++){b[i]=n&255;n=Math.floor(n/256)}return b}; function tagOf(ct,key,nonce){ const otk=chacha(key,nonce,0,new Uint8Array(32)); const pad=new Uint8Array(p16(ct.length)); const m=new Uint8Array(ct.length+pad.length+16); m.set(ct);m.set(pad,ct.length); m.set(le64(0),ct.length+pad.length);m.set(le64(ct.length),ct.length+pad.length+8); return poly(m,otk)} const seal=(key,nonce,pt)=>{ const ct=chacha(key,nonce,1,pt),tg=tagOf(ct,key,nonce); const o=new Uint8Array(ct.length+16);o.set(ct);o.set(tg,ct.length);return o}; const open=(key,nonce,box)=>{ if(box.length<16)return null; const ct=box.subarray(0,box.length-16),tg=box.subarray(box.length-16); const w=tagOf(ct,key,nonce); let bad=0;for(let i=0;i<16;i++)bad|=w[i]^tg[i]; return bad?null:chacha(key,nonce,1,ct)}; const hex=b=>{let s="";for(const x of b)s+=x.toString(16).padStart(2,"0");return s}; const unhex=h=>{const b=new Uint8Array(h.length/2); for(let i=0;i<b.length;i++)b[i]=parseInt(h.substr(i*2,2),16);return b}; const bin=b=>{let s="";for(let i=0;i<b.length;i+=4096)s+=String.fromCharCode.apply(null,b.subarray(i,i+4096));return s}; const b64=b=>btoa(bin(b)); const unb64=s=>{const t=atob(s),b=new Uint8Array(t.length); for(let i=0;i<t.length;i++)b[i]=t.charCodeAt(i);return b}; const b64u=b=>b64(b).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,""); const utf8=s=>new TextEncoder().encode(s); const B58="123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; const b58=b=>{let n=0n;for(const x of b)n=n*256n+BigInt(x); let s="";while(n>0n){s=B58[Number(n%58n)]+s;n/=58n} for(const x of b){if(x)break;s="1"+s}return s}; const rnd=n=>crypto.getRandomValues(new Uint8Array(n)); const sha256=async b=>new Uint8Array(await crypto.subtle.digest("SHA-256",b)); const topicOf=async k=>hex(await sha256(k)); const encode=(symKey,payload,senderPub)=>{ const iv=rnd(12),pt=utf8(JSON.stringify(payload)); const sealed=seal(symKey,iv,pt); const t=senderPub?1:0; const head=senderPub?1+32:1; const out=new Uint8Array(head+12+sealed.length); out[0]=t; if(senderPub)out.set(senderPub,1); out.set(iv,head);out.set(sealed,head+12); return b64(out)}; const decode=(symKey,msg)=>{ const b=unb64(msg); if(!b.length)return null; const t=b[0]; if(t!==0&&t!==1)return null; const head=t===1?33:1; if(b.length<head+12+16)return null; const iv=b.subarray(head,head+12),box=b.subarray(head+12); const pt=open(symKey,iv,box); if(!pt)return null; try{return JSON.parse(new TextDecoder().decode(pt))}catch{return null}}; const didKey=pub=>{const b=new Uint8Array(34);b[0]=0xed;b[1]=0x01;b.set(pub,2); return"did:key:z"+b58(b)}; async function relayJwt(aud){ let kp; try{kp=await crypto.subtle.generateKey({name:"Ed25519"},true,["sign","verify"])} catch{throw Error("This browser cannot do Ed25519. WalletConnect needs a newer " +"browser; an extension wallet works here today.")} const pub=new Uint8Array(await crypto.subtle.exportKey("raw",kp.publicKey)); const iss=didKey(pub); const iat=Math.floor(Date.now()/1e3); const h=b64u(utf8(JSON.stringify({alg:"EdDSA",typ:"JWT"}))); const p=b64u(utf8(JSON.stringify({iss,sub:hex(rnd(32)),aud,iat,exp:iat+86400}))); const sig=new Uint8Array(await crypto.subtle.sign({name:"Ed25519"},kp.privateKey,utf8(h+"."+p))); return h+"."+p+"."+b64u(sig)} async function kex(){ let kp; try{kp=await crypto.subtle.generateKey({name:"X25519"},true,["deriveBits"])} catch{throw Error("This browser cannot do X25519. WalletConnect needs a newer " +"browser; an extension wallet works here today.")} const pub=new Uint8Array(await crypto.subtle.exportKey("raw",kp.publicKey)); return{priv:kp.privateKey,pub}} async function sessionKey(priv,peerHex){ const peer=await crypto.subtle.importKey("raw",unhex(peerHex),{name:"X25519"},false,[]); const bits=new Uint8Array(await crypto.subtle.deriveBits({name:"X25519",public:peer},priv,256)); const hk=await crypto.subtle.importKey("raw",bits,"HKDF",false,["deriveBits"]); const out=await crypto.subtle.deriveBits( {name:"HKDF",hash:"SHA-256",salt:new Uint8Array(0),info:new Uint8Array(0)},hk,256); return new Uint8Array(out)} return{seal,open,chacha,poly,hex,unhex,b64,unb64,b64u,b58,didKey,rnd,sha256, topicOf,encode,decode,relayJwt,kex,sessionKey}; })(); const WCCLI=(U,WSC)=>{ const RELAY="wss://relay.walletconnect.org"; const T_PROPOSE=1100,T_APPROVE=1101,T_SETTLE=1102,T_SETTLE_RES=1103,T_REQ=1108,T_RES=1109; const TTL=300,STTL=604800; let sock=null,nextId=1,waiters={},subs={},onEvent=null,alive=false,gone=""; const rpcId=()=>Date.now()*1000+(nextId++%1000); const sendRaw=o=>{ if(!alive)throw Error(gone||"The wallet connection is closed."); sock.send(JSON.stringify(o))}; function drop(why){ if(!alive&&gone)return; alive=false;gone=why; const ws=waiters;waiters={};subs={}; for(const k in ws)ws[k].rej(Error(why)); if(onEvent)onEvent({t:"closed",why})} function relayCall(method,params){ const id=rpcId(); return new Promise((res,rej)=>{ waiters[id]={res,rej}; sendRaw({id,jsonrpc:"2.0",method,params}); setTimeout(()=>{if(waiters[id]){delete waiters[id];rej(Error("relay timeout"))}},15000)})} function handle(raw){ let m;try{m=JSON.parse(raw)}catch{return} if(m.method==="irn_subscription"){ sendRaw({id:m.id,jsonrpc:"2.0",result:true}); const d=m.params&&m.params.data; if(d&&subs[d.topic])subs[d.topic](d.message); return} if(m.id&&waiters[m.id]){ const w=waiters[m.id];delete waiters[m.id]; if(m.error)w.rej(Error(m.error.message||"relay error"));else w.res(m.result)}} async function connect(projectId){ try{sock&&sock.close()}catch{} sock=null;alive=false;gone="";waiters={};subs={}; const jwt=await U.relayJwt(RELAY); const url=`${RELAY}/?auth=${jwt}&projectId=${projectId}&ua=zswap`; const ws=WSC(url); sock=ws; await new Promise((res,rej)=>{ let d=0; const t=setTimeout(()=>{if(!d){d=1;rej(Error("The relay did not answer."))}},15000); const fail=m=>{try{ws.close()}catch{}if(sock===ws)sock=null;rej(Error(m))}; const end=f=>x=>{if(d)return;d=1;clearTimeout(t);f(x)}; sock.onopen=end(()=>{alive=true;res()}); sock.onerror=end(()=>fail("The relay could not be reached.")); sock.onclose=end(e=>fail("The relay closed the connection" +(e&&e.code?" (code "+e.code+(e.reason?": "+e.reason:"")+")":"")+"."))}) sock.onclose=()=>drop("The relay connection dropped. Reconnect your wallet."); sock.onmessage=e=>handle(typeof e.data==="string"?e.data:""); } const subscribe=async(topic,fn)=>{subs[topic]=fn;await relayCall("irn_subscribe",{topic})}; const publish=(topic,message,tag,ttl)=> relayCall("irn_publish",{topic,message,ttl:ttl||TTL,tag,prompt:tag===T_PROPOSE||tag===T_REQ}); async function pair(projectId,meta){ await connect(projectId); const symKey=U.rnd(32); const pTopic=await U.topicOf(symKey); const kp=await U.kex(); const proposeId=rpcId(); let settle=null; const done=new Promise((res,rej)=>{settle={res,rej}}); await subscribe(pTopic,async msg=>{ const m=U.decode(symKey,msg); if(!m||m.id!==proposeId)return; if(m.error)return settle.rej(Error(m.error.message||"rejected")); const rp=m.result&&m.result.responderPublicKey; if(!rp)return; const sKey=await U.sessionKey(kp.priv,rp); const sTopic=await U.topicOf(sKey); await subscribe(sTopic,async smsg=>{ const s=U.decode(sKey,smsg); if(!s)return; if(s.method==="wc_sessionSettle"){ await publish(sTopic,U.encode(sKey,{id:s.id,jsonrpc:"2.0",result:true}),T_SETTLE_RES,STTL); const ns=s.params&&s.params.namespaces||{}; const accts=[]; for(const k in ns)for(const a of ns[k].accounts||[])accts.push(a); settle.res({topic:sTopic,key:sKey,accounts:accts, methods:(ns.eip155&&ns.eip155.methods)||[]}); return} if(s.method==="wc_sessionDelete")return drop("The wallet ended the session."); if(s.method&&onEvent)onEvent({t:"session",m:s}); if(s.id&&waiters["s"+s.id]){ const w=waiters["s"+s.id];delete waiters["s"+s.id]; if(s.error)w.rej(Object.assign(Error(s.error.message||"rejected"),{code:s.error.code})); else w.res(s.result)}}); }); const proposal={id:proposeId,jsonrpc:"2.0",method:"wc_sessionPropose",params:{ relays:[{protocol:"irn"}], proposer:{publicKey:U.hex(kp.pub),metadata:meta}, requiredNamespaces:{}, optionalNamespaces:{eip155:{chains:["eip155:1"], methods:["eth_sendTransaction","personal_sign","eth_signTypedData_v4", "eth_call","eth_chainId","eth_accounts","eth_getBalance", "wallet_switchEthereumChain"], events:["chainChanged","accountsChanged"]}}}}; await publish(pTopic,U.encode(symKey,proposal),T_PROPOSE); const uri=`wc:${pTopic}@2?relay-protocol=irn&symKey=${U.hex(symKey)}`; return{uri,done}} function request(sess,method,params){ const id=rpcId(); const body={id,jsonrpc:"2.0",method:"wc_sessionRequest", params:{request:{method,params:params||[]},chainId:"eip155:1"}}; const p=new Promise((res,rej)=>{ waiters["s"+id]={res,rej}; setTimeout(()=>{if(waiters["s"+id]){delete waiters["s"+id];rej(Error("wallet timed out"))}},300000)}); publish(sess.topic,U.encode(sess.key,body),T_REQ).catch(e=>{ const w=waiters["s"+id];if(!w)return;delete waiters["s"+id];w.rej(e)}); return p} return{pair,request,close:()=>{drop("The wallet connection was closed.");try{sock&&sock.close()}catch{}sock=null}, on:f=>{onEvent=f},_relayCall:relayCall}; }; const QR8=(()=>{ const EXP=new Uint8Array(512),LOG=new Uint8Array(256); (()=>{let x=1;for(let i=0;i<255;i++){EXP[i]=x;LOG[x]=i;x<<=1;if(x&256)x^=285} for(let i=255;i<512;i++)EXP[i]=EXP[i-255]})(); const mul=(a,b)=>a&&b?EXP[LOG[a]+LOG[b]]:0; const gen=n=>{let p=[1]; for(let i=0;i<n;i++){const q=new Array(p.length+1).fill(0); for(let j=0;j<p.length;j++){q[j]^=mul(p[j],EXP[i]);q[j+1]^=p[j]}p=q} return p}; const rs=(data,n)=>{ const g=gen(n),r=new Uint8Array(data.length+n); r.set(data); for(let i=0;i<data.length;i++){ const c=r[i];if(!c)continue; for(let j=0;j<=n;j++)r[i+j]^=mul(g[n-j],c)} return r.subarray(data.length)}; const SZ=49,NBLK=2,DPB=97,ECB=24,ALIGN=[6,24,42]; const bch=(v,g,n)=>{let d=v<<n; const bits=x=>{let c=0;while(x){c++;x>>>=1}return c}; const gl=bits(g); while(bits(d)>=gl)d^=g<<(bits(d)-gl); return d}; const fmtBits=mask=>{ const v=(1<<3)|mask; return ((v<<10)|bch(v,0x537,10))^0x5412}; const verBits=()=>(8<<12)|bch(8,0x1f25,12); function encode(str){ const bytes=new TextEncoder().encode(str); if(bytes.length>NBLK*DPB-2)throw Error("pairing code too long to draw"); const bits=[]; const push=(v,n)=>{for(let i=n-1;i>=0;i--)bits.push((v>>i)&1)}; push(4,4);push(bytes.length,8); for(const b of bytes)push(b,8); const cap=(NBLK*DPB)*8; for(let i=0;i<4&&bits.length<cap;i++)bits.push(0); while(bits.length%8)bits.push(0); const cw=[]; for(let i=0;i<bits.length;i+=8){let v=0;for(let j=0;j<8;j++)v=(v<<1)|bits[i+j];cw.push(v)} const pads=[0xec,0x11]; for(let i=0;cw.length<NBLK*DPB;i++)cw.push(pads[i%2]); const blocks=[],ecs=[]; for(let i=0;i<NBLK;i++){ const d=Uint8Array.from(cw.slice(i*DPB,(i+1)*DPB)); blocks.push(d);ecs.push(rs(d,ECB))} const out=[]; for(let i=0;i<DPB;i++)for(const b of blocks)out.push(b[i]); for(let i=0;i<ECB;i++)for(const e of ecs)out.push(e[i]); return out} function frame(){ const m=[],fn=[]; for(let i=0;i<SZ;i++){m.push(new Array(SZ).fill(0));fn.push(new Array(SZ).fill(0))} const put=(r,c,v)=>{m[r][c]=v;fn[r][c]=1}; const finder=(r,c)=>{ for(let i=-1;i<8;i++)for(let j=-1;j<8;j++){ const rr=r+i,cc=c+j; if(rr<0||cc<0||rr>=SZ||cc>=SZ)continue; const on=(i>=0&&i<=6&&(j===0||j===6))||(j>=0&&j<=6&&(i===0||i===6))||(i>=2&&i<=4&&j>=2&&j<=4); put(rr,cc,on?1:0)}}; finder(0,0);finder(0,SZ-7);finder(SZ-7,0); for(const r of ALIGN)for(const c of ALIGN){ if((r===6&&c===6)||(r===6&&c===ALIGN[ALIGN.length-1])||(r===ALIGN[ALIGN.length-1]&&c===6))continue; for(let i=-2;i<=2;i++)for(let j=-2;j<=2;j++) put(r+i,c+j,(Math.abs(i)===2||Math.abs(j)===2||(i===0&&j===0))?1:0)} for(let i=8;i<SZ-8;i++){put(6,i,i%2?0:1);put(i,6,i%2?0:1)} put(SZ-8,8,1); for(let i=0;i<9;i++){if(!fn[8][i])put(8,i,0);if(!fn[i][8])put(i,8,0)} for(let i=0;i<8;i++){if(!fn[8][SZ-1-i])put(8,SZ-1-i,0);if(!fn[SZ-1-i][8])put(SZ-1-i,8,0)} const vb=verBits(); for(let i=0;i<18;i++){ const b=(vb>>i)&1,r=Math.floor(i/3),c=i%3; put(SZ-11+c,r,b);put(r,SZ-11+c,b)} return{m,fn}} function place(m,fn,cw){ let bit=0,up=1; const get=i=>(cw[i>>3]>>(7-(i&7)))&1; for(let c=SZ-1;c>0;c-=2){ if(c===6)c--; for(let k=0;k<SZ;k++){ const r=up?SZ-1-k:k; for(let j=0;j<2;j++){ const cc=c-j; if(fn[r][cc])continue; m[r][cc]=bit<cw.length*8?get(bit):0;bit++}} up^=1}} const maskFn=(k,r,c)=>[ (r+c)%2===0,r%2===0,c%3===0,(r+c)%3===0, (((r/2)|0)+((c/3)|0))%2===0,((r*c)%2+(r*c)%3)===0, (((r*c)%2+(r*c)%3)%2)===0,(((r+c)%2+(r*c)%3)%2)===0][k]; function penalty(m){ let p=0; for(let i=0;i<SZ;i++){ let rc=1,cc=1; for(let j=1;j<SZ;j++){ rc=m[i][j]===m[i][j-1]?rc+1:1;if(rc===5)p+=3;else if(rc>5)p++; cc=m[j][i]===m[j-1][i]?cc+1:1;if(cc===5)p+=3;else if(cc>5)p++}} for(let i=0;i<SZ-1;i++)for(let j=0;j<SZ-1;j++){ const s=m[i][j]+m[i][j+1]+m[i+1][j]+m[i+1][j+1]; if(s===0||s===4)p+=3} const pat=[1,0,1,1,1,0,1]; const hit=(get)=>{ for(let i=0;i<SZ;i++)for(let j=0;j<SZ-6;j++){ let okk=1;for(let k=0;k<7;k++)if(get(i,j+k)!==pat[k]){okk=0;break} if(!okk)continue; let before=1,after=1; for(let k=j-4;k<j;k++)if(k<0||get(i,k)!==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 RPC_STORE="zswap:rpc"; const WC_NODE="https://ethereum.publicnode.com"; const wcNode=()=>{try{const v=localStorage.getItem(RPC_STORE); if(v&&/^https:\/\/\S+$/i.test(v))return v}catch{}return WC_NODE}; const wcHost=()=>{try{return new URL(wcNode()).host}catch{return wcNode()}}; const WC_SIGN=["eth_sendTransaction","eth_signTransaction","personal_sign","eth_sign", "eth_signTypedData","eth_signTypedData_v3","eth_signTypedData_v4"]; const wcToWallet=m=>m.indexOf("wallet_")===0||WC_SIGN.indexOf(m)>=0; let wcRid=0; async function wcRpc(method,params){ let r; try{r=await fetch(wcNode(),{method:"POST",headers:{"content-type":"application/json"}, body:JSON.stringify({id:++wcRid,jsonrpc:"2.0",method,params:params||[]})})} catch{throw Error("Could not reach "+wcHost()+" for reads. Pick another node from the WalletConnect panel.")} if(!r.ok)throw Error(wcHost()+" answered "+r.status+" for reads."); const j=await r.json(); if(j&&j.error)throw Object.assign(Error(j.error.message||"the read node refused"),{code:j.error.code}); return j?j.result:null} const wcRelay=WCCLI(WCU,u=>new WebSocket(u)); 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>'}; const wcShow=uri=>{ wkList.textContent=""; const box=document.createElement("div"); box.className="wcq"; box.innerHTML=qrSvg(QR8.build(uri)); const cp=document.createElement("button"); cp.type="button";cp.className="fclink";cp.textContent="copy link"; cp.onclick=()=>{navigator.clipboard&&navigator.clipboard.writeText(uri);cp.textContent="copied"}; const note=document.createElement("div"); note.className="wcn"; note.textContent="Scan with a mobile wallet, or "; note.appendChild(cp); const nd=document.createElement("div"); nd.className="wcn wcnode"; nd.appendChild(document.createTextNode("Reads via "+wcHost()+" ")); const ch=document.createElement("button"); ch.type="button";ch.className="fclink";ch.textContent="change"; ch.onclick=()=>{ const v=prompt("HTTPS Ethereum RPC used for reads while WalletConnect is connected." +" Signing always goes to your wallet. Leave blank for the default.",wcNode()); if(v===null)return; const t=v.trim(); try{if(!t)localStorage.removeItem(RPC_STORE); else if(/^https:\/\/\S+$/i.test(t))localStorage.setItem(RPC_STORE,t); else return}catch{} nd.firstChild.nodeValue="Reads via "+wcHost()+" "}; nd.appendChild(ch); wkList.appendChild(box);wkList.appendChild(note);wkList.appendChild(nd); wkHdr.textContent="WalletConnect"; wkWrap.classList.remove("hide"); return new Promise((res,rej)=>{ const esc=e=>{if(e.key==="Escape")stop()}; const stop=()=>{removeEventListener("keydown",esc);wkWrap.onclick=null;rej(Object.assign(Error(""),{code:4001}))}; wkWrap.onclick=e=>{if(e.target===wkWrap)stop()}; addEventListener("keydown",esc); wcShow.settle=()=>{removeEventListener("keydown",esc);wkWrap.onclick=null;res()}})}; async function wcConnect(){ const meta={name:"zSwap",description:"zSwap",url:location.origin,icons:[]}; const{uri,done}=await wcRelay.pair(WC_PID,meta); const bail=wcShow(uri); stat.textContent="Scan the code with your wallet."; let sess; try{sess=await Promise.race([done,bail])} catch(e){wcRelay.close();throw e} finally{if(wcShow.settle)wcShow.settle(); wkHdr.textContent="Choose a wallet"; wkWrap.classList.add("hide")} const accts=sess.accounts.map(a=>a.split(":").pop()); if(!accts.length){wcRelay.close();throw Error("That wallet approved no account.")} wcRead=sess.methods.indexOf("eth_call")>=0; const read=async(method,params)=>{ try{return await wcRpc(method,params)} catch(e){ if(e&&e.code!==undefined||!wcRead)throw e; return wcRelay.request(sess,method,params||[])}}; wcRelay.on(ev=>{if(ev&&ev.t==="closed")location.reload()}); return{request:({method,params})=> method==="eth_requestAccounts"||method==="eth_accounts"?Promise.resolve(accts): method==="eth_chainId"?Promise.resolve("0x1"): wcToWallet(method)?wcRelay.request(sess,m
0xacfba7ce…4082·#25,784,648·0xa3bffaab…d60c9d
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
0xacfba7ce…4082·#25,784,647·0x4a0607fc…0a7ddc
spec of row.v.split(";")){ const p=spec.trim().split(":"); if(p.length!==5||p[0]!=="v1")continue; const other=p[1]==="0"?ZERO:p[1]; const hooks=p[4]==="0"?ZERO:p[4]; if(!/^0x[0-9a-f]{40}$/i.test(other)||!/^0x[0-9a-f]{40}$/i.test(hooks))continue; if(!/^\d{1,7}$/.test(p[2])||!/^\d{1,7}$/.test(p[3]))continue; const fee=Number(p[2]),ts=Number(p[3]); if(!Number.isInteger(fee)||!Number.isInteger(ts)||ts<1)continue; if(other.toLowerCase()===tokenAddr.toLowerCase())continue; const a=tokenAddr.toLowerCase(),b=other.toLowerCase(); const zeroFirst=BigInt(a)<BigInt(b); out.push({c0:zeroFirst?tokenAddr:other,c1:zeroFirst?other:tokenAddr,fee,ts,hooks}); if(out.length===8)break; } return out; } function v4PoolsFor(f,t){ const fa=f.addr.toLowerCase(),ta=t.addr.toLowerCase(); const seen=new Set(),out=[]; for(const tok of [f,t])for(const p of (tok.v4||[])){ const c0=p.c0.toLowerCase(),c1=p.c1.toLowerCase(); if(!((c0===fa&&c1===ta)||(c0===ta&&c1===fa)))continue; const id=`${c0}:${c1}:${p.fee}:${p.ts}:${p.hooks.toLowerCase()}`; if(seen.has(id))continue; seen.add(id); out.push({...p,zeroForOne:c0===fa}); } return out; } const encPoolKey=p=>encAddr(p.c0)+encAddr(p.c1)+encUint(p.fee)+encUint(p.ts)+encAddr(p.hooks); const SEL_QUOTE_BEST="2adaa389",SEL_EFFFEE="ef66de32"; const PROUTE="0x0000007Be74558A1F8c9045301c6F44C8eD0c9eB"; const SEL_PROUTE="5d6498e1",SEL_PCHECKPOINT="0b7c6c6c"; const SEL_PROUTEWETH="78fd78b0"; const SEL_ZAPIN="c98c2c0b"; const SEL_PREVIEW_ZAP="e7cddab0"; const encPRouteWeth=(pools,tokenOut,amountIn,minOut,to)=> "0x"+SEL_PROUTEWETH+encUint(160)+encAddr(tokenOut)+encUint(amountIn) +encUint(minOut)+encAddr(to)+encUint(pools.length)+pools.map(encAddr).join(""); const encPRoute=(pools,tokenIn,tokenOut,amountIn,minOut,to)=> "0x"+SEL_PROUTE+encUint(192)+encAddr(tokenIn)+encAddr(tokenOut)+encUint(amountIn) +encUint(minOut)+encAddr(to)+encUint(BigInt(pools.length))+pools.map(encAddr).join(""); const SEL_PAIR_COUNT="355da246",SEL_CREATORBPS="17773ebb"; const PPOLICY="0x00000045fc7b570Be4d71F67219508ebD295EC6D"; const SEL_ROUTABLE="9662c498",SEL_QUOTE_ALL="89941805"; const LQ_SCAN=128; const LQ_SHOW=24; const isEthLike=a=>a===ZERO||a.toLowerCase()===WETH.toLowerCase(); async function quotePrecision(f,t,amountIn,sender,block){ if(PPLENS===ZERO||amountIn<=0n)return null; const nat=a=>a.toLowerCase()===WETH.toLowerCase()?ZERO:a; const vars=[]; const addVar=(a,b)=>{ const [x,y]=a.toLowerCase()<b.toLowerCase()?[a,b]:[b,a]; if(!vars.some(v=>v.c0===x&&v.c1===y&&v.tokenIn===a))vars.push({c0:x,c1:y,tokenIn:a}); }; const forms=a=>a===ZERO?[ZERO,WETH] :a.toLowerCase()===WETH.toLowerCase()?[WETH,ZERO]:[a]; for(const a of forms(f.addr))for(const b of forms(t.addr)){ if(nat(a)===nat(b))continue; addVar(a,b); } const reqs=[]; for(const v of vars)reqs.push( {to:PPLENS,data:"0x"+SEL_QUOTE_BEST+encAddr(v.c0)+encAddr(v.c1) +encAddr(sender)+encAddr(v.tokenIn)+encUint(amountIn)+encUint(0n)+encUint(BigInt(LQ_SCAN))}, {to:PFACTORY,data:"0x"+SEL_PAIR_COUNT+encAddr(v.c0)+encAddr(v.c1)}); const res=await mc3Deep(reqs,block); let pool="",out=0n,n=0,fA=f.addr,won=null; for(let i=0;i<vars.length;i++){ const q=res[i*2],c=res[i*2+1]; const b=decBest(q); if(!b)continue; if(b.out<=out)continue; pool=b.pool;out=b.out;fA=vars[i].tokenIn;won=vars[i]; try{if(c)n=Number(BigInt(c))}catch{n=0} } if(!pool||!won)return await hubHop(f,t,amountIn,sender,block); const tA=won.c0===fA?won.c1:won.c0; let fee=0n; const feeReq=p=>({to:PPLENS, data:"0x"+SEL_EFFFEE+encAddr(p)+encAddr(sender)+encAddr(fA)+encUint(amountIn)}); const [fx,rt]=await mc3Deep([feeReq(pool),routableReq([pool])],block); if(policyRefuses(rt,1)){ const alt=await bestRoutable(won.c0,won.c1,fA,amountIn,sender,block); if(!alt)return await hubHop(f,t,amountIn,sender,block); pool=alt.pool;out=alt.out; const [fx2]=await mc3Deep([feeReq(pool)],block); if(fx2)fee=BigInt(fx2); }else if(fx)fee=BigInt(fx); return {pools:[pool],out,fee,capped:n>LQ_SCAN,tokenIn:fA,tokenOut:tA, unwrap:fA===ZERO&&f.addr!==ZERO?amountIn:0n, wrap:fA!==ZERO&&f.addr===ZERO?amountIn:0n, outDiffers:(tA===ZERO)!==(t.addr===ZERO)}; } const routableReq=pools=>({to:PPOLICY, data:"0x"+SEL_ROUTABLE+encUint(32)+encUint(BigInt(pools.length))+pools.map(encAddr).join("")}); function policyRefuses(raw,n){ if(!raw)return false; const h=strip0x(raw); if(h.length<128+n*64)return false; const len=Number(BigInt("0x"+h.slice(64,128))); if(len!==n)return false; for(let i=0;i<n;i++)if(BigInt("0x"+h.slice(128+i*64,128+(i+1)*64))===0n)return true; return false; } async function bestRoutable(c0,c1,tokenIn,amountIn,sender,block){ const [raw]=await mc3Deep([{to:PPLENS,data:"0x"+SEL_QUOTE_ALL+encAddr(c0)+encAddr(c1) +encAddr(sender)+encAddr(tokenIn)+encUint(amountIn)+encUint(0n)+encUint(BigInt(LQ_SCAN))}],block); const cands=decAll(raw); if(!cands.length)return null; const [rt]=await mc3Deep([routableReq(cands.map(c=>c.pool))],block); if(!rt)return null; const h=strip0x(rt); if(h.length<128+cands.length*64)return null; let win=null; for(let i=0;i<cands.length;i++){ if(BigInt("0x"+h.slice(128+i*64,128+(i+1)*64))===0n)continue; if(!win||cands[i].out>win.out)win=cands[i]; } return win; } function decAll(raw){ if(!raw)return[]; const h=strip0x(raw); if(h.length<128)return[]; const n=Number(BigInt("0x"+h.slice(64,128))); const out=[]; for(let i=0;i<n;i++){ const b=128+i*192; if(h.length<b+192)break; const pool="0x"+h.slice(b+24,b+64),o=BigInt("0x"+h.slice(b+64,b+128)); if(o<=0n||/^0x0{40}$/i.test(pool))continue; out.push({pool,out:o}); } return out; } function decBest(raw){ if(!raw)return null; const h=strip0x(raw); if(h.length<128)return null; const pool="0x"+h.slice(24,64),out=BigInt("0x"+h.slice(64,128)); if(out<=0n||/^0x0{40}$/i.test(pool))return null; return {pool,out}; } async function hubHop(f,t,amountIn,sender,block){ if(isEthLike(f.addr)||isEthLike(t.addr))return null; const enc=(a,b,tokenIn,amt)=>{ const [c0,c1]=a.toLowerCase()<b.toLowerCase()?[a,b]:[b,a]; return [{to:PPLENS,data:"0x"+SEL_QUOTE_BEST+encAddr(c0)+encAddr(c1) +encAddr(sender)+encAddr(tokenIn)+encUint(amt)+encUint(0n)+encUint(BigInt(LQ_SCAN))}, {to:PFACTORY,data:"0x"+SEL_PAIR_COUNT+encAddr(c0)+encAddr(c1)}]}; const count=raw=>{try{return raw?Number(BigInt(raw)):0}catch{return 0}}; const hubs=[ZERO,WETH]; const legs=[]; const first=await mc3Deep(hubs.flatMap(h=>enc(f.addr,h,f.addr,amountIn)),block); for(let i=0;i<hubs.length;i++){ const b=decBest(first[i*2]); if(b)legs.push({hub:hubs[i],pool:b.pool,mid:b.out,n:count(first[i*2+1])}); } if(!legs.length)return null; const second=await mc3Deep(legs.flatMap(l=>enc(l.hub,t.addr,l.hub,l.mid)),block); let win=null; for(let i=0;i<legs.length;i++){ const b=decBest(second[i*2]); if(!b||(win&&b.out<=win.out))continue; win={pools:[legs[i].pool,b.pool],out:b.out,mid:legs[i].mid,hub:legs[i].hub, n:Math.max(legs[i].n,count(second[i*2+1]))}; } if(!win)return null; let fee=0n; const [a,b,rt]=await mc3Deep([ {to:PPLENS,data:"0x"+SEL_EFFFEE+encAddr(win.pools[0])+encAddr(sender)+encAddr(f.addr)+encUint(amountIn)}, {to:PPLENS,data:"0x"+SEL_EFFFEE+encAddr(win.pools[1])+encAddr(sender)+encAddr(win.hub)+encUint(win.mid)}, routableReq(win.pools), ],block); if(policyRefuses(rt,win.pools.length))return null; const fa=a?BigInt(a):0n,fb=b?BigInt(b):0n; fee=fa+fb-(fa*fb)/1000000n; return {pools:win.pools,out:win.out,fee,capped:win.n>LQ_SCAN, tokenIn:f.addr,tokenOut:t.addr,unwrap:0n,wrap:0n,outDiffers:false}; } const QPAIR=2; const SEL_BBS="e7798987"; const HUBS=()=>[WETH,...NUMERAIRES,"0x2260fac5e5542a773aa44fbcfedf7c193bc2c599","0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0"]; async function hubRoute(f,t,amt,sb,dl,to,rf,block){ const nf=(f.addr===ZERO?WETH:f.addr).toLowerCase(),nt=(t.addr===ZERO?WETH:t.addr).toLowerCase(); const H=HUBS().filter(h=>h.toLowerCase()!==nf&&h.toLowerCase()!==nt); if(!H.length)return null; const E=(x,i,o,n)=>({to:ZQUOTER,data:"0x"+SEL_BBS+encAddr(x)+encBool(false)+encAddr(i)+encAddr(o)+encUint(n)+encUint(sb)+encUint(dl)}); const D=h=>{try{return decQ(h,sb,false,0,4,0,6)}catch{return null}}; const L=[],r1=await mc3Deep(H.map(h=>E(ZROUTER,f.addr,h,amt)),block); for(let i=0;i<H.length;i++){const a=r1[i]&&D(r1[i]);if(a&&a.amountLimit>0n)L.push({h:H[i],a})} if(!L.length)return null; const r2=await mc3Deep(L.map(x=>E(to,x.h,t.addr,x.a.amountLimit)),block); let B=null; for(let i=0;i<L.length;i++){const b=r2[i]&&D(r2[i]); if(b&&b.best.amountOut>0n&&(!B||b.best.amountOut>B.b.best.amountOut))B={...L[i],b}} if(!B)return null; const c=[B.a.callData,B.b.callData,encSweep(B.h,0n,rf),encSweep(ZERO,0n,rf)]; return{best:{source:B.b.best.source,feeBps:B.b.best.feeBps,amountIn:amt,amountOut:B.b.best.amountOut}, callData:encCalls(c),amountLimit:B.b.amountLimit, msgValue:f.addr===ZERO?amt:0n,spender:f.addr===ZERO?null:ZROUTER}; } async function quoteV4Pools(f,t,amountIn,block){ const pools=v4PoolsFor(f,t); if(!pools.length||amountIn<=0n)return null; const rs=(await mc3Deep(pools.map(p=>({to:V4LENS, data:"0x"+SEL_V4QUOTE+encBool(false)+encAddr(f.addr)+encAddr(t.addr) +encUint(p.fee)+encUint(p.ts)+encAddr(p.hooks)+encUint(amountIn)})),block)) .map(h=>{try{const x=strip0x(h||"");return x.length>=128?BigInt("0x"+x.slice(64,128)):0n}catch{return 0n}}); let best=null; for(let i=0;i<pools.length;i++)if(rs[i]>0n&&(!best||rs[i]>best.amountOut)) best={pool:pools[i],amountIn,amountOut:rs[i]}; return best; } const setListNote=t=>{ const el=document.getElementById("listNote"); if(!el)return; el.textContent=t||""; el.classList.toggle("hide",!t)}; const setListSource=w=>setListNote(w?"Built-in token list — "+w:""); let rankFellBack=false; const rankedIds=async()=>{ rankFellBack=false; try{ const ids=decUintArr(await rpc(C,[{to:ZLISTLENS,data:"0x"+SEL_RANKEDIDS},L])); if(ids.length)return ids; }catch{} try{ const ids=decUintArr(await rpc(C,[{to:TOKENLIST,data:"0x"+SEL_RANKEDIDS},L])); if(ids.length)rankFellBack=true; return ids; }catch{return[]} }; async function loadTokenList(){ if(TOKENLIST===ZERO)return setListSource("registry address not set"); try{ const ids=await rankedIds(); if(!ids.length)return setListSource("registry is empty"); const raw=await mc3Deep(ids.slice(0,64).map(id=>({to:TOKENLIST,data:"0x"+SEL_JSON+encUint(id)}))); const rows=raw.map(h=>{try{return h?decStr(h):null}catch{return null}}); const next=[]; for(const row of rows){ if(!row)continue; let t;try{t=JSON.parse(row)}catch{continue} if(t.k!=="eip155")continue; const std=t.p==="Native"||t.p==="ERC-20"?"ft":t.p==="ERC-721"?"nft":""; if(!std)continue; if(!/^0x[0-9a-f]{40}$/i.test(t.a||""))continue; if(!Number.isInteger(t.d)||t.d<0||t.d>36)continue; if(next.some(x=>x.addr.toLowerCase()===t.a.toLowerCase()))continue; const sym=safeSym(t.s).slice(0,12); const logo=safeUrl(t.l); next.push({sym,addr:t.a.toLowerCase(),dec:t.d,std,icon:logo?`<img src="${logo}" width="20" height="20">`:genIcon(sym), name:String(t.n??"").slice(0,64),desc:String(t.desc??"").slice(0,240), v4:parseV4Pools(t.e,t.a)}); } next.sort((a,b)=>(a.std==="nft")-(b.std==="nft")); if(!next.some(t=>t.std==="ft"))return setListSource("registry returned nothing swappable"); const mine=TOKENS.filter(t=>t.cust&&!next.some(x=>x.addr===t.addr.toLowerCase())); let born=[]; LAUNCHED.length=0; try{ const cnt=Number(BigInt(await rpc(C,[{to:PFACTORY,data:"0x"+SEL_BYCREATOR_N+encAddr(PLAUNCH)},L])||"0x0")); const take=Math.min(cnt,LAUNCH_SCAN); const raw=take?await rpc(C,[{to:PFACTORY,data:"0x"+SEL_BYCREATOR+encAddr(PLAUNCH)+encUint(BigInt(cnt-take))+encUint(BigInt(take))},L]):"0x"; const h=strip0x(raw||""),n=h.length>=128?Number(BigInt("0x"+h.slice(64,128)||"0")):0; let pools=[]; for(let i=0;i<n&&i<take;i++)pools.push("0x"+h.slice(128+i*64+24,128+(i+1)*64)); if(pools.length){ const depth=await mc3Deep(pools.map(a=>[{to:a,data:"0x"+SEL_RESERVE0}]).flat()); const byDepth=pools.map((p,i)=>{let d=0n;try{d=BigInt(depth[i]||"0x0")}catch{}return{p,d}}) .sort((x,y)=>x.d===y.d?0:(x.d<y.d?1:-1)).slice(0,LAUNCH_SHOW); pools=byDepth.map(x=>x.p); const meta=await mc3Deep(pools.flatMap(a=>[{to:a,data:"0x"+SEL_TOKEN1}])); const toks=[]; meta.forEach((x,i)=>{const a=x?("0x"+strip0x(x).slice(24)).toLowerCase():""; if(/^0x[0-9a-f]{40}$/.test(a))toks.push({a,pi:i})}); const seen=new Set(next.map(x=>x.addr.toLowerCase())); const info=await mc3Deep(toks.flatMap(({a})=>[{to:a,data:"0x95d89b41"},{to:a,data:"0x"+SEL_CONTRACTURI}, {to:PLAUNCH,data:"0x"+SEL_CREATOROF+encAddr(a)}])); const taken=new Set(next.map(x=>x.sym.toLowerCase())); toks.forEach(({a,pi},i)=>{ if(seen.has(a))return; seen.add(a); let sym="?";try{sym=safeSym(decodeString(info[i*3]||"0x").trim()).slice(0,12)}catch{} if(!sym||sym==="?")return; if(taken.has(sym.toLowerCase()))sym+=" "+a.slice(0,6)+"…";else taken.add(sym.toLowerCase()); let art="";try{art=imgFromContractURI(decodeString(info[i*3+1]||"0x"))}catch{} if(art)logoCache[a]=art; const dep=byDepth[pi]?byDepth[pi].d:0n; let by="";try{const c="0x"+strip0x(info[i*3+2]||"").slice(24);if(/^0x[0-9a-f]{40}$/i.test(c)&&!/^0x0{40}$/i.test(c))by=short(c)}catch{} const desc=[dep>0n?`${(+formatUnits(dep,18)).toFixed(dep<10n**16n?5:3)} ETH liquidity`:"no liquidity yet", by?`by ${by}`:""].filter(Boolean).join(" \u00b7 "); born.push({sym,addr:a,dec:18,std:"ft",name:"",desc,launched:1,dep, icon:art?`<img width="20" height="20" style="border-radius:50%;object-fit:cover" src="${art}" alt="">`:genIcon(sym)}); LAUNCHED.push({addr:a,sym,pool:pools[pi]||""}); }); } }catch{} const anchors=BUILTIN.filter(b=>(b.addr===ZERO||b.addr===WETH)&&!next.some(x=>x.addr===b.addr)); const bornAt=new Set(born.map(x=>x.addr.toLowerCase())); TOKENS.splice(0,TOKENS.length,...next,...anchors,...born, ...mine.filter(t=>!bornAt.has(t.addr.toLowerCase()))); setListNote(rankFellBack?"Ranked by listing order — conviction unavailable":""); }catch(e){setListSource("could not be reached")} } const strip0x=h=>h.startsWith("0x")?h.slice(2):h; const pad32=h=>strip0x(h).padStart(64,"0"); const encUint=v=>pad32(BigInt(v).toString(16)); const encAddr=a=>pad32((a||ZERO).toLowerCase()); const isAddr=a=>/^0x[0-9a-fA-F]{40}$/.test(a||""); const decStr=hex=>{const h=strip0x(hex);const n=parseInt(h.slice(64,128),16); return new TextDecoder().decode(hexToBytes(h.slice(128,128+n*2)))}; const decUintArr=hex=>{const h=strip0x(hex);const n=parseInt(h.slice(64,128),16);const o=[]; for(let i=0;i<n;i++)o.push(BigInt("0x"+h.slice(128+i*64,192+i*64)));return o}; const encBool=b=>pad32(b?"1":"0"); const toHex=v=>"0x"+BigInt(v).toString(16); const parseUnits=(s,dec)=>{ const t=s.trim().replace(/,/g,""); if(!/^(?:\d+|\d*\.\d+|\d+\.)$/.test(t))throw Error("invalid amount"); const[ip,fp=""]=t.split("."); if(fp.length>dec)throw Error("too many decimals"); return BigInt((ip||"0")+fp.padEnd(dec,"0")); }; const formatUnits=(v,dec)=>{ if(!dec)return BigInt(v).toString(); const s=BigInt(v).toString().padStart(dec+1,"0"); const ip=s.slice(0,-dec); const fp=s.slice(-dec).replace(/0+$/,""); return fp?`${ip}.${fp}`:ip; }; const trimAmt=(v,dec)=>{const s=formatUnits(v,dec),i=s.indexOf(".");if(i<0)return s;const r=s.slice(0,i+7).replace(/\.?0+$/,"");return r==="0"&&v?s:r}; const maxAmt=(v,dec)=>{v=BigInt(v);if(dec<=6)return formatUnits(v,dec); const u=10n**BigInt(dec-6);return formatUnits((v+u-1n)/u,6)}; const fitFont=el=>{ const l=el.value.length,s=l>18?".9em":l>14?"1.1em":l>10?"1.25em":"1.65em"; if(el.style.fontSize!==s)el.style.fontSize=s; }; const decQ=(hex,slip,eo,u,v,S,mv)=>{ const h=strip0x(hex),num=i=>BigInt("0x"+h.slice(i*64,(i+1)*64)),E=()=>{throw Error("bad quote")}; if(h.length<(v+2)*64)E(); const o=Number(num(v))*2;if(o+64>h.length)E(); const n=Number(BigInt("0x"+h.slice(o,o+64)));if(!n||o+64+n*2>h.length)E(); const g=num(u+3),t=g>0n,k=S?(num(3)>0n?0:u):t?u:0,B=10000n,ai=S?num(2)+num(u+2):num(2),ao=S?num(3)+g:t?g:num(3),q=eo?ai:ao; const src=[];for(let w=0;w<=u;w+=4)if(num(w+2)>0n||num(w+3)>0n)src.push(Number(num(w))); return{best:{source:Number(num(k)),feeBps:num(k+1),amountIn:ai,amountOut:ao},sources:src,callData:"0x"+h.slice(o+64,o+64+n*2),amountLimit:eo?(q*(B+slip)+B-1n)/B:q*(B-slip)/B,msgValue:num(mv||v+1),slipBps:slip}; }; const merge=(est,exe)=>{if(!est)return exe;if(!exe)return null;return{...exe,best:est.best}}; const NO_WALLET="No wallet detected."; let PROV=null; const prov=()=>PROV||window.ethereum; const rpc=(method,params)=>{const e=prov(); return e?e.request({method,params}) :Promise.reject(Object.assign(Error(NO_WALLET),{noWallet:true}))}; const rpcRead=async(method,params,tries=4)=>{ let last; for(let i=0;i<tries;i++){ try{return await rpc(method,params)} catch(e){last=e;await new Promise(r=>setTimeout(r,300*(i+1)))} } throw last; }; const okRet=h=>{h=strip0x(h);if(h&&BigInt("0x"+h)===0n)throw Error("token returned false")}; const waitTx=async hash=>{ const end=Date.now()+600000; let misses=0,wait=1500; while(Date.now()<end){ try{ const r=await rpc("eth_getTransactionReceipt",[hash]); misses=0; if(r){ if(r.status!=="0x1")throw Error("tx reverted: "+hash); return r; } wait=Math.min(wait+500,5000); }catch(e){ if(/tx reverted/.test(e.message||""))throw e; if(++misses>2)wait=Math.min(wait*2,15000); } await new Promise(res=>setTimeout(res,wait)); } throw Error("tx not mined in time"); }; const checkWallet=async()=>{if(!account)throw Error("Connect a wallet first.");const[c,a]=await Promise.all([rpc(I,[]),rpc("eth_accounts",[])]);if(+c!==1||a[0]?.toLowerCase()!==account.toLowerCase())throw Error("wallet changed")}; const hexToBytes=h=>{const s=strip0x(h);const o=new Uint8Array(s.length/2);for(let i=0;i<o.length;i++)o[i]=parseInt(s.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=n=>{let nd="0x"+"0".repeat(64); if(n)for(const l of n.split(".").reverse())nd=keccak(new Uint8Array([...hexToBytes(nd),...hexToBytes(keccak(new TextEncoder().encode(l)))])); return nd}; const encStr=v=>{const b=new TextEncoder().encode(v);let h=""; for(const x of b)h+=x.toString(16).padStart(2,"0"); return pad32("20")+encUint(b.length)+h.padEnd(Math.ceil(h.length/64)*64,"0")}; const retAddr=h=>{const s=strip0x(h||""); if(s.length!==64||/[^0-9a-fA-F]/.test(s)||/[^0]/.test(s.slice(0,24)))return""; const a="0x"+s.slice(24).toLowerCase(); return /^0x0{40}$/.test(a)?"":a}; const nameOk=n=>/^[\x21-\x7e]+$/.test(n)&&n.split(".").every(l=>l.length>0); const nsFwd=async(reg,n)=>{const id=await rpc(C,[{to:reg,data:"0x"+SEL_CID+encStr(n)},L]); if(strip0x(id||"").length!==64)return""; return retAddr(await rpc(C,[{to:reg,data:"0x"+SEL_RES+strip0x(id)},L]))}; 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 encBytes=h=>{h=strip0x(h);return encUint(h.length/2)+h.padEnd(Math.ceil(h.length/64)*64,"0")}; const encResolve=(dns,inner)=>{const a=encBytes(dns); return pad32("40")+encUint(64+a.length/2)+a+encBytes(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=> rpc(C,[{to:ENSREG,data:"0x"+SEL_RSLV+pad32(nd)},L]).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 s=await rpc(C,[{to:r,data:supports(SEL_ERESOLVE)},L]); return strip0x(s||"").length===64&&BigInt(s)===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 rpc(C,[{to:f.r,data:inner},L])); if(!await ensWild(f.r))return""; try{return retAddr(decBytes(await rpc(C,[{to:f.r, data:"0x"+SEL_ERESOLVE+encResolve(dnsEnc(n),inner)},L])))} 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 rpc(C,[{to:r,data:"0x"+SEL_ENAME+pad32(nd)},L])).trim()}catch{return""}}; const nameFwd=n=>/\.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=>rpc(C,[c,L]).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()===a.toLowerCase()?n:""}; return await agrees(str(w))||await agrees(str(g)) ||await agrees(await ensRevName(nd,retAddr(er)))||""}; const decodeString=hex=>{ const h=strip0x(hex); 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; return new TextDecoder().decode(hexToBytes(h.slice(off+64,off+64+len))); }; const kecStr=v=>keccak(new TextEncoder().encode(v)); const ckAddr=a=>{const s=strip0x(a); if(s===s.toLowerCase()||s===s.toUpperCase())return true; const h=strip0x(kecStr(s.toLowerCase())); for(let i=0;i<40;i++){const c=s[i]; if(c>="a"&&c<="f"&&parseInt(h[i],16)>=8)return false; if(c>="A"&&c<="F"&&parseInt(h[i],16)<8)return false} return true}; const domainSep=(name,ver,tok)=>keccak(hexToBytes( kecStr(EIP712DOM)+pad32(kecStr(name))+pad32(kecStr(ver))+encUint(CHAIN_ID)+encAddr(tok))); const permitInfo=async(tok,owner)=>{ try{ const[ds,th,nc,nm]=await mc3Deep([ {to:tok,data:"0x"+SEL_DS},{to:tok,data:"0x"+SEL_PERMIT_TH}, {to:tok,data:"0x"+SEL_NONCES+encAddr(owner)},{to:tok,data:"0x"+SEL_NAME}]); if(!ds||ds==="0x")return null; if(th&&strip0x(th).length>=64&&strip0x(th).slice(0,64).toLowerCase()!==TH_2612)return null; if(!nc||!nm)return null; const nonce=BigInt(nc); const name=decodeString(nm); for(const ver of ["1","2"])if(domainSep(name,ver,tok).toLowerCase()===ds.toLowerCase()) return{name,ver,nonce}; }catc
0xacfba7ce…4082·#25,784,646·0x904a063e…6864a3
itle="Toggle theme" aria-label="Toggle theme"></button></div> <div class="tabs" role="tablist"><button id="tabSwap" class="on" role="tab" aria-selected="true">Swap</button><button id="tabSend" role="tab" aria-selected="false">Send</button><button id="tabBook" role="tab" aria-selected="false">Orders</button></div> <div class="panel"><div class="hdr"><small id="payL">You pay</small><span class="pill"><span id="fromIcon"></span><select id="fromSel"></select><button type="button" class="tkb" id="fromPick" aria-haspopup="listbox" aria-expanded="false"><span class="tkbs"></span><span class="tkbc">▾</span></button></span></div><div class="row" id="amtRow"><input id="amt" type="text" inputmode="decimal" placeholder="0.0" aria-label="Amount to pay" autocomplete="off" spellcheck="false"></div><small id="bal"></small></div> <button id="flip" class="flip" title="Flip tokens" aria-label="Flip tokens"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14m-5-5 5 5 5-5"/></svg></button> <div class="panel" id="rcvPanel"><div class="hdr"><small id="rcvHdr">You receive</small><span class="pill"><span id="toIcon"></span><select id="toSel"></select><button type="button" class="tkb" id="toPick" aria-haspopup="listbox" aria-expanded="false"><span class="tkbs"></span><span class="tkbc">▾</span></button></span></div><div class="row" id="outRow"><input id="outAmt" type="text" inputmode="decimal" placeholder="0.0" aria-label="Amount to receive" autocomplete="off" spellcheck="false"></div><small id="bal1" class="hide"></small><small id="rate"></small></div> <div id="lqPanel" class="hide"><div class="hdr"><small>Liquidity</small><small id="lqSub"></small></div><div id="lqList"></div></div> <div id="lnPanel" class="hide"> <div class="hdr lnhdr"><small>Launch a coin</small><small id="lnSub">one transaction, no ether needed</small></div> <label class="dly">Name<input id="lnName" maxlength="64" placeholder="Zero Cat" autocomplete="off" spellcheck="false"></label> <label class="dly">Symbol<input id="lnSym" maxlength="16" placeholder="ZCAT" autocomplete="off" spellcheck="false"></label> <label class="dly">Supply<input id="lnSupply" inputmode="decimal" value="1,000,000,000" autocomplete="off" spellcheck="false"></label> <div class="lngap">Market</div> <label class="dly" title="The pool holds NO ether at launch - this is its virtual ether reserve, and it is the only thing deciding where on the curve your market opens. It sets the opening price and how hard that price is to move, which are the same knob: roughly this much buying moves the market halfway. Small numbers mean the first buyer takes a huge share of the supply.">Starting market cap<span class="lnu"><input id="lnMcap" inputmode="decimal" value="30" autocomplete="off" spellcheck="false">ETH</span></label> <label class="dly" title="The percentage of total supply paid to the creator at launch. Everything else is seeded into the pool. Capped at 20%.">Creator keeps<span class="lnu"><input id="lnAlloc" inputmode="decimal" value="0" autocomplete="off" spellcheck="false">%</span></label> <div class="lngap">Optional</div> <label class="dly" id="lnArtRow" title="Held on-chain as contract code, not as a link. Nothing to pin and no gateway to go dark. Editable later, so a rough mark now costs nothing.">Logo<span><img id="lnPrev" alt=""><small id="lnArtNote">none</small><button type="button" id="lnPick" class="lnbtn">Choose</button></span></label> <input id="lnArt" type="file" accept="image/png,image/jpeg,image/webp,image/gif,image/svg+xml" class="hide"> </div> <input id="rc" class="rcpt" placeholder="Recipient (optional) — 0x or .eth/.(g)wei" spellcheck="false" autocomplete="off"> <small id="rcvEl"></small> <small id="fcEl" class="hide" role="status" aria-live="polite"></small> <small id="lnNote" class="hide" role="status" aria-live="polite"></small> <button id="lnGo" class="primary hide">Launch</button> <label class="dly hide" id="kindL">Order type<select id="kind"><option value="fixed">Fixed limit</option><option value="dutch">Dutch decay</option><option value="floor">Climbing bid</option></select></label> <label class="dly hide" id="dlyL">SLOW<select id="dly"><option value="0">Instant</option><option value="3600">1 hour</option><option value="86400">1 day</option><option value="259200">3 days</option><option value="604800">7 days</option></select></label> <label class="dly hide" id="tipL" title="A time-locked transfer does not move on its own: once the lock expires somebody has to send the claim transaction. Tick this and the page posts a small ETH tip alongside the deposit, which pays a keeper to claim it for the recipient the moment it matures. The tip is refundable — if you reverse the transfer, the ether comes back to you.">Auto-claim<span class="tipr"><small id="tipNote"></small><input id="tipCk" type="checkbox" aria-describedby="tipNote"></span></label> <label class="dly hide" id="fillL">Fill<select id="fill"><option value="1">Any amount</option><option value="0">All or nothing</option></select></label> <label class="dly hide" id="floorL">Floor total<input id="floorAmt" inputmode="decimal" placeholder="0.0"></label> <label class="dly hide" id="dRestL" title="A Dutch lot has TWO clocks. It decays to your floor over the window above, and then - unless it is given an end - it sits there fillable forever, until you spend gas cancelling. This is how long it rests at the floor before the escrow simply comes back to you.">Rest at floor<select id="dRest"><option value="0">Forever</option><option value="86400">1 day</option><option value="604800">1 week</option><option value="2592000">30 days</option></select></label> <label class="dly hide" id="nftIdL">Token ID<input id="nftId" inputmode="numeric" placeholder="Any from collection" spellcheck="false" autocomplete="off"></label> <label class="dly hide" id="ethModeL" title="Applies when YOU fill an order from the book, or cancel one of yours and the escrow comes back wrapped. It does NOT change what an order you place pays you: boards are denominated in WETH, and a maker's proceeds are transferred as WETH with no unwrap available - so a limit order asking for ETH is an order asking for WETH, and fills as such. Paying is not a setting either: the page spends whichever of your ETH and WETH covers it, and wraps the difference if only the two together do.">Fills &amp; cancels pay<select id="ethMode"><option value="auto">ETH</option><option value="weth">WETH</option></select></label> <small id="floorNote" class="hide" role="status" aria-live="polite"></small> <button id="swap" class="primary">Connect Wallet</button> <div id="listNote" class="hide" role="status" aria-live="polite"></div> <div id="wkWrap" class="hide"><div id="wkPanel" role="dialog" aria-modal="true" aria-label="Choose a wallet"><div id="wkHdr">Choose a wallet</div><div id="wkList" role="listbox"></div></div></div> <div id="tkPanel" class="tkp hide"><input id="tkFind" type="text" placeholder="Search name, symbol or address" autocomplete="off" spellcheck="false" aria-label="Filter tokens"><div id="tkList" role="listbox"></div></div> <div id="stat" role="status" aria-live="polite"></div> <button id="chTog" class="chtog hide" aria-expanded="false">Chart <span>▾</span></button> <div id="chBox" class="hide"><div class="chtf" id="chTf"></div><div id="chArt"></div><small id="chNote"></small></div> <div id="pos"></div> <button id="bkTog" class="chtog hide" aria-expanded="true">Orderbook <span>▾</span></button> <div id="book"></div> <div id="docPanel" class="docs hide"> <details><summary>Swapping</summary> <p>Every quote is compared across Uniswap v2, v3 and v4, Curve, Sushi and our own Precision pools, and the best one wins. One route, one signature.</p> <p><b>Slippage</b> is the worst price you will accept &mdash; move past it and the trade reverts rather than filling badly. <b>Price impact</b> is how far your own size moves the price; the page warns as it grows and will not let a very bad one through quietly.</p> <p>ETH and WETH are interchangeable here. The page wraps or unwraps whichever way the pool needs, inside the same transaction.</p></details> <details><summary>Sending</summary> <p>A plain transfer, or a <b>SLOW</b> one: time-locked, and reversible by you at any point until it matures. Useful for large amounts and for addresses you have not paid before.</p> <p>A locked transfer does not deliver itself &mdash; someone must send the claim once it matures. Tick <b>auto-claim</b> and the page posts a small tip so a keeper does that for the recipient. The tip is refundable: reverse the transfer and it comes back.</p></details> <details><summary>Orders</summary> <p><b>Fixed limit</b> &mdash; a price you name. It rests until someone fills it or you cancel.</p> <p><b>Dutch decay</b> &mdash; starts above your floor and falls to it over a window you choose. It has two clocks: the decay, then how long it rests at the floor before the escrow returns to you. Set that to <em>Forever</em> and it waits until you cancel.</p> <p><b>Climbing bid</b> &mdash; the mirror: a bid that rises toward a ceiling you name.</p> <p>Orders are escrowed on chain when placed, so a fill cannot fail for want of funds, and cancelling returns the escrow. Boards are denominated in WETH &mdash; an order asking for ETH is asking for WETH, and pays out as such.</p></details> <details><summary>Liquidity <em><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3.2c3.6 4 6 7.2 6 10.1a6 6 0 0 1-12 0c0-2.9 2.4-6.1 6-10.1z"/></svg></em></summary> <p>Precision pools are <b>bands</b>, not endless curves. A position earns fees only while the price is inside its band, and holds one asset or the other at the edges. A wider band is safer and earns less; a narrow one is the opposite.</p> <p>The bar on each row shows where the price sits in that band. <em>Out of range</em> means it is earning nothing right now.</p></details> <details><summary>Launching a coin <em><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="3.5"/></svg></em></summary> <p>One transaction, and no ether needed aside from gas. A fixed supply is minted, your allocation is paid to you immediately, and <b>everything else is seeded into a fresh ETH pool</b> &mdash; one-sided, so the pool opens holding only your token.</p> <p><b>Starting market cap</b> is the pool's virtual ether reserve. It sets the opening price and how hard that price is to move, which are the same dial: roughly that much buying moves the market halfway. Small numbers mean the first buyer takes a large share of the supply, which is why the form says what one ether would buy.</p> <p><b>Creator keeps</b> is a share of the token SUPPLY, minted straight to you when the coin launches &mdash; tokens, not ether, and not a fee on trading. Keep 10% of a billion and 100,000,000 land in your wallet at once, unlocked, with nothing vesting.</p> <p>Whatever you keep does not go into the pool, and selling it later pulls ether back out of that pool &mdash; so a modest-looking percentage can be a claim on most of what early buyers put in. Anyone deciding whether to buy your coin can work that out too.</p> <p>A <b>logo</b> is stored as contract code on Ethereum &mdash; no IPFS, no pinning service, nothing to go dark. Upload anything; the page compresses it and shows the size before you sign. You can replace it later.</p></details> <details><summary>Fees</summary> <p>A launched market charges <b>1%</b> per swap. Half of that stays in the pool as reserves, which raises the floor for every holder. The other half is collectable, and splits <b>80% creator, 10% protocol, 10% tithe</b>.</p> <p>The <b>tithe</b> is burned to Ethereum itself. The ether goes to the BETH burner, which destroys it and mints a BETH receipt recording that it happened. Nobody can spend it and it cannot be redeemed &mdash; the ether no longer exists. What it buys is a smaller ether supply, which accrues to everyone holding ether rather than to us.</p> <p>The token side of collected fees is burned outright too &mdash; a launched coin's supply only ever falls.</p></details> <details><summary>What this page is</summary> <p>There is no server. This page is a contract on Ethereum and its bytes cannot be changed by anyone, us included &mdash; a new version is a new address, not an edit. It talks to your wallet and to Ethereum, and to nothing else. Connecting by <b>WalletConnect</b> is the one exception: that needs WalletConnect's relay to reach your phone, and a read node, which you can point anywhere you like.</p> <p>Your keys never leave your wallet. Nothing here custodies your funds; every trade settles from your address to a contract you can read.</p> <p>The token list is <b>zList</b>, ranked on chain by conviction staking. Anything not on it can still be traded by pasting its address.</p></details> <div class="docsmore">Longer notes at <a href="https://docs.zswap.wei.limo" target="_blank" rel="noopener">docs.zswap.wei.limo</a></div> </div> <footer id="foot"><span id="footV"></span><span id="footAddr"></span><span id="footBurn"></span><span>&middot; <a id="footDoc" href="#" role="button">how it works</a></span><span>&middot; <a id="footList" target="_blank" rel="noopener" title="Token order here comes from zList, ranked by onchain conviction staking. Stake to move a token up the list - or to get one listed at all.">token list</a></span></footer> </div> <script>const IC0='<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">', ICMOON=IC0+'<path d="M20.5 14.6A8.6 8.6 0 0 1 9.4 3.5a8.6 8.6 0 1 0 11.1 11.1z"/></svg>', ICSUN=IC0+'<circle cx="12" cy="12" r="4.2"/><path d="M12 2v2m0 16v2M4.2 4.2l1.5 1.5m12.6 12.6 1.5 1.5M2 12h2m16 0h2M4.2 19.8l1.5-1.5M18.3 5.7l1.5-1.5"/></svg>'; const ic=()=>{const d=H.className=='d';th.innerHTML=d?ICSUN:ICMOON;tc.content=d?'#101010':'#fafafa'};ic(); const ZSWAP_VERSION="0.2"; const ZSWAP_SELF=""; const ZSWAP_PREVIOUS="0x00000095643CFfA7D9fae407a84dfCB6406456c6"; const ZLIST_URL="https://0x0000006d936ba3653b8854490e16e782cd32a9a8.w4eth.io/"; const selfFromUrl=()=>{ const h=(location.hostname||"").split("."); if(!/^0x[0-9a-fA-F]{40}$/.test(h[0]||""))return ""; if(/^\d+$/.test(h[1]||"")&&h[1]!=="1")return ""; return h[0]; }; const short=a=>a.slice(0,6)+"\u2026"+a.slice(-4); (()=>{ const self=selfFromUrl()||ZSWAP_SELF; footV.textContent=`zSwap v${ZSWAP_VERSION}`; footList.href=ZLIST_URL; if(self){ footAddr.innerHTML=`\u00b7 <a href="https://etherscan.io/address/${self}#code" target="_blank" rel="noopener" ` +`title="This exact build, on chain. Its bytes cannot change; a newer version is a different address.">${short(self)}</a>`; if(ZSWAP_PREVIOUS)footAddr.innerHTML+=` \u00b7 <a href="https://etherscan.io/address/${ZSWAP_PREVIOUS}#code" ` +`target="_blank" rel="noopener" title="The version this one succeeded. It still serves its own page, unchanged.">prev</a>`; } })(); th.onclick=()=>{H.classList.toggle('d');LS.t=H.className=='d'?'d':'l';ic()}; const ZQUOTER="0xc7a03f9ed2be5feea18ce93e12f4f05c98287c16"; const WNS="0x0000000000696760E15f265e828DB644A0c242EB"; const GNS="0x9D51D507BC7264d4fE8Ad1cf7Fe191933A0a81d6"; const ENSREG="0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e"; const ZROUTER="0x000000000000FB114709235f1ccBFfb925F600e4"; const PFACTORY="0x000000Eb27B557aB426d9E99cFd54EC455799e81"; const PLQLENS="0x000000956bf20A41C54BaE4a4b6F5C8A166DAB4E"; const PPLENS="0x000000Bad3a2fa57ed74fa06000573ccddF6B7fB"; const PI_WORDS=19,PI_POOL=0,PI_LIQ=9,PI_HOOK=10,PI_SL=3,PI_SH=4, PI_FEE=5,PI_R0=6,PI_R1=7,PI_PX=8; const CHAIN_ID=1; const ZERO="0x0000000000000000000000000000000000000000"; const WETH="0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"; const TOKENLIST="0x0000006013dF75A31678B786061C2B54bf531524"; const ZLISTLENS="0x000000cEa3AB048d59473F3fb116A8D7F1abd247"; const SEL_RANKEDIDS="df7ca268"; const SEL_JSON="74e18e96"; const SEL_CID="fb021939",SEL_RES="4f896d4f",SEL_REV="9af8b7aa"; const SEL_RSLV="0178b8bf",SEL_EADDR="3b3b57de",SEL_ENAME="691f3431"; const SEL_ERESOLVE="9061b923"; const NAME_OFFCHAIN="That name keeps its records off chain, and this page reads only on-chain state."; const SEL_ALLOWANCE="dd62ed3e"; const SEL_LATEST="52bfe789",SEL_PREV="247dfaa8",SEL_SUCCAT="451aae60"; const MATURITY=259200; const SEL_DS="3644e515",SEL_NONCES="7ecebe00",SEL_NAME="06fdde03",SEL_PERMIT_TH="30adf81f"; const TH_2612="6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9"; const SEL_RPERMIT="7ac2ff7b",SEL_MULTICALL="ac9650d8",SEL_SWEEP="cb019b84"; const PERMIT2="0x000000000022D473030F116dDEE9F6B43aC78BA3"; const SEL_P2TF="09d31579"; const EIP712DOM="EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"; const SEL_APPROVE="095ea7b3"; const SEL_BALANCEOF="70a08231"; const V4PORT="0x000000dfb53Fa7f1c486470034741d5BCBE14BE9"; const V4LENS="0x000000c3aE1692983941495162A4AAB40660E65F"; const SEL_V4QUOTE="d500463c",SEL_V4SWAP="48e6f730"; const V4POOL_KEY="0x95a932c205571d4d1ca72715c642a2eca21dde79ffc28ff11509681f9383385f"; const C="eth_call",S="eth_sendTransaction",L="latest",I="eth_chainId"; const SOURCES=["UniV2","Sushi","zAMM","UniV3","UniV4","Curve","Lido","Wrap","Precision"]; const SRC_PRECISION=8; const ETH_ICON=`<svg width="20" height="20" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" r="16" fill="#627EEA"/><g fill="#FFF" fill-rule="nonzero"><path fill-opacity=".602" d="M16.498 4v8.87l7.497 3.35z"/><path d="M16.498 4L9 16.22l7.498-3.35z"/><path fill-opacity=".602" d="M16.498 21.968v6.027L24 17.616z"/><path d="M16.498 27.995v-6.028L9 17.616z"/><path fill-opacity=".2" d="M16.498 20.573l7.497-4.353-7.497-3.348z"/><path fill-opacity=".602" d="M9 16.22l7.498 4.353v-7.701z"/></g></g></svg>`; const USDC_ICON=`<svg width="20" height="20" viewBox="0 0 32 32"><g fill="none"><circle fill="#2775CA" cx="16" cy="16" r="16"/><g fill="#FFF"><path d="M20.022 18.124c0-2.124-1.28-2.852-3.84-3.156-1.828-.243-2.193-.728-2.193-1.578 0-.85.61-1.396 1.828-1.396 1.097 0 1.707.364 2.011 1.275a.458.458 0 00.427.303h.975a.416.416 0 00.427-.425v-.06a3.04 3.04 0 00-2.743-2.489V9.142c0-.243-.183-.425-.487-.486h-.915c-.243 0-.426.182-.487.486v1.396c-1.829.242-2.986 1.456-2.986 2.974 0 2.002 1.218 2.791 3.778 3.095 1.707.303 2.255.668 2.255 1.639 0 .97-.853 1.638-2.011 1.638-1.585 0-2.133-.667-2.316-1.578-.06-.242-.244-.364-.427-.364h-1.036a.416.416 0 00-.426.425v.06c.243 1.518 1.219 2.61 3.23 2.914v1.457c0 .242.183.425.487.485h.915c.243 0 .426-.182.487-.485V21.34c1.829-.303 3.047-1.578 3.047-3.217z"/><path d="M12.892 24.497c-4.754-1.7-7.192-6.98-5.424-11.653.914-2.55 2.925-4.491 5.424-5.402.244-.121.365-.303.365-.607v-.85c0-.242-.121-.424-.365-.485-.061 0-.183 0-.244.06a10.895 10.895 0 00-7.13 13.717c1.096 3.4 3.717 6.01 7.13 7.102.244.121.488 0 .548-.243.061-.06.061-.122.061-.243v-.85c0-.182-.182-.424-.365-.546zm6.46-18.936c-.244-.122-.488 0-.548.242-.061.061-.061.122-.061.243v.85c0 .243.182.485.365.607 4.754 1.7 7.192 6.98 5.424 11.653-.914 2.55-2.925 4.491-5.424 5.402-.244.121-.365.303-.365.607v.85c0 .242.121.424.365.485.061 0 .183 0 .244-.06a10.895 10.895 0 007.13-13.717c-1.096-3.46-3.778-6.07-7.13-7.162z"/></g></g></svg>`; const USDT_ICON=`<svg width="20" height="20" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" r="16" fill="#26A17B"/><path fill="#FFF" d="M17.922 17.383v-.002c-.11.008-.677.042-1.942.042-1.01 0-1.721-.03-1.971-.042v.003c-3.888-.171-6.79-.848-6.79-1.658 0-.809 2.902-1.486 6.79-1.66v2.644c.254.018.982.061 1.988.061 1.207 0 1.812-.05 1.925-.06v-2.643c3.88.173 6.775.85 6.775 1.658 0 .81-2.895 1.485-6.775 1.657m0-3.59v-2.366h5.414V7.819H8.595v3.608h5.414v2.365c-4.4.202-7.709 1.074-7.709 2.118 0 1.044 3.309 1.915 7.709 2.118v7.582h3.913v-7.584c4.393-.202 7.694-1.073 7.694-2.116 0-1.043-3.301-1.914-7.694-2.117"/></g></svg>`; const WBTC_ICON=`<svg width="20" height="20" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" r="16" fill="#F7931A"/><path fill="#FFF" fill-rule="nonzero" d="M23.189 14.02c.314-2.096-1.283-3.223-3.465-3.975l.708-2.84-1.728-.43-.69 2.765c-.454-.114-.92-.22-1.385-.326l.695-2.783L15.596 6l-.708 2.839c-.376-.086-.746-.17-1.104-.26l.002-.009-2.384-.595-.46 1.846s1.283.294 1.256.312c.7.175.826.638.805 1.006l-.806 3.235c.048.012.11.03.18.057l-.183-.045-1.13 4.532c-.086.212-.303.531-.793.41.018.025-1.256-.313-1.256-.313l-.858 1.978 2.25.561c.418.105.828.215 1.231.318l-.715 2.872 1.727.43.708-2.84c.472.127.93.245 1.378.357l-.706 2.828 1.728.43.715-2.866c2.948.558 5.164.333 6.097-2.333.752-2.146-.037-3.385-1.588-4.192 1.13-.26 1.98-1.003 2.207-2.538zm-3.95 5.538c-.533 2.147-4.148.986-5.32.695l.95-3.805c1.172.293 4.929.872 4.37 3.11zm.535-5.569c-.487 1.953-3.495.96-4.47.717l.86-3.45c.975.243 4.118.696 3.61 2.733z"/></g></svg>`; const WSTETH_ICON=`<svg width="20" height="20" viewBox="0 0 32 32"><g fill="none"><circle fill="#00A3FF" cx="16" cy="16" r="16"/><path d="M9.437 14.864l-.181.275c-2.048 3.097-1.603 7.253 1.034 9.824 1.561 1.521 3.622 2.353 5.683 2.353 0 0 0 0-6.536-12.452z" fill="#FFF"/><path opacity=".6" d="M15.997 18.611l-6.56-3.747c6.56 12.452 6.56 12.452 6.56 12.452 0-2.683 0-5.623 0-8.705z" fill="#FFF"/><path opacity=".6" d="M22.563 14.864l.181.275c2.048 3.097 1.603 7.253-1.034 9.824-1.561 1.521-3.622 2.353-5.683 2.353 0 0 0 0 6.536-12.452z" fill="#FFF"/><path opacity=".2" d="M16.003 18.611l6.56-3.747c-6.56 12.452-6.56 12.452-6.56 12.452 0-2.683 0-5.623 0-8.705z" fill="#FFF"/><path opacity=".2" d="M16.004 10.239v6.459l5.654-3.23-5.654-3.229z" fill="#FFF"/><path opacity=".6" d="M16.005 10.239l-5.655 3.229 5.655 3.23v-6.46z" fill="#FFF"/><path d="M16.005 4.805l-5.655 8.668 5.655-3.233V4.805z" fill="#FFF"/><path opacity=".6" d="M16.004 10.238l5.658 3.23-5.658-8.674v5.444z" fill="#FFF"/></g></svg>`; const RETH_ICON=`<svg width="20" height="20" viewBox="0 0 32 32"><circle cx="16" cy="16" r="16" fill="#ED5A37"/><path d="M16 5.75L9.89 15.85 16 19.45l6.08-3.6z" fill="#FFF"/><path d="M16 20.6l-6.11-3.59L16 25.58l6.08-8.57z" fill="#FFF"/></svg>`; const BOLD_ICON=`<svg width="20" height="20" viewBox="0 0 32 32" fill="none"><circle cx="16" cy="16" r="16" fill="#63D77D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M12.1719 4.56641H8.58203V26.1016H15.7617V25.2422C16.8398 25.793 18.0586 26.1055 19.3555 26.1055C23.7148 26.1055 27.25 22.5703 27.25 18.207C27.25 13.8438 23.7148 10.3086 19.3555 10.3086C18.0586 10.3086 16.8398 10.6211 15.7617 11.1719V4.56641H12.1719ZM15.7617 11.1719C13.207 12.4805 11.457 15.1406 11.457 18.207C11.457 21.2734 13.207 23.9336 15.7617 25.2422V11.1719Z" fill="#1C1D4F"/></svg>`; const TOKENS=[ {sym:"ETH", addr:ZERO, dec:18, icon:ETH_ICON}, {sym:"WETH", addr:WETH, dec:18, icon:ETH_ICON}, {sym:"wstETH",addr:"0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0",dec:18,icon:WSTETH_ICON}, {sym:"rETH", addr:"0xae78736cd615f374d3085123a210448e74fc6393",dec:18,icon:RETH_ICON}, {sym:"WBTC", addr:"0x2260fac5e5542a773aa44fbcfedf7c193bc2c599",dec:8, icon:WBTC_ICON}, {sym:"USDC", addr:"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",dec:6, icon:USDC_ICON}, {sym:"USDT", addr:"0xdac17f958d2ee523a2206206994597c13d831ec7",dec:6, icon:USDT_ICON}, {sym:"BOLD", addr:"0x6440f144b7e50d6a8439336510312d2f54beb01d",dec:18,icon:BOLD_ICON}, ]; const BUILTIN=TOKENS.slice(); function parseV4Pools(extras,tokenAddr){ if(!Array.isArray(extras)||!/^0x[0-9a-f]{40}$/i.test(tokenAddr||""))return[]; const row=extras.find(x=>x&&typeof x.v==="string"&&String(x.k).toLowerCase()===V4POOL_KEY); if(!row)return[]; const out=[]; for(const
0xacfba7ce…4082·#25,784,645·0x796745d7…9bce28
<!doctype html> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>zSwap</title> <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 400' width='400' height='400'%3E%3Crect width='400' height='400' fill='%23000'/%3E%3CclipPath id='frame'%3E%3Crect width='400' height='400'/%3E%3C/clipPath%3E%3Cg clip-path='url(%23frame)'%3E%3Cpath d='M-60-20L460-20L460 90L80 310L460 310L460 420L-60 420L-60 310L320 90L-60 90Z' fill='white'/%3E%3C/g%3E%3C/svg%3E" type="image/svg+xml"> <meta name="theme-color" id="tc" content="#fafafa"> <style>:root{color-scheme:light;--b:#fafafa;--c:#fff;--p:#f6f6f6;--f:#000;--m:#6b6b6b;--n:#666;--e:#ddd;--k:#000;--kf:#fff;--kh:#333;--fh:#ebebeb;--e2:#ececec;--w:#c33;--s:0 1px 3px #0000000a,0 8px 24px #0000000f} .d{color-scheme:dark;--b:#101010;--c:#181818;--p:#242424;--f:#eee;--m:#999;--n:#999;--e:#3d3d3d;--k:#fff;--kf:#000;--kh:#ddd;--fh:#303030;--e2:#2c2c2c;--w:#e57373;--s:0 1px 2px #0000004d,0 12px 32px #00000052} :focus-visible{outline:2px solid var(--f);outline-offset:2px} @media(prefers-reduced-motion:reduce){*{transition:none!important}.flip:hover{transform:none}} body{background:var(--b);color:var(--f);font-family:system-ui,sans-serif;min-height:100dvh;margin:0;padding:1em .85em;display:flex;flex-direction:column;box-sizing:border-box} .card{border:1px solid var(--e);border-radius:.5em;padding:.9em;background:var(--c);max-width:22em;width:100%;margin:auto;box-shadow:var(--s);box-sizing:border-box} #addr{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} #addr.on{cursor:pointer} #addr.on:hover{color:var(--f);text-decoration:line-through} #logo{position:fixed;top:.85em;left:.85em;width:1.75em;height:1.75em; pointer-events:none;z-index:1} #logo rect{fill:#000} #logo path{fill:#fff} @media(max-width:32em){#logo{display:none}} .meta{display:grid;grid-template-columns:minmax(0,1fr) auto auto auto auto auto;align-items:center;gap:.4em;font-size:.85em;color:var(--n);margin-bottom:.75em} #slipL{display:inline-flex;align-items:baseline;gap:.15em;font-size:.72em;color:var(--m); letter-spacing:.04em;white-space:nowrap;margin-right:.15em} .meta input{width:3em;font-size:.9em;padding:.15em .1em;border:0;border-bottom:1px solid var(--e2);transition:border-color .15s;border-radius:0;background:transparent;text-align:right;outline:0;color:var(--f);-moz-appearance:textfield} .meta input::-webkit-outer-spin-button,.meta input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0} #slip{font-size:1.2em;width:2.4em;font-weight:600;font-variant-numeric:tabular-nums} #th,#lk,#lq,#ln{display:inline-flex;align-items:center;justify-content:center;border:0;border-radius:.35em;background:transparent;cursor:pointer;color:var(--m);opacity:.8;width:1.8em;height:1.8em;line-height:1;transition:color .15s,opacity .15s,background .15s} #th:hover,#lk:hover,#lq:hover,#ln:hover,#lq.on,#ln.on{opacity:1} #th:hover,#lk:hover,#lq:hover,#ln:hover{background:var(--p);color:var(--f)} .meta input:hover{border-bottom-color:var(--m)} .meta input:focus{border-bottom-color:currentcolor} .panel{background:var(--p);border:1px solid transparent;border-radius:.5em;padding:.82em .85em;margin:.32em 0;transition:border-color .15s} .panel:hover,.dly:hover,.rcpt:hover,.lqin:hover{border-color:var(--e)} .panel:focus-within,.dly:focus-within,.rcpt:focus-within,.lqin:focus-within{border-color:var(--f)} .panel small{color:var(--m);font-size:.8em} .hdr small{text-transform:uppercase;letter-spacing:.08em;font-size:.7em} .row{display:flex;gap:.4em;align-items:center;margin-top:.35em} .panel input{font-size:1.65em;font-weight:500;letter-spacing:-.02em;border:0;background:transparent;color:var(--f);outline:none;padding:0;flex:1;min-width:0;width:100%;font-variant-numeric:tabular-nums;transition:font-size .1s} .panel input::placeholder{color:var(--m);opacity:.32} .pill{display:inline-flex;align-items:center;gap:.4em;cursor:pointer;transition:opacity .15s} .pill:hover,.pill:focus-within{opacity:.6} .panel select,.dly select{appearance:none;-webkit-appearance:none;border:0;background:transparent url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5' viewBox='0 0 8 5'%3E%3Cpath d='M0 0h8L4 5z' fill='%23999'/%3E%3C/svg%3E") no-repeat right 0 center;padding:0 .85em 0 0;max-width:8em;font-size:.9em;font-weight:600;color:var(--f);letter-spacing:.02em;cursor:pointer;text-overflow:ellipsis} .panel select:focus{outline:0} option,optgroup{background-color:var(--c);color:var(--f)} svg{vertical-align:middle;flex-shrink:0} .flip{display:flex;align-items:center;justify-content:center;margin:-.62em auto;width:2em;height:2em;border-radius:50%;border:3px solid var(--c);background:var(--c);cursor:pointer;color:var(--m);padding:0;box-shadow:0 1px 2px #0000000f;transition:background .25s,color .25s,transform .25s} .flip:hover{background:var(--fh);color:var(--f);transform:rotate(180deg)} .primary{width:100%;box-sizing:border-box;border:1px solid transparent;min-height:3.1em;padding:.9em;overflow-wrap:anywhere;line-height:1.3;font-size:1em;border-radius:.5em;background:var(--k);color:var(--kf);cursor:pointer;margin-top:.75em;font-weight:600;text-transform:uppercase;letter-spacing:.08em;transition:background .15s,border-color .15s} .primary:hover:not(:disabled){background:var(--kh)} .primary:active:not(:disabled),.lqbtn:active:not(:disabled),.o button:active:not(:disabled),.p button:active:not(:disabled),.tabs button:active,.chtog:active,.bkc:active,.lnbtn:active{transform:translateY(1px)} .primary:disabled{background:var(--p);border-color:transparent;color:var(--m);cursor:not-allowed} .rcpt{width:100%;box-sizing:border-box;margin-top:.6em;padding:.66em .85em;border:1px solid transparent;border-radius:.5em;background:var(--p);transition:border-color .15s;color:var(--f);font-size:.75em;outline:0;font-family:inherit} #lq.on,#ln.on{background:var(--k);color:var(--kf)} .lqrow{padding:.62em .7em;background:var(--p);border:1px solid transparent;border-radius:.5em;margin:.35em 0} .lqrow.out{opacity:.55} .lqband{display:flex;align-items:center;gap:.4em;font-size:.82em} .lqfee{color:var(--m);font-size:.72em} .lqtag{font-size:.62em;padding:.1em .4em;border-radius:.35em;border:1px solid var(--e);color:var(--m)} .lqmeta{font-size:.7em;color:var(--m);margin-top:.25em;display:flex;align-items:center;gap:.4em} .lqacts{display:flex;gap:.4em;margin-top:.5em} .lqacts>.lqbtn{flex:1} .lqmine{font-size:.7em;margin-top:.25em;display:flex;align-items:center;gap:.4em} .lqempty{padding:.9em .7em;font-size:.75em;color:var(--m);text-align:center} .lqnewtog{width:100%;margin-top:.55em} .lqnew{margin-top:.5em;display:flex;flex-direction:column;gap:.35em;text-align:left} .lqnew .dly{margin-top:.4em;gap:.6em} .lqnew .dly>select{flex:1;min-width:0;max-width:none;text-align:right;text-align-last:right} .lqnew .dly .r{display:flex;gap:.4em;align-items:center} .lqnew .dly .r input{width:6.5em} .lqout{font-size:.72em;color:var(--m);line-height:1.55;min-height:1.1em} .lqbar{position:relative;height:3px;border-radius:2px;background:var(--e);margin:.45em 0 .3em} .lqbar>i{position:absolute;top:-2.5px;width:3px;height:8px;border-radius:1px;background:var(--f); left:var(--at,50%);transform:translateX(-1.5px)} .lqbar.out>i{background:var(--m)} .lqbar>b{position:absolute;top:0;height:3px;border-radius:2px;background:var(--m);opacity:.45; left:0;right:0} .lqnew .primary{margin-top:.5em;min-height:2.6em;font-size:.85em} #listNote{max-width:22em;margin:.4em auto 0;font-size:11px;opacity:.75;text-align:center} .lqbtn{font-size:.9em;padding:.3em .7em;border-radius:.5em;cursor:pointer;border:1px solid var(--e);background:transparent;color:var(--f);font-family:inherit;font-weight:600; white-space:nowrap;flex:0 0 auto;text-align:center;transition:background .15s,color .15s,border-color .15s} .lqbtn:hover:not(:disabled),.lqbtn:focus-visible:not(:disabled){background:var(--k);color:var(--kf);border-color:var(--k)} .lqbtn:disabled{opacity:.4;cursor:not-allowed} .lqadd{margin-top:.45em;display:flex;flex-direction:column;gap:.35em} .lqin{width:100%;box-sizing:border-box;padding:.5em .65em;font:inherit;font-size:.75em;border:1px solid transparent;border-radius:.5em;background:var(--p);transition:border-color .15s;color:var(--f);outline:0} .lqzap{display:flex;align-items:center;gap:.45em;font-size:.7em;color:var(--m);cursor:pointer;margin-bottom:.1em} .lqzap input{margin:0;cursor:pointer;accent-color:var(--f)} .lqpv{font-size:.7em;opacity:.7;min-height:1.2em;line-height:1.5} .lqadd .lqbtn{margin-left:0;align-self:stretch;font-size:.75em;padding:.5em .8em} .rcpt.bad{border-color:var(--w)} #rcvEl{display:block;font-size:.7em;color:var(--m);margin-top:.35em;word-break:break-all;text-align:center} #rcvEl:empty{display:none} #stat{font-size:.8em;color:var(--n);word-break:break-all;margin-top:.75em;text-align:center} #stat:not(:empty){margin-bottom:.5em} #stat a{color:var(--f);text-decoration:underline} .hdr{display:flex;justify-content:space-between;align-items:center;gap:.5em} .tabs{display:flex;gap:.18em;margin-bottom:.75em;background:var(--p);padding:.18em;border-radius:.5em} .tabs button{flex:1;padding:.5em;font-size:.75em;font-weight:600;text-transform:uppercase;letter-spacing:.08em;border:0;border-radius:.35em;background:transparent;color:var(--m);cursor:pointer;transition:background .15s,color .15s} .tabs button.on{background:var(--k);color:var(--kf)} .tabs button:hover:not(.on),.tabs button:focus-visible:not(.on){background:var(--fh);color:var(--f)} .tabs button.on:hover,.tabs button.on:focus-visible{background:var(--kh)} .hide{display:none!important} #book{margin-top:.4em;max-height:17em;overflow-y:auto;overscroll-behavior:contain} #book:empty{display:none} #book h6{font-size:.7em;text-transform:uppercase;letter-spacing:.08em;color:var(--m);margin:.6em 0 .3em;font-weight:600;display:flex;justify-content:space-between} .o{display:flex;justify-content:space-between;align-items:center;flex-wrap:nowrap;gap:.5em;padding:.5em .7em;background:var(--p);border-radius:.5em;margin-bottom:.3em;font-size:.75em} .o .l{display:flex;align-items:center;gap:.45em;min-width:0;flex:1 1 auto} .o .l>span{min-width:0;flex:1 1 auto} .o svg{flex-shrink:0} .o svg+svg{margin-left:-.5em} .o b{font-weight:600;font-variant-numeric:tabular-nums;display:block} .o i{font-style:normal;color:var(--m);display:block;font-size:.9em;overflow-wrap:break-word} .o i a{word-break:break-all} .o a{color:var(--m)} .o button{border:1px solid var(--e);border-radius:.5em;padding:.35em .7em;font-size:.95em;font-weight:600;background:transparent;color:var(--f);cursor:pointer;flex:0 0 auto;transition:background .15s,color .15s,border-color .15s} .o button:hover:not(:disabled),.o button:focus-visible:not(:disabled){background:var(--k);color:var(--kf);border-color:var(--k)} #foot{display:flex;justify-content:center;align-items:baseline;gap:.45em;flex-wrap:wrap;margin-top:1.1em;font-size:.65em;color:var(--m);letter-spacing:.02em} #foot span:empty{display:none} #foot a{color:var(--m);text-decoration:none;border-bottom:1px solid var(--e)} #foot a:hover{color:var(--f)} .o button:disabled{background:transparent;border-color:transparent;color:var(--m);cursor:default;font-weight:400} .bkf{display:flex;flex-wrap:wrap;gap:.3em;margin:.35em 0 .45em} .bkc{font-size:.68em;padding:.35em .6em;border-radius:.35em;border:0; background:transparent;color:var(--m);cursor:pointer;font-family:inherit;font-weight:600; white-space:nowrap;transition:background .15s,color .15s} .bkc:hover:not(.on),.bkc:focus-visible:not(.on){color:var(--f);background:var(--fh)} .bkc.on{background:var(--k);color:var(--kf)} .bkc.on:hover{background:var(--kh)} .ow{margin-bottom:.3em} .ow .o{margin-bottom:0} .o.ins{cursor:pointer} .o.ins:hover{background:var(--fh)} .ic:empty{display:none} .ic{padding:.5em .7em;background:var(--p);border:0;border-top:1px solid var(--e2);border-radius:0 0 .5em .5em;font-size:.72em;color:var(--n)} .ic img{display:block;width:100%;max-width:15em;margin:.5em auto 0;border-radius:.35em} .ic dl{display:grid;grid-template-columns:auto 1fr;gap:.15em .6em;margin:0} .ic dt{color:var(--m)} .ic dd{margin:0;color:var(--f);font-variant-numeric:tabular-nums;text-align:right} .tg{display:inline-block;font-size:.85em;padding:.05em .35em;border-radius:.35em;background:var(--e2);color:var(--n);margin-left:.3em} .tg.w{background:transparent;border:0;color:var(--w);font-weight:600; text-transform:uppercase;font-size:.62em;letter-spacing:.1em;padding:0; vertical-align:.1em;line-height:1;text-decoration:none;margin-left:.5em} .o a.tg.w{color:var(--w)} .dly{display:flex;justify-content:space-between;align-items:center;gap:.6em;font-size:.75em;color:var(--n);margin-top:.55em;padding:.55em .8em;background:var(--p);border:1px solid transparent;border-radius:.5em;transition:border-color .15s} .dly select{font-size:1em} #lnArtNote{color:var(--n);font-variant-numeric:tabular-nums} #lnArtNote.set{color:var(--f)} .lnbtn{font:inherit;font-size:1em;color:var(--f);background:transparent;border:1px solid var(--e);border-radius:.35em;padding:.2em .6em;cursor:pointer} #fcEl{display:block;text-align:center;color:var(--m);font-size:.72em;letter-spacing:.03em;margin-top:.9em} .fclink{font:inherit;color:var(--m);background:none;border:0;border-bottom:1px solid var(--e);padding:0 0 .1em;margin-left:.35em;cursor:pointer;letter-spacing:inherit} .fclink:hover{color:var(--f);border-color:var(--f)} .fclink:disabled{cursor:default;opacity:.6} .lnbtn:hover{background:var(--e)} #lnArtRow{gap:.6em} #lnArtRow>span{display:flex;align-items:center;gap:.6em} #lnPrev{width:1.6em;height:1.6em;border-radius:.35em;object-fit:cover;display:none} #lnPrev.set{display:block} .lnon #chTog,.lnon #chBox,.lnon #bkTog,.lnon #book,.lnon #pos{display:none!important} .docs{margin-top:.5em} .docs details{border-top:1px solid var(--e)} .docs details:last-child{border-bottom:1px solid var(--e)} .docs summary{cursor:pointer;list-style:none;padding:.7em .2em;font-size:.78em;font-weight:600; letter-spacing:.02em;color:var(--f);display:flex;justify-content:space-between;align-items:center} .docs summary::-webkit-details-marker{display:none} .docs summary::after{content:"+";color:var(--m);font-weight:400} .docs details[open] summary::after{content:"\2013"} .docs p{margin:0 0 .7em;padding:0 .2em;font-size:.72em;line-height:1.6;color:var(--n)} .docs b{color:var(--f);font-weight:600} .docs em{font-style:normal;color:var(--m)} .docsmore{padding:.75em .2em .2em;font-size:.7em;color:var(--m)} .docs summary em{display:inline-flex;align-items:center;margin:0 auto 0 .5em;opacity:.75} .lngap{font-size:.6em;text-transform:uppercase;letter-spacing:.1em;color:var(--m);margin:.9em .2em .1em} .lnhdr{margin-bottom:.2em} #lnSub{color:var(--m);text-transform:none;letter-spacing:0} #lnNote,#floorNote{display:block;font-size:.7em;line-height:1.45;color:var(--m);text-align:center;text-wrap:balance;margin:.55em .2em 0} #lnNote:empty,#floorNote:empty{display:none} .lnu{display:flex;align-items:baseline;gap:.3em;color:var(--m);flex:0 1 auto;min-width:0} .lnu input{width:7em;flex:0 1 7em;min-width:0} .tipr{display:flex;align-items:center;gap:.5em} .tipr small{color:var(--m);font-size:.95em} .dly input{width:9em;flex:0 1 9em;min-width:0;border:0;border-bottom:1px solid var(--e2);transition:border-color .15s;background:transparent;color:var(--f);text-align:right;outline:0;font:inherit;font-variant-numeric:tabular-nums} .dly input[type=checkbox]{width:auto;border:0;cursor:pointer;accent-color:var(--f)} .dly input:hover{border-bottom-color:var(--e)} .dly input:focus{border-bottom-color:var(--f)} #pos{margin-top:.4em} #pos:empty{display:none} .ph{margin:.9em 0 .45em;font-size:.9em;color:var(--f);font-weight:700} .p{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:.5em;padding:.55em .7em;border:0;background:var(--p);border-radius:.5em;margin-bottom:.35em;font-size:.75em} .p span{flex:1;min-width:8em} .p b{display:block;font-weight:600;font-variant-numeric:tabular-nums;word-break:break-word} .p i{font-style:normal;color:var(--m);display:block;font-size:.9em} .p button{border:0;border-radius:.35em;padding:.4em .7em;font-size:.95em;font-weight:600;background:var(--k);color:var(--kf);cursor:pointer;margin-left:auto} .p button:hover:not(:disabled),.p button:focus-visible:not(:disabled){background:var(--kh)} .p button:disabled{background:transparent;color:var(--m);cursor:default;font-weight:400} #rate{text-wrap:balance} #bal,#bal1,#rate{display:block;font-size:.75em;line-height:1.35;margin-top:.5em;text-align:right} #bal:empty,#bal1:empty,#rate:empty{display:none} #bal a,#bal1 a{color:var(--f);cursor:pointer;text-decoration:underline;font-weight:600} .meta input.warn{color:var(--w);border-bottom-color:var(--w)} #th:focus-visible,#lk:focus-visible,#lq:focus-visible,#ln:focus-visible{background:var(--p);color:var(--f)} .chtog{width:100%;margin-top:.5em;padding:.5em;border:0;border-radius:.5em;background:transparent;color:var(--m);cursor:pointer;font:inherit;font-size:.7em;font-weight:600;text-transform:uppercase;letter-spacing:.08em;transition:color .15s,background .15s} .chtog:hover{color:var(--f);background:var(--p)} .chtog span{display:inline-block;transition:transform .2s} .chtog[aria-expanded=true] span{transform:rotate(180deg)} #chBox{margin-top:.4em} .chtf{display:flex;gap:.18em;background:var(--p);padding:.18em;border-radius:.35em;margin-bottom:.4em} .chtf button{flex:1;padding:.35em;font:inherit;font-size:.65em;font-weight:600;text-transform:uppercase;letter-spacing:.06em;border:0;border-radius:.35em;background:transparent;color:var(--m);cursor:pointer} .chtf button.on{background:var(--k);color:var(--kf)} .chtf button:disabled{opacity:.35;cursor:default} .chtf .chk{flex:0 0 auto;padding:.35em .6em;border-left:1px solid var(--e);border-radius:0 .35em .35em 0;margin-left:.15em} .chtf .chk:hover{color:var(--f)} #chArt{touch-action:pan-y;position:relative;border:1px solid transparent;border-radius:.5em;overflow:hidden;background:var(--p)} #chArt svg{display:block;width:100%;height:auto} #chArt .hd{position:absolute;top:.4em;left:.55em;font-size:.7em;color:var(--m);font-variant-numeric:tabular-nums;pointer-events:none} #chArt .hd b{color:var(--f);font-size:1.25em;font-weight:600} #chArt .hd .ohlc{display:block;font-size:.9em;opacity:.85} #chArt .msg{padding:2.2em .8em;text-align:center;font-size:.72em;color:var(--m);line-height:1.6} #chArt .chflip{opacity:.55;font-size:.82em;letter-spacing:.02em;margin-left:.15em} #chNote{display:block;font-size:.65em;color:var(--m);margin-top:.35em;text-align:right;font-variant-numeric:tabular-nums} .panel select.rich{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none} .tkb{display:none;align-items:center;gap:.25em;border:0;background:transparent;padding:0; font:inherit;font-size:.9em;font-weight:600;letter-spacing:.02em;color:var(--f);cursor:pointer} .tkb.on{display:inline-flex} .tkbc{font-size:.7em;color:var(--m)} .tkp{position:absolute;z-index:40;width:min(20em,calc(100vw - 2em));max-height:min(32em,calc(100vh - 5em)); display:flex;flex-direction:column;background:var(--c);border:1px solid var(--e); border-radius:.5em;box-shadow:0 4px 12px #0000001f,0 16px 40px #00000024;overflow:hidden} #tkFind{border:0;border-bottom:1px solid var(--e);background:transparent;color:var(--f); font:inherit;font-size:.8em;padding:.7em .8em;outline:0;flex:none} #tkList{overflow-y:auto;overscroll-behavior:contain} #wkWrap{position:fixed;inset:0;z-index:60;background:#0006;display:flex;align-items:center;justify-content:center} #wkWrap.hide{display:none} #wkPanel{width:min(19em,calc(100vw - 2em));background:var(--c);border:1px solid var(--e); border-radius:.5em;overflow:hidden;box-shadow:0 16px 40px #00000024; max-height:min(32em,calc(100vh - 2em));display:flex;flex-direction:column} #wkHdr{flex:none} #wkList{overflow-y:auto;overscroll-behavior:contain} #wkHdr{padding:.7em .8em;border-bottom:1px solid var(--e);font-size:.72em; letter-spacing:.09em;text-transform:uppercase;color:var(--m)} #wkList img,#wkList .wki{width:20px;height:20px;border-radius:50%;flex:none} .wcq{padding:.7em .8em 0} .wcn{padding:.55em .8em .85em;font-size:.72em;color:var(--m)} .wcnode{padding-top:0;opacity:.85} #wkList .wki{display:block} #wkList .wki svg{width:100%;height:100%;display:block} .tkg{padding:.55em .8em .25em;font-size:.62em;letter-spacing:.09em;text-transform:uppercase;color:var(--m)} .tkr{display:flex;align-items:flex-start;gap:.55em;width:100%;box-sizing:border-box; padding:.5em .8em;border:0;background:transparent;color:var(--f);font:inherit; text-align:left;cursor:pointer} .tkr:hover,.tkr.cur{background:var(--fh)} .tkr[aria-disabled="true"]{opacity:.5;cursor:default} .tkr[aria-disabled="true"]:hover{background:transparent} .tkr img,.tkr svg{flex:none;border-radius:50%;margin-top:.1em} .tki{flex:none;width:20px;height:20px;display:flex;align-items:center;justify-content:center} .tkt{min-width:0;flex:1} .tks{display:flex;align-items:baseline;gap:.45em} .tks b{font-size:.85em;font-weight:600} .tkn{font-size:.7em;color:var(--m);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .tkd{font-size:.66em;color:var(--m);line-height:1.45;margin-top:.15em; display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden} .tkmark{flex:none;font-size:.75em;color:var(--m)} .tkempty{padding:1.4em .8em;text-align:center;font-size:.72em;color:var(--m)} @media(pointer:coarse){#th,#lk,#lq,#ln{width:2.4em;height:2.4em} body:has(.tkp:not(.hide))::before{content:"";position:fixed;inset:0;z-index:39; pointer-events:none;background:#0000008c} .tkp{position:fixed;left:0;right:0;bottom:0;top:auto;width:100%;max-width:none; max-height:82vh;border-radius:.75em .75em 0 0;box-shadow:0 -6px 32px #00000047; padding-bottom:env(safe-area-inset-bottom,0)} .tkp::before{content:"";flex:none;width:2.2em;height:.25em;border-radius:.2em; background:var(--e);margin:.5em auto .1em} .tkd{-webkit-line-clamp:1} .tkr{padding:.75em .9em} #tkFind{padding:.85em .9em;font-size:16px}.rcpt,.meta input,.dly input,.lqin{font-size:16px}.lnbtn{padding:.4em .8em}.p button,.tabs button,.o button,.lqbtn{padding:.6em .8em}.chtf button{padding:.5em}}</style> <script>const H=document.documentElement;let LS;try{LS=localStorage}catch{LS={}}if(LS.t?LS.t=='d':matchMedia('(prefers-color-scheme:dark)').matches)H.className='d'</script> <svg id="logo" viewBox="0 0 400 400" aria-hidden="true"><clipPath id="lgf"><rect width="400" height="400"/></clipPath><g clip-path="url(#lgf)"><rect width="400" height="400"/><path d="M-60-20L460-20L460 90L80 310L460 310L460 420L-60 420L-60 310L320 90L-60 90Z"/></g></svg> <div class="card"> <div class="meta"><span id="addr">Not connected</span><label id="slipL">Slippage <input id="slip" type="number" value="0.5" min="0.01" max="10" step="0.1">%</label><button id="lq" title="Liquidity for this pair" aria-label="Toggle liquidity mode" aria-pressed="false"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3.2c3.6 4 6 7.2 6 10.1a6 6 0 0 1-12 0c0-2.9 2.4-6.1 6-10.1z"/></svg></button><button id="ln" title="Launch a coin" aria-label="Toggle launch mode" aria-pressed="false"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="3.5"/></svg></button><button id="lk" title="Copy shareable link" aria-label="Copy shareable link"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2.5"/><path d="M5.5 15H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v.5"/></svg></button><button id="th" t
0xacfba7ce…4082·#25,751,354·0x6d2c4e23…dcf335
=a=>a?TOKENS.findIndex(t=>t.addr.toLowerCase()===a):-1; const tok=async v=>{if(!v)return "";const x=v.trim().toLowerCase(); if(!/^0x[0-9a-f]{40}$/.test(x))return symAddr(x); if(at(x)<0)try{await addCustomToken(x,0)}catch{} return at(x)<0?"":x}; if(symAddr((q.get("token")||"").trim().toLowerCase())||symAddr((q.get("out")||"").trim().toLowerCase()))pairChosen=true; const both=async()=>[await tok(q.get("token")),await tok(q.get("out"))]; let[fa,ta]=await both(); if(my!==lSeq)return; if((fa&&at(fa)<0)||(ta&&at(ta)<0)){[fa,ta]=await both();if(my!==lSeq)return} const fi=at(fa),oi=at(ta); if(fi>=0||oi>=0)pairChosen=true; rebuild(); if(fi>=0)fromSel.value=fi; if(oi>=0&&oi!==fi)toSel.value=oi; const to=q.get("to"),lk=lsecs(q.get("lock")),want=q.get("tab"),n=q.get("amount"); const t=want==="swap"||want==="send"||want==="book"?want :q.get("out")?"swap":(to||!isNaN(lk))?"send":tab; if(fromSel.value===toSel.value){ const alt=String(TOKENS.findIndex((x,i)=>String(i)!==fromSel.value&&(t==="book"||x.std!=="nft"))); if(alt!=="-1"){if(oi>=0)fromSel.value=alt;else toSel.value=alt} } if(lqMode&&(fi>=0||oi>=0||to||n||!isNaN(lk)))lqSet(false); setTab(t); if(to)rc.value=to; if(!isNaN(lk)){const os=[...dly.options].map(x=>+x.value).sort((a,b)=>a-b); const v=String(os.find(o=>o>=lk)??os[os.length-1]); if(t==="book")bookD=v;else sendD=v; if(t!=="swap")dly.value=v} if(n&&/^\d*\.?\d+$/.test(n)){ if(q.get("exactOut")==="1"&&q.get("out")){outAmt.value=n;setMode("out");fitFont(outAmt)} else{amt.value=n;setMode("in");fitFont(amt)} } refreshBalance();update(); loadChart(); if(t==="book")loadBook(); } addEventListener("hashchange",applyLink); const tname=t=>/\s/.test(t.sym)||!symAddr(t.sym.toLowerCase())?t.addr:t.sym; lk.onclick=async()=>{ const f=TOKENS[fromSel.value],t=TOKENS[toSel.value],p=new URLSearchParams(); p.set("token",tname(f)); if(tab==="send"){ const r=rc.value.trim();if(r)p.set("to",r); if(amt.value.trim())p.set("amount",amt.value.trim()); if(+dly.value)p.set("lock",dly.value); }else{ p.set("out",tname(t)); if(tab==="book"){p.set("tab","book");if(+dly.value)p.set("lock",dly.value)} if(mode==="out"&&outAmt.value.trim()){p.set("amount",outAmt.value.trim());p.set("exactOut","1")} else if(amt.value.trim())p.set("amount",amt.value.trim()); } const u=location.origin+location.pathname+"#"+p; try{await navigator.clipboard.writeText(u);stat.textContent="Link copied"} catch{stat.textContent=u} }; tabSwap.onclick=()=>setTab("swap"); tabSend.onclick=()=>setTab("send"); {const TABS=[tabSwap,tabSend,tabBook],NAMES=["swap","send","book"]; for(const el of TABS)el.addEventListener("keydown",e=>{ const i=TABS.indexOf(e.target); const j=e.key==="ArrowRight"?(i+1)%TABS.length:e.key==="ArrowLeft"?(i+TABS.length-1)%TABS.length :e.key==="Home"?0:e.key==="End"?TABS.length-1:-1; if(j<0)return; e.preventDefault();TABS[j].focus();setTab(NAMES[j]); }); const syncStops=()=>TABS.forEach(el=>el.tabIndex=el.classList.contains("on")?0:-1); syncStops();for(const el of TABS)el.addEventListener("click",syncStops); for(const el of TABS)el.addEventListener("keyup",syncStops);} applyLink(); if(window.ethereum)loadTokenList().then(()=>{ adoptList(pairAddrs()); return applyLink(); }).catch(()=>{}); async function doSend(){ if(!sendReady)return; swap.disabled=true; try{ const{to,amount,token}=sendReady; const fresh=await rcvOf(rc.value.trim(),true); if(fresh.toLowerCase()!==to.toLowerCase())throw Error("recipient changed \u2014 check and retry"); if(amount>fromBalance)throw Error("insufficient balance"); await checkWallet(); const dsec=BigInt(dly.value||0); let txReq,batch=null; if(dsec>0n){ const isE=token.addr===ZERO; if(tipCk.checked)await syncTip(); const tip=tipCk.checked?sendTipWei:0n; const val=isE?amount+tip:tip; if(isE&&val>fromBalance)throw Error("insufficient ETH for amount + keeper tip"); const dep=tip>0n ?"0x"+SEL_DEPOSITTIP+encAddr(isE?ZERO:token.addr)+encAddr(to) +encUint(amount)+encUint(dsec)+encUint(tip)+encUint(192n)+encUint(0n) :"0x"+SEL_DEPOSITTO+encAddr(isE?ZERO:token.addr)+encAddr(to) +encUint(isE?0n:amount)+encUint(dsec)+encUint(160n)+encUint(0n); txReq={from:account,to:SLOW,data:dep,value:toHex(val)}; if(!isE){ const aHex=await rpc(C,[{to:token.addr,data:"0x"+SEL_ALLOWANCE+encAddr(account)+encAddr(SLOW)},L]); const cur=BigInt(aHex); if(cur<amount){ const pre=cur>0n?[{to:token.addr,data:"0x"+SEL_APPROVE+encAddr(SLOW)+encUint(0),value:"0x0"}]:[]; const ap={to:token.addr,data:"0x"+SEL_APPROVE+encAddr(SLOW)+encUint(amount),value:"0x0"}; if(await canBatch(account))batch=[...pre,ap,{to:SLOW,data:dep,value:toHex(val)}]; else{stat.textContent="Approve "+token.sym+"..."; for(const c of[...pre,ap]){const at=await rpc(S,[{from:account,...c}]);await waitTx(at)}} }} }else{ txReq=token.addr===ZERO ?{from:account,to,value:toHex(amount)} :{from:account,to:token.addr,data:"0x"+SEL_TRANSFER+encAddr(to)+encUint(amount),value:"0x0"}; } let tx; if(batch){await checkWallet();tx=await sendBatch(account,batch)} else{const ret=await rpc(C,[txReq,L]);if(dsec===0n&&token.addr!==ZERO)okRet(ret);tx=await rpc(S,[txReq])} await settle(tx); amt.value="";fitFont(amt);sendReady=null; refreshBalance();render();loadPos(); }catch(e){err(e);render()} } async function sendRouterFunded(callData,fundedCallData,token,amount,msgValue,verb){ await checkWallet(); let data=callData,batch=null; if(token.addr!==ZERO){ const allowData="0x"+SEL_ALLOWANCE+encAddr(account)+encAddr(ZROUTER); const allowance=BigInt(await rpc(C,[{to:token.addr,data:allowData},L])); if(allowance<amount){ let ready=false; const pi=await permitInfo(token.addr,account); if(pi)try{ stat.textContent="Sign approval..."; const pd=BigInt(Math.floor(Date.now()/1e3)+1800); data=encCalls([await signPermit(token.addr,pi,account,amount,pd),callData]); ready=true; }catch(e){if(isRejection(e))throw e} const selected=TOKENS[fromSel.value]; const walletBal=selected&&selected.addr.toLowerCase()===token.addr.toLowerCase() ?fromBalance:BigInt(await rpc(C,[{to:token.addr,data:"0x"+SEL_BALANCEOF+encAddr(account)},L])); if(!ready&&walletBal>=amount&&await p2Ready(token.addr,account)>=amount)try{ stat.textContent="Sign approval..."; const pd=BigInt(Math.floor(Date.now()/1e3)+1800); data=encCalls([await signPermit2(token.addr,account,amount,pd),fundedCallData||callData]); ready=true; }catch(e){if(isRejection(e))throw e} if(!ready&&await canBatch(account)){ const pre=allowance>0n ?[{to:token.addr,data:"0x"+SEL_APPROVE+encAddr(ZROUTER)+encUint(0n),value:"0x0"}]:[]; batch=[...pre, {to:token.addr,data:"0x"+SEL_APPROVE+encAddr(ZROUTER)+encUint(amount),value:"0x0"}, {to:ZROUTER,data:callData,value:toHex(msgValue)}]; ready=true; } if(!ready){ const approvals=allowance>0n?[0n,amount]:[amount]; for(let i=0;i<approvals.length;i++){ stat.textContent=`Approve ${token.sym}${approvals.length>1?` (${i+1}/${approvals.length})`:""}...`; const h=await rpc(S,[{from:account,to:token.addr, data:"0x"+SEL_APPROVE+encAddr(ZROUTER)+encUint(approvals[i]),value:"0x0"}]); await waitTx(h); } const fresh=BigInt(await rpc(C,[{to:token.addr,data:allowData},L])); if(fresh<amount)throw Error("approval failed"); } } } stat.textContent=verb; let tx; if(batch){await checkWallet();tx=await sendBatch(account,batch)} else{ const req={from:account,to:ZROUTER,data,value:toHex(msgValue)}; await rpc(C,[req,L]);await checkWallet();tx=await rpc(S,[req]); } await settle(tx); } const PUSH_MAGIC="381bcb9ccaaab09ca206deb73834ed7b483c58bb3face9b43e8c7b50193253e2"; const SEL_SAFE_TRANSFER_DATA="b88d4fde",SEL_OWNEROF="6352211e"; async function pushNftOrder(f,t){ const id=nftId.value.trim(); if(!/^\d+$/.test(id))throw Error("name the token id you are selling"); const tokenId=BigInt(id); if(kind.value!=="fixed")throw Error("an NFT is indivisible, so it sells at a fixed price"); const want=parseUnits(outAmt.value.trim()||"0",t.dec); if(!want)throw Error("name a price"); const quote=t.addr===ZERO?WETH:t.addr; if(quote.toLowerCase()===f.addr.toLowerCase())throw Error("pick different tokens"); if((await rpc("eth_getCode",[SB2,L])).length<5)throw Error("fixed orderbook not deployed yet"); let cp=ZERO; const v=rc.value.trim(); if(v){cp=await rcvOf(v,true);if(!cp)throw Error("Private recipient not resolved")} let owner=ZERO; try{owner="0x"+strip0x(await rpc(C,[{to:f.addr,data:"0x"+SEL_OWNEROF+encUint(tokenId)},L])).slice(-40)} catch{throw Error(`${f.sym} #${id} does not exist`)} if(owner.toLowerCase()!==account.toLowerCase())throw Error(`you do not own ${f.sym} #${id}`); const exp=+dly.value?BigInt(Math.floor(Date.now()/1e3)+ +dly.value):0n; const terms=PUSH_MAGIC+encAddr(quote)+encUint(want)+encUint(exp)+encBool(false)+encAddr(cp); const data="0x"+SEL_SAFE_TRANSFER_DATA+encAddr(account)+encAddr(SB2)+encUint(tokenId) +encUint(128n)+encUint(192n)+terms; const req={from:account,to:f.addr,data,value:"0x0"}; await rpc(C,[req,L]); await checkWallet(); await settle(await rpc(S,[req])); } const SEL_CREATE_ETH="0e8e31d3",SEL_CREATE="8e14f9f8",SEL_FLOOR_BID="3d8d260b"; async function bidCollection(f,t,climbing){ if(nftId.value.trim())throw Error("clear the token id to bid on the whole collection"); const ceiling=parseUnits(amt.value.trim()||"0",f.dec); if(!ceiling)throw Error("name the most you will pay in total"); const open=climbing?parseUnits(floorAmt.value.trim()||"0",f.dec):ceiling; if(climbing&&!open)throw Error("set an opening bid"); if(open>ceiling)throw Error("the opening bid exceeds the maximum"); if(ceiling>(1n<<96n)-1n)throw Error("bid amount too large"); const wantRaw=outAmt.value.trim(); if(wantRaw&&!/^\d+$/.test(wantRaw))throw Error("how many tokens? a whole number"); const want=wantRaw?BigInt(wantRaw):1n; if(!want)throw Error("bid for at least one"); if(want>(1n<<128n)-1n)throw Error("too many to bid for at once"); const duration=BigInt(dly.value||0); if(!duration)throw Error("a bid needs a window — pick an expiry"); if((await rpc("eth_getCode",[FLOOR,L])).length<5)throw Error("bid board not deployed yet"); const terms=encAddr(t.addr)+encAddr(f.addr===ZERO?ZERO:f.addr)+encUint(want) +encUint(open)+encUint(ceiling)+encUint(0n)+encUint(duration)+encBool(true) +encUint(288n)+encUint(0n); const data="0x"+SEL_FLOOR_BID+encUint(32n)+terms; if(f.addr===ZERO){ const req={from:account,to:FLOOR,data,value:toHex(ceiling)}; await rpc(C,[req,L]);await checkWallet();await settle(await rpc(S,[req])); return; } const tx={from:account,to:FLOOR,data,value:"0x0"}; const al=BigInt(await rpc(C,[{to:f.addr,data:"0x"+SEL_ALLOWANCE+encAddr(account)+encAddr(FLOOR)},L])); const approvals=[]; if(al<ceiling){ if(al)approvals.push({to:f.addr,data:"0x"+SEL_APPROVE+encAddr(FLOOR)+encUint(0n),value:"0x0"}); approvals.push({to:f.addr,data:"0x"+SEL_APPROVE+encAddr(FLOOR)+encUint(ceiling),value:"0x0"}); } if(approvals.length&&await canBatch(account)){ await checkWallet(); await settle(await sendBatch(account,[...approvals,{to:FLOOR,data,value:"0x0"}])); return; } for(const a of approvals){await checkWallet();await settle(await rpc(S,[{from:account,...a}]))} await rpc(C,[tx,L]);await checkWallet();await settle(await rpc(S,[tx])); } async function buyNftOrder(f,t){ const id=nftId.value.trim(); if(!/^\d+$/.test(id))throw Error("name the token id you want to buy"); const tokenId=BigInt(id); if(kind.value!=="fixed")throw Error("a bid for one token is a fixed offer"); const price=parseUnits(amt.value.trim()||"0",f.dec); if(!price)throw Error("name what you will pay"); if((await rpc("eth_getCode",[SB2,L])).length<5)throw Error("fixed orderbook not deployed yet"); let cp=ZERO; const v=rc.value.trim(); if(v){cp=await rcvOf(v,true);if(!cp)throw Error("Private recipient not resolved")} try{await rpc(C,[{to:t.addr,data:"0x"+SEL_OWNEROF+encUint(tokenId)},L])} catch{throw Error(`${t.sym} #${id} does not exist`)} const exp=+dly.value?BigInt(Math.floor(Date.now()/1e3)+ +dly.value):0n; if(f.addr===ZERO){ const data="0x"+SEL_CREATE_ETH+encAddr(t.addr)+encUint(tokenId)+encBool(false) +encUint(exp)+encBool(true)+encAddr(cp); const req={from:account,to:SB2,data,value:toHex(price)}; await rpc(C,[req,L]);await checkWallet();await settle(await rpc(S,[req])); return; } const data="0x"+SEL_CREATE+encAddr(f.addr)+encUint(price)+encAddr(t.addr)+encUint(tokenId) +encBool(false)+encUint(exp)+encBool(false)+encBool(true)+encAddr(cp); const tx={from:account,to:SB2,data,value:"0x0"}; const al=BigInt(await rpc(C,[{to:f.addr,data:"0x"+SEL_ALLOWANCE+encAddr(account)+encAddr(SB2)},L])); const approvals=[]; if(al<price){ if(al)approvals.push({to:f.addr,data:"0x"+SEL_APPROVE+encAddr(SB2)+encUint(0n),value:"0x0"}); approvals.push({to:f.addr,data:"0x"+SEL_APPROVE+encAddr(SB2)+encUint(price),value:"0x0"}); } if(approvals.length&&await canBatch(account)){ await checkWallet(); await settle(await sendBatch(account,[...approvals,{to:SB2,data,value:"0x0"}])); return; } for(const a of approvals){await checkWallet();await settle(await rpc(S,[{from:account,...a}]))} await rpc(C,[tx,L]);await checkWallet();await settle(await rpc(S,[tx])); } async function placeOrder(){ const f=TOKENS[fromSel.value],t=TOKENS[toSel.value]; swap.disabled=true; try{ if(f.std==="nft"||t.std==="nft"){ if(f.std==="nft")await pushNftOrder(f,t); else if(nftId.value.trim())await buyNftOrder(f,t); else await bidCollection(f,t,kind.value==="floor"); amt.value="";outAmt.value="";nftId.value="";floorAmt.value=""; floorTouched=false; fitFont(amt);fitFont(outAmt);fitFont(floorAmt); stat.textContent=f.std==="nft"?"Listed.":"Bid placed."; loadBook();refreshBalance();render(); return; } const sell=parseUnits(amt.value.trim(),f.dec),want=parseUnits(outAmt.value.trim(),t.dec); const dutch=kind.value==="dutch",bid=kind.value==="floor"; const floor=(dutch||bid)&&floorAmt.value.trim()?parseUnits(floorAmt.value.trim(),bid?f.dec:t.dec):0n; if(dutch&&floor===0n){ try{ if(!confirm(`This lot decays to ZERO.\n\nOnce the window ends, anyone can take all ${trimAmt(sell,f.dec)} ${f.sym} for nothing — and it stays that way until you cancel.\n\nThat is how a "come and take it" launch works. If you meant to keep a reserve, set a Floor total instead.\n\nContinue with a zero floor?`))return; }catch{ stat.textContent="This lot would decay to zero and this browser blocked the confirmation. Set a Floor total, or open the page outside the wallet's in-app browser."; return; } } const board=bid?FLOOR:dutch?DUTCH:SB2; const quoteToken=!dutch&&!bid&&t.addr===ZERO?WETH:t.addr; const [bc,oc]=await Promise.all([rpc("eth_getCode",[board,L]),rpc("eth_getCode",[ORDERBOL,L])]); if(bc.length<5)throw Error(`${bid?"bid board":dutch?"Dutch":"fixed"} orderbook not deployed yet`); if(oc.length<5)throw Error("routed order adapter not deployed yet"); if(!bid&&f.addr===ZERO&&t.addr===WETH)throw Error("pick a non-WETH quote token"); if(!dutch&&!bid&&(f.addr===WETH||f.addr===ZERO)&&quoteToken===WETH)throw Error("pick different tokens"); if(dutch&&floor>want)throw Error("floor exceeds start price"); let cp=ZERO; const v=rc.value.trim(); if(!dutch&&!bid&&v){cp=await rcvOf(v,true);if(!cp)throw Error("Private recipient not resolved")} const pf=fill.value==="1"?1n:0n; const placementDeadline=BigInt(Math.floor(Date.now()/1e3)+1800); let orderData; if(bid){ const duration=BigInt(dly.value||0); if(!duration)throw Error("choose a bid window"); if(!floor)throw Error("set an opening bid"); if(floor>sell)throw Error("opening bid exceeds the maximum"); if(want>(1n<<128n)-1n||sell>(1n<<96n)-1n)throw Error("bid amount too large"); if(t.addr===ZERO)throw Error("a bid buys a token — pick one to bid for"); if((f.addr===ZERO?WETH:f.addr).toLowerCase()===t.addr.toLowerCase()) throw Error("pick different tokens"); orderData="0x"+SEL_ORDER_FLOOR+encAddr(FLOOR)+encAddr(account)+encAddr(account) +encAddr(t.addr)+encAddr(f.addr)+encUint(want)+encUint(floor)+encUint(sell) +encUint(0n)+encUint(duration)+encUint(placementDeadline); }else if(dutch){ const duration=BigInt(dly.value||0); if(!duration)throw Error("choose a decay duration"); if(sell>(1n<<128n)-1n||want>(1n<<96n)-1n||floor>(1n<<96n)-1n)throw Error("Dutch amount too large"); const dutchExpiry=dutchEnds(); orderData="0x"+SEL_ORDER_DUTCH+encAddr(DUTCH)+encAddr(account)+encAddr(account) +encAddr(f.addr)+encAddr(t.addr)+encUint(sell)+encUint(want)+encUint(floor) +encUint(0n)+encUint(duration)+encUint(dutchExpiry)+encUint(placementDeadline); }else{ const exp=+dly.value?BigInt(Math.floor(Date.now()/1e3)+ +dly.value):0n; orderData="0x"+SEL_ORDER_FIXED+encAddr(SB2)+encAddr(account)+encAddr(account) +encAddr(f.addr)+encUint(sell)+encAddr(quoteToken)+encUint(want)+encUint(pf) +encUint(exp)+encAddr(cp)+encUint(placementDeadline); } let direct,funded; if(f.addr===ZERO){ direct=funded=encSnwap(ZERO,0n,account,ZERO,0n,ORDERBOL,orderData); }else{ const checkpoint=encCheckpoint(ORDERBOL,f.addr,account); direct=encCalls([ checkpoint, encSnwap(f.addr,sell,account,ZERO,0n,ORDERBOL,orderData) ]); funded=encCalls([ checkpoint, encSweep(f.addr,sell,ORDERBOL), encSnwap(ZERO,0n,account,ZERO,0n,ORDERBOL,orderData) ]); } await sendRouterFunded(direct,funded,f,sell,f.addr===ZERO?sell:0n, bid?"Placing bid (escrowing the ceiling)...":dutch?"Placing Dutch order...":t.addr===ZERO?"Placing limit order (ETH quote settles as WETH)...":"Placing limit order..."); amt.value="";outAmt.value="";floorAmt.value=""; floorTouched=false; fitFont(amt);fitFont(outAmt);fitFont(floorAmt); loadBook();refreshBalance();render(); }catch(e){err(e);render()} } swap.onclick=async()=>{ if(tab==="book"){if(!account)return connect();return placeOrder()} if(tab==="send"){if(!account)return connect();return doSend()} if(!account) return connect(); if(!last)return; if(Date.now()>last.exp){stat.textContent="Quote expired — refreshing...";updateSoon();return} if(last.impact!==null&&last.impact!==undefined&&last.impact>=IMPACT_CONFIRM){ const pct=(Number(last.impact)/100).toFixed(2); const worse=`about ${trimAmt(last.lossTok,last.lossDec)} ${last.lossSym} ${last.isIn?"less than":"more than"} the market rate`; try{ if(last.impact>=IMPACT_TYPED){ const need=String(Math.floor(Number(last.impact)/100)); if(prompt(`STOP — this trade is priced ${pct}% away from the market.\n\nYou would ${last.isIn?"receive":"pay"} ${worse}.\n\nUsually this means the pool is illiquid or the amount is wrong.\n\nType ${need} to accept this loss:`)!==need)return; }else if(!confirm(`Price impact ${pct}%.\n\nYou will ${last.isIn?"receive":"pay"} ${worse}.\n\nContinue?`))return; }catch{ stat.textContent=`This trade is ${pct}% away from the market and this browser blocked the confirmation. Reduce the amount, or open the page outside the wallet's in-app browser.`; return; } } swap.disabled=true; try{ const q=last; const {callData,fundedCallData,msgValue,amountIn,from,to}=q; const wantsValue=q.wrap>0n?0n:(from.addr===ZERO?amountIn:0n); if(msgValue!==wantsValue)throw Error("bad value"); const rvNow=rc.value.trim(); if(((rvNow?await rcvOf(rvNow,true):"")||"").toLowerCase()!==(q.recipient||"").toLowerCase()) throw Error("recipient changed — check and retry"); await checkWallet(); try{await preflightFills(q.fills,q.tokenIn,q.tokenOut)} catch(e){stat.textContent=e.message;swap.disabled=false;last=null;updateSoon();return} let cd2=callData,batchCalls=null,prep=[]; const spender=q.spender!==undefined?q.spender:(to?null:ZROUTER); const target=to||ZROUTER; const fund=q.wrap>0n?{addr:WETH,sym:"WETH",dec:18}:from; if(q.source===SRC_PRECISION&&q.pool){ const ok=await mc3([{to:PFACTORY,data:"0x"+SEL_ISPOOL+encAddr(q.pool)}]) .then(([r])=>!!r&&BigInt(r)===1n).catch(()=>false); if(!ok)throw Error("that pool is not one the factory made — refusing to send"); if(q.wrap>0n){ const have=BigInt(await rpcRead("eth_getBalance",[account,L])); if(have<q.wrap)throw Error(`Not enough ETH: this needs ${trimAmt(q.wrap,18)} and you hold ${trimAmt(have,18)}.`); prep.push({to:WETH,data:"0xd0e30db0",value:toHex(q.wrap), note:`Wrapping ${trimAmt(q.wrap,18)} ETH — this pool holds WETH`}); } } const prepBatchable=prep.length?await canBatch(account):false; if(prep.length&&!prepBatchable){ for(const c of prep){ stat.textContent=c.note+"..."; const ctx={from:account,to:c.to,data:c.data,value:c.value}; await rpc(C,[ctx,L]);await checkWallet(); await settle(await rpc(S,[ctx])); } prep=[]; } if(fund.addr!==ZERO&&spender&&spender!==ZROUTER){ const allowData="0x"+SEL_ALLOWANCE+encAddr(account)+encAddr(spender); const a=BigInt(await rpc(C,[{to:fund.addr,data:allowData},L])); if(a<amountIn){ if(await canBatch(account)){ const pre=a>0n?[{to:fund.addr,data:"0x"+SEL_APPROVE+encAddr(spender)+encUint(0n),value:"0x0"}]:[]; batchCalls=[...pre,{to:fund.addr,data:"0x"+SEL_APPROVE+encAddr(spender)+encUint(amountIn),value:"0x0"}, {to:target,data:callData,value:toHex(msgValue)}]; }else{ const n=a>0n?2:1; if(a>0n){ stat.textContent=`Approving (1/${n})...`; await waitTx(await rpc(S,[{from:account,to:fund.addr, data:"0x"+SEL_APPROVE+encAddr(spender)+encUint(0)}])); } stat.textContent=n>1?"Approving (2/2)...":"Approving..."; await waitTx(await rpc(S,[{from:account,to:fund.addr, data:"0x"+SEL_APPROVE+encAddr(spender)+encUint(amountIn)}])); if(BigInt(await rpcRead(C,[{to:fund.addr,data:allowData},L]))<amountIn)throw Error("approval failed"); } } }else if(fund.addr!==ZERO&&!to&&!msgValue){ const allowData="0x"+SEL_ALLOWANCE+encAddr(account)+encAddr(ZROUTER); const aHex=await rpc(C,[{to:fund.addr,data:allowData},L]); const a=BigInt(aHex); if(a<amountIn){ let permitted=false; const sameAsQuote=fund===from; const pi=sameAsQuote?await permitInfo(fund.addr,account):null; if(pi){try{ stat.textContent="Sign approval..."; const pd=BigInt(Math.floor(Date.now()/1e3)+1800); cd2=encCalls([await signPermit(fund.addr,pi,account,amountIn,pd),callData]); permitted=true; }catch(e){if(isRejection(e)){stat.textContent="";swap.disabled=false;return}}} const p2Amount=amountIn; const p2Fundable=!!fundedCallData||q.source!==SRC_PRECISION; if(!permitted&&p2Fundable&&sameAsQuote&&fromBalance>=p2Amount&&await p2Ready(fund.addr,account)>=p2Amount){try{ stat.textContent="Sign approval..."; const pd=BigInt(Math.floor(Date.now()/1e3)+1800); const p2Call=await signPermit2(fund.addr,account,p2Amount,pd); cd2=fundedCallData?encCalls([p2Call,fundedCallData]) :encCalls(q.isIn?[p2Call,callData]:[p2Call,callData,encSweep(fund.addr,0n,account)]); permitted=true; }catch(e){if(isRejection(e)){stat.textContent="";swap.disabled=false;return}}} if(!permitted&&await canBatch(account)){ const pre=a>0n?[{to:fund.addr,data:"0x"+SEL_APPROVE+encAddr(ZROUTER)+encUint(0n),value:"0x0"}]:[]; batchCalls=[...pre,{to:fund.addr,data:"0x"+SEL_APPROVE+encAddr(ZROUTER)+encUint(amountIn),value:"0x0"}, {to:target,data:callData,value:toHex(msgValue)}]; permitted=true; } if(!permitted){ const n=a>0n?2:1; if(a>0n){ stat.textContent=`Approving (1/${n})...`; const zeroData="0x"+SEL_APPROVE+encAddr(ZROUTER)+encUint(0); const h1=await rpc(S,[{from:account,to:fund.addr,data:zeroData}]); await waitTx(h1); } stat.textContent=n>1?`Approving (2/2)...`:"Approving..."; const apprData="0x"+SEL_APPROVE+encAddr(ZROUTER)+encUint(amountIn); const h2=await rpc(S,[{from:account,to:fund.addr,data:apprData}]); await waitTx(h2); const nHex=await rpc(C,[{to:fund.addr,data:allowData},L]); if(BigInt(nHex)<amountIn)throw Error("approval failed"); } } } if(prep.length&&batchCalls){ batchCalls=[...prep.map(({to,data,value})=>({to,data,value})),...batchCalls]; prep=[]; } let tx; const mainTx={from:account,to:target,data:cd2,value:toHex(msgValue)}; if(prep.length&&!batchCalls&&await canBatch(account)){ stat.textContent="Swapping..."; await checkWallet(); tx=await sendBatch(account,[...prep.map(({to,data,value})=>({to,data,value})), {to:target,data:cd2,value:toHex(msgValue)}]); }else{ for(const c of prep){ stat.textContent=c.note+"..."; const ctx={from:account,to:c.to,data:c.data,value:c.value}; await rpc(C,[ctx,L]);await checkWallet(); await settle(await rpc(S,[ctx])); } stat.textContent="Swapping..."; if(batchCalls){ await checkWallet(); tx=await sendBatch(account,batchCalls); }else{ await rpc(C,[mainTx,L]); await checkWallet(); tx=await rpc(S,[mainTx]); } } await settle(tx); last=null; chCache={};loadChart(); refreshBalance(); render(); }catch(e){err(e);render()} };</script>
0xacfba7ce…4082·#25,751,352·0x70569700…27f53a
; ev.target.outerHTML=lqNewForm(pr[0],pr[1]); lqSyncAmounts(); lqNewPreview(); lqList.querySelector(".lqnew")?.scrollIntoView?.({block:"nearest"}); return} const b=ev.target.closest("[data-act]"); if(!b||busy)return; const p=chPair(); if(!p)return; const [t0,t1]=p,pool=b.dataset.pool; const row=lqRows.find(x=>x.pool.toLowerCase()===pool.toLowerCase()); if(!row)return; const act=b.dataset.act; if(act!=="a"&&!account)return void(stat.textContent="Connect a wallet first."); if(act==="a"){ const box=b.closest(".lqrow").querySelector(".lqadd"); box.classList.toggle("hide"); b.textContent=box.classList.contains("hide")?"Add":"Cancel"; if(!box.classList.contains("hide"))box.querySelector(".lqin").focus(); return; } if(act==="w"){ if(row.shares===0n)return; await doRemove(pool,row.shares,t0,t1); return; } if(act==="ac"){ const box=b.closest(".lqadd"); const ins=box.querySelectorAll(".lqin"); if(isZap(box)){ const z=zapSide(box,t0,t1); if(z.err){stat.textContent=z.err;return} if(!z.amt)return; await doZap(pool,z.tok,z.amt); return; } let a0,a1; try{ a0=parseUnits(ins[0].value.trim(),t0.dec);a1=parseUnits(ins[1].value.trim(),t1.dec); }catch{stat.textContent="Invalid amount";return} if(!a0||!a1)return; await doAdd(pool,a0,a1,t0,t1); } }; footDoc.setAttribute("aria-expanded","false"); footDoc.onclick=e=>{ e.preventDefault(); const on=docPanel.classList.contains("hide"); footDoc.setAttribute("aria-expanded",on?"true":"false"); docPanel.classList.toggle("hide",!on); footDoc.textContent=on?"hide":"how it works"; if(on)docPanel.scrollIntoView({behavior:"smooth",block:"nearest"}); }; lq.onclick=()=>lqSet(!lqMode); const PLAUNCH="0x0000002fC8E77585A008Aa45d78A71ad36293aEe"; const SEL_LAUNCH="6a648dc6"; const SEL_LAUNCHART="72cf7bd8"; const SEL_CREATOROF="dea5c2e0"; const LN_MIME={"image/png":0,"image/webp":1,"image/svg+xml":2,"image/gif":3,"image/jpeg":4}; const MIME_NAME=["image/png","image/webp","image/svg+xml","image/gif","image/jpeg","image/avif"]; const LN_MAX_ART=24575; const LN_BUDGET=8192; const LN_MAX_ALLOC=2000; const LN_MIN_POOLED=2000000000000n; const LN_MIN_MCAP=1000000000000n; const lnHex=b=>{let h="";for(const x of b)h+=x.toString(16).padStart(2,"0");return h}; const lnTail=b=>{const h=lnHex(b);return encUint(b.length)+h.padEnd(Math.ceil(h.length/64)*64,"0")}; const encLaunch=(n,y,u,sup,alloc,mcap,own,art,mime)=>{ const T=new TextEncoder(); const dyn=[T.encode(n),T.encode(y),T.encode(u)]; const statics=[encUint(sup),encUint(alloc),encUint(mcap),encAddr(own)]; if(art){dyn.push(art);statics.push(encUint(mime))} const words=statics.length+(art?4:3); let off=words*32,head="",tail=""; const heads=[]; for(const b of dyn){heads.push(encUint(off));off+=lnTail(b).length/2;tail+=lnTail(b)} head=heads.slice(0,3).join("")+statics.slice(0,4).join("") +(art?heads[3]+statics[4]:""); return "0x"+(art?SEL_LAUNCHART:SEL_LAUNCH)+head+tail; }; let lnMode=false,lnArtBytes=null,lnArtMime=0; function lnSet(on){ lnMode=on; if(on&&lqMode)lqSet(false); ln.setAttribute("aria-pressed",on?"true":"false"); ln.classList.toggle("on",on); for(const el of [rate,flip,swap])if(el)el.classList.toggle("hide",on); for(const el of [document.querySelector(".panel"),rcvPanel])if(el)el.classList.toggle("hide",on); document.querySelector(".card").classList.toggle("lnon",on); for(const el of [lnPanel,lnNote,lnGo])el.classList.toggle("hide",!on); rc.value="";rc.classList.remove("bad");rcvEl.textContent=""; rc.title=on?"Who the token belongs to: receives the creator allocation, collects the creator share of trading fees for the life of the token, and may edit its name, symbol and logo. Leave blank and it is you. Worth setting to a multisig if the project has one.":""; rc.placeholder=on?"Creator — defaults to you" :"Recipient (optional) — 0x or .eth/.(g)wei"; stat.textContent="";lnNote.textContent=""; if(on)lnQuote(); } function lnQuote(){ lnGo.textContent=account?"Launch":"Connect Wallet"; const ns=v=>v.replace(/[, ]/g,""); const sup=ns(lnSupply.value),mc=+ns(lnMcap.value),al=+ns(lnAlloc.value); lnNote.textContent=""; if(!(Number(sup)>0)||!(mc>0))return; const pooled=Number(sup)*(1-(al>0?al:0)/100); if(!(pooled>0))return; const px=mc/pooled; const sym=lnSym.value.trim().toUpperCase(); const poolFrac=100/(mc+1); const firstEth=poolFrac*pooled/Number(sup); lnNote.textContent=`1 ETH buys ${firstEth>=10?Math.round(firstEth):firstEth.toFixed(firstEth<1?2:1)}% of all ${sym||"tokens"}` +` \u00b7 opens at 1 ETH \u2248 ${trimNum(1/px)} ${sym||"tokens"}` +(al>0?` \u00b7 creator keeps ${trimNum(Number(sup)*al/100)} (${al}%)`:"") +(al>=10?` \u00b7 that much can sell for most of the ether early buyers put in`:"") +(lnArtBytes?` \u00b7 logo ${(lnArtBytes.length/1024).toFixed(1)} KB`:""); lnNote.style.color=poolFrac>=10?"#c33":""; } const trimNum=n=>n>=1000?Math.round(n).toLocaleString("en-US") :n>=1?n.toLocaleString("en-US",{maximumFractionDigits:4}):String(Number(n.toPrecision(4))); const blobText=b=>b.text?b.text():new Promise((res,rej)=>{ const r=new FileReader();r.onload=()=>res(String(r.result));r.onerror=()=>rej(r.error);r.readAsText(b)}); const blobBytes=async b=>b.arrayBuffer?new Uint8Array(await b.arrayBuffer()):await new Promise((res,rej)=>{ const r=new FileReader();r.onload=()=>res(new Uint8Array(r.result));r.onerror=()=>rej(r.error);r.readAsArrayBuffer(b)}); async function lnPrepare(file){ if(/svg/.test(file.type)){ const b=new TextEncoder().encode((await blobText(file)) .replace(/<\?xml[^>]*\?>/g,"").replace(/<!--[\s\S]*?-->/g,"").replace(/<!DOCTYPE[^>]*>/gi,"") .replace(/>\s+</g,"><").trim()); if(b.length>LN_MAX_ART)throw Error(`That SVG is ${(b.length/1024).toFixed(1)} KB and the on-chain limit is 24 KB.`); return{bytes:b,mime:LN_MIME["image/svg+xml"]}; } const bmp=await createImageBitmap(file); let smallest=null; for(const side of [512,384,256,192,128,96,64]){ for(const [type,q] of [["image/webp",1],["image/png",undefined],["image/webp",0.85],["image/webp",0.6]]){ const sc=Math.min(1,side/Math.max(bmp.width,bmp.height)); const cv=document.createElement("canvas"); cv.width=Math.max(1,Math.round(bmp.width*sc));cv.height=Math.max(1,Math.round(bmp.height*sc)); const cx=cv.getContext("2d");cx.imageSmoothingQuality="high"; cx.drawImage(bmp,0,0,cv.width,cv.height); const blob=await new Promise(r=>cv.toBlob(r,type,q)); if(!blob||blob.type!==type)continue; const bytes=await blobBytes(blob); if(!smallest||bytes.length<smallest.bytes.length)smallest={bytes,mime:LN_MIME[type]}; if(bytes.length<=LN_BUDGET)return{bytes,mime:LN_MIME[type]}; } } if(smallest&&smallest.bytes.length<=LN_MAX_ART)return smallest; throw Error("That image will not compress small enough. Flat colours and simple shapes do far better than photographs."); } lnPick.onclick=()=>lnArt.click(); lnArt.onchange=async()=>{ const f=lnArt.files&&lnArt.files[0]; if(!f)return; lnArtNote.textContent="reading…"; try{ const r=await lnPrepare(f); lnArtBytes=r.bytes;lnArtMime=r.mime; lnArtNote.textContent=`${(r.bytes.length/1024).toFixed(1)} KB`; lnArtNote.classList.add("set"); let b64="";for(const x of r.bytes)b64+=String.fromCharCode(x); lnPrev.src="data:"+MIME_NAME[r.mime]+";base64,"+btoa(b64); lnPrev.classList.add("set"); }catch(e){ lnArtBytes=null;lnArtNote.classList.remove("set");lnPrev.classList.remove("set"); lnArtNote.textContent="none";stat.textContent=e.message||String(e); } lnQuote(); }; const lnGroup=el=>{ const dot=el.value.indexOf("."); const intRaw=(dot<0?el.value:el.value.slice(0,dot)).replace(/\D/g,""); const frac=dot<0?"":"."+el.value.slice(dot+1).replace(/\D/g,""); const digitsBefore=el.value.slice(0,el.selectionStart??el.value.length).replace(/\D/g,"").length; const out=intRaw.replace(/\B(?=(\d{3})+(?!\d))/g,",")+frac; if(out===el.value)return; el.value=out; let seen=0,pos=digitsBefore?out.length:0; if(digitsBefore)for(let i=0;i<out.length;i++){ if(/\d/.test(out[i])&&++seen===digitsBefore){pos=i+1;break} } try{el.setSelectionRange(pos,pos)}catch{} }; lnSupply.oninput=()=>{lnGroup(lnSupply);lnQuote()}; for(const el of [lnName,lnSym,lnMcap,lnAlloc])el.oninput=lnQuote; ln.onclick=()=>lnSet(!lnMode); lnGo.onclick=async()=>{ stat.textContent=""; if(lnGo.disabled)return; if(!account)return void connect(); lnGo.disabled=true; try{ await checkWallet(); const name=lnName.value.trim(),sym=lnSym.value.trim().toUpperCase(); if(!name||!sym)throw Error("A name and a symbol, please."); const utf8=v=>new TextEncoder().encode(v).length; if(utf8(name)>64)throw Error(`That name is ${utf8(name)} bytes and the limit is 64.`); if(utf8(sym)>16)throw Error(`That symbol is ${utf8(sym)} bytes and the limit is 16.`); const supRaw=lnSupply.value.replace(/[, ]/g,""); if(!/^\d+(\.\d+)?$/.test(supRaw)||!(Number(supRaw)>0))throw Error("Supply has to be a positive number."); const supply=parseUnits(supRaw,18); const alloc=Math.round(Number(lnAlloc.value||0)*100); if(!(alloc>=0)||alloc>LN_MAX_ALLOC)throw Error("Allocation runs from 0 to 20%."); const mcap=parseUnits(String(lnMcap.value||"").trim(),18); if(mcap<LN_MIN_MCAP)throw Error("That opening valuation is too small to price."); const pooled=supply-supply*BigInt(alloc)/10000n; if(pooled<LN_MIN_POOLED)throw Error("Too little supply reaches the pool to price a market."); let owner=account; const rv=rc.value.trim(); if(rv){ if(/^0x[0-9a-fA-F]{40}$/.test(rv)){ if(/^0x0{40}$/i.test(rv))throw Error("The creator cannot be the zero address."); owner=rv; }else if(/\.(g?wei|eth)$/i.test(rv)){ rcvEl.textContent="resolving\u2026"; let r="";try{r=await nameFwd(rv.toLowerCase())}catch{r=""} rcvEl.textContent=""; if(!r||/^0x0{40}$/i.test(r))throw Error("That name is not registered."); owner=r; }else throw Error("The creator must be an address, or a .eth / .wei name."); } stat.textContent="Launching\u2026"; const req={from:account,to:PLAUNCH,value:"0x0", data:encLaunch(name,sym,"",supply,BigInt(alloc),mcap,owner,lnArtBytes,lnArtMime)}; const sim=await rpc(C,[req,L]); const candidate=/^0x[0-9a-fA-F]{128}$/.test(sim||"")?"0x"+strip0x(sim).slice(24,64):null; const receipt=await waitTx(await rpc(S,[req])); const topic=kecStr("Launched(address,address,address,uint256,uint256,uint256)"); const lg=(receipt.logs||[]).find(l=>(l.address||"").toLowerCase()===PLAUNCH.toLowerCase() &&(l.topics||[])[0]&&l.topics[0].toLowerCase()===topic.toLowerCase()); let token=lg?"0x"+strip0x(lg.topics[1]).slice(24):null; if(!token&&candidate){ try{ const c=await rpc(C,[{to:PLAUNCH,data:"0x"+SEL_CREATOROF+encAddr(candidate)},L]); if(c&&"0x"+strip0x(c).slice(24)===owner.toLowerCase())token=candidate; }catch{} } if(token){ try{ const idx=await addCustomToken(token); rebuild(); fromSel.value=String(Math.max(0,TOKENS.findIndex(t=>t.addr===ZERO))); fromSel.dataset.prev=fromSel.value; toSel.value=String(idx); toSel.dataset.prev=toSel.value; pairChosen=true; lnSet(false); syncDisabled();syncNftUI(); refreshBalance(); update(); }catch{} } stat.textContent=token?`${sym} is live at ${short(token)}`:`${sym} is live`; lnArtBytes=null;lnArtNote.textContent="none";lnArtNote.classList.remove("set"); lnPrev.classList.remove("set");lnArt.value=""; }catch(e){err(e)} finally{lnGo.disabled=false} }; const SEL_MARKETS="29c21083",SEL_TAPE="29a65241"; const FINE=300,COARSE=14400,TFS=[["5m",300],["1h",3600],["4h",14400],["1d",86400]]; const CH_POOLS=8; const srcFor=tf=>tf>=COARSE?COARSE:FINE; let chKind=LS.ck==="line"?"line":"candle",chInv=false,chInvKey="",chPeriod=300,chBars=null,chSeq=0,chPools=null,chOpen=LS.ch==="1",chCache={}; let chDraw=null; const unf=p=>(p&0xffffff)*2**(p>>>24); const decBar=w=>{if(!w)return null;const f=s=>Number((w>>BigInt(s))&0xffffffffn); return{b:Number(w&0xffffffffn),o:unf(f(32)),h:unf(f(64)),l:unf(f(96)),c:unf(f(128)),v:unf(f(160)),n:Number((w>>192n)&0xffffn)}}; const rollUp=(bars,src,dst)=>{if(dst<=src)return bars.filter(Boolean); const out=[];let g=null,k=null; for(let i=bars.length-1;i>=0;i--){const b=bars[i];if(!b)continue; const key=Math.floor(b.b*src/dst); if(key!==k){if(g)out.push(g);k=key;g={b:key,o:b.o,h:b.h,l:b.l,c:b.c,v:b.v,n:b.n}} else{g.h=Math.max(g.h,b.h);g.l=Math.min(g.l,b.l);g.c=b.c;g.v+=b.v;g.n+=b.n}} if(g)out.push(g);return out.reverse()}; const CH_WICK_SHARE=.01; const mergeTapes=tapes=>{const m=new Map(); for(const bars of tapes)for(const b of bars||[]){if(!b)continue; let e=m.get(b.b); if(!e)m.set(b.b,e={b:b.b,v:0,n:0,ov:0,cv:0,parts:[]}); e.v+=b.v;e.n+=b.n;e.ov+=b.o*b.v;e.cv+=b.c*b.v;e.parts.push(b)} const out=[...m.values()].sort((x,y)=>y.b-x.b); for(const e of out){ const big=e.v>0?e.parts.filter(x=>x.v>=e.v*CH_WICK_SHARE):[]; const src=big.length?big:e.parts; let h=-Infinity,l=Infinity; for(const x of src){if(x.h>h)h=x.h;if(x.l<l)l=x.l} e.h=h;e.l=l; if(e.v>0){e.o=e.ov/e.v;e.c=e.cv/e.v}else{e.o=src[0].o;e.c=src[0].c} delete e.ov;delete e.cv;delete e.parts} return out}; const chPair=()=>{const f=TOKENS[fromSel.value],t=TOKENS[toSel.value]; if(!f||!t||f.addr===t.addr)return null; return f.addr.toLowerCase()<t.addr.toLowerCase()?[f,t]:[t,f]}; async function loadChart(){ chBars=null;chPools=null; const p=chPair(); if(tab!=="swap"||!p){syncChartUI();return} const my=++chSeq,[t0,t1]=p,key=t0.addr+":"+t1.addr; const hit=chCache[key]; if(hit&&Date.now()-hit.at<30000){chBars=hit.bars;chPools=hit.pools;syncChartUI();return} try{ if(PPLENS===ZERO||(await rpc("eth_getCode",[PPLENS,L])).length<5)return syncChartUI(); const [raw]=await mc3([{to:PPLENS,data:"0x"+SEL_MARKETS+encAddr(t0.addr)+encAddr(t1.addr) +encAddr(account)+encUint(0)+encUint(LQ_SCAN)+encUint(0)}]); if(my!==chSeq)return; const found=[]; if(raw){const h=strip0x(raw),n=Number(BigInt("0x"+h.slice(64,128))),base=128; for(let i=0;i<n&&i<LQ_SCAN;i++){const at=k=>BigInt("0x"+h.slice(base+(i*PI_WORDS+k)*64,base+(i*PI_WORDS+k+1)*64)); if(at(PI_HOOK)!==0n||at(PI_LIQ)===0n)continue; found.push({a:"0x"+h.slice(base+(i*PI_WORDS+PI_POOL)*64+24,base+(i*PI_WORDS+PI_POOL+1)*64),liq:at(PI_LIQ)})}} found.sort((x,y)=>y.liq>x.liq?1:y.liq<x.liq?-1:0); const pools=found.slice(0,CH_POOLS).map(x=>x.a); if(!pools.length){chCache[key]={at:Date.now(),bars:null,pools:null};return syncChartUI()} const reqs=[]; for(const a of pools)reqs.push({to:a,data:"0x"+SEL_TAPE+encUint(FINE)+encUint(256)}, {to:a,data:"0x"+SEL_TAPE+encUint(COARSE)+encUint(256)}); const tapes=await mc3Deep(reqs); if(my!==chSeq)return; const dec1Tape=hex=>{if(!hex)return null; const h=strip0x(hex),n=Number(BigInt("0x"+h.slice(64,128))),bars=[]; for(let j=0;j<n;j++)bars.push(decBar(BigInt("0x"+h.slice(128+j*64,128+(j+1)*64)))); return bars}; const decoded=tapes.map(dec1Tape); const fine=[],coarse=[]; for(let i=0;i<pools.length;i++){fine.push(decoded[i*2]);coarse.push(decoded[i*2+1])} const sc=10**(t0.dec-t1.dec)/1e18; const build=set=>{const m=mergeTapes(set); for(const b of m){b.o*=sc;b.h*=sc;b.l*=sc;b.c*=sc}return m.length?m:null}; const f=build(fine); let c=build(coarse); if(f&&c){const lead=rollUp(f,FINE,COARSE).filter(b=>b.b>c[0].b); if(lead.length)c=lead.concat(c)} chBars=f||c?{[FINE]:f,[COARSE]:c}:null; const live=set=>set.filter(d=>d&&d.some(Boolean)).length; chPools=chBars?{[FINE]:live(fine),[COARSE]:live(coarse)}:null; chCache[key]={at:Date.now(),bars:chBars,pools:chPools}; }catch{chBars=null;chPools=null} if(my===chSeq)syncChartUI(); } const chPoolsAt=step=>chPools&&chPools[step]||0; const chLabel=p=>chInv?`${p[0].sym} per ${p[1].sym}`:`${p[1].sym} per ${p[0].sym}`; function syncChartUI(){ const on=tab==="swap"&&!!chBars; chTog.classList.toggle("hide",!on); chBox.classList.toggle("hide",!on||!chOpen); chTog.setAttribute("aria-expanded",on&&chOpen?"true":"false"); if(!on||!chOpen)return; const p=chPair(); chNote.textContent=p?chLabel(p):""; drawChart();syncInv(); } const NUMERAIRES=new Set([ "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "0xdac17f958d2ee523a2206206994597c13d831ec7", "0x6b175474e89094c44da98b954eedeac495271d0f", ]); const moneyRank=a=>{a=(a||"").toLowerCase(); return NUMERAIRES.has(a)?2:(a===ZERO||a===WETH.toLowerCase())?1:0}; const chPairKey=p=>p?p[0].addr.toLowerCase()+":"+p[1].addr.toLowerCase():""; const chNaturalInv=p=>!!p&&moneyRank(p[0].addr)>moneyRank(p[1].addr); const CH_SLOTS=Math.floor((340-2-42)/3); const chFinest=()=>chBars&&chBars[FINE]?FINE:COARSE; const chSeries=secs=>{ if(!chBars)return null; const src=srcFor(secs),step=chBars[src]?src:chFinest(),base=chBars[step]; if(!base||!base.length)return null; const per=Math.max(secs,step); let pts=rollUp(base,step,per).slice().reverse(); pts=pts.map(b=>{const{o,c}=b; if(!(o>0)||!(c>0)||!isFinite(o)||!isFinite(c))return null; const h=b.h>0&&isFinite(b.h)?b.h:0,l=b.l>0&&isFinite(b.l)?b.l:Infinity; return{...b,h:Math.max(h,o,c),l:Math.min(l,o,c)}}).filter(Boolean); return pts.length?{pts,per,step}:null; }; const chAutoPeriod=()=>{ if(!chBars)return null; const fit=secs=>{const s=chSeries(secs); if(!s||s.per!==secs)return null; const span=s.pts[s.pts.length-1].b-s.pts[0].b+1; return{n:s.pts.length,ok:s.pts.length>=6&&span<=CH_SLOTS}}; let best=null,bestN=-1; for(const[,secs] of TFS){const f=fit(secs); if(!f)continue; if(f.ok)return secs; if(f.n>bestN){best=secs;bestN=f.n}} return best==null?chFinest():best; }; let chTfKey=""; const chSyncPeriod=()=>{ const k=chPairKey(chPair()); if(k===chTfKey)return; const auto=chAutoPeriod(); if(auto==null)return; chTfKey=k;chPeriod=auto; }; const chSyncTfs=()=>{ const finest=chFinest(); if(chPeriod<finest)chPeriod=finest; for(const c of chTf.children){if(!c.dataset.secs)continue; const secs=Number(c.dataset.secs); c.disabled=!!chBars&&secs<finest; c.title=c.disabled?"This pair has no five-minute tape to draw":""; c.classList.toggle("on",secs===chPeriod)} }; const chSyncOrientation=()=>{ const p=chPair(),k=chPairKey(p); if(k===chInvKey)return; chInvKey=k;chInv=chNaturalInv(p); }; const hudFor=i=>{ if(!chDraw)return; const{pts,num,UP,DN,ch,per,hd}=chDraw; if(!hd)return; if(i<0){const b=pts[pts.length-1]; hd.innerHTML=`<b>${num(b.c)}</b> <span style="color:${ch>=0?UP:DN}">${ch>=0?"+":""}${ch.toFixed(2)}%</span>`; return} const b=pts[i],up=b.c>=b.o,d=new Date(b.b*per*1e3); const when=per>=86400 ?d.toLocaleDateString("en-US",{month:"short",day:"numeric"}) :d.toLocaleTimeString("en-US",{hour:"2-digit",minute:"2-digit",hour12:false}); hd.innerHTML=`<b style="color:${up?UP:DN}">${num(b.c)}</b> <span>${when}</span>` +`<span class="ohlc">H ${num(b.h)} L ${num(b.l)} · ${b.n} trade${b.n===1?"":"s"}</span>`; }; const chPoint=e=>{ if(!chDraw)return; const r=chArt.getBoundingClientRect(); if(!r.width)return; const{pts,W,PL,st,b0}=chDraw; const x=(e.clientX-r.left)/r.width*W; const at=b0+Math.round((x-PL)/st-.5); let i=0,near=Infinity; for(let j=0;j<pts.length;j++){const d=Math.abs(pts[j].b-at); if(d<near){near=d;i=j}} hudFor(i); if(!chDraw.svg)return; let el=chDraw.cross; if(!el){ el=document.createElementNS("http://www.w3.org/2000/svg","line"); el.setAttribute("class","cross"); el.setAttribute("y1","0");el.setAttribute("y2","150"); el.setAttribute("stroke","currentColor");el.setAttribute("stroke-opacity",".35"); el.setAttribute("stroke-width",".8"); chDraw.svg.appendChild(el);chDraw.cross=el; } const cx=chDraw.X(i).toFixed(1); el.setAttribute("x1",cx);el.setAttribute("x2",cx); }; chArt.addEventListener("pointermove",chPoint); chArt.addEventListener("pointerdown",chPoint); chArt.addEventListener("pointerleave",()=>{ if(chDraw&&chDraw.cross){chDraw.cross.remove();chDraw.cross=null} hudFor(-1)}); function drawChart(){ if(!chBars)return; const W=340,H=150,PL=2,PR=42,PT=10,PB=14,VH=22; const pw=W-PL-PR,ph=H-PT-PB-VH; chSyncPeriod();chSyncOrientation();chSyncTfs(); const ser=chSeries(chPeriod); if(!ser){chArt.innerHTML='<div class="msg">No trades in this window.</div>';chDraw=null;return} const per=ser.per; let pts=ser.pts; const newest=pts[pts.length-1].b,cap=Math.floor(pw/3); const kept=pts.filter(b=>newest-b.b<cap); const clipped=kept.length<pts.length; pts=kept; if(chInv)pts=pts.map(b=>({...b,o:1/b.o,h:1/b.l,l:1/b.h,c:1/b.c})); if(!pts.length){chArt.innerHTML='<div class="msg">No trades in this window.</div>';chDraw=null;return} let hi=-Infinity,lo=Infinity,vm=0; for(const b of pts){if(b.h>hi)hi=b.h;if(b.l<lo)lo=b.l;if(b.v>vm)vm=b.v} if(hi===lo){hi*=1.001;lo*=0.999} let sp=hi-lo;hi+=sp*.09;lo-=sp*.09;sp=hi-lo; const n=pts.length,b0=pts[0].b,slots=pts[n-1].b-b0+1; const st=pw/slots,bw=Math.max(1,Math.min(9,st*.68)); const X=i=>PL+st*(pts[i].b-b0+.5),Y=p=>PT+ph*(1-(p-lo)/sp),vt=PT+ph+6,VY=v=>vt+(VH-6)*(1-(vm?v/vm:0)); const num=v=>v>=1000?v.toLocaleString("en-US",{maximumFractionDigits:2}):v>=1?v.toFixed(3):v.toPrecision(3); const UP="#16a34a",DN="#c33"; const p0=chPair(),lbl=p0?chLabel(p0):"Price"; let s=`<svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="none" role="img" aria-label="${lbl}, ${n} bars, ${num(pts[n-1].c)} now, range ${num(lo)} to ${num(hi)}">`; for(let g=0;g<=3;g++){const p=lo+sp*g/3,y=Y(p).toFixed(1); s+=`<line x1="${PL}" y1="${y}" x2="${W-PR}" y2="${y}" stroke="currentColor" stroke-opacity=".1"/>` +`<text x="${W-PR+4}" y="${(Y(p)+2.5).toFixed(1)}" font-size="7" fill="currentColor" fill-opacity=".5">${num(p)}</text>`} for(let i=0;i<n;i++){const b=pts[i],y=VY(b.v); s+=`<rect x="${(X(i)-bw/2).toFixed(1)}" y="${y.toFixed(1)}" width="${bw.toFixed(1)}" height="${Math.max(.4,vt+VH-y).toFixed(1)}" fill="currentColor" fill-opacity=".18"/>`} const rise=pts[n-1].c>=pts[0].o; if(chKind==="line"){ const d=pts.map((b,i)=>`${i?"L":"M"}${X(i).toFixed(1)} ${Y(b.c).toFixed(1)}`).join(" "); const col=rise?UP:DN; s+=`<path d="${d} L${X(n-1).toFixed(1)} ${(PT+ph).toFixed(1)} L${X(0).toFixed(1)} ${(PT+ph).toFixed(1)} Z" fill="${col}" fill-opacity=".1"/>` +`<path d="${d}" fill="none" stroke="${col}" stroke-width="1.2" stroke-linejoin="round"/>` +`<circle cx="${X(n-1).toFixed(1)}" cy="${Y(pts[n-1].c).toFixed(1)}" r="1.8" fill="${col}"/>`; }else{ for(let i=0;i<n;i++){const b=pts[i],col=b.c>=b.o?UP:DN,x=X(i); const t=Y(Math.max(b.o,b.c)),bo=Y(Math.min(b.o,b.c)); s+=`<line x1="${x.toFixed(1)}" y1="${Y(b.h).toFixed(1)}" x2="${x.toFixed(1)}" y2="${Y(b.l).toFixed(1)}" stroke="${col}" stroke-width=".8"/>` +`<rect x="${(x-bw/2).toFixed(1)}" y="${t.toFixed(1)}" width="${bw.toFixed(1)}" height="${Math.max(.8,bo-t).toFixed(1)}" fill="${col}"/>`} } s+="</svg>"; const last=pts[n-1],first=pts[0],ch=((last.c-first.o)/first.o)*100; chArt.innerHTML=s+'<div class="hd"></div>'; chDraw={pts,X,W,PL,st,b0,num,UP,DN,ch,per, hd:chArt.querySelector(".hd"),svg:chArt.querySelector("svg"),cross:null}; hudFor(-1); const took=rel(slots*per); const np=chPoolsAt(ser.step); chNote.textContent=p0?`${chLabel(p0)} · ${np} pool${np===1?"":"s"} · ${clipped?"last ":""}${took}`:""; } for(const[label,secs]of TFS){const b=document.createElement("button"); b.textContent=label;b.dataset.secs=String(secs);if(secs===chPeriod)b.className="on"; b.onclick=()=>{chPeriod=secs;chTfKey=chPairKey(chPair());drawChart()}; chTf.appendChild(b)} const kindBtn=document.createElement("button"); kindBtn.className="chk"; const syncKind=()=>{kindBtn.textContent=chKind==="candle"?"Line":"Candle"; kindBtn.title=`Switch to ${kindBtn.textContent.toLowerCase()} chart`}; syncKind(); kindBtn.onclick=()=>{chKind=chKind==="candle"?"line":"candle";LS.ck=chKind;syncKind();drawChart()}; chTf.appendChild(kindBtn); const invBtn=document.createElement("button"); invBtn.className="chk"; invBtn.textContent="⇅"; const syncInv=()=>{const p=chPair(); invBtn.title=p?`Price ${chInv?p[1].sym:p[0].sym} instead — currently ${chInv?p[0].sym:p[1].sym} per ${chInv?p[1].sym:p[0].sym}`:"Invert the price"; invBtn.setAttribute("aria-pressed",chInv?"true":"false")}; syncInv(); invBtn.onclick=()=>{chInv=!chInv;syncInv();drawChart()}; chTf.appendChild(invBtn); const syncChart=()=>{chSyncOrientation();syncInv();loadChart()}; chTog.onclick=()=>{chOpen=!chOpen;LS.ch=chOpen?"1":"0";syncChartUI()}; const lsecs=v=>{const m=/^(\d+)([mhdw]?)$/.exec((v||"").trim()); return m?+m[1]*(m[2]=="d"?86400:m[2]=="h"?3600:m[2]=="w"?604800:m[2]=="m"?60:1):NaN}; const symAddr=x=>{const m=TOKENS.filter(t=>t.sym.toLowerCase()===x); return m.length===1?m[0].addr.toLowerCase():""}; let lSeq=0; async function applyLink(){ const q=new URLSearchParams(location.hash.slice(1)); if(![...q.keys()].length)return; const my=++lSeq; const at
0xacfba7ce…4082·#25,751,351·0x6b849b5d…0f9d11
class="lqin" data-side="0" inputmode="decimal" autocomplete="off" spellcheck="false" placeholder="0.0 ${t0.sym}" aria-label="${t0.sym} to deposit">` +`<input class="lqin" data-side="1" inputmode="decimal" autocomplete="off" spellcheck="false" placeholder="0.0 ${t1.sym}" aria-label="${t1.sym} to deposit">` +`<div class="lqpv"></div>` +`<button class="lqbtn" data-act="ac" ${p} disabled>Add liquidity</button></div>` +`</div>`; } const LQ_FEES=[[500,"0.05%","pairs that track each other"], [3000,"0.30%","most pairs"],[10000,"1.00%","thin or volatile"]]; const LQ_RANGES=[["full","Full range"],["100","Wide — 100×"],["10","10×"], ["2","2×"],["1.25","Tight — 25%"],["custom","Custom prices"]]; function lqNewForm(t0,t1){ return `<div class="lqnew" data-t0="${t0.addr}" data-t1="${t1.addr}">` +`<label class="dly" for="lqRange">Range<select id="lqRange">` +LQ_RANGES.map(([v,l])=>`<option value="${v}"${v==="full"?" selected":""}>${l}</option>`).join("") +`</select></label>` +`<label class="dly hide" id="lqCustom">Prices<span class="r">` +`<input id="lqLo" inputmode="decimal" autocomplete="off" spellcheck="false" placeholder="Low" aria-label="Lowest price in the band">` +`<input id="lqHi" inputmode="decimal" autocomplete="off" spellcheck="false" placeholder="High" aria-label="Highest price in the band"></span></label>` +`<label class="dly hide" id="lqPxRow" for="lqPx">Opening price` +`<input id="lqPx" inputmode="decimal" autocomplete="off" spellcheck="false" placeholder="${t1.sym} per ${t0.sym}" aria-label="Opening price"></label>` +`<label class="dly" for="lqFee">Fee<select id="lqFee">` +LQ_FEES.map(([v,l,h])=>`<option value="${v}"${v===3000?" selected":""}>${l} — ${h}</option>`).join("") +`</select></label>` +`<div class="lqout" id="lqRangeOut"></div>` +`<div class="lqout" id="lqPv"></div>` +`<div class="lqout" id="lqOwnNote"></div>` +`<div class="lqout" id="lqGas"></div>` +`<button class="primary" id="lqCreate" disabled>Create band</button></div>`; } const pxToSqrt=(px,d0,d1)=>{ const raw=px*10**(d1-d0); if(!isFinite(raw)||raw<=0)return 0n; return BigInt(Math.round(Math.sqrt(raw)*1e9))*10n**9n; }; const lqMarket=(t0,t1,lo,hi,fee,owner)=>encAddr(t0)+encAddr(t1)+encUint(lo)+encUint(hi) +encUint(fee)+encAddr(ZERO)+encAddr(owner)+encUint(0n); const lqSeedPrice=(sl,sh,in0,in1)=>{ const L=Number(sl),H=Number(sh),x=Number(in0),y=Number(in1),W=1e18; const g=s=>(x*s*H*(s-L))/((H-s)*W*y*W); let lo=L,hi=H; for(let i=0;i<90;i++){const m=(lo+hi)/2;if(g(m)<1)lo=m;else hi=m} const s=(lo+hi)/2; if(!(s>L&&s<H))return 0n; return BigInt(Math.round(s)); }; const lqBandFor=(px,mult,a0,a1)=>{ if(a0&&!a1)return[px,px*mult]; if(a1&&!a0)return[px/mult,px]; return[px/mult,px*mult]; }; const LQ_MIN_LIQUIDITY=1000n,LQ_MIN_RESOLUTION=1000000n,LQ_WAD=10n**18n, LQ_MAX_LIQUIDITY=(1n<<128n)-1n,LQ_MAX_SQRT=10n**36n,LQ_SEED_TOLERANCE=100n; const lqUp=(a,b,c)=>(a*b+c-1n)/c; const LQ_NOSEED={ok:false,lp:0n,used0:0n,used1:0n}; function lqSeedQuote(sl,sh,s,a0,a1){ if(!(sl>0n)||!(sh>sl)||sh>LQ_MAX_SQRT||s<sl||s>sh)return LQ_NOSEED; const f0=()=>a0*s/(sh-s)*sh/LQ_WAD,f1=()=>a1*LQ_WAD/(s-sl); let total; if(s===sl)total=f0(); else if(s===sh)total=f1(); else{const x=f0(),y=f1();total=x<y?x:y} if(total===0n||total>LQ_MAX_LIQUIDITY)return LQ_NOSEED; let used0=lqUp(total,(sh-s)*LQ_WAD,sh*s),used1=lqUp(total,s-sl,LQ_WAD); const v0=total*LQ_WAD/sh,v1=total*sl/LQ_WAD; if(v0<LQ_MIN_RESOLUTION||v1<LQ_MIN_RESOLUTION)return LQ_NOSEED; let x=v0+used0,y=v1+used1; const maxX=y*LQ_WAD*LQ_WAD/(sl*sl); if(x>maxX){used0=maxX>v0?maxX-v0:0n;x=v0+used0} const maxY=x*sh*sh/(LQ_WAD*LQ_WAD); if(y>maxY){used1=maxY>v1?maxY-v1:0n;y=v1+used1} if(used0>a0||used1>a1||total<=LQ_MIN_LIQUIDITY)return LQ_NOSEED; const wantSq=s*s,gotSq=x===0n?0n:y*LQ_WAD*LQ_WAD/x,slack=wantSq/LQ_SEED_TOLERANCE; if(gotSq<wantSq-slack||gotSq>wantSq+slack)return LQ_NOSEED; return {ok:true,lp:total-LQ_MIN_LIQUIDITY,used0,used1}; } const LQ_MAX128=(1n<<128n)-1n,LQ_FEE_DENOM=1000000n; const lqInBand=(sl,sh,vX,vY)=>{ if(vX<=0n)return false; const ratio=vY*LQ_WAD*LQ_WAD/vX,hi=sh+1n; return ratio>=sl*sl&&ratio<hi*hi; }; function lqExecutable(sl,sh,fee,augIn,augOut,rIn,rOut,zeroForOne){ if(augOut<=1n)return false; let need=lqUp(augIn,1n,augOut-1n); if(need===0n)need=1n; if(need>LQ_MAX128)return false; const net=lqUp(need,LQ_FEE_DENOM,LQ_FEE_DENOM-fee); if(net>LQ_MAX128)return false; const priced=net*(LQ_FEE_DENOM-fee)/LQ_FEE_DENOM; if(priced===0n)return false; if(net>LQ_MAX128-rIn)return false; const out=priced*augOut/(augIn+priced); if(out===0n||out>rOut)return false; return zeroForOne?lqInBand(sl,sh,augIn+net,augOut-out):lqInBand(sl,sh,augOut-out,augIn+net); } const lqTradeable=(sl,sh,fee,x,y,r0,r1)=> (r1!==0n&&lqExecutable(sl,sh,fee,x,y,r0,r1,true)) ||(r0!==0n&&lqExecutable(sl,sh,fee,y,x,r1,r0,false)); function lqSeedAdmits(sl,sh,s,fee,q){ if(!q||!q.ok)return false; const total=q.lp+LQ_MIN_LIQUIDITY; if(lqUp(total,(sh-s)*LQ_WAD,sh*s)>LQ_MAX128)return false; if(lqUp(total,s-sl,LQ_WAD)>LQ_MAX128)return false; const x=total*LQ_WAD/sh+q.used0,y=total*sl/LQ_WAD+q.used1; if(!lqInBand(sl,sh,x,y))return false; return lqTradeable(sl,sh,fee,x,y,q.used0,q.used1); } const lqBandFits=(sl,sh,sp,fee,in0,in1)=> lqSeedAdmits(sl,sh,sp,fee,lqSeedQuote(sl,sh,sp,in0,in1)); const LQ_WIDTHS=[1000,100,10,2]; function lqFullWidth(d0,d1,a0,a1,px,fee){ for(const mult of LQ_WIDTHS){ const [lo,hi]=lqBandFor(px,mult,a0,a1); const sl=pxToSqrt(lo,d0,d1),sh=pxToSqrt(hi,d0,d1); if(!sl||!sh||sh<=sl)continue; const sp=a0&&!a1?sl:a1&&!a0?sh:lqSeedPrice(sl,sh,a0,a1); if(sp&&lqBandFits(sl,sh,sp,fee,a0,a1))return mult; } return null; } async function loadLq(){ const p=chPair(); const my=++lqSeq; if(!p){lqList.innerHTML=`<div class="lqempty">Pick two different tokens.</div>`;lqSub.textContent="";lqRows=[];return}lqSyncAmounts(); const [t0,t1]=p; lqSub.textContent=`${t0.sym} / ${t1.sym}`; lqList.innerHTML=`<div class="lqempty">Loading…</div>`; try{ const who=account||ZERO; const [raw,c]=await mc3([ {to:PPLENS,data:"0x"+SEL_MARKETS+encAddr(t0.addr)+encAddr(t1.addr) +encAddr(who)+encUint(0)+encUint(LQ_SCAN)+encUint(0)}, {to:PFACTORY,data:"0x"+SEL_PAIR_COUNT+encAddr(t0.addr)+encAddr(t1.addr)}, ]); if(my!==lqSeq)return; let total=0;try{if(c)total=Number(BigInt(c))}catch{} const rows=[]; if(raw){const h=strip0x(raw),n=Number(BigInt("0x"+h.slice(64,128))),base=128; for(let i=0;i<n&&i<LQ_SCAN;i++){ const at=k=>BigInt("0x"+h.slice(base+(i*PI_WORDS+k)*64,base+(i*PI_WORDS+k+1)*64)); rows.push({pool:"0x"+h.slice(base+(i*PI_WORDS+PI_POOL)*64+24,base+(i*PI_WORDS+PI_POOL+1)*64), sl:at(PI_SL),sh:at(PI_SH),fee:at(PI_FEE),r0:at(PI_R0),r1:at(PI_R1),px:at(PI_PX), liq:at(PI_LIQ),hook:"0x"+h.slice(base+(i*PI_WORDS+PI_HOOK)*64+24,base+(i*PI_WORDS+PI_HOOK+1)*64), shares:0n,mine0:0n,mine1:0n})}} if(account&&rows.length){ const bs=await mc3Deep(rows.map(r=>({to:r.pool,data:"0x"+SEL_BALANCEOF+encAddr(account)}))); if(my!==lqSeq)return; rows.forEach((r,i)=>{ if(!bs[i]||r.liq===0n)return; const s=dec1(bs[i],0); r.shares=s;r.mine0=s*r.r0/r.liq;r.mine1=s*r.r1/r.liq; }); } if(my!==lqSeq)return; if(!rows.length){ lqList.innerHTML=`<div class="lqempty">No Precision band for this pair yet.</div>`+lqNewForm(t0,t1);lqSyncAmounts(); lqRows=[]; lqNewPreview(); return} rows.sort((a,b)=>(b.shares>0n)-(a.shares>0n)||(b.liq>a.liq?1:b.liq<a.liq?-1:0)); const shown=rows.slice(0,LQ_SHOW); if(total>shown.length)lqSub.textContent+=` · showing ${shown.length} of ${total}` +(total>LQ_SCAN?` (first ${LQ_SCAN} scanned)`:""); lqRows=shown; lqList.innerHTML=shown.map(o=>lqRow(o,t0,t1)).join("") +`<button class="lqbtn lqnewtog" id="lqNewTog">Create a new band</button>`; lqSyncAmounts(); }catch(e){ if(my!==lqSeq)return; lqRows=[]; lqList.innerHTML=`<div class="lqempty">Could not read pools.</div>`;lqSyncAmounts(); } } const SEL_REMOVE="e39b0eb5",SEL_ADDEXACT="cc0025e4",SEL_PREVIEW_REMOVE="a2eaee07", SEL_PREVIEW_ADD="e03ec807"; const dec1=(h,i)=>BigInt("0x"+strip0x(h).slice(i*64,(i+1)*64)); async function previewRemove(pool,shares){ const [r]=await mc3([{to:PLQLENS,data:"0x"+SEL_PREVIEW_REMOVE+encAddr(pool)+encUint(shares)}]); if(!r)return null; return dec1(r,0)===1n?{a0:dec1(r,1),a1:dec1(r,2)}:null; } async function previewAdd(pool,a0,a1){ const [r]=await mc3([{to:PLQLENS,data:"0x"+SEL_PREVIEW_ADD+encAddr(pool)+encUint(a0)+encUint(a1)}]); if(!r)return null; return dec1(r,0)===1n?{lp:dec1(r,1),used0:dec1(r,2),used1:dec1(r,3),ref0:dec1(r,4),ref1:dec1(r,5)}:null; } async function lqTo(){ const v=rc.value.trim(); if(!v)return account; const to=await rcvOf(v).catch(()=>""); if(!to){rc.classList.add("bad"); stat.textContent="Recipient must be an address or a .wei / .gwei / .eth name that resolves."; return null} rc.classList.remove("bad"); return to; } let lqToSeq=0; async function lqShowTo(){ const my=++lqToSeq,v=rc.value.trim(); if(!v){rc.classList.remove("bad");rcvEl.textContent="";return} rcvEl.textContent="resolving..."; const to=await rcvOf(v).catch(()=>""); if(my!==lqToSeq)return; rc.classList.toggle("bad",!to); rcvEl.textContent=to||""; } async function previewZap(pool,tokenIn,amountIn){ const [r]=await mc3([{to:PLQLENS,data:"0x"+SEL_PREVIEW_ZAP+encAddr(pool)+encAddr(tokenIn)+encUint(amountIn)}]); if(!r)return null; return dec1(r,0)===1n?{portion:dec1(r,1),lp:dec1(r,2)}:null; } const lessSlip=v=>{const b=BigInt(Math.round(Math.min(10,Math.max(.01,+slip.value||.5))*100)); return v-(v*b)/10000n}; async function ensureAllowance(token,spender,need){ if(token===ZERO||need===0n)return; const [r]=await mc3([{to:token,data:"0x"+SEL_ALLOWANCE+encAddr(account)+encAddr(spender)}]); if(r&&dec1(r,0)>=need)return; stat.textContent="Approving…"; if(r&&dec1(r,0)>0n)await waitTx(await rpc(S,[{from:account,to:token, data:"0x"+SEL_APPROVE+encAddr(spender)+encUint(0),value:"0x0"}])); await waitTx(await rpc(S,[{from:account,to:token, data:"0x"+SEL_APPROVE+encAddr(spender)+encUint(need),value:"0x0"}])); } async function factoryPool(pool){ const [r]=await mc3([{to:PFACTORY,data:"0x"+SEL_ISPOOL+encAddr(pool)}]).catch(()=>[null]); return !!r&&dec1(r,0)===1n; } const SEL_REMOVE_LOSSY="0cc55f06"; const encLossy=(shares,to,take0,take1)=>"0x"+SEL_REMOVE_LOSSY+encUint(shares) +encUint(0n)+encUint(0n)+encAddr(to)+encBool(take0)+encBool(take1); async function lossyExit(pool,shares,t0,t1){ const attempt=async(take0,take1)=>{ const req={from:account,to:pool,value:"0x0",data:encLossy(shares,account,take0,take1)}; try{await rpc(C,[req,L]);return req}catch{return null} }; const both=await attempt(true,true); if(both)return{req:both,note:`This pool cannot pay out in full — one of its` +` tokens is short, or takes a fee on transfer.\n\nYou would receive your share` +` of what it can still pay, on both sides, which is less than the amounts` +` shown.\n\nContinue?`}; for(const [take0,take1,keep,drop] of [[true,false,t0,t1],[false,true,t1,t0]]){ const req=await attempt(take0,take1); if(!req)continue; return{req,note:`The ${drop.sym} side of this pool cannot be transferred.\n\n` +`You can still leave through ${keep.sym} alone — but doing so GIVES UP your` +` ${drop.sym} claim permanently, to the holders who remain.\n\nContinue?`}; } return null; } async function doRemove(pool,shares,t0,t1){ const pv=await previewRemove(pool,shares); if(!pv){stat.textContent="This amount redeems nothing — the pool would refuse it.";return} if(!await factoryPool(pool)){stat.textContent="Not a Precision pool — refusing to send.";return} ++busy; try{ await checkWallet(); stat.textContent="Withdrawing…"; let req={from:account,to:pool,value:"0x0", data:"0x"+SEL_REMOVE+encUint(shares)+encUint(lessSlip(pv.a0))+encUint(lessSlip(pv.a1))+encAddr(account)}; try{await rpc(C,[req,L])} catch(strict){ const alt=await lossyExit(pool,shares,t0,t1); if(!alt)throw strict; let go=false; try{go=confirm(alt.note)} catch{go=false} if(!go){stat.textContent="Withdrawal cancelled.";return} req=alt.req; stat.textContent="Withdrawing what the pool can still pay…"; } await settle(await rpc(S,[req])); await loadLq();refreshBalance(); }catch(e){err(e)} finally{--busy} } async function doAdd(pool,a0,a1,t0,t1){ const to=await lqTo(); if(!to)return; const pv=await previewAdd(pool,a0,a1); if(!pv){stat.textContent="The pool would refuse this deposit.";return} if(!await factoryPool(pool)){stat.textContent="Not a Precision pool — refusing to send.";return} ++busy; try{ await checkWallet(); await ensureAllowance(t0.addr,pool,a0); await ensureAllowance(t1.addr,pool,a1); stat.textContent="Adding…"; const value=t0.addr===ZERO?a0:0n; const req={from:account,to:pool,value:toHex(value), data:"0x"+SEL_ADDEXACT+encUint(0)+encUint(a0)+encUint(a1)+encUint(lessSlip(pv.lp))+encAddr(to)}; await rpc(C,[req,L]); await settle(await rpc(S,[req])); await loadLq();refreshBalance(); }catch(e){err(e)} finally{--busy} } async function doZap(pool,tok,amountIn){ const to=await lqTo(); if(!to)return; const pv=await previewZap(pool,tok.addr,amountIn); if(!pv||pv.lp===0n){stat.textContent="The pool would refuse this deposit.";return} if(!await factoryPool(pool)){stat.textContent="Not a Precision pool — refusing to send.";return} ++busy; try{ await checkWallet(); const minLp=lessSlip(pv.lp); const zap="0x"+SEL_ZAPIN+encAddr(pool)+encAddr(tok.addr)+encUint(amountIn) +encUint(pv.portion)+encUint(minLp)+encAddr(to)+encAddr(account); const leg=encSnwap(tok.addr,amountIn,to,pool,minLp,PROUTE,zap); let data,value; if(tok.addr===ZERO){data=leg;value=toHex(amountIn)} else{ stat.textContent="Approving…"; await ensureAllowance(tok.addr,ZROUTER,amountIn); data=encCalls([ encSnwap(ZERO,0n,account,ZERO,0n,PROUTE, "0x"+SEL_PCHECKPOINT+encAddr(tok.addr)+strip0x(keccak(hexToBytes(zap)))+encAddr(account)), leg]); value="0x0"; } stat.textContent="Zapping…"; const req={from:account,to:ZROUTER,value,data}; await rpc(C,[req,L]); await settle(await rpc(S,[req])); await loadLq();refreshBalance(); }catch(e){err(e)} finally{--busy} } let lqPvT,lqPvSeq=0; async function lqPreview(box){ const pool=box.closest(".lqrow")?.dataset.pool||""; const row=lqRows.find(x=>x.pool.toLowerCase()===pool.toLowerCase()); const p=chPair(); const out=box.querySelector(".lqpv"),go=box.querySelector('[data-act="ac"]'); if(!row||!p){out.textContent="";go.disabled=true;return} const [t0,t1]=p,ins=box.querySelectorAll(".lqin"); let a0,a1; try{ a0=ins[0].value.trim()?parseUnits(ins[0].value.trim(),t0.dec):0n; a1=ins[1].value.trim()?parseUnits(ins[1].value.trim(),t1.dec):0n; }catch{out.textContent="Invalid amount.";go.disabled=true;return} if(isZap(box)){ const z=zapSide(box,t0,t1); if(z.err){out.textContent=z.err;go.disabled=true;return} if(!z.amt){out.textContent=`Enter an amount of ${t0.sym} or ${t1.sym}.`;go.disabled=true;return} const mz=++lqPvSeq; out.textContent="Checking…";go.disabled=true; const zp=await previewZap(row.pool,z.tok.addr,z.amt).catch(()=>null); if(mz!==lqPvSeq)return; if(!zp||zp.lp===0n){out.textContent="The pool would refuse this deposit.";go.disabled=true;return} out.textContent=`swaps ${trimAmt(zp.portion,z.tok.dec)} of ${trimAmt(z.amt,z.tok.dec)} ${z.tok.sym}` +` for ${t1.sym}, then deposits both · mints ~${trimAmt(zp.lp,18)} LP`; go.disabled=false;return; } if(!a0||!a1){ out.textContent="Enter both amounts.";go.disabled=true;return} const my=++lqPvSeq; out.textContent="Checking…";go.disabled=true; const pv=await previewAdd(row.pool,a0,a1).catch(()=>null); if(my!==lqPvSeq)return; if(!pv){out.textContent="The pool would refuse this deposit.";go.disabled=true;return} const ref=(pv.ref0>0n?` · refunds ${trimAmt(pv.ref0,t0.dec)} ${t0.sym}`:"") +(pv.ref1>0n?` · refunds ${trimAmt(pv.ref1,t1.dec)} ${t1.sym}`:""); out.textContent=`deposits ${trimAmt(pv.used0,t0.dec)} ${t0.sym} + ${trimAmt(pv.used1,t1.dec)} ${t1.sym}${ref}`; go.disabled=false; } const isZap=box=>!!box.querySelector(".lqz")?.checked; function zapSide(box,t0,t1){ const ins=box.querySelectorAll(".lqin"); const v0=ins[0].value.trim(),v1=ins[1].value.trim(); if(v0&&v1)return{err:`One side only — clear the ${t1.sym} field, or turn one-sided off.`}; if(!v0&&!v1)return{err:`Enter an amount of ${t0.sym} or ${t1.sym}.`}; const tok=v0?t0:t1; try{return{tok,amt:parseUnits(v0||v1,tok.dec)}}catch{return{err:"Invalid amount."}} } function lqMirror(box,src){ if(isZap(box))return; const pool=box.closest(".lqrow")?.dataset.pool||""; const row=lqRows.find(x=>x.pool.toLowerCase()===pool.toLowerCase()); const p=chPair(); if(!row||!p||!row.r0||!row.r1)return; const [t0,t1]=p,ins=box.querySelectorAll(".lqin"); const i=src===ins[0]?0:src===ins[1]?1:-1; if(i<0)return; const other=ins[1-i],v=src.value.trim(); if(!v){other.value="";return} try{ const raw=parseUnits(v,i===0?t0.dec:t1.dec); other.value=formatUnits(i===0?raw*row.r1/row.r0:raw*row.r0/row.r1,i===0?t1.dec:t0.dec); }catch{} } lqList.addEventListener("input",e=>{ if(!e.target.classList.contains("lqin"))return; const box=e.target.closest(".lqadd"); if(box){lqMirror(box,e.target);clearTimeout(lqPvT);lqPvT=setTimeout(()=>lqPreview(box),300);return} if(e.target.closest(".lqnew")){clearTimeout(lqPvT);lqPvT=setTimeout(lqNewPreview,300)} }); lqList.addEventListener("change",e=>{ if(e.target.id==="lqFee"||e.target.id==="lqRange")return lqNewPreview(); if(!e.target.classList.contains("lqz"))return; const box=e.target.closest(".lqadd"),ins=box.querySelectorAll(".lqin"); const on=e.target.checked; if(!on)lqMirror(box,ins[0]); box.querySelector('[data-act="ac"]').textContent=on?"Zap in":"Add liquidity"; lqPreview(box); }); let lqNewSeq=0; let lqPvT2; const lqPreviewSoon=()=>{clearTimeout(lqPvT2);lqPvT2=setTimeout(lqNewPreview,320)}; async function lqNewPreview(){ const box=lqList.querySelector(".lqnew"); if(!box)return; const out=box.querySelector("#lqPv"),go=box.querySelector("#lqCreate"); const bandOut=box.querySelector("#lqRangeOut"); const my=++lqNewSeq; go.disabled=true; const pair=chPair(); if(!pair){out.textContent="";return} const [t0,t1]=pair; const range=box.querySelector("#lqRange").value; box.querySelector("#lqCustom").classList.toggle("hide",range!=="custom"); const payTok=TOKENS[fromSel.value]; const payIs0=!!payTok&&payTok.addr.toLowerCase()===t0.addr.toLowerCase(); const el0=payIs0?amt:outAmt,el1=payIs0?outAmt:amt; let a0=0n,a1=0n; try{ const v0=el0.value.trim(),v1=el1.value.trim(); a0=v0?parseUnits(v0,t0.dec):0n;a1=v1?parseUnits(v1,t1.dec):0n; }catch{out.textContent="Check the amounts.";bandOut.textContent="";return} const oneSided=(a0&&!a1)?0:(a1&&!a0)?1:-1; box.querySelector("#lqPxRow").classList.toggle("hide",oneSided<0); if(!a0&&!a1){ bandOut.textContent=""; out.textContent=`Enter how much ${t0.sym} and ${t1.sym} to deposit — the ratio sets the opening price.`; return} const pxIn=box.querySelector("#lqPx").value.trim(); const px=oneSided<0 ?(Number(a1)/10**t1.dec)/(Number(a0)/10**t0.dec) :(pxIn?+pxIn:NaN); if(!(px>0)){ bandOut.textContent=""; const only=oneSided===0?t0.sym:t1.sym; out.textContent=`${/^[AEIOU]/i.test(only)?"An":"A"} ${only}-only band has no ratio to` +` read a price from — name one.`; return} const fee=BigInt(box.querySelector("#lqFee").value); let lo,hi; if(range==="custom"){ const n=id=>{const v=box.querySelector("#"+id).value.trim();return v?+v:NaN}; lo=n("lqLo");hi=n("lqHi"); if(!(lo>0)||!(hi>0)){bandOut.textContent="";out.textContent="Name both prices.";return} if(lo>=hi){out.textContent="The low price must be below the high one.";bandOut.textContent="";return} if(oneSided<0&&(px<lo||px>hi)){ out.textContent=`These amounts imply ${fmtPx(px)}, which is outside that band.`;return} }else{ let mult=+range; if(range==="full"){ const found=lqFullWidth(t0.dec,t1.dec,a0,a1,px,fee); if(!found){ bandOut.textContent=""; out.textContent="Even the narrowest range needs more than this deposit.";return} mult=found; } [lo,hi]=lqBandFor(px,mult,a0,a1); } const sl=pxToSqrt(lo,t0.dec,t1.dec),sh=pxToSqrt(hi,t0.dec,t1.dec); if(!sl||!sh||sh<=sl){out.textContent="Check the prices.";bandOut.textContent="";return} const sp=oneSided===0?sl:oneSided===1?sh:lqSeedPrice(sl,sh,a0,a1); if(!sp){out.textContent="Those amounts cannot open a band that wide.";return} const openAt=bandPrice(sp,t0.dec,t1.dec); bandOut.innerHTML=lqBar(lo,hi,openAt)+`<span></span>`; bandOut.querySelector("span").textContent= `${fmtPx(lo)} – ${fmtPx(hi)} · opens at ${fmtPx(openAt)} ${t1.sym} per ${t0.sym}` +(oneSided===0?` · ${t0.sym} only`:oneSided===1?` · ${t1.sym} only`:""); box.querySelector("#lqOwnNote").textContent= `Unowned, like any AMM pool: anyone may add, and no creator fee can ever be` +` charged. Prices are ${t1.sym} per ${t0.sym}.`; lqGasNote(box); const market=lqMarket(t0.addr,t1.addr,sl,sh,fee,ZERO); box.dataset.sl=sl;box.dataset.sh=sh;box.dataset.sp=sp;box.dataset.fee=fee; box.dataset.a0=a0;box.dataset.a1=a1;box.dataset.lp=0; const q=lqSeedQuote(sl,sh,sp,a0,a1); if(!q.ok){out.textContent="This band cannot open at that price with those amounts.";return} if(!lqSeedAdmits(sl,sh,sp,fee,q)){ out.textContent="A band this size could not be traded once opened —" +" widen the range, or deposit more.";return} out.textContent=`Seeds ${trimAmt(q.used0,t0.dec)} ${t0.sym} + ${trimAmt(q.used1,t1.dec)} ${t1.sym}` +((q.used0<a0||q.used1<a1)?" — the rest is returned":""); box.dataset.lp=q.lp; try{ const [addr]=await mc3([{to:PFACTORY,data:"0x"+SEL_POOLFOR+market}]); if(my!==lqNewSeq)return; const pool="0x"+strip0x(addr).slice(24); const [sup]=await mc3([{to:pool,data:"0x"+SEL_TOTALSUPPLY}]).catch(()=>[null]); if(my!==lqNewSeq)return; if(sup&&strip0x(sup).length>=64&&dec1(sup,0)>0n){ out.textContent="That exact band already exists — add to it from the list above."; return} go.disabled=!account; }catch{ if(my!==lqNewSeq)return; out.textContent="Could not reach the factory."; } } const LQ_CREATE_GAS=4700000n; async function lqGasNote(box){ const el=box.querySelector("#lqGas"); if(!el)return; el.textContent=`Creating deploys the pool contract — about ` +`${(Number(LQ_CREATE_GAS)/1e6).toFixed(1)}M gas.`; try{ const wei=BigInt(await rpc("eth_gasPrice",[]))*LQ_CREATE_GAS; el.textContent=`Creating deploys the pool contract — about ` +`${(Number(LQ_CREATE_GAS)/1e6).toFixed(1)}M gas, roughly ` +`${trimAmt(wei,18)} ETH at the moment. Adding to a band that already ` +`exists costs a fraction of it, so check the fee your wallet offers.`; }catch{} } async function lqCreate(){ const box=lqList.querySelector(".lqnew"); if(!box||busy||!account)return; const pair=chPair(); if(!pair)return; const [t0,t1]=pair; const go=box.querySelector("#lqCreate"),out=box.querySelector("#lqPv"); const sl=BigInt(box.dataset.sl||0),sh=BigInt(box.dataset.sh||0),sp=BigInt(box.dataset.sp||0); const a0=BigInt(box.dataset.a0||0),a1=BigInt(box.dataset.a1||0),fee=BigInt(box.dataset.fee||0); if(!sl||!sh||!sp){out.textContent="Preview the band first.";return} const to=await lqTo(); if(!to)return; ++busy;go.disabled=true; try{ const bps=BigInt(Math.round(Math.min(10,Math.max(.01,+slip.value||.5))*100)); const lp=BigInt(box.dataset.lp||0); const minLP=lp>0n?lp*(10000n-bps)/10000n:0n; const market=lqMarket(t0.addr,t1.addr,sl,sh,fee,ZERO); const native=t0.addr===ZERO; if(!native&&a0)await lqApprove(t0.addr,a0); if(a1)await lqApprove(t1.addr,a1); const data="0x"+SEL_CREATE_SEED+market+encUint(sp)+encUint(a0)+encUint(a1) +encUint(minLP)+encAddr(to); const tx={from:account,to:PFACTORY,data,value:native?toHex(a0):"0x0"}; stat.textContent="Creating the band..."; await rpc(C,[tx,L]);await checkWallet();await settle(await rpc(S,[tx])); stat.textContent="Band created."; loadLq();refreshBalance(); }catch(e){err(e);out.textContent="";} finally{--busy;go.disabled=false} } async function lqApprove(token,need){ const al=BigInt(await rpc(C,[{to:token,data:"0x"+SEL_ALLOWANCE+encAddr(account)+encAddr(PFACTORY)},L])); if(al>=need)return; if(al)await settle(await rpc(S,[{from:account,to:token, data:"0x"+SEL_APPROVE+encAddr(PFACTORY)+encUint(0n),value:"0x0"}])); await settle(await rpc(S,[{from:account,to:token, data:"0x"+SEL_APPROVE+encAddr(PFACTORY)+encUint(need),value:"0x0"}])); } lqList.onclick=async ev=>{ if(ev.target.id==="lqCreate"){await lqCreate();return} if(ev.target.id==="lqNewTog"){ const pr=chPair();if(!pr)return
0xacfba7ce…4082·#25,751,350·0xc8d2fdec…3c134a
; return `<h6><span>Orderbook</span><span>${hidden?hidden+" hidden":""}</span></h6>` +`<div class="bkf">` +chip(bookPairOnly,"bf","1",`${f}/${t}`,"Only orders on the pair selected above") +chip(!bookPairOnly,"bf","0","All pairs","Every order on every board") +`</div><div class="bkf">` +BOOK_TYPES.map(([v,l])=>chip(bookType===v,"bt",v,l)).join("") +`</div>`; } let bookShown=0; function syncBook(n,total){ bookShown=n; total=total===undefined?n:total; const on=tab==="book"&&total>0; bkTog.classList.toggle("hide",!on); bkTog.firstChild.nodeValue=total ?`Orderbook (${n<total?`${n} of ${total}`:total}) `:"Orderbook "; bkTog.setAttribute("aria-expanded",on&&bkOpen?"true":"false"); book.classList.toggle("hide",!on||!bkOpen); } bkTog.onclick=()=>{bkOpen=!bkOpen;LS.bk=bkOpen?"1":"0";syncBook(bookShown,bookRows.length)}; book.addEventListener("click",async e=>{ const fc=e.target.closest("[data-bf]"),tc=e.target.closest("[data-bt]"); if(fc){bookPairOnly=fc.dataset.bf==="1";LS.bkp=bookPairOnly?"1":"0";paintBook();return} if(tc){bookType=tc.dataset.bt;paintBook();return} const b=e.target.closest("button[data-i]"); if(!b){ const row=e.target.closest(".o.ins"); if(row&&!e.target.closest("a"))inspect(row,row.dataset.k); return; } b.disabled=true; ++busy; try{ await checkWallet(); const id=BigInt(b.dataset.i),to=b.dataset.b,dl=BigInt(Math.floor(Date.now()/1e3)+1800); const row=bookRows.find(x=>String(x.id)===b.dataset.i&&x.board.toLowerCase()===to.toLowerCase()); if(!row)throw Error("order changed — refresh and retry"); if(b.dataset.x==="c"){ const native=!row.nA&&row.tA.toLowerCase()===WETH.toLowerCase()&&wantsUnwrap(); const floorish=b.dataset.d==="1"||b.dataset.f==="1"; const sel=floorish ?(native?SEL_DUTCH_CANCEL_UNWRAP:SEL_DUTCH_CANCEL) :(native&&b.dataset.v==="1"?SEL_CANCEL_UNWRAP:SEL_CANCELORD); const data="0x"+sel+encUint(id); const lapsed=row.exp&&BigInt(Math.floor(Date.now()/1e3))>row.exp; stat.textContent=`${lapsed?"Reclaiming":"Cancelling"} — the ${b.dataset.f==="1"?"unspent escrow":"unfilled remainder"} returns as ${native&&(floorish||b.dataset.v==="1")?"ETH":"its escrow token"}`; const tx={from:account,to,data,value:"0x0"}; await rpc(C,[tx,L]); await settle(await rpc(S,[tx])); loadBook();refreshBalance();return; } const r=row; let payB=r.aB,getA=r.aA; if(r.pf&&!r.nA&&!r.nB&&!r.dutch&&!r.floor&&r.v2&&r.maker.toLowerCase()!==account.toLowerCase()){ const dB=r.dB??18,dA=r.dA??18,sB=r.sB||"tokens",sA=r.sA||"tokens"; let have=0n; try{ have=r.tB===ZERO||r.tB.toLowerCase()===WETH.toLowerCase() ?BigInt(await rpc("eth_getBalance",[account,L])) +(r.tB===ZERO?0n:BigInt(await rpc(C,[{to:WETH,data:"0x"+SEL_BALANCEOF+encAddr(account)},L]))) :BigInt(await rpc(C,[{to:r.tB,data:"0x"+SEL_BALANCEOF+encAddr(account)},L])); }catch{} const suggest=have&&have<r.aB?have:r.aB; const answer=prompt( `Fill how much of this order?\n\nIt sells ${trimAmt(r.aA,dA)} ${sA} for ${trimAmt(r.aB,dB)} ${sB} in total, ` +`and can be filled in part.\n\nEnter the ${sB} you want to spend:`, trimAmt(suggest,dB)); if(answer===null)return; let asked; try{asked=parseUnits(String(answer).trim(),dB)}catch{throw Error("that is not an amount")} if(!asked)throw Error("enter an amount above zero"); if(asked>r.aB)throw Error(`that is more than the order needs — it costs ${trimAmt(r.aB,dB)} ${sB} in full`); if(asked<r.aB){ const q=await rpc(C,[{to:r.board,data:"0x"+SEL_QUOTEFILL+encUint(id)+encUint(asked)},L]); const h=strip0x(q); if(h.length<128)throw Error("the board would not quote that amount"); getA=BigInt("0x"+h.slice(0,64)); payB=BigInt("0x"+h.slice(64,128)); if(!getA)throw Error("that amount buys nothing — try a larger one"); stat.textContent=`Filling ${trimAmt(payB,dB)} ${sB} of ${trimAmt(r.aB,dB)} for ${trimAmt(getA,dA)} ${sA}...`; } } const nativeOut=!r.nA&&r.tA.toLowerCase()===WETH.toLowerCase()&&wantsUnwrap(); const wethLeg=!r.nB&&r.tB.toLowerCase()===WETH.toLowerCase()&&payB>0n; const plan=wethLeg?await planWethPayment(payB):null; if(plan&&plan.rail==="short") throw Error(`Not enough ether to fill: it costs ${trimAmt(payB,18)} and you hold ` +`${trimAmt(plan.w,18)} WETH plus ${trimAmt(plan.e,18)} ETH. ` +(r.pf ?`This button takes the whole order — to buy part of it, use the Swap tab, which fills this order alongside the pools.` :`This order is all-or-nothing, so it cannot be filled in smaller pieces.`)); if(plan&&plan.wrap>0n){ stat.textContent=`Wrapping ${trimAmt(plan.wrap,18)} ETH to cover the rest...`; const wtx={from:account,to:WETH,data:"0xd0e30db0",value:toHex(plan.wrap)}; await rpc(C,[wtx,L]);await checkWallet();await settle(await rpc(S,[wtx])); } const nativeIn=plan?.rail==="eth"; const routeIn=nativeIn?ZERO:r.tB; const routeOut=nativeOut?ZERO:r.tA; const canonicalNative=routeIn.toLowerCase()===routeOut.toLowerCase() ||(routeOut===ZERO&&routeIn.toLowerCase()===WETH.toLowerCase()); if(!r.nB)await assertFunded(routeIn,payB, known(routeIn)||(routeIn===ZERO?{sym:"ETH",dec:18}:{sym:r.sB||"TOKEN",dec:r.dB??18})); if(!r.nA&&!r.nB&&r.cp===ZERO&&!canonicalNative){ if((await rpc("eth_getCode",[SWAPBOL,L])).length<5)throw Error("orderbook executor not deployed yet"); await preflightLeg(r,routeIn,routeOut,payB); const pay=payB,fillPlan=encFillPlan([ {id:r.id,board:r.board,pay,get:getA,part:false} ],routeIn,routeOut,account,account,dl); const planCall=encSnwap(pay?routeIn:ZERO,pay,account,routeOut,getA,SWAPBOL,fillPlan); const checkpoint=routeIn===ZERO?null:encCheckpoint(SWAPBOL,routeIn,account); const direct=checkpoint?encCalls([checkpoint,planCall]):planCall; const funded=routeIn===ZERO||!pay?direct:encCalls([ checkpoint, encSweep(routeIn,pay,SWAPBOL), encSnwap(ZERO,0n,account,routeOut,getA,SWAPBOL,fillPlan) ]); const meta=known(routeIn)||{addr:routeIn,sym:r.sB||"TOKEN",dec:r.dB??18}; await sendRouterFunded(direct,funded,meta,pay,routeIn===ZERO?pay:0n,"Filling order..."); loadBook();refreshBalance();return; } const data=r.dutch ?"0x"+SEL_DUTCH_FILL+encUint(id)+encUint(r.nA?0n:getA)+encAddr(account)+encUint(payB) :r.v2 ?"0x"+(nativeIn?SEL_FILL2_ETH:nativeOut?SEL_FILL2_UNWRAP:SEL_FILL2) +encUint(id)+encUint(dl)+(nativeIn?"":encUint(payB))+encUint(r.nA?0n:getA)+encAddr(ZERO) :"0x"+SEL_FILL1+encUint(id)+encUint(dl); const payNative=(nativeIn&&!r.dutch&&r.v2)||(r.dutch&&r.tB===ZERO); const wrap=nativeIn&&!payNative&&payB ?{to:WETH,data:"0xd0e30db0",value:toHex(payB)}:null; const tx={from:account,to,data,value:payNative?toHex(payB):"0x0"}; const approvals=[]; if(r.nB){ let ap=ZERO; try{ap="0x"+strip0x(await rpc(C,[{to:r.tB,data:"0x081812fc"+encUint(payB)},L])).slice(-40)}catch{} if(ap.toLowerCase()!==to.toLowerCase()){ approvals.push({to:r.tB,data:"0x"+SEL_APPROVE+encAddr(to)+encUint(payB),value:"0x0"}); } }else if(r.tB!==ZERO&&payB&&!(nativeIn&&!r.dutch&&r.v2)){ const al=BigInt(await rpc(C,[{to:r.tB,data:"0x"+SEL_ALLOWANCE+encAddr(account)+encAddr(to)},L])); if(al<payB){ if(al)approvals.push({to:r.tB,data:"0x"+SEL_APPROVE+encAddr(to)+encUint(0n),value:"0x0"}); approvals.push({to:r.tB,data:"0x"+SEL_APPROVE+encAddr(to)+encUint(payB),value:"0x0"}); } } const revoke=r.dutch&&!r.nB&&r.tB!==ZERO ?{to:r.tB,data:"0x"+SEL_APPROVE+encAddr(to)+encUint(0n),value:"0x0"}:null; if((wrap||approvals.length||revoke)&&await canBatch(account)){ stat.textContent="Preparing and filling atomically..."; await settle(await sendBatch(account,[ ...(wrap?[wrap]:[]),...approvals,{to,data,value:tx.value},...(revoke?[revoke]:[]) ])); loadBook();refreshBalance();return; } if(wrap){ stat.textContent="Wrapping ETH..."; await waitTx(await rpc(S,[{from:account,...wrap}])); } for(let i=0;i<approvals.length;i++){ stat.textContent=`Approve${r.nB?" NFT":""}${approvals.length>1?` (${i+1}/${approvals.length})`:""}...`; await waitTx(await rpc(S,[{from:account,...approvals[i]}])); } await rpc(C,[tx,L]); await settle(await rpc(S,[tx])); if(revoke)try{ stat.textContent="Filled · revoking quote allowance..."; await waitTx(await rpc(S,[{from:account,...revoke}])); stat.textContent="Done"; }catch{stat.textContent="Filled · quote allowance remains; revoke it in your wallet"} loadBook();refreshBalance(); }catch(x){err(x);b.disabled=false} finally{--busy} }); const SLOW="0x000000000000888741B254d37e1b27128AfEAaBC"; const SEL_DEPOSITTO="94eeaec9",SEL_CLAIM="379607f5",SEL_REVERSE="97d15425",SEL_WITHDRAWFROM="d4fdc309"; const SEL_UNLOCK="6198e339",SEL_CLAWBACK="fcc36bc9",SEL_GUARDIAN="0633b14a"; const GRACE=2592000n; const SLOW_GATE="0xb8B546b93a82f4Aa6f0345142dF5679B659ef3D4"; const SEL_DEPOSITTIP="75f92e42",SEL_TIPS="a5c68c59",SEL_REFUNDTIP="d27e1e72"; const TIP_GAS=180000n; let sendTipWei=0n,tipSeq=0; async function syncTip(){ const my=++tipSeq; const on=tab==="send"&&+dly.value>0; tipL.classList.toggle("hide",!on); if(!on||!tipCk.checked){sendTipWei=0n;tipNote.textContent="";return} tipNote.textContent="estimating…"; let wei=0n; try{wei=BigInt(await rpc("eth_gasPrice",[]))*TIP_GAS}catch{} if(my!==tipSeq)return; sendTipWei=wei>0n?wei:10n**15n/2n; tipNote.textContent="tip ≈ "+trimAmt(sendTipWei,18)+" ETH"; render(); } const SEL_OUT="d40d4bc6",SEL_IN="e3993ee7",SEL_PENDING="6577b86a"; const idTok=i=>"0x"+(i&((1n<<160n)-1n)).toString(16).padStart(40,"0"); const idDelay=i=>i>>160n; const w=(h,n)=>BigInt("0x"+(strip0x(h).slice(n*64,n*64+64)||"0")); const metaCache=new Map(); async function tokMeta(a){ a=a.toLowerCase(); if(a===ZERO)return{sym:"ETH",dec:18}; const k=TOKENS.find(t=>t.addr.toLowerCase()===a); if(k)return k; if(metaCache.has(a))return metaCache.get(a); let m={sym:a.slice(0,6)+"…",dec:18}; try{const[sr,dr]=await Promise.all([rpc(C,[{to:a,data:"0x95d89b41"},L]),rpc(C,[{to:a,data:"0x313ce567"},L])]); const d=Number(BigInt(dr)); if(Number.isInteger(d)&&d>=0&&d<=36)m={sym:decodeString(sr).trim().replace(/[<>&"'`]/g,"").slice(0,10)||m.sym,dec:d}; }catch{} metaCache.set(a,m);return m; } const rel=s=>{const a=Math.abs(s),u=[[86400,"d"],[3600,"h"],[60,"m"]]; for(const[n,l]of u)if(a>=n)return Math.floor(a/n)+l;return a+"s"}; let posSeq=0,posShown=25,slowGuard=false; const POSPAGE=25; async function loadPos(){ if(!account){pos.innerHTML="";tabSend.textContent="Send";slowGuard=false;return} const my=++posSeq; let outIds=[],inIds=[]; try{ const[oh,ih,gd]=await Promise.all([ rpc(C,[{to:SLOW,data:"0x"+SEL_OUT+encAddr(account)},L]), rpc(C,[{to:SLOW,data:"0x"+SEL_IN+encAddr(account)},L]), rpc(C,[{to:SLOW,data:"0x"+SEL_GUARDIAN+encAddr(account)},L]).catch(()=>"0x")]); outIds=decUintArr(oh);inIds=decUintArr(ih); if(my===posSeq)slowGuard=w(gd,0)!==0n; }catch{return} if(my!==posSeq)return; const dirs=new Map(); for(const[t,d]of[...outIds.map(i=>[i,"out"]),...inIds.map(i=>[i,"in"])]){ const k=t.toString();dirs.set(k,(dirs.get(k)||"")+d)} const allKeys=[...dirs.keys()].sort((a,b)=>a.length-b.length||(a<b?1:-1)); const keys=allKeys.slice(0,posShown); const more=allKeys.length-keys.length; const ps=[]; for(let i=0;i<keys.length;i+=10){ ps.push(...await Promise.all(keys.slice(i,i+10).map(k=> rpc(C,[{to:SLOW,data:"0x"+SEL_PENDING+encUint(BigInt(k))},L]).catch(()=>0)))); if(my!==posSeq)return; } if(my!==posSeq)return; const rows=[],settled=[]; keys.forEach((k,i)=>{const p=ps[i];if(!p)return; const ts=w(p,0); if(ts===0n){if(dirs.get(k).includes("out"))settled.push(k);return} rows.push({tid:BigInt(k),dir:dirs.get(k),ts,id:w(p,3),amount:w(p,4)})}); const stale=[]; for(let i=0;i<settled.length;i+=10){ const ts=await Promise.all(settled.slice(i,i+10).map(k=> rpc(C,[{to:SLOW_GATE,data:"0x"+SEL_TIPS+encUint(BigInt(k))},L]).catch(()=>0))); if(my!==posSeq)return; ts.forEach((t,j)=>{if(t&&w(t,0)>0n&&idTok(w(t,1))===account.toLowerCase()) stale.push({tid:settled[i+j],amount:w(t,0)})}); } const now=BigInt(Math.floor(Date.now()/1e3)); const ready=rows.filter(r=>r.dir.includes("in")&&now>=r.ts+idDelay(r.id)).length; tabSend.textContent=ready?`Send (${ready}${more?"+":""})`:"Send"; if(tab!=="send"||!(rows.length||stale.length)){pos.innerHTML="";return} rows.sort((a,b)=>Number((a.ts+idDelay(a.id))-(b.ts+idDelay(b.id)))); const metas=await Promise.all(rows.map(r=>tokMeta(idTok(r.id)))); if(my!==posSeq)return; let html=`<div class="ph">Time-locked</div>`; rows.forEach((r,n)=>{ const m=metas[n],exp=r.ts+idDelay(r.id),left=Number(exp-now); const ripe=now>=exp; const amt=`${trimAmt(r.amount,m.dec)} ${m.sym}`; const io=r.dir.includes("in"),oo=r.dir.includes("out"); const held=`data-t="${r.tid}" data-i="${r.id}" data-m="${r.amount}"`; const cb=oo&&ripe&&now>=exp+GRACE; let act,note; if(cb)note="Unclaimed past grace · recoverable"; else if(ripe)note=io?"Ready to claim":"Matured · recipient can claim, you in "+rel(Number(exp+GRACE-now)); else note=(io&&!oo?"Arrives in ":"Reversible · sends in ")+rel(left); if(io&&ripe)act=`<button data-a="c" data-t="${r.tid}">${slowGuard?"Unlock":"Claim"}</button>`; else if(oo&&!ripe)act=`<button data-a="r" ${held}>Reverse</button>`; else if(cb)act=`<button data-a="k" ${held}>Clawback</button>`; else act=`<button disabled>${io?"In":"Out"}</button>`; html+=`<div class="p"><span><b>${oo&&io?"↺":oo?"→":"←"} ${amt}</b><i>${note}</i></span>${act}</div>`; }); stale.forEach(s=>{html+=`<div class="p"><span><b>↩ ${trimAmt(s.amount,18)} ETH</b>` +`<i>Keeper tip on a settled transfer · unspent</i></span>` +`<button data-a="t" data-t="${s.tid}">Reclaim tip</button></div>`}); if(more)html+=`<div class="p" style="justify-content:center"><button data-a="m">Show ${Math.min(more,POSPAGE)} more of ${more}</button></div>`; pos.innerHTML=html; } pos.addEventListener("click",async e=>{ const m=e.target.closest('button[data-a="m"]'); if(m){posShown+=POSPAGE;m.disabled=true;loadPos();return} const b=e.target.closest("button[data-t]"); if(!b)return; b.disabled=true; ++busy; try{ await checkWallet(); const a=b.dataset.a; if(a==="t"){if(!await refundTipBestEffort(b.dataset.t,"Tip"))throw Error("the tip is no longer refundable"); refreshBalance();loadPos();return} const back=a==="r"||a==="k"; let data="0x"+(a==="r"?SEL_REVERSE:a==="k"?SEL_CLAWBACK:slowGuard?SEL_UNLOCK:SEL_CLAIM)+encUint(BigInt(b.dataset.t)); if(back&&!slowGuard)data=encCalls([data,"0x"+SEL_WITHDRAWFROM+encAddr(account)+encAddr(account) +encUint(BigInt(b.dataset.i))+encUint(BigInt(b.dataset.m))]); const txReq={from:account,to:SLOW,data,value:"0x0"}; await rpc(C,[txReq,L]); await settle(await rpc(S,[txReq])); if(back)await refundTipBestEffort(b.dataset.t,a==="r"?"Reversed":"Recovered"); refreshBalance();loadPos(); }catch(x){err(x);b.disabled=false} finally{--busy} }); async function refundTipBestEffort(tid,verb="Reversed"){ try{ const t=await rpc(C,[{to:SLOW_GATE,data:"0x"+SEL_TIPS+encUint(BigInt(tid))},L]); if(w(t,0)===0n)return false; const req={from:account,to:SLOW_GATE,data:"0x"+SEL_REFUNDTIP+encUint(BigInt(tid)),value:"0x0"}; await rpc(C,[req,L]); stat.textContent=verb+" · reclaiming keeper tip..."; await settle(await rpc(S,[req])); stat.textContent=verb+" · keeper tip refunded"; return true; }catch{return false} } const rcErr=v=>isAddr(v) ?(/^0x0{40}$/i.test(v)?"Recipient cannot be the zero address" :!ckAddr(v)?"That address fails its checksum — a character is wrong. Paste it again.":"") :!/\.(g?wei|eth)$/i.test(v)?"Recipient must be an address or a .wei / .gwei / .eth name" :!nameOk(v.toLowerCase())?"That name uses characters this page cannot resolve.":""; const rcvOf=async(v,fresh)=>{ if(isAddr(v))return /^0x0{40}$/i.test(v)||!ckAddr(v)?"":v; if(!/\.(g?wei|eth)$/i.test(v))return ""; const n=v.toLowerCase(); if(!nameOk(n))return ""; let a=""; try{a=fresh?await nameFwd(n):await nameFwdCached(n)} catch(e){if(e&&e.offchain)throw e} return !isAddr(a)||/^0x0{40}$/i.test(a)?"":a; }; async function updateSend(){ const my=++sSeq; sendReady=null; const f=TOKENS[fromSel.value]; const v=rc.value.trim(); let amount=0n,amtErr=""; const q=amt.value.trim(); try{amount=parseUnits(q,f.dec)} catch(e){if(q&&!/\.$/.test(q))amtErr=/decimals/.test(e.message)?`${f.sym} has ${f.dec} decimals`:"Invalid amount"} if(!v){rc.classList.remove("bad");rcvEl.textContent="";stat.textContent=amtErr;render();return} const bad=rcErr(v); if(bad){rc.classList.add("bad");rcvEl.textContent=""; stat.textContent=bad;render();return} if(!/^0x/.test(v))rcvEl.textContent="resolving..."; let to="",toErr=""; try{to=await rcvOf(v)}catch(e){toErr=e&&e.offchain?e.message:""} if(my!==sSeq)return; if(!to){rc.classList.add("bad");rcvEl.textContent=""; stat.textContent=toErr||"Name not registered";render();return} rc.classList.remove("bad"); rcvEl.textContent=to; stat.textContent=amtErr; if(amount>0n)sendReady={to,amount,token:f,label:/^0x/.test(v)?to.slice(0,6)+"\u2026"+to.slice(-4):v}; render(); } const syncNftUI=()=>{ const anyNft=tab==="book"&&(isNft(fromSel.value)||isNft(toSel.value)); nftIdL.classList.toggle("hide",!anyNft); if(!anyNft)nftId.value=""; nftId.placeholder=isNft(fromSel.value)?"Which one you are selling":"Any from collection"; syncOrderType(); showFloor(); }; const setTab=t=>{ if(tab==="send")sendD=dly.value;else if(tab==="book")bookD=dly.value; if(t!=="swap"&&lqMode)lqSet(false); if(t!=="swap"&&lnMode)lnSet(false); lq.classList.toggle("hide",t!=="swap"); ln.classList.toggle("hide",t!=="swap"); if(t!==tab){rc.value="";rc.classList.remove("bad");rcvEl.textContent=""} tab=t;last=null;sendReady=null;stat.textContent="";posShown=POSPAGE; const send=t==="send",bk=t==="book"; tabSwap.classList.toggle("on",t==="swap"); tabSend.classList.toggle("on",send); tabBook.classList.toggle("on",bk); for(const[el,on]of[[tabSwap,t==="swap"],[tabSend,send],[tabBook,bk]])el.setAttribute("aria-selected",on?"true":"false"); for(const el of[rcvPanel,flip])el.classList.toggle("hide",send); if(!bk)for(const[sel,other]of[[fromSel,toSel],[toSel,fromSel]]){ if(!isNft(sel.value))continue; const alt=TOKENS.findIndex((t,i)=>t.std!=="nft"&&String(i)!==other.value); if(alt>=0)sel.value=String(alt); } if(!send&&fromSel.value===toSel.value){ const alt=TOKENS.findIndex((t,i)=>String(i)!==fromSel.value&&(bk||t.std!=="nft")); if(alt>=0)toSel.value=String(alt); } syncDisabled(); syncNftUI(); slipL.classList.toggle("hide",t!=="swap"); dlyL.classList.toggle("hide",t==="swap"); kindL.classList.toggle("hide",!bk); ethModeL.classList.toggle("hide",!bk); dlyL.firstChild.nodeValue=bk?"Expires":"Time lock"; dly.options[0].textContent=bk?"Never":"Instant"; if(send)dly.value=sendD;else if(bk)dly.value=bookD; payL.textContent=send?"Amount":bk?"You sell":"You pay"; rcvHdr.textContent=bk&&kind.value==="dutch"?"Start ask total":bk?"You want":"You receive"; rc.placeholder=send?"Recipient \u2014 0x or .eth/.(g)wei" :bk?"Private to (optional) \u2014 0x or .eth/.(g)wei" :"Recipient (optional) \u2014 0x or .eth/.(g)wei"; syncOrderType();syncChart();syncTip(); loadPos();loadBook(); outAmt.value="";mode="in"; update(); }; dly.onchange=()=>{if(tab==="book")bookD=dly.value;else sendD=dly.value;syncTip();render()}; tipCk.onchange=()=>{syncTip();render()}; fill.onchange=render; dRest.onchange=render; const syncOrderType=()=>{ const dutch=tab==="book"&&kind.value==="dutch"; const bid=tab==="book"&&kind.value==="floor"; const sched=dutch||bid; const nftLeg=tab==="book"&&(isNft(fromSel.value)||isNft(toSel.value)); dRestL.classList.toggle("hide",!dutch); const collectionBid=nftLeg&&isNft(toSel.value)&&!nftId.value.trim(); floorL.classList.toggle("hide",!sched); floorL.firstChild.nodeValue=bid?"Opening bid":"Floor total"; if(tab==="book"){ dlyL.firstChild.nodeValue=dutch?"Decays over":bid||collectionBid?"Bid window":"Expires"; dly.options[0].textContent=sched||collectionBid?"—":"Never"; } fillL.classList.toggle("hide",tab!=="book"||sched||nftLeg); rc.classList.toggle("hide",sched); rcvEl.classList.toggle("hide",sched); if((sched||collectionBid)&&+dly.value===0){dly.value="86400";if(tab==="book")bookD=dly.value} if(tab==="book"){ payL.textContent=bid?"You escrow (max)":"You sell"; rcvHdr.textContent=bid?"You want to buy":dutch?"Start ask total":"You want"; } }; kind.onchange=()=>{syncOrderType();seedFloor();render()}; let floorTouched=false; const dutchEnds=()=>{ const rest=BigInt(dRest.value||0); if(!rest)return 0n; return BigInt(Math.floor(Date.now()/1e3))+BigInt(dly.value||0)+rest; }; const seedFloor=()=>{ if(floorTouched||kind.value!=="dutch")return; const t=TOKENS[toSel.value]; const raw=outAmt.value.trim(); if(!raw||!t)return; let start;try{start=parseUnits(raw,t.dec)}catch{return} if(!start)return; floorAmt.value=trimAmt(start/2n,t.dec); fitFont(floorAmt); }; floorAmt.addEventListener("input",()=>{ floorTouched=true; fitFont(floorAmt);render(); }); tabBook.onclick=()=>setTab("book"); setInterval(()=>{if(account&&!document.hidden&&!busy){refreshBalance();loadPos();if(tab==="book")loadBook()}},3e4); setInterval(()=>{ if(tab!=="swap"||!account||document.hidden||!last||swap.disabled||busy)return; if(Date.now()>=last.exp-5000&&autoQ++<AUTO_Q_MAX)update(1); },5e3); document.addEventListener("visibilitychange",()=>{ if(document.hidden||!account||busy)return; autoQ=0; refreshBalance();loadPos(); if(tab==="book")loadBook(); if(tab==="swap"&&!swap.disabled&&last&&Date.now()>=last.exp-5000)update(); }); let lqSeq=0,lqRows=[]; const SEL_ISPOOL="5b16ebb7",SEL_POOLFOR="83bd1387",SEL_TOTALSUPPLY="18160ddd",SEL_CREATE_SEED="7163352a"; const bandPrice=(sqrtP,d0,d1)=>{const r=Number(sqrtP)/1e18;return r*r*10**(d0-d1)}; const lqBar=(lo,hi,at)=>{ if(!(lo>0)||!(hi>lo)||!(at>0))return ""; const f=(Math.log(at)-Math.log(lo))/(Math.log(hi)-Math.log(lo)); const out=f<0||f>1; const pct=(Math.max(0,Math.min(1,f))*100).toFixed(1); return `<div class="lqbar${out?" out":""}" style="--at:${pct}%" aria-hidden="true"><b></b><i></i></div>`; }; const fmtPx=v=>!isFinite(v)||v<=0?"—":v>=1000?v.toLocaleString(undefined,{maximumFractionDigits:0}) :v>=1?v.toFixed(4):v.toPrecision(3); const lqSyncAmounts=()=>{ const show=!lqMode||!!lqList.querySelector(".lqnew"); for(const el of [amtRow,outRow])if(el)el.classList.toggle("hide",!show); }; function lqSet(on){ lqMode=on; if(on&&lnMode)lnSet(false); lq.setAttribute("aria-pressed",on?"true":"false"); lq.classList.toggle("on",on); for(const el of [rate,flip,swap]) if(el)el.classList.toggle("hide",on); rc.value="";rc.classList.remove("bad");rcvEl.textContent=""; rc.placeholder=on?"LP shares to (optional) — 0x or .eth/.(g)wei" :"Recipient (optional) — 0x or .eth/.(g)wei"; if(on){amt.value="";outAmt.value="";fitFont(amt);fitFont(outAmt)} for(const el of [amtRow,outRow])if(el)el.classList.toggle("hide",on); rcvHdr.textContent=on?"and":"You receive"; payL.textContent=on?"Liquidity for":"You pay"; lqPanel.classList.toggle("hide",!on); refreshBalance1(); if(on)loadLq();else{lqRows=[];lqList.innerHTML=""} lqSyncAmounts(); } const fullBand=(lo,hi)=>lo>0&&isFinite(hi)&&hi/lo>=9.9e5; function lqRow(o,t0,t1){ const px=bandPrice(o.px,t0.dec,t1.dec),lo=bandPrice(o.sl,t0.dec,t1.dec),hi=bandPrice(o.sh,t0.dec,t1.dec); const inRange=px>=lo&&px<=hi&&o.liq>0n; const mine=o.shares>0n; const p=`data-pool="${o.pool}"`; return `<div class="lqrow${inRange?"":" out"}" ${p}>` +`<div class="lqband"><b>${fmtPx(lo)} – ${fmtPx(hi)}</b> <span class="lqfee">${(Number(o.fee)/10000).toFixed(2)}%</span>` +(o.hook!==ZERO?` <span class="lqtag" title="Hooked: may add a surcharge, and cannot be used in a clamped route">hook</span>`:"") +(fullBand(lo,hi) ?` <span class="lqtag" title="The widest band this page creates: 1000x either side of the opening price, a millionfold span. Price is effectively always inside it, so the position behaves like an ordinary constant-product pool. It is still bounded - no band here is infinite.">full range</span>`:"") +(inRange?"":` <span class="lqtag">out of range</span>`)+`</div>` +lqBar(lo,hi,px) +`<div class="lqmeta">${fmtPx(px)} ${t1.sym} per ${t0.sym}` +` · holds ${trimAmt(o.r0,t0.dec)} ${t0.sym} + ${trimAmt(o.r1,t1.dec)} ${t1.sym}` +` <button class="lqbtn" data-act="a" ${p}>Add</button></div>` +(mine?`<div class="lqmine">yours: ${trimAmt(o.mine0,t0.dec)} ${t0.sym} + ${trimAmt(o.mine1,t1.dec)} ${t1.sym}` +` <button class="lqbtn" data-act="w" ${p}>Withdraw all</button></div>`:"") +`<div class="lqadd hide">` +(o.liq>0n ?`<label class="lqzap"><input type="checkbox" class="lqz"> One-sided — deposit just ${t0.sym} or just ${t1.sym}</label>`:"") +`<input
0xacfba7ce…4082·#25,751,349·0xa333f3b8…cc8952
rs){ const want=[...new Set(addrs.map(a=>a.toLowerCase()))] .filter(a=>a!==ZERO&&!known(a)&&logoCache[a]===undefined); if(!want.length)return; let raw=[]; if(TOKENLIST!==ZERO)raw=await mc3Deep(want.map(a=>({to:TOKENLIST,data:"0x"+SEL_LOGOOF+encAddr(a)}))).catch(()=>[]); want.forEach((a,i)=>{let u="";try{if(raw[i])u=safeUrl(decodeString(raw[i]))}catch{}logoCache[a]=u}); const self=want.filter(a=>!logoCache[a]); if(!self.length)return; const cu=await mc3Deep(self.map(a=>({to:a,data:"0x"+SEL_CONTRACTURI}))).catch(()=>[]); self.forEach((a,i)=>{try{ const u=imgFromContractURI(cu[i]?decodeString(cu[i]):""); if(u)logoCache[a]=u; }catch{}}); } async function leg(addr,amt,isNft,meta){ const k=known(addr); if(isNft)return{ic:genIcon("#"),txt:`#${amt}`,ok:0,link:escan(addr,amt)}; const raw=meta&&meta.sym?meta:await tokMeta(addr),m={...raw,sym:safeSym(raw.sym)}; const lg=k?"":logoCache[addr.toLowerCase()]; const ic=k?sm(k.icon):lg?`<img src="${lg}" width="15" height="15">`:genIcon(m.sym); return{ic,txt:`${trimAmt(amt,m.dec)} ${k?safeSym(k.sym):'“'+m.sym+'”'}`,ok:!!k,link:addr===ZERO?"ETH":escan(addr)}; } let floorSeq=0; nftId.addEventListener("input",()=>{syncOrderType();showFloor();render()}); async function showFloor(){ const el=document.getElementById("floorNote");if(!el)return; const side=isNft(fromSel.value)?fromSel.value:isNft(toSel.value)?toSel.value:null; const live=tab==="book"&&side!==null&&!nftId.value.trim(); if(!live){el.classList.add("hide");el.textContent="";return} const seq=++floorSeq,t=TOKENS[side]; el.classList.remove("hide");el.textContent="Checking floor bids…"; const rows=await collectionFloor(t.addr).catch(()=>[]); if(seq!==floorSeq)return; if(!rows.length){el.textContent="No standing bids on this collection.";return} const best=rows[0],q=known(best.quote); el.textContent=`${rows.length} collection bid${rows.length>1?"s":""} · best pays ` +`${trimAmt(best.proceeds,q?q.dec:18)} ${q?q.sym:"?"} for any ${t.sym}`; } function decBidRows(hex){ const out=[]; try{ const h=strip0x(hex),bytes=h.length/2; const word=b=>{if(b<0||b+32>bytes)throw 0;return BigInt("0x"+h.slice(b*2,(b+32)*2))}; const num=b=>{const v=word(b);if(v>0xffffffffffffffffn)throw 0;return Number(v)}; const arr=num(0); if(arr<64||arr+32>bytes)throw 0; const n=num(arr),base=arr+32; if(n>256||base+n*32>bytes)throw 0; for(let i=0;i<n;i++){ try{ const row=base+num(base+i*32); if(row+17*32>bytes)throw 0; const f=k=>word(row+k*32); const txt=k=>{const p=row+num(row+k*32),len=num(p); if(p+32+len>bytes)throw 0; return safeSym(new TextDecoder().decode(hexToBytes(h.slice((p+32)*2,(p+32+Math.min(len,1024))*2))))}; const dec=k=>{const v=num(row+k*32);return v>0&&v<=37?v-1:null}; out.push({ id:f(0), bidder:"0x"+h.slice((row+1*32+12)*2,(row+2*32)*2), token:"0x"+h.slice((row+2*32+12)*2,(row+3*32)*2), quote:"0x"+h.slice((row+3*32+12)*2,(row+4*32)*2), isNFT:f(4)!==0n, anyId:f(5)!==0n, remaining:f(7), initial:f(8), price:f(9), proceeds:f(10), expiry:f(12), tDec:dec(13),qDec:dec(14), tSym:txt(15),qSym:txt(16), }); }catch{continue} } return out; }catch{return out} } async function collectionFloor(collection){ if(FLOORVIEW===ZERO||FLOOR===ZERO)return[]; const data="0x"+SEL_COLL_BIDS+encAddr(FLOOR)+encAddr(collection)+encAddr(ZERO) +encUint(0)+encUint(16)+encUint(256); const raw=await rpc(C,[{to:FLOORVIEW,data},L]).catch(()=>null); if(!raw)return[]; const rows=decBidRows(raw); return rows.filter(r=>r.anyId&&r.remaining>0n) .sort((a,b)=>{const l=b.proceeds*a.remaining,r=a.proceeds*b.remaining;return l>r?1:l<r?-1:0}); } async function floorCandidates(tokenIn,tokenOut,block){ const bin=tokenIn===ZERO?WETH:tokenIn,bout=tokenOut===ZERO?WETH:tokenOut; const data="0x"+SEL_FLOOR_CANDS+encAddr(FLOOR)+encAddr(bin)+encAddr(bout) +encUint(0)+encUint(64)+encUint(512); const raw=await rpc(C,[{to:FLOORVIEW,data},block]).catch(()=>null); if(!raw)return[]; const out=[]; for(const b of decBidRows(raw)){ if(b.isNFT||b.remaining<=0n||b.initial<=0n||b.price<=0n||b.proceeds<=0n)continue; if(b.token.toLowerCase()!==bin.toLowerCase())continue; if(b.quote.toLowerCase()!==bout.toLowerCase())continue; out.push({ id:b.id,board:FLOOR,floor:1,v2:0,dutch:0, maker:b.bidder,pf:1,exp:b.expiry,nA:0,nB:0,cp:ZERO, tA:b.quote,aA:b.proceeds,sA:b.qSym,dA:b.qDec, tB:b.token,aB:b.remaining,sB:b.tSym,dB:b.tDec, price:b.price,initial:b.initial, }); } return out; } function floorGet(o,pay){return o.price*pay/o.initial} function floorPay(o,want){ if(want<=0n)return 0n; const g=(want*o.initial+o.price-1n)/o.price; return g>o.aB?o.aB:g; } function rowGet(o,pay){return o.floor?floorGet(o,pay):o.aA*pay/o.aB} function decViewPage(hex){ const h=strip0x(hex),bytes=h.length/2; const word=b=>{if(b<0||b+32>bytes)throw Error("bad lens page");return BigInt("0x"+h.slice(b*2,(b+32)*2))}; const addr=(b)=>"0x"+h.slice((b+12)*2,(b+32)*2); const arr=Number(word(0)),next=word(32); if(!Number.isSafeInteger(arr)||arr<64||arr+32>bytes)throw Error("bad lens offset"); const n=Number(word(arr)),head=arr+32; if(!Number.isSafeInteger(n)||n>256||head+n*32>bytes)throw Error("bad lens length"); const text=(base,k)=>{const off=Number(word(base+k*32)),p=base+off,len=Number(word(p)); if(!Number.isSafeInteger(off)||!Number.isSafeInteger(len)||p+32+len>bytes)throw Error("bad lens string"); const cap=Math.min(len,1024),a=new Uint8Array(cap); for(let i=0;i<cap;i++)a[i]=parseInt(h.slice((p+32+i)*2,(p+33+i)*2),16); return new TextDecoder().decode(a)}; const rows=[]; const dec=v=>{const d=Number(v);return Number.isInteger(d)&&d>=0&&d<=36?d:null}; for(let i=0;i<n;i++){ try{ const off=Number(word(head+i*32)),b=head+off; if(!Number.isSafeInteger(off)||b+16*32>bytes)throw Error("bad lens row"); rows.push({id:word(b),maker:addr(b+32),pf:word(b+64)===1n,exp:word(b+96), nA:word(b+128)===1n,nB:word(b+160)===1n,cp:addr(b+192), tA:addr(b+224),aA:word(b+256),sA:safeSym(text(b,9)),dA:dec(word(b+320)), tB:addr(b+352),aB:word(b+384),sB:safeSym(text(b,13)),dB:dec(word(b+448)),board:addr(b+480)}); }catch{continue} } return{rows,next}; } const betterRate=(a,b)=>a.aA*b.aB>b.aA*a.aB; const MAX_BOOK_LEGS=32,AON_PAIR_SEEDS=24; function planBookExactIn(rows,amountIn,ammOut,ammIn){ if(!amountIn||!ammIn)return null; let c=rows.filter(o=>!o.nA&&!o.nB&&o.aA&&(o.aB||o.dutch)&&(o.pf||o.aB<=amountIn) &&!(account&&o.maker&&o.maker.toLowerCase()===account.toLowerCase())); const candidateCapped=c.length>256; const aonCount=c.reduce((n,o)=>n+!o.pf,0); const aonBounded=aonCount>=3; c.sort((a,b)=>betterRate(a,b)?-1:betterRate(b,a)?1:0); if(c.length>256)c.length=256; const build=(seed=-1,seed2=-1)=>{ let rem=amountIn,bin=0n,bout=0n,fills=[],legCapped=false; const take=o=>{ if(o.aA*ammIn<=ammOut*o.aB)return; if(!o.pf&&o.aB>rem)return; if(fills.length>=MAX_BOOK_LEGS){legCapped=true;return} if(o.aB===0n){ fills.push({id:o.id,board:o.board,pay:0n,get:o.aA,part:false}); bout+=o.aA;return; } const pay=o.aB<rem?o.aB:rem; if(!pay)return; const get=o.pf?(pay===o.aB?o.aA:rowGet(o,pay)):o.aA; if(!get)return; fills.push({id:o.id,board:o.board,pay,get,part:o.pf&&pay<o.aB}); rem-=pay;bin+=pay;bout+=get; }; if(seed>=0)take(c[seed]); if(seed2>=0)take(c[seed2]); for(let i=0;i<c.length;i++)if(i!==seed&&i!==seed2)take(c[i]); return{fills,bookIn:bin,bookOut:bout,ammIn:rem,score:bout+rem*ammOut/ammIn, bounded:candidateCapped||aonBounded||legCapped}; }; let best=build(-1); for(let i=0;i<c.length;i++)if(!c[i].pf){ const p=build(i);if(p.score>best.score)best=p; } const aon=[];for(let i=0;i<c.length&&aon.length<AON_PAIR_SEEDS;i++)if(!c[i].pf)aon.push(i); for(let i=0;i<aon.length;i++)for(let j=i+1;j<aon.length;j++){ const p=build(aon[i],aon[j]);if(p.score>best.score)best=p; } return best.fills.length?best:null; } const cheaperRate=(a,b)=>a.aB*b.aA<b.aB*a.aA; function planBookExactOut(rows,amountOut,ammIn,ammOut){ if(!amountOut||!ammOut)return null; let c=rows.filter(o=>!o.nA&&!o.nB&&o.aA&&(o.aB||o.dutch)&&(o.pf||o.aA<=amountOut) &&!(account&&o.maker&&o.maker.toLowerCase()===account.toLowerCase())); const candidateCapped=c.length>256; const aonCount=c.reduce((n,o)=>n+!o.pf,0); const aonBounded=aonCount>=3; c.sort((a,b)=>cheaperRate(a,b)?-1:cheaperRate(b,a)?1:0); if(c.length>256)c.length=256; const build=(seed=-1,seed2=-1)=>{ let left=amountOut,bin=0n,bout=0n,fills=[],legCapped=false; const take=o=>{ if(o.aB*ammOut>=ammIn*o.aA)return; if(!o.pf&&o.aA>left)return; if(fills.length>=MAX_BOOK_LEGS){legCapped=true;return} const want=o.aA<left?o.aA:left; let pay,get; if(o.aB===0n){pay=0n;get=want} else if(want===o.aA){pay=o.aB;get=o.aA} else if(o.floor){ pay=floorPay(o,want); get=floorGet(o,pay); } else{ pay=(want*o.aB+o.aA-1n)/o.aA; if(pay>o.aB)pay=o.aB; get=pay*o.aA/o.aB; } if(!get)return; fills.push({id:o.id,board:o.board,pay,get,part:want!==o.aA}); bin+=pay;bout+=get;left-=get<left?get:left; }; if(seed>=0)take(c[seed]); if(seed2>=0)take(c[seed2]); for(let i=0;i<c.length&&left;i++)if(i!==seed&&i!==seed2)take(c[i]); return{fills,bookIn:bin,bookOut:bout,ammOut:left,score:bin+left*ammIn/ammOut, bounded:candidateCapped||aonBounded||legCapped}; }; let best=build(-1); for(let i=0;i<c.length;i++)if(!c[i].pf){ const p=build(i);if(p.score<best.score)best=p; } const aon=[];for(let i=0;i<c.length&&aon.length<AON_PAIR_SEEDS;i++)if(!c[i].pf)aon.push(i); for(let i=0;i<aon.length;i++)for(let j=i+1;j<aon.length;j++){ const p=build(aon[i],aon[j]);if(p.score<best.score)best=p; } return best.fills.length?best:null; } function encFillPlan(fills,tokenIn,tokenOut,recipient,refundTo,deadline){ let tail=encUint(fills.length); for(const f of fills)tail+=encUint(f.id)+encAddr(f.board)+encUint(f.pay)+encUint(f.get)+encBool(f.part); return"0x"+SEL_FILLPLAN+encAddr(tokenIn)+encAddr(tokenOut)+encAddr(recipient)+encAddr(refundTo) +encUint(deadline)+encUint(192)+tail; } function encFillPlanAndSwap(fills,tokenIn,tokenOut,recipient,refundTo,deadline,ammIn,ammData){ let ft=encUint(fills.length); for(const f of fills)ft+=encUint(f.id)+encAddr(f.board)+encUint(f.pay)+encUint(f.get)+encBool(f.part); const d=strip0x(ammData||"0x"),dt=encUint(d.length/2)+d.padEnd(Math.ceil(d.length/64)*64,"0"); return"0x"+SEL_FILLPLAN_SWAP+encAddr(tokenIn)+encAddr(tokenOut)+encAddr(recipient)+encAddr(refundTo) +encUint(deadline)+encUint(256)+encUint(ammIn)+encUint(256+ft.length/2)+ft+dt; } function encSnwap(tokenIn,amountIn,recipient,tokenOut,minOut,executor,data){ const d=strip0x(data),body=encUint(d.length/2)+d.padEnd(Math.ceil(d.length/64)*64,"0"); return"0x"+SEL_SNWAP+encAddr(tokenIn)+encUint(amountIn)+encAddr(recipient)+encAddr(tokenOut) +encUint(minOut)+encAddr(executor)+encUint(224)+body; } const encCheckpoint=(executor,token,recipient)=>encSnwap( ZERO,0n,recipient,ZERO,0n,executor,"0x"+SEL_CHECKPOINT+encAddr(token) ); const encSweep=(token,amount,to)=>"0x"+SEL_SWEEP+encAddr(token)+encUint(0)+encUint(amount)+encAddr(to); function encPermit2Hybrid(ammData,fills,tokenIn,tokenOut,recipient,refundTo,deadline,bookIn,bookOut){ const bookData=encFillPlan(fills,tokenIn,tokenOut,recipient,refundTo,deadline); const steps=[encCheckpoint(SWAPBOL,tokenIn,refundTo)]; if(bookIn)steps.push(encSweep(tokenIn,bookIn,SWAPBOL)); steps.push(encSnwap(ZERO,0n,recipient,tokenOut,bookOut,SWAPBOL,bookData)); if(ammData&&ammData!=="0x")steps.push(ammData); steps.push(encSweep(tokenIn,0n,refundTo)); return encCalls(steps); } async function swapCandidates(tokenIn,tokenOut,block){ const key=tokenIn.toLowerCase()+":"+tokenOut.toLowerCase(),old=candCache[key]; if(old&&Date.now()-old.at<15000&&old.block===block){ old.rows.incomplete=old.incomplete;old.rows.unavailable=old.unavailable; return old.rows; } const snapshotAt=Date.now(); const boardIn=tokenIn===ZERO?WETH:tokenIn; const boardOut=tokenOut===ZERO?WETH:tokenOut; const [lc,le]=await Promise.all([ rpc("eth_getCode",[SBVIEW,block]),rpc("eth_getCode",[SWAPBOL,block]) ]); if(lc.length<5||le.length<5){ const rows=[];rows.unavailable=true;return rows; } const specs=[...BOARDS]; if(DUTCH!==ZERO&&(await rpc("eth_getCode",[DUTCH,block])).length>4){ specs.push({a:DUTCH,dutch:1,ti:tokenIn}); if(tokenIn===ZERO)specs.push({a:DUTCH,dutch:1,ti:WETH}); } const groups=await Promise.all(specs.map(async b=>{ let cursor=0n,out=[],turns=0,incomplete=false,failed=false; try{do{ const ti=b.dutch?b.ti:boardIn,to=boardOut; const data="0x"+(b.dutch?SEL_DUTCH_CANDS:SEL_CANDS)+encAddr(b.a) +(b.dutch?"":encBool(b.v2))+encAddr(ti)+encAddr(to)+encAddr(SWAPBOL) +encUint(cursor)+encUint(512); const p=decViewPage(await rpc(C,[{to:SBVIEW,data},block])); out.push(...p.rows.map(r=>({...r,v2:b.v2||0,dutch:b.dutch||0}))); cursor=p.next; ++turns; }while(cursor&&turns<64)}catch{failed=true;incomplete=true} incomplete=incomplete||!!cursor; return{out,incomplete,failed}; })); let rows=groups.flatMap(g=>g.out); if(tokenIn===ZERO&&tokenOut.toLowerCase()===WETH.toLowerCase()){ rows=rows.filter(r=>r.dutch&&r.tA.toLowerCase()===WETH.toLowerCase()&&r.tB===ZERO); } else if(await floorRoutable(block)){ rows=rows.concat(await floorCandidates(tokenIn,tokenOut,block).catch(()=>[])); } const incomplete=groups.some(g=>g.incomplete); const unavailable=groups.every(g=>g.failed&&!g.out.length); rows.incomplete=incomplete; rows.unavailable=unavailable; const current=candCache[key],bn=BigInt(block),cn=current?BigInt(current.block):-1n; if(!current||bn>cn||(bn===cn&&snapshotAt>=current.at)){ candCache[key]={at:snapshotAt,block,rows,incomplete,unavailable}; } for(const k in candCache)if(k!==key&&snapshotAt-candCache[k].at>60000)delete candCache[k]; return rows; } async function lensBookRows(my){ if((await rpc("eth_getCode",[SBVIEW,L])).length<5)return null; const specs=[...BOARDS]; if(DUTCH!==ZERO&&(await rpc("eth_getCode",[DUTCH,L])).length>4)specs.push({a:DUTCH,dutch:1}); const pages=await Promise.all(specs.map(async b=>{ let cursor=0n,out=[],turns=0; do{ const data="0x"+(b.dutch?SEL_RECENT_DUTCH:SEL_RECENT)+encAddr(b.a) +(b.dutch?"":encBool(b.v2))+encUint(cursor)+encUint(80)+encUint(256); const p=decViewPage(await rpc(C,[{to:SBVIEW,data},L])); out.push(...p.rows.map(r=>({...r,v2:b.v2||0,dutch:b.dutch||0}))); cursor=p.next; }while(cursor&&out.length<160&&++turns<64&&my===bookSeq); return out; })); const rows=pages.flat(); if(FLOOR!==ZERO&&FLOORVIEW!==ZERO){ await floorRoutable(L); try{ const raw=await rpc(C,[{to:FLOORVIEW, data:"0x"+SEL_RECENT_FLOOR+encAddr(FLOOR)+encUint(0)+encUint(80)+encUint(256)},L]); for(const b of decBidRows(raw)){ if(b.remaining<=0n)continue; rows.push({ id:b.id,board:FLOOR,floor:1,v2:0,dutch:0, maker:b.bidder,pf:b.isNFT?0:1,exp:b.expiry, nA:0,nB:0,cp:ZERO, tA:b.quote,aA:b.proceeds,sA:b.qSym,dA:b.qDec, tB:b.token,aB:b.remaining,sB:b.tSym,dB:b.isNFT?0:b.tDec, price:b.price,initial:b.initial,anyId:b.anyId,isNFT:b.isNFT?1:0, }); } }catch{} } return rows; } async function loadBook(){ if(tab!=="book"){book.innerHTML="";bookRows=[];syncBook(0);return} const my=++bookSeq; const live=[]; for(const b of BOARDS){ let code="0x"; try{code=await rpc("eth_getCode",[b.a,L])}catch{} if(code.length>4)live.push(b); } if(my!==bookSeq)return; if(!live.length){ bookRows=[];bookLegs=[]; book.innerHTML='<h6><span>Orderbook</span></h6><div class="o"><span class="l"><i>No Swapboard deployed on this network yet.</i></span></div>'; syncBook(0);book.classList.remove("hide");return} let rows=null; try{rows=await lensBookRows(my)}catch{rows=null} if(my!==bookSeq)return; if(!rows)rows=[]; for(const b of rows.length?[]:live){ let total=0; try{total=Number(BigInt(await rpc(C,[{to:b.a,data:"0x"+SEL_NEXTID},L])))}catch{continue} if(!total)continue; const hi=total,lo=Math.max(0,hi-40); const ids=[];for(let i=hi-1;i>=lo;--i)ids.push(i); let raw=""; try{raw=await rpc(C,[{to:b.a,data:"0x"+SEL_GETORDERS+encUint(32)+encUint(ids.length)+ids.map(i=>encUint(i)).join("")},L])}catch{continue} if(my!==bookSeq)return; const h=strip0x(raw);if(!h)continue; const w=WORDS(b.v2),n=Number(BigInt("0x"+h.slice(64,128))); const at=(r,k)=>BigInt("0x"+h.slice(128+(r*w+k)*64,128+(r*w+k+1)*64)); const ad=(r,k)=>"0x"+h.slice(128+(r*w+k)*64+24,128+(r*w+k+1)*64); for(let r=0;r<n&&r<ids.length;++r){ if(at(r,1)!==1n)continue; const o=b.v2 ?{id:ids[r],board:b.a,v2:1,maker:ad(r,0),pf:at(r,2)===1n,exp:at(r,3), nA:at(r,4)===1n,nB:at(r,5)===1n,cp:ad(r,6), tA:ad(r,7),aA:at(r,8),tB:ad(r,9),aB:at(r,10)} :{id:ids[r],board:b.a,v2:0,maker:ad(r,0),pf:0,exp:0n,nA:0,nB:0, cp:ZERO,tA:ad(r,2),aA:at(r,3),tB:ad(r,4),aB:at(r,5)}; if(o.exp&&BigInt(Math.floor(Date.now()/1e3))>o.exp &&o.maker.toLowerCase()!==account.toLowerCase())continue; if(o.cp!==ZERO &&o.cp.toLowerCase()!==account.toLowerCase() &&o.maker.toLowerCase()!==account.toLowerCase())continue; rows.push(o); } } if(my!==bookSeq)return; const cutoff=BigInt(Math.floor(Date.now()/1e3)); rows=rows.filter(o=>{ const mine=o.maker.toLowerCase()===account.toLowerCase(); if(o.exp&&cutoff>o.exp&&!mine)return false; return o.cp===ZERO ||o.cp.toLowerCase()===account.toLowerCase() ||o.maker.toLowerCase()===account.toLowerCase(); }); const dutchRows=rows.filter(r=>r.dutch); const bidRows=rows.filter(r=>r.floor); const fillRows=rows.filter(r=>!r.dutch&&!r.floor&&r.v2&&r.pf&&!r.nA&&!r.nB); if(dutchRows.length||bidRows.length||fillRows.length){ const reqs=[ ...dutchRows.map(r=>({to:DUTCH,data:"0x"+SEL_DUTCH_LISTING+encUint(r.id)})), ...bidRows.map(r=>({to:FLOOR,data:"0x"+SEL_BIDS+encUint(r.id)})), ...fillRows.map(r=>({to:r.board,data:"0x"+SEL_INITIAL_B+encUint(r.id)})), ]; const raw=await mc3Deep(reqs); if(my!==bookSeq)return; let k=0; for(const r of dutchRows){const x=raw[k++];if(!x)continue; const h=strip0x(x),w=n=>BigInt("0x"+h.slice(n*64,(n+1)*64)); if(h.length>=640)Object.assign(r,{ start:w(2),duration:w(3),startPrice:w(5),endPrice:w(7),initial:w(8),remaining:w(9)}); } for(const r of bidRows){const x=raw[k++];if(!x)continue; const h=strip0x(x),w=n=>BigInt("0x"+h.slice(n*64,(n+1)*64)); if(h.length>=11*64)Object.assign(r,{start:w(2),duration:w(3),startPrice:w(5),endPrice:w(7)}); } for(const r of fillRows){const x=raw[k++];if(!x)continue; const h=strip0x(x);if(h.length<64)continue; const init=BigInt("0x"+h.slice(0,64)); if(init>r.aB)r.filled=(init-r.aB)*10000n/init; } } if(!rows.length){bookRows=[];bookLegs=[];book.innerHTML="";syncBook(0);return} await loadLogos(rows.flatMap(r=>[r.tA,r.tB])); if(my!==bookSeq)return; const legs=await Promise.all(rows.map(r=>Promise.all([ leg(r.tA,r.aA,r.nA,r.sA!=null&&r.dA!=null?{sym:r.sA,dec:r.dA}:null), leg(r.tB,r.aB,r.nB,r.sB!=null&&r.dB!=null?{sym:r.sB,dec:r.dB}:null) ]))); if(my!==bookSeq)return; bookRows=rows;bookLegs=legs; paintBook(); } let bookPairOnly=LS.bkp!=="0",bookType="all",bookLegs=[]; const sameAsset=(a,b)=>{ const w=x=>{x=(x||"").toLowerCase();return x===ZERO.toLowerCase()?WETH.toLowerCase():x}; return w(a)===w(b); }; const rowInPair=r=>{ const f=TOKENS[fromSel.value]?.addr,t=TOKENS[toSel.value]?.addr; if(!f||!t)return true; return (sameAsset(r.tA,f)&&sameAsset(r.tB,t))||(sameAsset(r.tA,t)&&sameAsset(r.tB,f)); }; const rowIsType=r=>bookType==="all" ||(bookType==="dutch"&&r.dutch) ||(bookType==="bid"&&r.floor) ||(bookType==="nft"&&(r.nA||r.nB||r.isNFT)) ||(bookType==="limit"&&!r.dutch&&!r.floor); function paintBook(){ const rows=bookRows,legs=bookLegs; if(!rows.length){book.innerHTML="";syncBook(0);return} let own="",rest="",nOwn=0,nRest=0,hidden=0; const now=BigInt(Math.floor(Date.now()/1e3)); rows.forEach((r,i)=>{ const[a,bg]=legs[i]||[]; if(!a||!bg)return; const mine=r.maker.toLowerCase()===account.toLowerCase(); const expired=!!(r.exp&&now>r.exp); const tags=(expired?'<span class="tg w" title="This order has lapsed and can no longer be filled, but its escrow is still held by the board. Reclaim returns it to you.">expired</span>':"") +(r.nA||r.nB||r.isNFT?'<span class="tg">NFT</span>':"") +(r.v2&&!r.pf&&!r.nA&&!r.nB?'<span class="tg">AON</span>':"") +(r.dutch?'<span class="tg">Dutch</span>':"") +(r.floor?'<span class="tg">Bid</span>':"") +(r.floor&&r.isNFT&&r.anyId?'<span class="tg">any id</span>':"") +(r.filled!==undefined&&r.filled>0n?`<span class="tg">${(Number(r.filled)/100).toFixed(0)}% filled</span>`:"") +(r.cp!==ZERO?'<span class="tg">private</span>':"") +(!a.ok||!bg.ok?'<a class="tg w" href="'+ZLIST_URL+'" target="_blank" rel="noopener"' +' title="Not on the staked token list. Its symbol and decimals are read straight from the contract' +' and nothing has vouched for it - check the address before filling. Stake on zList to get a real' +' token listed.">unverified</a>':""); let when=expired?" · lapsed "+rel(Number(now-r.exp))+" ago — escrow still held" :r.exp?" · "+rel(Number(r.exp-now))+" left":""; if(!expired&&r.dutch&&r.duration){ const end=r.start+r.duration; const floor=r.initial?(r.endPrice*r.remaining+r.initial-1n)/r.initial:0n; when=now>=end?` · floor reached: ${trimAmt(floor,r.dB)} ${r.sB}` :` · floor ${trimAmt(floor,r.dB)} ${r.sB} in ${rel(Number(end-now))}`; } if(!expired&&r.floor&&r.duration){ const end=r.start+r.duration; when=now>=end?" · window closed" :` · climbs to ${trimAmt(r.initial?r.endPrice*r.aB/r.initial:r.endPrice,r.dA)} ${r.sA} in ${rel(Number(end-now))}`; } const dset=`data-i="${r.id}" data-b="${r.board}" data-v="${r.v2}" data-d="${r.dutch||0}" data-f="${r.floor||0}"`; const takeable=!r.floor||(!r.isNFT&&floorReady); const act=mine?`<button data-x="c" ${dset}>${expired?"Reclaim":"Cancel"}</button>` :takeable?`<button data-x="f" ${dset}>${r.floor?"Sell into":"Fill"}</button>` :`<button disabled>${r.isNFT?"NFT bid":"Not routable"}</button>`; const card=r.board.toLowerCase()!==SB1.toLowerCase(); const row=`<div class="ow"><div class="o${card?" ins":""}"${card?` data-k="${r.board}:${r.id}"`:""}>` +`<span class="l">${a.ic}${bg.ic}<span><b>${a.txt} → ${bg.txt}</b>` +`<i>${a.link}${tags}${when}</i></span></span>${act}</div><div class="ic"></div></div>`; if(mine){own+=row;++nOwn;return} if(!(bookPairOnly?rowInPair(r):true)||!rowIsType(r)){++hidden;return} rest+=row;++nRest; }); let html=""; if(nOwn)html+=`<h6><span>Your orders</span><span>${nOwn}</span></h6>`+own; html+=bookFilters(hidden); if(nRest)html+=rest; else html+=`<div class="lqempty">${emptyBook()}${hidden?` ${hidden} hidden by the filters.`:""}</div>`; book.innerHTML=html; syncBook(nOwn+nRest,rows.length); } const cardCache={}; async function loadCard(board,id){ const k=board.toLowerCase()+":"+id; if(cardCache[k])return cardCache[k]; const raw=await rpc(C,[{to:board,data:"0x"+SEL_TOKENURI+encUint(id)},L]); const uri=decodeString(raw); const i=uri.indexOf("base64,"); if(i<0)throw Error("card is not inline"); const j=JSON.parse(atob(uri.slice(i+7))); const out={attrs:Array.isArray(j.attributes)?j.attributes:[],img:safeUrl(j.image), name:safeSym(j.name).slice(0,40)}; cardCache[k]=out;return out; } function cardHtml(c){ const rows=c.attrs.slice(0,10).map(a=>{ const t=safeSym(a&&a.trait_type).slice(0,20); let v=a&&a.value; if(a&&a.display_type==="date"&&+v)v=new Date(+v*1e3).toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"}); else if(a&&a.display_type==="number")v=String(v); return`<dt>${t}</dt><dd>${safeSym(v).slice(0,28)}</dd>`}).join(""); return(rows?`<dl>${rows}</dl>`:"")+(c.img?`<img src="${c.img}" alt="" loading="lazy">`:""); } async function inspect(el,k){ const box=el.parentNode.querySelector(".ic"); if(!box)return; if(box.innerHTML){box.innerHTML="";return} const at=k.lastIndexOf(":"),board=k.slice(0,at),id=BigInt(k.slice(at+1)); box.textContent="Reading the receipt\u2026"; try{box.innerHTML=cardHtml(await loadCard(board,id))||"No card for this order."} catch{box.textContent="This receipt could not be read."} } const BOOK_TYPES=[["all","All"],["limit","Limit"],["dutch","Dutch"],["bid","Bids"],["nft","NFT"]]; const emptyBook=()=>{ const ty=BOOK_TYPES.find(([v])=>v===bookType); const what=bookType==="all"?"orders":`${ty?ty[1]:bookType} orders`; const where=bookPairOnly ?` on ${TOKENS[fromSel.value]?.sym||"?"}/${TOKENS[toSel.value]?.sym||"?"}`:""; return `No ${what.toLowerCase()}${where} from anyone else.`; }; function bookFilters(hidden){ const f=TOKENS[fromSel.value]?.sym||"?",t=TOKENS[toSel.value]?.sym||"?"; const chip=(on,attr,val,label,title)=>`<button class="bkc${on?" on":""}" ` +`data-${attr}="${val}"${title?` title="${title}"`:""}>${label}</button>`
0xacfba7ce…4082·#25,751,347·0x3189e787…52b1cc
+ps,4,8,1,SS)]); } const tailStep=swapAmt/100n||1n,tailAmt=swapAmt>tailStep?swapAmt-tailStep:0n; const tailJobs=[]; if(tailAmt){ const z=encBool(!isIn)+encAddr(f.addr)+encAddr(t.addr)+encUint(tailAmt)+encUint(0n)+encUint(deadline); tailJobs.push(Q("e453166e",a+rf+z,4,9),Q("4c464f59",a+z,8,13)); if(isIn)for(const x of["892af013","85f86a90"]){ const ps=encAddr(f.addr)+encAddr(t.addr)+encUint(tailAmt)+encUint(splitSlip)+encUint(deadline); tailJobs.push(Q(x,a+ps,4,8,1,splitSlip)); } } const refAmt=swapAmt/REF_N; const iRef=refAmt>0n ?Q("e453166e",a+rf+encBool(!isIn)+encAddr(f.addr)+encAddr(t.addr)+encUint(refAmt)+encUint(0n)+encUint(deadline),4,9) :-1; const mcRaw=await mc3Deep(_rq.map(q=>({to:q.to,data:q.data})),quoteBlock); if(my!==seq)return; const dec=_rq.map((q,i)=>{try{return mcRaw[i]?decQ(mcRaw[i],q.sb||slipBps,!isIn,q.u,q.v,q.S,q.mv):null}catch{return null}}); for(const g of jobs)pick(g.length>1?merge(dec[g[0]],dec[g[1]]):dec[g[0]]); if(isIn&&!(dec[jobs[1][0]]||dec[jobs[1][1]]||dec[jobs[2][0]]||dec[jobs[2][1]])){ const hq=await hubRoute(f,t,swapAmt,slipBps,deadline,rcv||acct||ZERO,acct||ZERO,quoteBlock).catch(()=>null); if(my!==seq)return; if(hq)pick(hq); } let tail=null; for(const i of tailJobs){const y=dec[i];if(!y)continue; const v=isIn?y.best.amountOut:y.best.amountIn; if(v&&(!tail||(isIn?v>tail.best.amountOut:v<tail.best.amountIn)))tail=y; } if(my!==seq)return; const v4q=await v4Job; let standalone=null; let v4c=null; if(v4q&&v4q.amountOut>0n){ const v4Min=v4q.amountOut-(v4q.amountOut*slipBps)/10000n; const p=v4q.pool; v4c={best:{source:4,amountIn:v4q.amountIn,amountOut:v4q.amountOut,feeBps:BigInt(p.fee)/100n}, callData:"0x"+SEL_V4SWAP+encPoolKey(p)+encBool(p.zeroForOne)+encUint(v4q.amountIn) +encUint(v4Min)+encAddr(rcv||acct||ZERO)+encUint(deadline), fundedCallData:null,amountLimit:v4Min,msgValue:f.addr===ZERO?swapAmt:0n, to:V4PORT,spender:f.addr===ZERO?null:V4PORT}; if(!r){r=v4c;standalone=v4c} } const pq=await preJob; if(my!==seq)return; let pc=null; if(pq&&pq.out>0n){ const pMin=pq.out-(pq.out*slipBps)/10000n; const pNative=pq.tokenIn===ZERO; const pWethIn=pNative&&(pq.unwrap||0n)>0n; const pTo=rcv||acct||ZERO; const pFund=pWethIn?WETH:pq.tokenIn; const routeData=pWethIn ?encPRouteWeth([pq.pool],pq.tokenOut,swapAmt,pMin,pTo) :encPRoute([pq.pool],pq.tokenIn,pq.tokenOut,swapAmt,pMin,pTo); const routedLeg=encSnwap(pFund,swapAmt,pTo,pq.tokenOut,pMin,PROUTE,routeData); const checkpointLeg=encSnwap(ZERO,0n,acct||ZERO,ZERO,0n,PROUTE, "0x"+SEL_PCHECKPOINT+encAddr(pFund)+strip0x(keccak(hexToBytes(routeData)))+encAddr(acct||ZERO)); const pCall=(pNative&&!pWethIn)?routedLeg:encCalls([checkpointLeg,routedLeg]); const pFunded=(pNative&&!pWethIn)?null:encCalls([ checkpointLeg, encSweep(pFund,swapAmt,PROUTE), encSnwap(ZERO,0n,pTo,pq.tokenOut,pMin,PROUTE,routeData), ]); pc={best:{source:SRC_PRECISION,amountIn:swapAmt,amountOut:pq.out,feeBps:pq.fee/100n}, callData:pCall, fundedCallData:pFunded,amountLimit:pMin,msgValue:(pNative&&!pWethIn)?swapAmt:0n, wrap:pq.wrap||0n,outDiffers:!!pq.outDiffers,poolOut:pq.tokenOut, to:null,spender:(pNative&&!pWethIn)?null:ZROUTER,pool:pq.pool}; if(!r){r=pc;standalone=pc} } if(!r&&curveOnlyOut)throw Error(`exact-output is unavailable for this pair — enter an amount to PAY instead`); if(!r)throw Error("bad quote"); if(r.best.amountIn<=0n||r.best.amountOut<=0n||(!isIn&&r.amountLimit<r.best.amountIn))throw Error("bad quote"); let hybrid=null,bookIncomplete=false,bookUnavailable=false,plannerBounded=false; if(acct&&!standalone){ const candidates=await candJob; bookIncomplete=!!candidates.incomplete; bookUnavailable=!!candidates.unavailable; if(my!==seq)return; const quoteRem=async(x,eo)=>{ const qs=[],groups=[],add=(sel,data,u,v,S,sb)=>{ qs.push({data:"0x"+sel+data,u,v,S,sb});return qs.length-1}; const z0=encBool(eo)+encAddr(f.addr)+encAddr(t.addr)+encUint(x)+encUint(0n)+encUint(deadline); const zs=encBool(eo)+encAddr(f.addr)+encAddr(t.addr)+encUint(x)+encUint(slipBps)+encUint(deadline); groups.push([add("e453166e",a+rf+z0,4,9),add("e453166e",a+rf+zs,4,9)]); groups.push([add("4c464f59",a+z0,8,13),add("4c464f59",a+zs,8,13)]); if(!eo){ const w=slipBps*3n,ss=w<150n?150n:w>500n?500n:w; const ps=encAddr(f.addr)+encAddr(t.addr)+encUint(x)+encUint(ss)+encUint(deadline); for(const sel of["892af013","85f86a90"])groups.push([add(sel,a+ps,4,8,1,ss)]); } const raw=await mc3Deep(qs.map(q=>({to:ZQUOTER,data:q.data})),quoteBlock); const ds=qs.map((q,i)=>{try{return raw[i]?decQ(raw[i],q.sb||slipBps,eo,q.u,q.v,q.S):null}catch{return null}}); let best=null; for(const g of groups){ const y=g.length>1?merge(ds[g[0]],ds[g[1]]):ds[g[0]]; if(!y)continue; if(eo&&y.sources&&y.sources.includes(SRC_CURVE))continue; const v=eo?y.best.amountIn:y.best.amountOut; if(v&&(!best||(eo?v<best.best.amountIn:v>best.best.amountOut)))best=y; } return best; }; const gasValue=async wei=>{ const token=isIn?t.addr:f.addr; if(token===ZERO||token.toLowerCase()===WETH.toLowerCase())return wei; const eo=!isIn,ti=isIn?ZERO:token,to=isIn?token:ZERO; const z=encBool(eo)+encAddr(ti)+encAddr(to)+encUint(wei)+encUint(0n)+encUint(deadline); try{ const raw=await rpc(C,[{to:ZQUOTER,data:"0xe453166e"+a+rf+z},quoteBlock]); const q=decQ(raw,0n,eo,4,9); return isIn?q.best.amountOut:q.best.amountIn; }catch{return null} }; const tailDelta=swapAmt-tailAmt; const seedOut=isIn&&tail&&r.best.amountOut>tail.best.amountOut ?r.best.amountOut-tail.best.amountOut:r.best.amountOut; const seedIn=!isIn&&tail&&r.best.amountIn>tail.best.amountIn ?r.best.amountIn-tail.best.amountIn:r.best.amountIn; let p=isIn ?planBookExactIn(candidates,swapAmt,seedOut,tail&&seedOut!==r.best.amountOut?tailDelta:r.best.amountIn) :planBookExactOut(candidates,swapAmt,seedIn,tail&&seedIn!==r.best.amountIn?tailDelta:r.best.amountOut); plannerBounded=!!(p&&p.bounded); let amm=null; for(let round=0;p&&(isIn?p.ammIn:p.ammOut)&&round<2;round++){ const rem=isIn?p.ammIn:p.ammOut; amm=await quoteRem(rem,!isIn).catch(()=>null); if(!amm)break; const next=isIn ?planBookExactIn(candidates,swapAmt,amm.best.amountOut,rem) :planBookExactOut(candidates,swapAmt,amm.best.amountIn,rem); if(!next){p=null;break} const stable=(isIn?next.ammIn===p.ammIn:next.ammOut===p.ammOut); p=next;plannerBounded=plannerBounded||!!p.bounded;if(stable)break; } if(p){ const rem=isIn?p.ammIn:p.ammOut; amm=rem?await quoteRem(rem,!isIn).catch(()=>null):null; if(!rem||amm){ const recipient=rcv||acct; const ammBudget=amm?(isIn?rem:amm.amountLimit):0n; const totalBudget=p.bookIn+ammBudget; const expectedIn=p.bookIn+(amm?amm.best.amountIn:0n); const expectedOut=p.bookOut+(amm?amm.best.amountOut:0n); const saving=isIn ?(expectedOut>r.best.amountOut?expectedOut-r.best.amountOut:0n) :(expectedIn<r.best.amountIn?r.best.amountIn-expectedIn:0n); let gasFloor=null; if(saving){ const gasPrice=await gasPriceJob; if(gasPrice){ const extraWei=gasPrice*(120000n+105000n*BigInt(p.fills.length)); gasFloor=await gasValue(extraWei); } } const edgeBps=5n+2n*BigInt(p.fills.length); const fallbackWin=isIn ?expectedOut*10000n>r.best.amountOut*(10000n+edgeBps) :expectedIn*(10000n+edgeBps)<r.best.amountIn*10000n; const wins=saving>0n&&(gasFloor===null?fallbackWin:saving*10n>gasFloor*12n); if(my!==seq)return; if(wins){ const fp=encFillPlanAndSwap( p.fills,f.addr,t.addr,recipient,acct,deadline,ammBudget,amm?amm.callData:"0x" ); const minOut=isIn?p.bookOut+(amm?amm.amountLimit:0n):swapAmt; const snwapIn=totalBudget===0n?ZERO:f.addr; const planCall=encSnwap(snwapIn,totalBudget,recipient,t.addr,minOut,SWAPBOL,fp); const callData=f.addr===ZERO?planCall:encCalls([ encCheckpoint(SWAPBOL,f.addr,acct),planCall ]); let fundedCallData=null; if(f.addr!==ZERO){ fundedCallData=encPermit2Hybrid( amm?amm.callData:"0x",p.fills,f.addr,t.addr,recipient,acct,deadline,p.bookIn,p.bookOut ); } r={...r,best:{...r.best,amountIn:expectedIn,amountOut:expectedOut},callData, fills:p.fills, fundedCallData,amountLimit:isIn?minOut:totalBudget,msgValue:f.addr===ZERO?totalBudget:0n, slipBps:amm&&amm.slipBps}; hybrid=isIn ?{bookIn:p.bookIn,bookOut:p.bookOut,ammIn:p.ammIn} :{bookIn:p.bookIn,bookOut:p.bookOut,ammOut:p.ammOut}; plannerBounded=plannerBounded||!!p.bounded; } } } } for(const c of [v4c,pc]) if(c&&c!==r&&c.best.amountOut>r.best.amountOut){r=c;hybrid=null} const finalIn=r.best.amountIn; const finalOut=r.best.amountOut; if(isIn){outAmt.value=trimAmt(finalOut,t.dec);fitFont(outAmt)} else{amt.value=trimAmt(finalIn,f.dec);fitFont(amt)} const rx=(+formatUnits(finalOut,t.dec))/(+formatUnits(finalIn,f.dec)); const s=r.best.source,raw=SOURCES[s]||"?"; const src=hybrid?"Orderbook + AMM":(s===3||s===4||s===SRC_PRECISION)&&r.best.feeBps?`${raw}\u00a0${Number(r.best.feeBps)/100}%`:raw; rate.textContent=isFinite(rx)?`1 ${f.sym} ≈ ${rx>=1?rx.toLocaleString("en-US",{maximumFractionDigits:4}):rx.toPrecision(4)} ${t.sym} · ${src}`:""; const paysSym=r.poolOut===ZERO?"ETH":"WETH"; rcvHdr.textContent=r.outDiffers?`You receive \u2014 as ${paysSym}`:"You receive"; if(r.outDiffers&&rate.textContent)rate.textContent+=` \u00b7 pays ${paysSym}`; rate.title=""; if(isIn)rate.textContent+=` · Min ${trimAmt(r.amountLimit,t.dec)} ${t.sym}`; else{ const exactMax=formatUnits(r.amountLimit,f.dec); rate.textContent+=` · Max ${maxAmt(r.amountLimit,f.dec)} ${f.sym}`; rate.title=`Exact maximum: ${exactMax} ${f.sym}`; } if(r.slipBps>slipBps)rate.textContent+=` · Split — bound widened to ${Number(r.slipBps)/100}%`; if(bookIncomplete)rate.textContent+=" · Book scan capped"; if(plannerBounded)rate.textContent+=" · Planner heuristic/capped"; if(bookUnavailable)rate.textContent+=" · AMM only (books offline)"; if(pq&&pq.capped)rate.textContent+=` · Precision scan capped (${LQ_SCAN})`; const refQ=iRef>=0&&dec[iRef]?dec[iRef].best:null; const imp=refQ?impactBps(isIn?finalOut:finalIn,isIn?refQ.amountOut:refQ.amountIn,!isIn):null; const mkt=refQ?(isIn?refQ.amountOut:refQ.amountIn)*REF_N:0n; const lossTok=imp!==null&&mkt?(isIn?(mkt>finalOut?mkt-finalOut:0n):(finalIn>mkt?finalIn-mkt:0n)):0n; if(imp!==null&&imp>=IMPACT_HIDE){ const pctTxt=(Number(imp)/100).toFixed(2); rate.textContent+=` · Impact ${pctTxt}%`; if(imp>=IMPACT_WARN)stat.textContent=`High price impact: ${pctTxt}% — about ${trimAmt(lossTok,isIn?t.dec:f.dec)} ${isIn?t.sym:f.sym} worse than the market rate.`; } const inApprove=isIn?swapAmt:r.msgValue||r.amountLimit; if(acct)last={callData:r.callData,fundedCallData:r.fundedCallData,msgValue:r.msgValue, to:r.to,spender:r.spender, amountIn:inApprove,from:f,recipient:rcv||"",exp:Date.now()+QUOTE_TTL,impact:imp, lossTok,lossSym:isIn?t.sym:f.sym,lossDec:isIn?t.dec:f.dec, fills:r.fills||null,tokenIn:f.addr,tokenOut:t.addr, source:r.best.source, wrap:r.wrap||0n,pool:r.pool||null, mkt,got:isIn?finalOut:finalIn,isIn}; quoting=false; render(); }catch(e){if(my===seq){ quoting=false; if(advancePastDeadPair()){loadChart();update();return} outEl.value="";fitFont(outEl);render(); stat.textContent=e?.noWallet?NO_WALLET :mcGaveUp>gaveUp0||(mcFailed>failed0&&mcOk===ok0) ?"Your RPC could not complete the quote — some venues were never asked. Try again, or switch networks in your wallet and back." :"No route: "+(e.message||e)}} } let debounceT=0; const showAddr=async()=>{const a=account,short=a.slice(0,6)+"..."+a.slice(-4); addr.textContent=short;addr.title=a+" - click to disconnect";addr.classList.add("on"); addr.setAttribute("role","button");addr.tabIndex=0; const n=await nameRev(a);if(n&&a===account)addr.textContent=n}; async function disconnect(){ sessionStorage.setItem("dc","1"); try{await rpc("wallet_revokePermissions",[{eth_accounts:{}}])}catch{} location.reload(); } addr.onclick=()=>{if(account)disconnect()}; addr.onkeydown=e=>{if(account&&(e.key==="Enter"||e.key===" ")){e.preventDefault();disconnect()}}; const AUTO_Q_MAX=5; const updateSoon=()=>{clearTimeout(debounceT);debounceT=setTimeout(()=>{debounceT=0;update()},250)}; const setMode=m=>{mode=m;const o=m==="in"?outAmt:amt;o.value="";fitFont(o);last=null;sendReady=null;render()}; amt.addEventListener("input",()=>{ if(lqMode){fitFont(amt);lqPreviewSoon();return} if(tab==="book"){fitFont(amt);render();return}setMode("in");fitFont(amt);updateSoon()}); outAmt.addEventListener("input",()=>{ if(lqMode){fitFont(outAmt);lqPreviewSoon();return} if(tab==="book"){fitFont(outAmt);seedFloor();render();return}setMode("out");fitFont(outAmt);updateSoon()}); const slipWarn=()=>slip.classList.toggle("warn",+slip.value>=3); if(LS.slip&&+LS.slip>=.01&&+LS.slip<=10)slip.value=LS.slip; slipWarn(); slip.addEventListener("input",()=>{last=null;slipWarn();render();updateSoon()}); rc.addEventListener("input",()=>{ if(lnMode)return; if(lqMode){clearTimeout(lqToT);lqToT=setTimeout(lqShowTo,300);return} last=null;sendReady=null;render();updateSoon()}); let lqToT; const onEnter=e=>{ if(e.key!=="Enter")return; const go=lnMode?lnGo:swap; if(!go||go.classList.contains("hide"))return; e.preventDefault(); if(debounceT){clearTimeout(debounceT);debounceT=0;update();return} if(!go.disabled)go.click(); }; for(const el of[amt,outAmt,rc,slip,floorAmt,nftId,lnName,lnSym,lnSupply,lnMcap,lnAlloc]) if(el)el.addEventListener("keydown",onEnter); slip.addEventListener("change",()=>{slip.value=Math.min(10,Math.max(.01,+slip.value||.5));slipWarn();LS.slip=slip.value}); for(const sel of [fromSel,toSel]){ sel.dataset.prev=sel.value; sel.addEventListener("change",async()=>{ if(sel.value!=="__custom"){ sel.dataset.prev=sel.value; sel.dataset.addr=TOKENS[sel.value]?.addr||""; pairChosen=true; syncDisabled(sel); syncNftUI(); if(sel===fromSel)refreshBalance();else refreshBalance1(); loadChart(); if(tab==="book"&&bookRows.length)paintBook(); update(); return; } last=null;render(); const prev=sel.dataset.prev; let a; try{a=(prompt("Paste token address:")||"").trim()} catch{sel.value=prev;syncDisabled(); stat.textContent="This browser blocked the address prompt — add the token with a #token=0x… link instead."; return} if(!a){sel.value=prev;update();return} stat.textContent="Loading token..."; try{ const idx=await addCustomToken(a); rebuild(); const otherSel=sel===fromSel?toSel:fromSel; if(String(idx)===otherSel.value){otherSel.value=prev;otherSel.dataset.prev=prev} sel.value=String(idx); sel.dataset.prev=sel.value; pairChosen=true; stat.textContent=""; syncDisabled(sel);syncNftUI(); refreshBalance(); update(); }catch(e){ const msg="Couldn't load token: "+(e.message||e); sel.value=prev; syncDisabled();syncNftUI();refreshBalance();update(); stat.textContent=msg; } }); } flip.onclick=()=>{pairChosen=true;const a=fromSel.value,b=toSel.value,v=outAmt.value.replace(/,/g,"");fromSel.value=b;toSel.value=a;if(v&&!isNaN(+v))amt.value=v;setMode("in");fitFont(amt);syncDisabled();syncNftUI();refreshBalance();loadChart();update()}; const sameAcct=(a,b)=>!!a&&!!b&&a.toLowerCase()===b.toLowerCase(); const wireWallet=eth=>{ if(!eth||wireWallet.done)return; wireWallet.done=true; eth.on?.("chainChanged",cid=>{if(connecting||(+cid===1&&account))return;location.reload()}); eth.on?.("accountsChanged",accs=>{if(connecting||sameAcct(accs?.[0],account))return;location.reload()}); (async()=>{ if(sessionStorage.getItem("dc"))return; try{ const accs=await rpc("eth_accounts",[]); const cid=await rpc(I,[]); if(accs[0]&&+cid===1){ account=accs[0]; showAddr(); const keep2=pairAddrs(); await loadTokenList(); adoptList(keep2); refreshBalance();update();loadPos();syncChart(); } }catch{} })(); (async()=>{ const self=selfFromUrl()||ZSWAP_SELF; if(!self)return; try{ if(+(await rpc(I,[]))!==CHAIN_ID)return; const ret=strip0x(await rpc(C,[{to:self,data:"0x"+SEL_LATEST},L])||""); if(ret.length!==64)return; let tip="0x"+ret.slice(24); const me=self.toLowerCase(); if(BigInt(tip)===0n||tip===me)return; const rd=async(to,sel)=>{const h=strip0x(await rpc(C,[{to,data:"0x"+sel},L])||"");return h.length===64?h:""}; const now=Date.now()/1000; for(let i=0;i<4&&tip!==me;i++){ const pv=await rd(tip,SEL_PREV); if(!pv)break; const prev="0x"+pv.slice(24); if(BigInt(prev)===0n)break; const at=await rd(prev,SEL_SUCCAT); const born=at?Number(BigInt("0x"+at)):0; if(!born||now>=born+MATURITY)break; tip=prev; } if(tip===me)return; const h=(location.hostname||"").split("."); const url=/^0x[0-9a-fA-F]{40}$/.test(h[0]||"") ?`${location.protocol}//${[tip,...h.slice(1)].join(".")}/` :`https://${tip}.w4eth.io/`; footAddr.innerHTML+=` · <a href="${url}" target="_blank" rel="noopener" ` +`title="A newer version exists on chain. This page is unchanged and always will be; the link opens the successor.">newer →</a>`; }catch{} })(); }; wireWallet(window.ethereum); if(!window.ethereum)window.addEventListener("ethereum#initialized",()=>wireWallet(window.ethereum),{once:true}); const SEL_TRANSFER="a9059cbb"; const SB2="0x000000dA7bb4B2A9E3e80e9A4D4157E26CA6189b"; const SB1="0x000000fF3D7A2d373615141d7489Ca66683DbecF"; const SBVIEW="0x000000E0b25449F32f7D9259aC449bA88E78dFCE"; const SWAPBOL="0x00000087A6dc5071779Ed1F8274A39230768B976"; const DUTCH="0x000000a213b430D14Bae6062c176289B05e04489"; const ORDERBOL="0x000000e6c7a12C80525ee74e7434aAb919447D95"; const FLOOR="0x00000080198137F790DA4C52bb902cf87c276748"; const FLOORVIEW="0x0000004E376e9dB5D9EC28E6711E1a64997C6ba7"; const SEL_NEXTID="2a58b330",SEL_GETORDERS="03652027",SEL_CANCELORD="514fcac7"; const SEL_FILL1="c37dfc5b",SEL_FILL2="9d136c7f",SEL_FILL2_UNWRAP="3987baf4",SEL_FILL2_ETH="6f608bab"; const SEL_RECENT="6a9849c1",SEL_RECENT_DUTCH="98035c9a",SEL_DUTCH_FILL="ae7a8260",SEL_DUTCH_CANCEL="40e58ee5"; const SEL_CANCEL_UNWRAP="21dd76f9",SEL_DUTCH_CANCEL_UNWRAP="8382de65"; const SEL_DUTCH_LISTING="de74e57b"; const SEL_CANDS="5f452988",SEL_DUTCH_CANDS="eb33e466",SEL_FILLPLAN="c277f67c"; const SEL_FILLPLAN_SWAP="9090c8e5",SEL_SNWAP="5f3bd1c8"; const SEL_COLL_BIDS="16bb24eb"; const SEL_FLOOR_CANDS="c587d927",SEL_RECENT_FLOOR="a5edd13d"; const SEL_ORDER_FLOOR="23e93357"; const SEL_BOL_FLOOR="b732d224"; let floorReady=null; async function floorRoutable(block){ if(FLOOR===ZERO||FLOORVIEW===ZERO)return false; if(floorReady!==null)return floorReady; try{ const h=await rpc(C,[{to:SWAPBOL,data:"0x"+SEL_BOL_FLOOR},block||L]); floorReady=("0x"+strip0x(h).slice(-40)).toLowerCase()===FLOOR.toLowerCase(); }catch{return false} return floorReady; } const SEL_BIDS="4423c5f1"; const SEL_INITIAL_B="878ea250",SEL_TOKENURI="c87b56dd",SEL_LOGOOF="6ce273ac"; const SEL_QUOTEFILL="f325beda"; async function preflightLeg(row,routeIn,routeOut,pay){ if(!row.floor)return preflightAsk(row); if(!await floorRoutable(L))throw Error("this executor cannot route bids yet"); const want=routeIn===ZERO?WETH:routeIn,gets=routeOut===ZERO?WETH:routeOut; let h; try{h=await rpc(C,[{to:FLOOR,data:"0x"+SEL_BIDS+encUint(row.id)},L])} catch{throw Error("bid could not be read — refresh and retry")} const at=i=>strip0x(h).slice(i*64,(i+1)*64); if(strip0x(h).length<11*64)throw Error("bid could not be read — refresh and retry"); const addr=i=>"0x"+at(i).slice(24); if(/^0x0{40}$/i.test(addr(0)))throw Error("bid is closed — refresh and retry"); if(BigInt("0x"+at(1)))throw Error("NFT bids are not routable from here"); if(addr(4).toLowerCase()!==want.toLowerCase() ||addr(6).toLowerCase()!==gets.toLowerCase()) throw Error("bid changed — refresh and retry"); if(BigInt("0x"+at(10))<pay)throw Error("bid partly taken — refresh and retry"); } async function preflightFills(fills,tokenIn,tokenOut){ if(!fills||!fills.length)return; const inA=(tokenIn===ZERO?WETH:tokenIn).toLowerCase(); const outA=(tokenOut===ZERO?WETH:tokenOut).toLowerCase(); const reads=fills.map(f=>f.board.toLowerCase()===FLOOR.toLowerCase() ?{to:FLOOR,data:"0x"+SEL_BIDS+encUint(f.id)} :f.board.toLowerCase()===DUTCH.toLowerCase() ?{to:DUTCH,data:"0x"+SEL_DUTCH_LISTING+encUint(f.id)} :{to:f.board,data:"0x"+SEL_GETORDERS+encUint(32)+encUint(1)+encUint(f.id)}); const res=await mc3Deep(reads); const stale=()=>{throw Error("an order in this route just changed — refreshing")}; for(let i=0;i<fills.length;i++){ const f=fills[i],raw=res[i]; if(!raw)stale(); const s=strip0x(raw),at=k=>BigInt("0x"+s.slice(k*64,(k+1)*64)); if(f.board.toLowerCase()===FLOOR.toLowerCase()){ if(s.length<11*64)stale(); const ad=k=>"0x"+s.slice(k*64+24,(k+1)*64); if(/^0x0{40}$/i.test(ad(0))||at(1))stale(); if(ad(4).toLowerCase()!==inA||ad(6).toLowerCase()!==outA)stale(); if(at(10)<f.pay)stale(); }else if(f.board.toLowerCase()===DUTCH.toLowerCase()){ if(s.length<640||at(9)===0n)stale(); }else{ const wds=WORDS(f.board.toLowerCase()===SB2.toLowerCase()?1:0); if(s.length<(2+wds)*64)stale(); const w=k=>at(2+k),ad=k=>"0x"+s.slice((2+k)*64+24,(3+k)*64); if(w(1)!==1n)stale(); const v2=wds===11,iA=v2?7:2,iB=v2?9:4; if(ad(iA).toLowerCase()!==outA||ad(iB).toLowerCase()!==inA)stale(); if(w(iB+1)<f.pay)stale(); } } } async function preflightAsk(row){ if(row.dutch){ let h; try{h=await rpc(C,[{to:DUTCH,data:"0x"+SEL_DUTCH_LISTING+encUint(row.id)},L])} catch{throw Error("listing could not be read — refresh and retry")} const s=strip0x(h); if(s.length<640)throw Error("listing could not be read — refresh and retry"); if(BigInt("0x"+s.slice(9*64,10*64))===0n)throw Error("listing is closed — refresh and retry"); return; } let h; try{h=await rpc(C,[{to:row.board, data:"0x"+SEL_GETORDERS+encUint(32)+encUint(1)+encUint(row.id)},L])} catch{throw Error("order could not be read — refresh and retry")} const s=strip0x(h),wds=WORDS(row.v2); if(s.length<(2+wds)*64)throw Error("order could not be read — refresh and retry"); const at=k=>BigInt("0x"+s.slice(128+k*64,128+(k+1)*64)); const ad=k=>"0x"+s.slice(128+k*64+24,128+(k+1)*64); if(at(1)!==1n)throw Error("order is no longer active — refresh and retry"); const iA=row.v2?7:2,iB=row.v2?9:4; if(ad(iA).toLowerCase()!==row.tA.toLowerCase() ||ad(iB).toLowerCase()!==row.tB.toLowerCase()) throw Error("order changed — refresh and retry"); if(at(iA+1)!==row.aA||at(iB+1)!==row.aB)throw Error("order changed — refresh and retry"); } const SEL_ORDER_FIXED="bcdb7936",SEL_ORDER_DUTCH="bbd94e1a",SEL_CHECKPOINT="a972985e"; const BOARDS=[{a:SB2,v2:1},{a:SB1,v2:0}]; const WORDS=b=>b?11:6; let bookSeq=0,bookRows=[],candCache={},bkOpen=LS.bk!=="0"; let busy=0; if(LS.wm==="weth"||LS.wm==="auto")ethMode.value=LS.wm; ethMode.onchange=()=>{LS.wm=ethMode.value}; const wantsUnwrap=()=>ethMode.value!=="weth"; const planWethPayment=async need=>{ let w=0n,e=0n; try{ const[wh,eh]=await Promise.all([ rpc(C,[{to:WETH,data:"0x"+SEL_BALANCEOF+encAddr(account)},L]), rpc("eth_getBalance",[account,L])]); w=BigInt(wh);e=BigInt(eh); }catch{return{rail:"weth",wrap:0n,w,e}} if(w>=need)return{rail:"weth",wrap:0n,w,e}; if(e>=need)return{rail:"eth",wrap:0n,w,e}; let gas=0n; try{gas=BigInt(await rpc("eth_gasPrice",[]))*400000n}catch{} const spendable=e>gas?e-gas:0n; if(w+spendable>=need)return{rail:"weth",wrap:need-w,w,e}; return{rail:"short",wrap:0n,w,e}; }; const assertFunded=async(token,need,meta)=>{ if(!need)return; let have; try{ have=token===ZERO ?BigInt(await rpc("eth_getBalance",[account,L])) :BigInt(await rpc(C,[{to:token,data:"0x"+SEL_BALANCEOF+encAddr(account)},L])); }catch{return} if(have>=need)return; const m=meta||known(token)||{sym:token===ZERO?"ETH":"tokens",dec:18}; throw Error(`Not enough ${m.sym} to fill: it costs ${trimAmt(need,m.dec)} ` +`and you hold ${trimAmt(have,m.dec)}.`); }; const known=a=>TOKENS.find(t=>t.addr.toLowerCase()===a.toLowerCase()); const sm=i=>i.replace('width="20" height="20"','width="15" height="15"'); const escan=(a,id)=>`<a href="https://etherscan.io/${id!=null?`nft/${a}/${id}`:`token/${a}`}" target="_blank" rel="noreferrer">${a.slice(0,6)}…${a.slice(-4)} ↗</a>`; const SEL_CONTRACTURI="e8a3d485"; const okDataImg=u=>typeof u==="string"&&u.length<=60000 &&/^data:image\/[a-z.+-]{2,20};base64,[A-Za-z0-9+/=]+$/i.test(u); const imgFromContractURI=raw=>{ const P="data:application/json;base64,"; const u=String(raw||"").trim(); if(!u.startsWith(P)||u.length>400000)return ""; let txt; try{ const bin=atob(u.slice(P.length)); txt=new TextDecoder().decode(Uint8Array.from(bin,c=>c.charCodeAt(0))); }catch{return ""} let j;try{j=JSON.parse(txt)}catch{return ""} return okDataImg(j&&j.image)?j.image:""; }; const logoCache={}; async function loadLogos(add
0xacfba7ce…4082·#25,751,346·0xf5c4519e…e193ba
atUnits(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=""; TOKENS.forEach((t,i)=>{const o=`<option value="${i}">${t.sym}</option>`; if(t.std==="nft")nft+=o;else ft+=o}); return ft +(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 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 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;return} if(!sell||!want){swap.textContent=bid?"Place bid":"Place order";swap.disabled=true;return} if(bid){ if(t.addr===ZERO){swap.textContent="Pick a token to bid for";swap.disabled=true;return} if((f.addr===ZERO?WETH:f.addr).toLowerCase()===t.addr.toLowerCase()){ swap.textContent="Pick different token";swap.disabled=true;return} if(!floor){swap.textContent="Set an opening bid";swap.disabled=true;return} if(floor>sell){swap.textContent="Opening exceeds maximum";swap.disabled=true;return} if(+dly.value===0){swap.textContent="Choose a bid window";swap.disabled=true;return} if(sell>fromBalance){swap.textContent="Insufficient balance";swap.disabled=true;return} swap.textContent=`Bid ${trimAmt(floor,f.dec)} → ${trimAmt(sell,f.dec)} ${f.sym} for ${trimAmt(want,t.dec)} ${t.sym}`; swap.disabled=false;return; } if(f.addr===ZERO&&t.addr===WETH){swap.textContent="Pick different token";swap.disabled=true;return} if(!dutch&&f.addr===WETH&&t.addr===ZERO){swap.textContent="Pick different token";swap.disabled=true;return} if(dutch&&floor>want){swap.textContent="Floor exceeds start";swap.disabled=true;return} if(dutch&&+dly.value===0){swap.textContent="Choose decay duration";swap.disabled=true;return} if(sell>fromBalance){swap.textContent="Insufficient balance";swap.disabled=true;return} swap.textContent=dutch ?`Dutch ${trimAmt(sell,f.dec)} ${f.sym} · ${trimAmt(want,t.dec)} → ${trimAmt(floor,t.dec)} ${t.sym}` :`Place ${fill.value==="1"?"":"all-or-nothing "}order \u2014 ${trimAmt(sell,f.dec)} ${f.sym} \u2192 ${trimAmt(want,t.dec)} ${t.addr===ZERO?"WETH":t.sym}`; { const qs=t.addr===ZERO?"ETH":t.sym; rate.textContent=dutch ?`Falls from ${trimAmt(want,t.dec)} to ${trimAmt(floor,t.dec)} ${qs} over ${rel(+dly.value)}, then rests at ${trimAmt(floor,t.dec)} ${qs} ` +(+dRest.value?`for ${rel(+dRest.value)} and returns to you`:`until you cancel`) :`${fill.value==="1"?"Fills in any amount":"Fills all at once"} at ${trimAmt(want,t.dec)} ${t.addr===ZERO?"WETH":t.sym} for ${trimAmt(sell,f.dec)} ${f.sym}` +(+dly.value?`, expiring in ${rel(+dly.value)}`:", and never expires"); } swap.disabled=false;return; } if(tab==="send"){ if(!sendReady){ let n=0n;try{n=parseUnits(amt.value.trim(),TOKENS[fromSel.value].dec)}catch{} swap.textContent=n>0n?"Name a recipient":"Enter an amount"; swap.disabled=true;return} if(sendReady.amount>fromBalance){swap.textContent="Insufficient balance";swap.disabled=true;return} const a=`${trimAmt(sendReady.amount,sendReady.token.dec)} ${sendReady.token.sym}`,dv=+dly.value; if(dv&&sendTipWei>0n&&sendReady.token.addr===ZERO &&sendReady.amount+sendTipWei>fromBalance){ swap.textContent="Insufficient ETH for amount + keeper tip";swap.disabled=true;return} swap.textContent=(dv?`Lock ${a} for ${rel(dv)}`:`Send ${a}`)+` \u2192 ${sendReady.label}`; swap.disabled=false;return; } if(quoting){swap.textContent="Quoting…";swap.disabled=true;return} if(!last){swap.textContent="Swap";swap.disabled=true;return} if(last.amountIn>fromBalance){swap.textContent="Insufficient balance";swap.disabled=true;return} swap.textContent="Swap";swap.disabled=false; }; syncIcons(); render(); const isRejection=e=>e?.code===4001||/user\s+reject|user\s+denied/i.test(e?.message||""); async function connect(){ if(!window.ethereum){stat.textContent=NO_WALLET;return} if(connecting)return; connecting=true; swap.textContent="Connecting…";swap.disabled=true; lnGo.textContent="Connecting…";lnGo.disabled=true; try{ let accs; try{accs=await rpc("eth_requestAccounts",[])} catch(e){err(e);return} if(!accs[0])return; let chainId=await rpc(I,[]); if(+chainId!==1){ try{await rpc("wallet_switchEthereumChain",[{chainId:"0x1"}])} catch(e){stat.textContent=isRejection(e)?"":"Switch to Ethereum mainnet.";return} chainId=await rpc(I,[]); if(+chainId!==1){stat.textContent="Switch to Ethereum mainnet.";return} } sessionStorage.removeItem("dc"); account=accs[0]; const keepPair=pairAddrs(); await loadTokenList(); adoptList(keepPair); showAddr(); refreshBalance(); update(); loadPos(); syncChart(); if(lnMode)lnQuote(); }finally{connecting=false;swap.disabled=false; lnGo.disabled=false;lnGo.textContent=account?"Launch":"Connect Wallet";render()} } const lqReserveGas=()=>lqList.querySelector(".lqnew")?LQ_CREATE_GAS+300000n:400000n; const lqFillMax=(tok,max)=>{ const box=lqList.querySelector(".lqadd:not(.hide)"); const pair=chPair(); if(!box||!pair)return false; const side=pair[0].addr.toLowerCase()===tok.addr.toLowerCase()?"0":"1"; const el=box.querySelector(`.lqin[data-side="${side}"]`); if(!el)return false; el.value=formatUnits(max,tok.dec); el.dispatchEvent(new Event("input",{bubbles:true})); return true; }; bal1.addEventListener("click",async e=>{ if(e.target.tagName!=="A")return; e.preventDefault(); const t=TOKENS[toSel.value]; if(!t)return; let reserve=0n; if(t.addr===ZERO){ try{reserve=BigInt(await rpc("eth_gasPrice",[]))*lqReserveGas()}catch{} } const max=toBalance>reserve?toBalance-reserve:0n; if(lqFillMax(t,max))return; outAmt.value=formatUnits(max,t.dec); fitFont(outAmt); lqPreviewSoon(); }); bal.addEventListener("click",async e=>{ if(e.target.tagName!=="A")return; e.preventDefault(); const f=TOKENS[fromSel.value]; if(!f)return; let reserve=0n; if(f.addr===ZERO){ try{reserve=BigInt(await rpc("eth_gasPrice",[])) *(lqMode?lqReserveGas():tab==="swap"?900000n:tab==="book"?350000n :+dly.value?(tipCk.checked?200000n+TIP_GAS:200000n):30000n)}catch{} } const max=fromBalance>reserve?fromBalance-reserve:0n; if(lqMode&&lqFillMax(f,max))return; amt.value=formatUnits(max,f.dec); fitFont(amt); if(lqMode)return; setMode("in"); update(); }); let seq=0; const REVERTS={ "1ab7da6b":"That took too long and the deadline passed, so nothing was placed. Try again — you were not charged for an order.", "98c2c368":"The order was refused as malformed. Check the amounts and the expiry.", "7939f424":"The token transfer was refused — usually a missing or spent approval. Try again and approve first.", "90b8ec18":"The token transfer was refused by the token itself.", "3e3f8f73":"The approval was refused by the token.", "b12d13eb":"Ether could not be sent to the recipient — a contract there may not accept it.", "ab143c06":"That call re-entered and was stopped.", }; const explain=e=>{ if(e?.noWallet)return NO_WALLET; const s=String(e?.data?.data||e?.data||e?.message||e||""); const m=/0x([0-9a-fA-F]{8})\b/.exec(s); const hit=m&&REVERTS[m[1].toLowerCase()]; return hit||("Error: "+s.slice(0,110)); }; const err=e=>stat.textContent=isRejection(e)?"":explain(e); const settle=async tx=>{ if(!tx){stat.textContent="Done";return} if(!/^0x[0-9a-f]{64}$/i.test(tx))throw Error("bad tx"); stat.innerHTML=txLink(tx,"Sent");await waitTx(tx);stat.innerHTML=txLink(tx,"Done")}; const txLink=(h,label)=>`${label} <a href="https://etherscan.io/tx/${h}" target="_blank" rel="noreferrer">${h.slice(0,10)}…${h.slice(-6)}</a>`; let autoQ=0; async function update(auto){ if(!auto)autoQ=0; syncIcons(); if(lqMode)return loadLq(); if(tab==="book")return render(); if(tab==="send")return updateSend(); const my=++seq; quoting=false; last=null; rate.textContent=""; if(!lqMode&&!lnMode)rcvHdr.textContent="You receive"; render(); const f=TOKENS[fromSel.value], t=TOKENS[toSel.value]; if(!f||!t)return; if(f.addr===t.addr){stat.textContent="Pick different tokens.";return} stat.textContent=""; const isIn=mode==="in"; const inEl=isIn?amt:outAmt, outEl=isIn?outAmt:amt, inTok=isIn?f:t; outEl.value=""; fitFont(outEl); if(!inEl.value.trim())return; let swapAmt; try{swapAmt=parseUnits(inEl.value.trim(),inTok.dec)} catch(e){const q=inEl.value.trim(); if(q&&!/\.$/.test(q))stat.textContent=/decimals/.test(e.message)?`${inTok.sym} has ${inTok.dec} decimals`:"Invalid amount"; return} if(swapAmt<=0n)return; const rv=rc.value.trim(); let rcv=""; if(rv){ const fail=m=>{rc.classList.add("bad");rcvEl.textContent=""; outEl.value="";fitFont(outEl);stat.textContent=m}; const bad=rcErr(rv); if(bad){fail(bad);return} if(isAddr(rv))rcv=rv; else{ rcvEl.textContent="resolving..."; try{rcv=await rcvOf(rv)} catch(e){if(my!==seq)return;fail(e&&e.offchain?e.message:"Name not registered");return} if(my!==seq)return; if(!rcv){fail("Name not registered");return} } rcvEl.textContent=rcv; }else rcvEl.textContent=""; rc.classList.remove("bad"); if(!rv&&f.addr===WETH&&t.addr===ZERO){ outEl.value=inEl.value;fitFont(outEl); rate.textContent=`1 ${f.sym} = 1 ${t.sym}`; last={callData:"0x2e1a7d4d"+encUint(swapAmt),msgValue:0n,amountIn:swapAmt,from:f,to:WETH,recipient:"",exp:Date.now()+QUOTE_TTL}; render();return; } if(!rv&&f.addr===ZERO&&t.addr===WETH){ outEl.value=inEl.value;fitFont(outEl); rate.textContent=`1 ${f.sym} = 1 ${t.sym}`; last={callData:"0xd0e30db0",msgValue:swapAmt,amountIn:swapAmt,from:f,to:WETH,recipient:"",exp:Date.now()+QUOTE_TTL}; render();return; } const pct=Math.min(10,Math.max(.01,+slip.value||.5)); const slipBps=BigInt(Math.round(pct*100)); const deadline=BigInt(Math.floor(Date.now()/1e3)+1800); outEl.value="...";fitFont(outEl);quoting=true;render(); const acct=account; const gaveUp0=mcGaveUp,ok0=mcOk,failed0=mcFailed; try{ const a=encAddr(rcv||acct||ZERO),rf=encAddr(acct||ZERO),tl=encBool(!isIn)+encAddr(f.addr)+encAddr(t.addr)+encUint(swapAmt)+encUint(slipBps)+encUint(deadline); const _rq=[]; const Q=(x,d,u,v,S,sb,mv)=>{_rq.push({to:ZQUOTER,data:"0x"+x+d,u,v,S,sb,mv});return _rq.length-1}; let r=null; const candKey=f.addr.toLowerCase()+":"+t.addr.toLowerCase(); const cachedCandidates=candCache[candKey]; const quoteBlock=cachedCandidates&&Date.now()-cachedCandidates.at<15000 ?cachedCandidates.block:await rpc("eth_blockNumber",[]); const candJob=acct?swapCandidates(f.addr,t.addr,quoteBlock).catch(()=>{ const x=[];x.unavailable=true;return x }):Promise.resolve([]); const gasPriceJob=acct?rpc("eth_gasPrice",[]).then(BigInt).catch(()=>0n):Promise.resolve(0n); const v4Job=isIn?quoteV4Pools(f,t,swapAmt,quoteBlock).catch(()=>null):Promise.resolve(null); const preJob=isIn?quotePrecision(f,t,swapAmt,PROUTE,quoteBlock).catch(()=>null):Promise.resolve(null); const SRC_CURVE=5; let curveOnlyOut=false; const exactOutSafe=y=>{if(isIn||!y.sources||!y.sources.includes(SRC_CURVE))return true;curveOnlyOut=true;return false}; const pick=y=>{if(!y)return;if(!exactOutSafe(y))return;const v=isIn?y.best.amountOut:y.best.amountIn;if(!v)return; if(!r||(isIn?v>r.best.amountOut:v<r.best.amountIn))r=y}; const tl0=encBool(!isIn)+encAddr(f.addr)+encAddr(t.addr)+encUint(swapAmt)+encUint(0n)+encUint(deadline); const jobs=[ [Q("e7798987",a+tl,0,4,0,0,6)], [Q("e453166e",a+rf+tl0,4,9),Q("e453166e",a+rf+tl,4,9)], [Q("4c464f59",a+tl0,8,13),Q("4c464f59",a+tl,8,13)]]; let splitSlip=0n; if(isIn){ const w=slipBps*3n,SS=w<150n?150n:w>500n?500n:w; splitSlip=SS; const ps=encAddr(f.addr)+encAddr(t.addr)+encUint(swapAmt)+encUint(SS)+encUint(deadline); for(const x of["892af013","85f86a90"])jobs.push([Q(x,a
0xacfba7ce…4082·#25,751,344·0xedbc2e66…a09f8d
;i++){ const q=res[i*2],c=res[i*2+1]; if(!q)continue; const h=strip0x(q); if(h.length<128)continue; const p2="0x"+h.slice(24,64),o2=BigInt("0x"+h.slice(64,128)); if(o2<=0n||/^0x0{40}$/i.test(p2))continue; if(o2<=out)continue; pool=p2;out=o2;fA=vars[i].tokenIn;won=vars[i]; try{if(c)n=Number(BigInt(c))}catch{n=0} } if(!pool||!won)return null; const tA=won.c0===fA?won.c1:won.c0; let fee=0n; try{const [fx]=await mc3Deep([{to:PPLENS, data:"0x"+SEL_EFFFEE+encAddr(pool)+encAddr(sender)+encAddr(fA)+encUint(amountIn)}],block); if(fx)fee=BigInt(fx)}catch{} return {pool,out,fee,capped:n>LQ_SCAN,tokenIn:fA,tokenOut:tA, unwrap:fA===ZERO&&f.addr!==ZERO?amountIn:0n, wrap:fA!==ZERO&&f.addr===ZERO?amountIn:0n, outDiffers:(tA===ZERO)!==(t.addr===ZERO)}; } const SEL_BBS="e7798987"; const HUBS=()=>[WETH,...NUMERAIRES,"0x2260fac5e5542a773aa44fbcfedf7c193bc2c599","0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0"]; async function hubRoute(f,t,amt,sb,dl,to,rf,block){ const nf=(f.addr===ZERO?WETH:f.addr).toLowerCase(),nt=(t.addr===ZERO?WETH:t.addr).toLowerCase(); const H=HUBS().filter(h=>h.toLowerCase()!==nf&&h.toLowerCase()!==nt); if(!H.length)return null; const E=(x,i,o,n)=>({to:ZQUOTER,data:"0x"+SEL_BBS+encAddr(x)+encBool(false)+encAddr(i)+encAddr(o)+encUint(n)+encUint(sb)+encUint(dl)}); const D=h=>{try{return decQ(h,sb,false,0,4,0,6)}catch{return null}}; const L=[],r1=await mc3Deep(H.map(h=>E(ZROUTER,f.addr,h,amt)),block); for(let i=0;i<H.length;i++){const a=r1[i]&&D(r1[i]);if(a&&a.amountLimit>0n)L.push({h:H[i],a})} if(!L.length)return null; const r2=await mc3Deep(L.map(x=>E(to,x.h,t.addr,x.a.amountLimit)),block); let B=null; for(let i=0;i<L.length;i++){const b=r2[i]&&D(r2[i]); if(b&&b.best.amountOut>0n&&(!B||b.best.amountOut>B.b.best.amountOut))B={...L[i],b}} if(!B)return null; const c=[B.a.callData,B.b.callData,encSweep(B.h,0n,rf),encSweep(ZERO,0n,rf)]; return{best:{source:B.b.best.source,feeBps:B.b.best.feeBps,amountIn:amt,amountOut:B.b.best.amountOut}, callData:encCalls(c),amountLimit:B.b.amountLimit, msgValue:f.addr===ZERO?amt:0n,spender:f.addr===ZERO?null:ZROUTER}; } async function quoteV4Pools(f,t,amountIn,block){ const pools=v4PoolsFor(f,t); if(!pools.length||amountIn<=0n)return null; const rs=(await mc3Deep(pools.map(p=>({to:V4LENS, data:"0x"+SEL_V4QUOTE+encBool(false)+encAddr(f.addr)+encAddr(t.addr) +encUint(p.fee)+encUint(p.ts)+encAddr(p.hooks)+encUint(amountIn)})),block)) .map(h=>{try{const x=strip0x(h||"");return x.length>=128?BigInt("0x"+x.slice(64,128)):0n}catch{return 0n}}); let best=null; for(let i=0;i<pools.length;i++)if(rs[i]>0n&&(!best||rs[i]>best.amountOut)) best={pool:pools[i],amountIn,amountOut:rs[i]}; return best; } const setListNote=t=>{ const el=document.getElementById("listNote"); if(!el)return; el.textContent=t||""; el.classList.toggle("hide",!t)}; const setListSource=w=>setListNote(w?"Built-in token list — "+w:""); let rankFellBack=false; const rankedIds=async()=>{ rankFellBack=false; try{ const ids=decUintArr(await rpc(C,[{to:ZLISTLENS,data:"0x"+SEL_RANKEDIDS},L])); if(ids.length)return ids; }catch{} try{ const ids=decUintArr(await rpc(C,[{to:TOKENLIST,data:"0x"+SEL_RANKEDIDS},L])); if(ids.length)rankFellBack=true; return ids; }catch{return[]} }; async function loadTokenList(){ if(TOKENLIST===ZERO)return setListSource("registry address not set"); try{ const ids=await rankedIds(); if(!ids.length)return setListSource("registry is empty"); const raw=await mc3Deep(ids.slice(0,64).map(id=>({to:TOKENLIST,data:"0x"+SEL_JSON+encUint(id)}))); const rows=raw.map(h=>{try{return h?decStr(h):null}catch{return null}}); const next=[]; for(const row of rows){ if(!row)continue; let t;try{t=JSON.parse(row)}catch{continue} if(t.k!=="eip155")continue; const std=t.p==="Native"||t.p==="ERC-20"?"ft":t.p==="ERC-721"?"nft":""; if(!std)continue; if(!/^0x[0-9a-f]{40}$/i.test(t.a||""))continue; if(!Number.isInteger(t.d)||t.d<0||t.d>36)continue; if(next.some(x=>x.addr.toLowerCase()===t.a.toLowerCase()))continue; const sym=safeSym(t.s).slice(0,12); const logo=safeUrl(t.l); next.push({sym,addr:t.a.toLowerCase(),dec:t.d,std,icon:logo?`<img src="${logo}" width="20" height="20">`:genIcon(sym), name:String(t.n??"").slice(0,64),desc:String(t.desc??"").slice(0,240), v4:parseV4Pools(t.e,t.a)}); } next.sort((a,b)=>(a.std==="nft")-(b.std==="nft")); if(!next.some(t=>t.std==="ft"))return setListSource("registry returned nothing swappable"); const mine=TOKENS.filter(t=>t.cust&&!next.some(x=>x.addr===t.addr.toLowerCase())); const anchors=BUILTIN.filter(b=>(b.addr===ZERO||b.addr===WETH)&&!next.some(x=>x.addr===b.addr)); TOKENS.splice(0,TOKENS.length,...next,...anchors,...mine); setListNote(rankFellBack?"Ranked by listing order — conviction unavailable":""); }catch(e){setListSource("could not be reached")} } const strip0x=h=>h.startsWith("0x")?h.slice(2):h; const pad32=h=>strip0x(h).padStart(64,"0"); const encUint=v=>pad32(BigInt(v).toString(16)); const encAddr=a=>pad32(a.toLowerCase()); const isAddr=a=>/^0x[0-9a-fA-F]{40}$/.test(a||""); const decStr=hex=>{const h=strip0x(hex);const n=parseInt(h.slice(64,128),16); return new TextDecoder().decode(hexToBytes(h.slice(128,128+n*2)))}; const decUintArr=hex=>{const h=strip0x(hex);const n=parseInt(h.slice(64,128),16);const o=[]; for(let i=0;i<n;i++)o.push(BigInt("0x"+h.slice(128+i*64,192+i*64)));return o}; const encBool=b=>pad32(b?"1":"0"); const toHex=v=>"0x"+BigInt(v).toString(16); const parseUnits=(s,dec)=>{ const t=s.trim().replace(/,/g,""); if(!/^(?:\d+|\d*\.\d+|\d+\.)$/.test(t))throw Error("invalid amount"); const[ip,fp=""]=t.split("."); if(fp.length>dec)throw Error("too many decimals"); return BigInt((ip||"0")+fp.padEnd(dec,"0")); }; const formatUnits=(v,dec)=>{ if(!dec)return BigInt(v).toString(); const s=BigInt(v).toString().padStart(dec+1,"0"); const ip=s.slice(0,-dec); const fp=s.slice(-dec).replace(/0+$/,""); return fp?`${ip}.${fp}`:ip; }; const trimAmt=(v,dec)=>{const s=formatUnits(v,dec),i=s.indexOf(".");if(i<0)return s;const r=s.slice(0,i+7).replace(/\.?0+$/,"");return r==="0"&&v?s:r}; const maxAmt=(v,dec)=>{v=BigInt(v);if(dec<=6)return formatUnits(v,dec); const u=10n**BigInt(dec-6);return formatUnits((v+u-1n)/u,6)}; const fitFont=el=>{ const l=el.value.length,s=l>18?".9em":l>14?"1.1em":l>10?"1.25em":"1.65em"; if(el.style.fontSize!==s)el.style.fontSize=s; }; const decQ=(hex,slip,eo,u,v,S,mv)=>{ const h=strip0x(hex),num=i=>BigInt("0x"+h.slice(i*64,(i+1)*64)),E=()=>{throw Error("bad quote")}; if(h.length<(v+2)*64)E(); const o=Number(num(v))*2;if(o+64>h.length)E(); const n=Number(BigInt("0x"+h.slice(o,o+64)));if(!n||o+64+n*2>h.length)E(); const g=num(u+3),t=g>0n,k=S?(num(3)>0n?0:u):t?u:0,B=10000n,ai=S?num(2)+num(u+2):num(2),ao=S?num(3)+g:t?g:num(3),q=eo?ai:ao; const src=[];for(let w=0;w<=u;w+=4)if(num(w+2)>0n||num(w+3)>0n)src.push(Number(num(w))); return{best:{source:Number(num(k)),feeBps:num(k+1),amountIn:ai,amountOut:ao},sources:src,callData:"0x"+h.slice(o+64,o+64+n*2),amountLimit:eo?(q*(B+slip)+B-1n)/B:q*(B-slip)/B,msgValue:num(mv||v+1),slipBps:slip}; }; const merge=(est,exe)=>{if(!est)return exe;if(!exe)return null;return{...exe,best:est.best}}; const NO_WALLET="No wallet detected."; const rpc=(method,params)=>window.ethereum ?window.ethereum.request({method,params}) :Promise.reject(Object.assign(Error(NO_WALLET),{noWallet:true})); const rpcRead=async(method,params,tries=4)=>{ let last; for(let i=0;i<tries;i++){ try{return await rpc(method,params)} catch(e){last=e;await new Promise(r=>setTimeout(r,300*(i+1)))} } throw last; }; const okRet=h=>{h=strip0x(h);if(h&&BigInt("0x"+h)===0n)throw Error("token returned false")}; const waitTx=async hash=>{ const end=Date.now()+600000; let misses=0,wait=1500; while(Date.now()<end){ try{ const r=await rpc("eth_getTransactionReceipt",[hash]); misses=0; if(r){ if(r.status!=="0x1")throw Error("tx reverted: "+hash); return r; } wait=Math.min(wait+500,5000); }catch(e){ if(/tx reverted/.test(e.message||""))throw e; if(++misses>2)wait=Math.min(wait*2,15000); } await new Promise(res=>setTimeout(res,wait)); } throw Error("tx not mined in time"); }; const checkWallet=async()=>{if(!account)throw Error("Connect a wallet first.");const[c,a]=await Promise.all([rpc(I,[]),rpc("eth_accounts",[])]);if(+c!==1||a[0]?.toLowerCase()!==account.toLowerCase())throw Error("wallet changed")}; const hexToBytes=h=>{const s=strip0x(h);const o=new Uint8Array(s.length/2);for(let i=0;i<o.length;i++)o[i]=parseInt(s.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=n=>{let nd="0x"+"0".repeat(64); if(n)for(const l of n.split(".").reverse())nd=keccak(new Uint8Array([...hexToBytes(nd),...hexToBytes(keccak(new TextEncoder().encode(l)))])); return nd}; const encStr=v=>{const b=new TextEncoder().encode(v);let h=""; for(const x of b)h+=x.toString(16).padStart(2,"0"); return pad32("20")+encUint(b.length)+h.padEnd(Math.ceil(h.length/64)*64,"0")}; const retAddr=h=>{const s=strip0x(h||""); if(s.length!==64||/[^0-9a-fA-F]/.test(s)||/[^0]/.test(s.slice(0,24)))return""; const a="0x"+s.slice(24).toLowerCase(); return /^0x0{40}$/.test(a)?"":a}; const nameOk=n=>/^[\x21-\x7e]+$/.test(n)&&n.split(".").every(l=>l.length>0); const nsFwd=async(reg,n)=>{const id=await rpc(C,[{to:reg,data:"0x"+SEL_CID+encStr(n)},L]); if(strip0x(id||"").length!==64)return""; return retAddr(await rpc(C,[{to:reg,data:"0x"+SEL_RES+strip0x(id)},L]))}; 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 encBytes=h=>{h=strip0x(h);return encUint(h.length/2)+h.padEnd(Math.ceil(h.length/64)*64,"0")}; const encResolve=(dns,inner)=>{const a=encBytes(dns); return pad32("40")+encUint(64+a.length/2)+a+encBytes(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=> rpc(C,[{to:ENSREG,data:"0x"+SEL_RSLV+pad32(nd)},L]).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 s=await rpc(C,[{to:r,data:supports(SEL_ERESOLVE)},L]); return strip0x(s||"").length===64&&BigInt(s)===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 rpc(C,[{to:f.r,data:inner},L])); if(!await ensWild(f.r))return""; try{return retAddr(decBytes(await rpc(C,[{to:f.r, data:"0x"+SEL_ERESOLVE+encResolve(dnsEnc(n),inner)},L])))} 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 rpc(C,[{to:r,data:"0x"+SEL_ENAME+pad32(nd)},L])).trim()}catch{return""}}; const nameFwd=n=>/\.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=>rpc(C,[c,L]).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()===a.toLowerCase()?n:""}; return await agrees(str(w))||await agrees(str(g)) ||await agrees(await ensRevName(nd,retAddr(er)))||""}; const decodeString=hex=>{ const h=strip0x(hex); 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; return new TextDecoder().decode(hexToBytes(h.slice(off+64,off+64+len))); }; const kecStr=v=>keccak(new TextEncoder().encode(v)); const ckAddr=a=>{const s=strip0x(a); if(s===s.toLowerCase()||s===s.toUpperCase())return true; const h=strip0x(kecStr(s.toLowerCase())); for(let i=0;i<40;i++){const c=s[i]; if(c>="a"&&c<="f"&&parseInt(h[i],16)>=8)return false; if(c>="A"&&c<="F"&&parseInt(h[i],16)<8)return false} return true}; const domainSep=(name,ver,tok)=>keccak(hexToBytes( kecStr(EIP712DOM)+pad32(kecStr(name))+pad32(kecStr(ver))+encUint(CHAIN_ID)+encAddr(tok))); const permitInfo=async(tok,owner)=>{ try{ const[ds,th,nc,nm]=await mc3Deep([ {to:tok,data:"0x"+SEL_DS},{to:tok,data:"0x"+SEL_PERMIT_TH}, {to:tok,data:"0x"+SEL_NONCES+encAddr(owner)},{to:tok,data:"0x"+SEL_NAME}]); if(!ds||ds==="0x")return null; if(th&&strip0x(th).length>=64&&strip0x(th).slice(0,64).toLowerCase()!==TH_2612)return null; if(!nc||!nm)return null; const nonce=BigInt(nc); const name=decodeString(nm); for(const ver of ["1","2"])if(domainSep(name,ver,tok).toLowerCase()===ds.toLowerCase()) return{name,ver,nonce}; }catch{} 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=6; 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); } }; const REF_N=100n; 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=+form
0xacfba7ce…4082·#25,751,342·0x8981a598…09da86
lqPanel" class="hide"><div class="hdr"><small>Liquidity</small><small id="lqSub"></small></div><div id="lqList"></div></div> <div id="lnPanel" class="hide"> <div class="hdr lnhdr"><small>Launch a coin</small><small id="lnSub">one transaction, no ether needed</small></div> <label class="dly">Name<input id="lnName" maxlength="64" placeholder="Zero Cat" autocomplete="off" spellcheck="false"></label> <label class="dly">Symbol<input id="lnSym" maxlength="16" placeholder="ZCAT" autocomplete="off" spellcheck="false"></label> <label class="dly">Supply<input id="lnSupply" inputmode="decimal" value="1,000,000,000" autocomplete="off" spellcheck="false"></label> <div class="lngap">Market</div> <label class="dly" title="The pool holds NO ether at launch - this is its virtual ether reserve, and it is the only thing deciding where on the curve your market opens. It sets the opening price and how hard that price is to move, which are the same knob: roughly this much buying moves the market halfway. Small numbers mean the first buyer takes a huge share of the supply.">Starting market cap<span class="lnu"><input id="lnMcap" inputmode="decimal" value="30" autocomplete="off" spellcheck="false">ETH</span></label> <label class="dly" title="The percentage of total supply paid to the creator at launch. Everything else is seeded into the pool. Capped at 20%.">Creator keeps<span class="lnu"><input id="lnAlloc" inputmode="decimal" value="0" autocomplete="off" spellcheck="false">%</span></label> <div class="lngap">Optional</div> <label class="dly" id="lnArtRow" title="Held on-chain as contract code, not as a link. Nothing to pin and no gateway to go dark. Editable later, so a rough mark now costs nothing.">Logo<span><img id="lnPrev" alt=""><small id="lnArtNote">none</small><button type="button" id="lnPick" class="lnbtn">Choose</button></span></label> <input id="lnArt" type="file" accept="image/png,image/jpeg,image/webp,image/gif,image/svg+xml" class="hide"> </div> <input id="rc" class="rcpt" placeholder="Recipient (optional) — 0x or .eth/.(g)wei" spellcheck="false" autocomplete="off"> <small id="rcvEl"></small> <small id="lnNote" class="hide" role="status" aria-live="polite"></small> <button id="lnGo" class="primary hide">Launch</button> <label class="dly hide" id="kindL">Order type<select id="kind"><option value="fixed">Fixed limit</option><option value="dutch">Dutch decay</option><option value="floor">Climbing bid</option></select></label> <label class="dly hide" id="dlyL">SLOW<select id="dly"><option value="0">Instant</option><option value="3600">1 hour</option><option value="86400">1 day</option><option value="259200">3 days</option><option value="604800">7 days</option></select></label> <label class="dly hide" id="tipL" title="A time-locked transfer does not move on its own: once the lock expires somebody has to send the claim transaction. Tick this and the page posts a small ETH tip alongside the deposit, which pays a keeper to claim it for the recipient the moment it matures. The tip is refundable — if you reverse the transfer, the ether comes back to you.">Auto-claim<span class="tipr"><small id="tipNote"></small><input id="tipCk" type="checkbox" aria-describedby="tipNote"></span></label> <label class="dly hide" id="fillL">Fill<select id="fill"><option value="1">Any amount</option><option value="0">All or nothing</option></select></label> <label class="dly hide" id="floorL">Floor total<input id="floorAmt" inputmode="decimal" placeholder="0.0"></label> <label class="dly hide" id="dRestL" title="A Dutch lot has TWO clocks. It decays to your floor over the window above, and then - unless it is given an end - it sits there fillable forever, until you spend gas cancelling. This is how long it rests at the floor before the escrow simply comes back to you.">Rest at floor<select id="dRest"><option value="0">Forever</option><option value="86400">1 day</option><option value="604800">1 week</option><option value="2592000">30 days</option></select></label> <label class="dly hide" id="nftIdL">Token ID<input id="nftId" inputmode="numeric" placeholder="Any from collection" spellcheck="false" autocomplete="off"></label> <label class="dly hide" id="ethModeL" title="Applies when YOU fill an order from the book, or cancel one of yours and the escrow comes back wrapped. It does NOT change what an order you place pays you: boards are denominated in WETH, and a maker's proceeds are transferred as WETH with no unwrap available - so a limit order asking for ETH is an order asking for WETH, and fills as such. Paying is not a setting either: the page spends whichever of your ETH and WETH covers it, and wraps the difference if only the two together do.">Fills &amp; cancels pay<select id="ethMode"><option value="auto">ETH</option><option value="weth">WETH</option></select></label> <small id="floorNote" class="hide" role="status" aria-live="polite"></small> <button id="swap" class="primary">Connect Wallet</button> <div id="listNote" class="hide" role="status" aria-live="polite"></div> <div id="tkPanel" class="tkp hide"><input id="tkFind" type="text" placeholder="Search name, symbol or address" autocomplete="off" spellcheck="false" aria-label="Filter tokens"><div id="tkList" role="listbox"></div></div> <div id="stat" role="status" aria-live="polite"></div> <button id="chTog" class="chtog hide" aria-expanded="false">Chart <span>▾</span></button> <div id="chBox" class="hide"><div class="chtf" id="chTf"></div><div id="chArt"></div><small id="chNote"></small></div> <div id="pos"></div> <button id="bkTog" class="chtog hide" aria-expanded="true">Orderbook <span>▾</span></button> <div id="book"></div> <div id="docPanel" class="docs hide"> <details><summary>Swapping</summary> <p>Every quote is compared across Uniswap v2, v3 and v4, Curve, Sushi and our own Precision pools, and the best one wins. One route, one signature.</p> <p><b>Slippage</b> is the worst price you will accept &mdash; move past it and the trade reverts rather than filling badly. <b>Price impact</b> is how far your own size moves the price; the page warns as it grows and will not let a very bad one through quietly.</p> <p>ETH and WETH are interchangeable here. The page wraps or unwraps whichever way the pool needs, inside the same transaction.</p></details> <details><summary>Sending</summary> <p>A plain transfer, or a <b>SLOW</b> one: time-locked, and reversible by you at any point until it matures. Useful for large amounts and for addresses you have not paid before.</p> <p>A locked transfer does not deliver itself &mdash; someone must send the claim once it matures. Tick <b>auto-claim</b> and the page posts a small tip so a keeper does that for the recipient. The tip is refundable: reverse the transfer and it comes back.</p></details> <details><summary>Orders</summary> <p><b>Fixed limit</b> &mdash; a price you name. It rests until someone fills it or you cancel.</p> <p><b>Dutch decay</b> &mdash; starts above your floor and falls to it over a window you choose. It has two clocks: the decay, then how long it rests at the floor before the escrow returns to you. Set that to <em>Forever</em> and it waits until you cancel.</p> <p><b>Climbing bid</b> &mdash; the mirror: a bid that rises toward a ceiling you name.</p> <p>Orders are escrowed on chain when placed, so a fill cannot fail for want of funds, and cancelling returns the escrow. Boards are denominated in WETH &mdash; an order asking for ETH is asking for WETH, and pays out as such.</p></details> <details><summary>Liquidity <em>the droplet</em></summary> <p>Precision pools are <b>bands</b>, not endless curves. A position earns fees only while the price is inside its band, and holds one asset or the other at the edges. A wider band is safer and earns less; a narrow one is the opposite.</p> <p>The bar on each row shows where the price sits in that band. <em>Out of range</em> means it is earning nothing right now.</p></details> <details><summary>Launching a coin <em>the coin</em></summary> <p>One transaction, and no ether needed aside from gas. A fixed supply is minted, your allocation is paid to you immediately, and <b>everything else is seeded into a fresh ETH pool</b> &mdash; one-sided, so the pool opens holding only your token.</p> <p><b>Starting market cap</b> is the pool's virtual ether reserve. It sets the opening price and how hard that price is to move, which are the same dial: roughly that much buying moves the market halfway. Small numbers mean the first buyer takes a large share of the supply, which is why the form says what one ether would buy.</p> <p><b>Creator keeps</b> is a share of the token SUPPLY, minted straight to you when the coin launches &mdash; tokens, not ether, and not a fee on trading. Keep 10% of a billion and 100,000,000 land in your wallet at once, unlocked, with nothing vesting.</p> <p>Whatever you keep does not go into the pool, and selling it later pulls ether back out of that pool &mdash; so a modest-looking percentage can be a claim on most of what early buyers put in. Anyone deciding whether to buy your coin can work that out too.</p> <p>A <b>logo</b> is stored as contract code on Ethereum &mdash; no IPFS, no pinning service, nothing to go dark. Upload anything; the page compresses it and shows the size before you sign. You can replace it later.</p></details> <details><summary>Fees</summary> <p>A launched market charges <b>1%</b> per swap. Half of that stays in the pool as reserves, which raises the floor for every holder. The other half is collectable, and splits <b>80% creator, 10% protocol, 10% tithe</b>.</p> <p>The <b>tithe</b> is burned to Ethereum itself. The ether goes to the BETH burner, which destroys it and mints a BETH receipt recording that it happened. Nobody can spend it and it cannot be redeemed &mdash; the ether no longer exists. What it buys is a smaller ether supply, which accrues to everyone holding ether rather than to us.</p> <p>The token side of collected fees is burned outright too &mdash; a launched coin's supply only ever falls.</p></details> <details><summary>What this page is</summary> <p>There is no server. This page is a contract on Ethereum and its bytes cannot be changed by anyone, us included &mdash; a new version is a new address, not an edit. It talks to your wallet and to Ethereum, and to nothing else.</p> <p>Your keys never leave your wallet. Nothing here custodies your funds; every trade settles from your address to a contract you can read.</p> <p>The token list is <b>zList</b>, ranked on chain by conviction staking. Anything not on it can still be traded by pasting its address.</p></details> </div> <footer id="foot"><span id="footV"></span><span id="footAddr"></span><span>&middot; <a id="footDoc" href="#" role="button">how it works</a></span><span>&middot; <a id="footList" target="_blank" rel="noopener" title="Token order here comes from zList, ranked by onchain conviction staking. Stake to move a token up the list - or to get one listed at all.">token list</a></span></footer> </div> <script>const IC0='<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">', ICMOON=IC0+'<path d="M20.5 14.6A8.6 8.6 0 0 1 9.4 3.5a8.6 8.6 0 1 0 11.1 11.1z"/></svg>', ICSUN=IC0+'<circle cx="12" cy="12" r="4.2"/><path d="M12 2v2m0 16v2M4.2 4.2l1.5 1.5m12.6 12.6 1.5 1.5M2 12h2m16 0h2M4.2 19.8l1.5-1.5M18.3 5.7l1.5-1.5"/></svg>'; const ic=()=>{const d=H.className=='d';th.innerHTML=d?ICSUN:ICMOON;tc.content=d?'#101010':'#fafafa'};ic(); const ZSWAP_VERSION="0.1"; const ZSWAP_SELF=""; const ZSWAP_PREVIOUS=""; const ZLIST_URL="https://0x0000006d936ba3653b8854490e16e782cd32a9a8.w4eth.io/"; const selfFromUrl=()=>{ const h=(location.hostname||"").split("."); if(!/^0x[0-9a-fA-F]{40}$/.test(h[0]||""))return ""; if(/^\d+$/.test(h[1]||"")&&h[1]!=="1")return ""; return h[0]; }; const short=a=>a.slice(0,6)+"\u2026"+a.slice(-4); (()=>{ const self=selfFromUrl()||ZSWAP_SELF; footV.textContent=`zSwap v${ZSWAP_VERSION}`; footList.href=ZLIST_URL; if(self){ footAddr.innerHTML=`\u00b7 <a href="https://etherscan.io/address/${self}#code" target="_blank" rel="noopener" ` +`title="This exact build, on chain. Its bytes cannot change; a newer version is a different address.">${short(self)}</a>`; if(ZSWAP_PREVIOUS)footAddr.innerHTML+=` \u00b7 <a href="https://etherscan.io/address/${ZSWAP_PREVIOUS}#code" ` +`target="_blank" rel="noopener" title="The version this one succeeded. It still serves its own page, unchanged.">prev</a>`; } })(); th.onclick=()=>{H.classList.toggle('d');LS.t=H.className=='d'?'d':'l';ic()}; const ZQUOTER="0xc7a03f9ed2be5feea18ce93e12f4f05c98287c16"; const WNS="0x0000000000696760E15f265e828DB644A0c242EB"; const GNS="0x9D51D507BC7264d4fE8Ad1cf7Fe191933A0a81d6"; const ENSREG="0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e"; const ZROUTER="0x000000000000FB114709235f1ccBFfb925F600e4"; const PFACTORY="0x000000Eb27B557aB426d9E99cFd54EC455799e81"; const PLQLENS="0x000000956bf20A41C54BaE4a4b6F5C8A166DAB4E"; const PPLENS="0x000000Bad3a2fa57ed74fa06000573ccddF6B7fB"; const PI_WORDS=19,PI_POOL=0,PI_LIQ=9,PI_HOOK=10,PI_SL=3,PI_SH=4, PI_FEE=5,PI_R0=6,PI_R1=7,PI_PX=8; const CHAIN_ID=1; const ZERO="0x0000000000000000000000000000000000000000"; const WETH="0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"; const TOKENLIST="0x0000006013dF75A31678B786061C2B54bf531524"; const ZLISTLENS="0x000000cEa3AB048d59473F3fb116A8D7F1abd247"; const SEL_RANKEDIDS="df7ca268"; const SEL_JSON="74e18e96"; const SEL_CID="fb021939",SEL_RES="4f896d4f",SEL_REV="9af8b7aa"; const SEL_RSLV="0178b8bf",SEL_EADDR="3b3b57de",SEL_ENAME="691f3431"; const SEL_ERESOLVE="9061b923"; const NAME_OFFCHAIN="That name keeps its records off chain, and this page reads only on-chain state."; const SEL_ALLOWANCE="dd62ed3e"; const SEL_LATEST="52bfe789",SEL_PREV="247dfaa8",SEL_SUCCAT="451aae60"; const MATURITY=259200; const SEL_DS="3644e515",SEL_NONCES="7ecebe00",SEL_NAME="06fdde03",SEL_PERMIT_TH="30adf81f"; const TH_2612="6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9"; const SEL_RPERMIT="7ac2ff7b",SEL_MULTICALL="ac9650d8",SEL_SWEEP="cb019b84"; const PERMIT2="0x000000000022D473030F116dDEE9F6B43aC78BA3"; const SEL_P2TF="09d31579"; const EIP712DOM="EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"; const SEL_APPROVE="095ea7b3"; const SEL_BALANCEOF="70a08231"; const V4PORT="0x000000dfb53Fa7f1c486470034741d5BCBE14BE9"; const V4LENS="0x000000c3aE1692983941495162A4AAB40660E65F"; const SEL_V4QUOTE="d500463c",SEL_V4SWAP="48e6f730"; const V4POOL_KEY="0x95a932c205571d4d1ca72715c642a2eca21dde79ffc28ff11509681f9383385f"; const C="eth_call",S="eth_sendTransaction",L="latest",I="eth_chainId"; const SOURCES=["UniV2","Sushi","zAMM","UniV3","UniV4","Curve","Lido","Wrap","Precision"]; const SRC_PRECISION=8; const ETH_ICON=`<svg width="20" height="20" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" r="16" fill="#627EEA"/><g fill="#FFF" fill-rule="nonzero"><path fill-opacity=".602" d="M16.498 4v8.87l7.497 3.35z"/><path d="M16.498 4L9 16.22l7.498-3.35z"/><path fill-opacity=".602" d="M16.498 21.968v6.027L24 17.616z"/><path d="M16.498 27.995v-6.028L9 17.616z"/><path fill-opacity=".2" d="M16.498 20.573l7.497-4.353-7.497-3.348z"/><path fill-opacity=".602" d="M9 16.22l7.498 4.353v-7.701z"/></g></g></svg>`; const USDC_ICON=`<svg width="20" height="20" viewBox="0 0 32 32"><g fill="none"><circle fill="#2775CA" cx="16" cy="16" r="16"/><g fill="#FFF"><path d="M20.022 18.124c0-2.124-1.28-2.852-3.84-3.156-1.828-.243-2.193-.728-2.193-1.578 0-.85.61-1.396 1.828-1.396 1.097 0 1.707.364 2.011 1.275a.458.458 0 00.427.303h.975a.416.416 0 00.427-.425v-.06a3.04 3.04 0 00-2.743-2.489V9.142c0-.243-.183-.425-.487-.486h-.915c-.243 0-.426.182-.487.486v1.396c-1.829.242-2.986 1.456-2.986 2.974 0 2.002 1.218 2.791 3.778 3.095 1.707.303 2.255.668 2.255 1.639 0 .97-.853 1.638-2.011 1.638-1.585 0-2.133-.667-2.316-1.578-.06-.242-.244-.364-.427-.364h-1.036a.416.416 0 00-.426.425v.06c.243 1.518 1.219 2.61 3.23 2.914v1.457c0 .242.183.425.487.485h.915c.243 0 .426-.182.487-.485V21.34c1.829-.303 3.047-1.578 3.047-3.217z"/><path d="M12.892 24.497c-4.754-1.7-7.192-6.98-5.424-11.653.914-2.55 2.925-4.491 5.424-5.402.244-.121.365-.303.365-.607v-.85c0-.242-.121-.424-.365-.485-.061 0-.183 0-.244.06a10.895 10.895 0 00-7.13 13.717c1.096 3.4 3.717 6.01 7.13 7.102.244.121.488 0 .548-.243.061-.06.061-.122.061-.243v-.85c0-.182-.182-.424-.365-.546zm6.46-18.936c-.244-.122-.488 0-.548.242-.061.061-.061.122-.061.243v.85c0 .243.182.485.365.607 4.754 1.7 7.192 6.98 5.424 11.653-.914 2.55-2.925 4.491-5.424 5.402-.244.121-.365.303-.365.607v.85c0 .242.121.424.365.485.061 0 .183 0 .244-.06a10.895 10.895 0 007.13-13.717c-1.096-3.46-3.778-6.07-7.13-7.162z"/></g></g></svg>`; const USDT_ICON=`<svg width="20" height="20" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" r="16" fill="#26A17B"/><path fill="#FFF" d="M17.922 17.383v-.002c-.11.008-.677.042-1.942.042-1.01 0-1.721-.03-1.971-.042v.003c-3.888-.171-6.79-.848-6.79-1.658 0-.809 2.902-1.486 6.79-1.66v2.644c.254.018.982.061 1.988.061 1.207 0 1.812-.05 1.925-.06v-2.643c3.88.173 6.775.85 6.775 1.658 0 .81-2.895 1.485-6.775 1.657m0-3.59v-2.366h5.414V7.819H8.595v3.608h5.414v2.365c-4.4.202-7.709 1.074-7.709 2.118 0 1.044 3.309 1.915 7.709 2.118v7.582h3.913v-7.584c4.393-.202 7.694-1.073 7.694-2.116 0-1.043-3.301-1.914-7.694-2.117"/></g></svg>`; const WBTC_ICON=`<svg width="20" height="20" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" r="16" fill="#F7931A"/><path fill="#FFF" fill-rule="nonzero" d="M23.189 14.02c.314-2.096-1.283-3.223-3.465-3.975l.708-2.84-1.728-.43-.69 2.765c-.454-.114-.92-.22-1.385-.326l.695-2.783L15.596 6l-.708 2.839c-.376-.086-.746-.17-1.104-.26l.002-.009-2.384-.595-.46 1.846s1.283.294 1.256.312c.7.175.826.638.805 1.006l-.806 3.235c.048.012.11.03.18.057l-.183-.045-1.13 4.532c-.086.212-.303.531-.793.41.018.025-1.256-.313-1.256-.313l-.858 1.978 2.25.561c.418.105.828.215 1.231.318l-.715 2.872 1.727.43.708-2.84c.472.127.93.245 1.378.357l-.706 2.828 1.728.43.715-2.866c2.948.558 5.164.333 6.097-2.333.752-2.146-.037-3.385-1.588-4.192 1.13-.26 1.98-1.003 2.207-2.538zm-3.95 5.538c-.533 2.147-4.148.986-5.32.695l.95-3.805c1.172.293 4.929.872 4.37 3.11zm.535-5.569c-.487 1.953-3.495.96-4.47.717l.86-3.45c.975.243 4.118.696 3.61 2.733z"/></g></svg>`; const WSTETH_ICON=`<svg width="20" height="20" viewBox="0 0 32 32"><g fill="none"><circle fill="#00A3FF" cx="16" cy="16" r="16"/><path d="M9.437 14.864l-.181.275c-2.048 3.097-1.603 7.253 1.034 9.824 1.561 1.521 3.622 2.353 5.683 2.353 0 0 0 0-6.536-12.452z" fill="#FFF"/><path opacity=".6" d="M15.997 18.611l-6.56-3.747c6.56 12.452 6.56 12.452 6.56 12.452 0-2.683 0-5.623 0-8.705z" fill="#FFF"/><path opacity=".6" d="M22.563 14.864l.181.275c2.048 3.097 1.603 7.253-1.034 9.824-1.561 1.521-3.622 2.353-5.683 2.353 0 0 0 0 6.536-12.452z" fill="#FFF"/><path opacity=".2" d="M16.003 18.611l6.56-3.747c-6.56 12.452-6.56 12.452-6.56 12.452 0-2.683 0-5.623 0-8.705z" fill="#FFF"/><path opacity=".2" d="M16.004 10.239v6.459l5.654-3.23-5.654-3.229z" fill="#FFF"/><path opacity=".6" d="M16.005 10.239l-5.655 3.229 5.655 3.23v-6.46z" fill="#FFF"/><path d="M16.005 4.805l-5.655 8.668 5.655-3.233V4.805z" fill="#FFF"/><path opacity=".6" d="M16.004 10.238l5.658 3.23-5.658-8.674v5.444z" fill="#FFF"/></g></svg>`; const RETH_ICON=`<svg width="20" height="20" viewBox="0 0 32 32"><circle cx="16" cy="16" r="16" fill="#ED5A37"/><path d="M16 5.75L9.89 15.85 16 19.45l6.08-3.6z" fill="#FFF"/><path d="M16 20.6l-6.11-3.59L16 25.58l6.08-8.57z" fill="#FFF"/></svg>`; const BOLD_ICON=`<svg width="20" height="20" viewBox="0 0 32 32" fill="none"><circle cx="16" cy="16" r="16" fill="#63D77D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M12.1719 4.56641H8.58203V26.1016H15.7617V25.2422C16.8398 25.793 18.0586 26.1055 19.3555 26.1055C23.7148 26.1055 27.25 22.5703 27.25 18.207C27.25 13.8438 23.7148 10.3086 19.3555 10.3086C18.0586 10.3086 16.8398 10.6211 15.7617 11.1719V4.56641H12.1719ZM15.7617 11.1719C13.207 12.4805 11.457 15.1406 11.457 18.207C11.457 21.2734 13.207 23.9336 15.7617 25.2422V11.1719Z" fill="#1C1D4F"/></svg>`; const TOKENS=[ {sym:"ETH", addr:ZERO, dec:18, icon:ETH_ICON}, {sym:"WETH", addr:WETH, dec:18, icon:ETH_ICON}, {sym:"wstETH",addr:"0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0",dec:18,icon:WSTETH_ICON}, {sym:"rETH", addr:"0xae78736cd615f374d3085123a210448e74fc6393",dec:18,icon:RETH_ICON}, {sym:"WBTC", addr:"0x2260fac5e5542a773aa44fbcfedf7c193bc2c599",dec:8, icon:WBTC_ICON}, {sym:"USDC", addr:"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",dec:6, icon:USDC_ICON}, {sym:"USDT", addr:"0xdac17f958d2ee523a2206206994597c13d831ec7",dec:6, icon:USDT_ICON}, {sym:"BOLD", addr:"0x6440f144b7e50d6a8439336510312d2f54beb01d",dec:18,icon:BOLD_ICON}, ]; const BUILTIN=TOKENS.slice(); function parseV4Pools(extras,tokenAddr){ if(!Array.isArray(extras)||!/^0x[0-9a-f]{40}$/i.test(tokenAddr||""))return[]; const row=extras.find(x=>x&&typeof x.v==="string"&&String(x.k).toLowerCase()===V4POOL_KEY); if(!row)return[]; const out=[]; for(const spec of row.v.split(";")){ const p=spec.trim().split(":"); if(p.length!==5||p[0]!=="v1")continue; const other=p[1]==="0"?ZERO:p[1]; const hooks=p[4]==="0"?ZERO:p[4]; if(!/^0x[0-9a-f]{40}$/i.test(other)||!/^0x[0-9a-f]{40}$/i.test(hooks))continue; if(!/^\d{1,7}$/.test(p[2])||!/^\d{1,7}$/.test(p[3]))continue; const fee=Number(p[2]),ts=Number(p[3]); if(!Number.isInteger(fee)||!Number.isInteger(ts)||ts<1)continue; if(other.toLowerCase()===tokenAddr.toLowerCase())continue; const a=tokenAddr.toLowerCase(),b=other.toLowerCase(); const zeroFirst=BigInt(a)<BigInt(b); out.push({c0:zeroFirst?tokenAddr:other,c1:zeroFirst?other:tokenAddr,fee,ts,hooks}); if(out.length===8)break; } return out; } function v4PoolsFor(f,t){ const fa=f.addr.toLowerCase(),ta=t.addr.toLowerCase(); const seen=new Set(),out=[]; for(const tok of [f,t])for(const p of (tok.v4||[])){ const c0=p.c0.toLowerCase(),c1=p.c1.toLowerCase(); if(!((c0===fa&&c1===ta)||(c0===ta&&c1===fa)))continue; const id=`${c0}:${c1}:${p.fee}:${p.ts}:${p.hooks.toLowerCase()}`; if(seen.has(id))continue; seen.add(id); out.push({...p,zeroForOne:c0===fa}); } return out; } const encPoolKey=p=>encAddr(p.c0)+encAddr(p.c1)+encUint(p.fee)+encUint(p.ts)+encAddr(p.hooks); const SEL_QUOTE_BEST="2adaa389",SEL_EFFFEE="ef66de32"; const PROUTE="0x0000007Be74558A1F8c9045301c6F44C8eD0c9eB"; const SEL_PROUTE="5d6498e1",SEL_PCHECKPOINT="0b7c6c6c"; const SEL_PROUTEWETH="78fd78b0"; const SEL_ZAPIN="c98c2c0b"; const SEL_PREVIEW_ZAP="e7cddab0"; const encPRouteWeth=(pools,tokenOut,amountIn,minOut,to)=> "0x"+SEL_PROUTEWETH+encUint(160)+encAddr(tokenOut)+encUint(amountIn) +encUint(minOut)+encAddr(to)+encUint(pools.length)+pools.map(encAddr).join(""); const encPRoute=(pools,tokenIn,tokenOut,amountIn,minOut,to)=> "0x"+SEL_PROUTE+encUint(192)+encAddr(tokenIn)+encAddr(tokenOut)+encUint(amountIn) +encUint(minOut)+encAddr(to)+encUint(BigInt(pools.length))+pools.map(encAddr).join(""); const SEL_PAIR_COUNT="355da246"; const LQ_SCAN=128; const LQ_SHOW=24; async function quotePrecision(f,t,amountIn,sender,block){ if(PPLENS===ZERO||amountIn<=0n)return null; const nat=a=>a.toLowerCase()===WETH.toLowerCase()?ZERO:a; const vars=[]; const addVar=(a,b)=>{ const [x,y]=a.toLowerCase()<b.toLowerCase()?[a,b]:[b,a]; if(!vars.some(v=>v.c0===x&&v.c1===y&&v.tokenIn===a))vars.push({c0:x,c1:y,tokenIn:a}); }; const forms=a=>a===ZERO?[ZERO,WETH] :a.toLowerCase()===WETH.toLowerCase()?[WETH,ZERO]:[a]; for(const a of forms(f.addr))for(const b of forms(t.addr)){ if(nat(a)===nat(b))continue; addVar(a,b); } const reqs=[]; for(const v of vars)reqs.push( {to:PPLENS,data:"0x"+SEL_QUOTE_BEST+encAddr(v.c0)+encAddr(v.c1) +encAddr(sender)+encAddr(v.tokenIn)+encUint(amountIn)+encUint(0n)+encUint(BigInt(LQ_SCAN))}, {to:PFACTORY,data:"0x"+SEL_PAIR_COUNT+encAddr(v.c0)+encAddr(v.c1)}); const res=await mc3Deep(reqs,block); let pool="",out=0n,n=0,fA=f.addr,won=null; for(let i=0;i<vars.length
0xacfba7ce…4082·#25,751,341·0x9f9d3b56…07af7f
<!doctype html> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>zSwap</title> <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 400' width='400' height='400'%3E%3Crect width='400' height='400' fill='%23000'/%3E%3CclipPath id='frame'%3E%3Crect width='400' height='400'/%3E%3C/clipPath%3E%3Cg clip-path='url(%23frame)'%3E%3Cpath d='M-60-20L460-20L460 90L80 310L460 310L460 420L-60 420L-60 310L320 90L-60 90Z' fill='white'/%3E%3C/g%3E%3C/svg%3E" type="image/svg+xml"> <meta name="theme-color" id="tc" content="#fafafa"> <style>:root{color-scheme:light;--b:#fafafa;--c:#fff;--p:#f6f6f6;--f:#000;--m:#6b6b6b;--n:#666;--e:#ddd;--k:#000;--kf:#fff;--kh:#333;--fh:#ebebeb;--e2:#ececec;--w:#c33;--s:0 1px 3px #0000000a,0 8px 24px #0000000f} .d{color-scheme:dark;--b:#101010;--c:#181818;--p:#242424;--f:#eee;--m:#999;--n:#999;--e:#3d3d3d;--k:#fff;--kf:#000;--kh:#ddd;--fh:#303030;--e2:#2c2c2c;--w:#e57373;--s:0 1px 2px #0000004d,0 12px 32px #00000052} :focus-visible{outline:2px solid var(--f);outline-offset:2px} @media(prefers-reduced-motion:reduce){*{transition:none!important}.flip:hover{transform:none}} body{background:var(--b);color:var(--f);font-family:system-ui,sans-serif;min-height:100dvh;margin:0;padding:1em .85em;display:flex;flex-direction:column;box-sizing:border-box} .card{border:1px solid var(--e);border-radius:.5em;padding:.9em;background:var(--c);max-width:22em;width:100%;margin:auto;box-shadow:var(--s);box-sizing:border-box} #addr{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} #addr.on{cursor:pointer} #addr.on:hover{color:var(--f);text-decoration:line-through} #logo{position:fixed;top:.85em;left:.85em;width:1.75em;height:1.75em; pointer-events:none;z-index:1} #logo rect{fill:#000} #logo path{fill:#fff} @media(max-width:32em){#logo{display:none}} .meta{display:grid;grid-template-columns:minmax(0,1fr) auto auto auto auto auto;align-items:center;gap:.4em;font-size:.85em;color:var(--n);margin-bottom:.75em} #slipL{display:inline-flex;align-items:baseline;gap:.15em;font-size:.72em;color:var(--m); letter-spacing:.04em;white-space:nowrap;margin-right:.15em} .meta input{width:3em;font-size:.9em;padding:.15em .1em;border:0;border-bottom:1px solid var(--e2);transition:border-color .15s;border-radius:0;background:transparent;text-align:right;outline:0;color:var(--f);-moz-appearance:textfield} .meta input::-webkit-outer-spin-button,.meta input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0} #slip{font-size:1.2em;width:2.4em;font-weight:600;font-variant-numeric:tabular-nums} #th,#lk,#lq,#ln{display:inline-flex;align-items:center;justify-content:center;border:0;border-radius:.35em;background:transparent;cursor:pointer;color:var(--m);opacity:.8;width:1.8em;height:1.8em;line-height:1;transition:color .15s,opacity .15s,background .15s} #th:hover,#lk:hover,#lq:hover,#ln:hover,#lq.on,#ln.on{opacity:1} #th:hover,#lk:hover,#lq:hover,#ln:hover{background:var(--p);color:var(--f)} .meta input:hover{border-bottom-color:var(--m)} .meta input:focus{border-bottom-color:currentcolor} .panel{background:var(--p);border:1px solid transparent;border-radius:.5em;padding:.82em .85em;margin:.32em 0;transition:border-color .15s} .panel:hover,.dly:hover,.rcpt:hover,.lqin:hover{border-color:var(--e)} .panel:focus-within,.dly:focus-within,.rcpt:focus-within,.lqin:focus-within{border-color:var(--f)} .panel small{color:var(--m);font-size:.8em} .hdr small{text-transform:uppercase;letter-spacing:.08em;font-size:.7em} .row{display:flex;gap:.4em;align-items:center;margin-top:.35em} .panel input{font-size:1.65em;font-weight:500;letter-spacing:-.02em;border:0;background:transparent;color:var(--f);outline:none;padding:0;flex:1;min-width:0;width:100%;font-variant-numeric:tabular-nums;transition:font-size .1s} .panel input::placeholder{color:var(--m);opacity:.32} .pill{display:inline-flex;align-items:center;gap:.4em;cursor:pointer;transition:opacity .15s} .pill:hover,.pill:focus-within{opacity:.6} .panel select,.dly select{appearance:none;-webkit-appearance:none;border:0;background:transparent url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5' viewBox='0 0 8 5'%3E%3Cpath d='M0 0h8L4 5z' fill='%23999'/%3E%3C/svg%3E") no-repeat right 0 center;padding:0 .85em 0 0;max-width:8em;font-size:.9em;font-weight:600;color:var(--f);letter-spacing:.02em;cursor:pointer;text-overflow:ellipsis} .panel select:focus{outline:0} svg{vertical-align:middle;flex-shrink:0} .flip{display:flex;align-items:center;justify-content:center;margin:-.62em auto;width:2em;height:2em;border-radius:50%;border:3px solid var(--c);background:var(--c);cursor:pointer;color:var(--m);padding:0;box-shadow:0 1px 2px #0000000f;transition:background .25s,color .25s,transform .25s} .flip:hover{background:var(--fh);color:var(--f);transform:rotate(180deg)} .primary{width:100%;box-sizing:border-box;border:1px solid transparent;min-height:3.1em;padding:.9em;overflow-wrap:anywhere;line-height:1.3;font-size:1em;border-radius:.5em;background:var(--k);color:var(--kf);cursor:pointer;margin-top:.75em;font-weight:600;text-transform:uppercase;letter-spacing:.08em;transition:background .15s,border-color .15s} .primary:hover:not(:disabled){background:var(--kh)} .primary:active:not(:disabled),.lqbtn:active:not(:disabled),.o button:active:not(:disabled),.p button:active:not(:disabled),.tabs button:active,.chtog:active,.bkc:active,.lnbtn:active{transform:translateY(1px)} .primary:disabled{background:var(--p);border-color:transparent;color:var(--m);cursor:not-allowed} .rcpt{width:100%;box-sizing:border-box;margin-top:.6em;padding:.66em .85em;border:1px solid transparent;border-radius:.5em;background:var(--p);transition:border-color .15s;color:var(--f);font-size:.75em;outline:0;font-family:inherit} #lq.on,#ln.on{background:var(--k);color:var(--kf)} .lqrow{padding:.62em .7em;background:var(--p);border:1px solid transparent;border-radius:.5em;margin:.35em 0} .lqrow.out{opacity:.55} .lqband{display:flex;align-items:center;gap:.4em;font-size:.82em} .lqfee{color:var(--m);font-size:.72em} .lqtag{font-size:.62em;padding:.1em .4em;border-radius:.35em;border:1px solid var(--e);color:var(--m)} .lqmeta{font-size:.7em;color:var(--m);margin-top:.25em;display:flex;align-items:center;gap:.4em} .lqmeta>.lqbtn,.lqmine>.lqbtn{margin-left:auto;min-width:7em} .lqmine{font-size:.7em;margin-top:.25em;display:flex;align-items:center;gap:.4em} .lqempty{padding:.9em .7em;font-size:.75em;color:var(--m);text-align:center} .lqnewtog{width:100%;margin-top:.55em} .lqnew{margin-top:.5em;display:flex;flex-direction:column;gap:.35em;text-align:left} .lqnew .dly{margin-top:.4em;gap:.6em} .lqnew .dly>select{flex:1;min-width:0;max-width:none;text-align:right;text-align-last:right} .lqnew .dly .r{display:flex;gap:.4em;align-items:center} .lqnew .dly .r input{width:6.5em} .lqout{font-size:.72em;color:var(--m);line-height:1.55;min-height:1.1em} .lqbar{position:relative;height:3px;border-radius:2px;background:var(--e);margin:.45em 0 .3em} .lqbar>i{position:absolute;top:-2.5px;width:3px;height:8px;border-radius:1px;background:var(--f); left:var(--at,50%);transform:translateX(-1.5px)} .lqbar.out>i{background:var(--m)} .lqbar>b{position:absolute;top:0;height:3px;border-radius:2px;background:var(--m);opacity:.45; left:0;right:0} .lqnew .primary{margin-top:.5em;min-height:2.6em;font-size:.85em} #listNote{max-width:22em;margin:.4em auto 0;font-size:11px;opacity:.75;text-align:center} .lqbtn{font-size:.9em;padding:.3em .7em;border-radius:.5em;cursor:pointer;border:1px solid var(--e);background:transparent;color:var(--f);font-family:inherit;font-weight:600; white-space:nowrap;flex:0 0 auto;text-align:center;transition:background .15s,color .15s,border-color .15s} .lqbtn:hover:not(:disabled),.lqbtn:focus-visible:not(:disabled){background:var(--k);color:var(--kf);border-color:var(--k)} .lqbtn:disabled{opacity:.4;cursor:not-allowed} .lqadd{margin-top:.45em;display:flex;flex-direction:column;gap:.35em} .lqin{width:100%;box-sizing:border-box;padding:.5em .65em;font:inherit;font-size:.75em;border:1px solid transparent;border-radius:.5em;background:var(--p);transition:border-color .15s;color:var(--f);outline:0} .lqzap{display:flex;align-items:center;gap:.45em;font-size:.7em;color:var(--m);cursor:pointer;margin-bottom:.1em} .lqzap input{margin:0;cursor:pointer;accent-color:var(--f)} .lqpv{font-size:.7em;opacity:.7;min-height:1.2em;line-height:1.5} .lqadd .lqbtn{margin-left:0;align-self:stretch;font-size:.75em;padding:.5em .8em} .rcpt.bad{border-color:var(--w)} #rcvEl{display:block;font-size:.7em;color:var(--m);margin-top:.35em;word-break:break-all;text-align:center} #rcvEl:empty{display:none} #stat{font-size:.8em;color:var(--n);word-break:break-all;margin-top:.75em;text-align:center} #stat:not(:empty){margin-bottom:.5em} #stat a{color:var(--f);text-decoration:underline} .hdr{display:flex;justify-content:space-between;align-items:center;gap:.5em} .tabs{display:flex;gap:.18em;margin-bottom:.75em;background:var(--p);padding:.18em;border-radius:.5em} .tabs button{flex:1;padding:.5em;font-size:.75em;font-weight:600;text-transform:uppercase;letter-spacing:.08em;border:0;border-radius:.35em;background:transparent;color:var(--m);cursor:pointer;transition:background .15s,color .15s} .tabs button.on{background:var(--k);color:var(--kf)} .tabs button:hover:not(.on),.tabs button:focus-visible:not(.on){background:var(--fh);color:var(--f)} .tabs button.on:hover,.tabs button.on:focus-visible{background:var(--kh)} .hide{display:none!important} #book{margin-top:.4em;max-height:17em;overflow-y:auto;overscroll-behavior:contain} #book:empty{display:none} #book h6{font-size:.7em;text-transform:uppercase;letter-spacing:.08em;color:var(--m);margin:.6em 0 .3em;font-weight:600;display:flex;justify-content:space-between} .o{display:flex;justify-content:space-between;align-items:center;flex-wrap:nowrap;gap:.5em;padding:.5em .7em;background:var(--p);border-radius:.5em;margin-bottom:.3em;font-size:.75em} .o .l{display:flex;align-items:center;gap:.45em;min-width:0;flex:1 1 auto} .o .l>span{min-width:0;flex:1 1 auto} .o svg{flex-shrink:0} .o svg+svg{margin-left:-.5em} .o b{font-weight:600;font-variant-numeric:tabular-nums;display:block} .o i{font-style:normal;color:var(--m);display:block;font-size:.9em;overflow-wrap:break-word} .o i a{word-break:break-all} .o a{color:var(--m)} .o button{border:1px solid var(--e);border-radius:.5em;padding:.35em .7em;font-size:.95em;font-weight:600;background:transparent;color:var(--f);cursor:pointer;flex:0 0 auto;transition:background .15s,color .15s,border-color .15s} .o button:hover:not(:disabled),.o button:focus-visible:not(:disabled){background:var(--k);color:var(--kf);border-color:var(--k)} #foot{display:flex;justify-content:center;gap:.5em;flex-wrap:wrap;margin-top:1.1em;font-size:.68em;color:var(--m);letter-spacing:.03em} #foot span:empty{display:none} #foot a{color:var(--m);text-decoration:none;border-bottom:1px solid var(--e)} #foot a:hover{color:var(--f)} .o button:disabled{background:transparent;border-color:transparent;color:var(--m);cursor:default;font-weight:400} .bkf{display:flex;flex-wrap:wrap;gap:.3em;margin:.35em 0 .45em} .bkc{font-size:.68em;padding:.35em .6em;border-radius:.35em;border:0; background:transparent;color:var(--m);cursor:pointer;font-family:inherit;font-weight:600; white-space:nowrap;transition:background .15s,color .15s} .bkc:hover:not(.on),.bkc:focus-visible:not(.on){color:var(--f);background:var(--fh)} .bkc.on{background:var(--k);color:var(--kf)} .bkc.on:hover{background:var(--kh)} .ow{margin-bottom:.3em} .ow .o{margin-bottom:0} .o.ins{cursor:pointer} .o.ins:hover{background:var(--fh)} .ic:empty{display:none} .ic{padding:.5em .7em;background:var(--p);border:0;border-top:1px solid var(--e2);border-radius:0 0 .5em .5em;font-size:.72em;color:var(--n)} .ic img{display:block;width:100%;max-width:15em;margin:.5em auto 0;border-radius:.35em} .ic dl{display:grid;grid-template-columns:auto 1fr;gap:.15em .6em;margin:0} .ic dt{color:var(--m)} .ic dd{margin:0;color:var(--f);font-variant-numeric:tabular-nums;text-align:right} .tg{display:inline-block;font-size:.85em;padding:.05em .35em;border-radius:.35em;background:var(--e2);color:var(--n);margin-left:.3em} .tg.w{background:transparent;border:0;color:var(--w);font-weight:600; text-transform:uppercase;font-size:.62em;letter-spacing:.1em;padding:0; vertical-align:.1em;line-height:1;text-decoration:none;margin-left:.5em} .o a.tg.w{color:var(--w)} .dly{display:flex;justify-content:space-between;align-items:center;gap:.6em;font-size:.75em;color:var(--n);margin-top:.55em;padding:.55em .8em;background:var(--p);border:1px solid transparent;border-radius:.5em;transition:border-color .15s} .dly select{font-size:1em} #lnArtNote{color:var(--n);font-variant-numeric:tabular-nums} #lnArtNote.set{color:var(--f)} .lnbtn{font:inherit;font-size:1em;color:var(--f);background:transparent;border:1px solid var(--e);border-radius:.35em;padding:.2em .6em;cursor:pointer} .lnbtn:hover{background:var(--e)} #lnArtRow{gap:.6em} #lnArtRow>span{display:flex;align-items:center;gap:.6em} #lnPrev{width:1.6em;height:1.6em;border-radius:.35em;object-fit:cover;display:none} #lnPrev.set{display:block} .lnon #chTog,.lnon #chBox,.lnon #bkTog,.lnon #book,.lnon #pos{display:none!important} .docs{margin-top:.5em} .docs details{border-top:1px solid var(--e)} .docs details:last-child{border-bottom:1px solid var(--e)} .docs summary{cursor:pointer;list-style:none;padding:.7em .2em;font-size:.78em;font-weight:600; letter-spacing:.02em;color:var(--f);display:flex;justify-content:space-between;align-items:center} .docs summary::-webkit-details-marker{display:none} .docs summary::after{content:"+";color:var(--m);font-weight:400} .docs details[open] summary::after{content:"\2013"} .docs p{margin:0 0 .7em;padding:0 .2em;font-size:.72em;line-height:1.6;color:var(--n)} .docs b{color:var(--f);font-weight:600} .docs em{font-style:normal;color:var(--m)} .lngap{font-size:.6em;text-transform:uppercase;letter-spacing:.1em;color:var(--m);margin:.9em .2em .1em} .lnhdr{margin-bottom:.2em} #lnSub{color:var(--m);text-transform:none;letter-spacing:0} #lnNote,#floorNote{display:block;font-size:.7em;line-height:1.45;color:var(--m);text-align:center;text-wrap:balance;margin:.55em .2em 0} #lnNote:empty,#floorNote:empty{display:none} .lnu{display:flex;align-items:baseline;gap:.3em;color:var(--m);flex:0 1 auto;min-width:0} .lnu input{width:7em;flex:0 1 7em;min-width:0} .tipr{display:flex;align-items:center;gap:.5em} .tipr small{color:var(--m);font-size:.95em} .dly input{width:9em;flex:0 1 9em;min-width:0;border:0;border-bottom:1px solid var(--e2);transition:border-color .15s;background:transparent;color:var(--f);text-align:right;outline:0;font:inherit;font-variant-numeric:tabular-nums} .dly input[type=checkbox]{width:auto;border:0;cursor:pointer;accent-color:var(--f)} .dly input:hover{border-bottom-color:var(--e)} .dly input:focus{border-bottom-color:var(--f)} #pos{margin-top:.4em} #pos:empty{display:none} .ph{margin:.9em 0 .45em;font-size:.9em;color:var(--f);font-weight:700} .p{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:.5em;padding:.55em .7em;border:0;background:var(--p);border-radius:.5em;margin-bottom:.35em;font-size:.75em} .p span{flex:1;min-width:8em} .p b{display:block;font-weight:600;font-variant-numeric:tabular-nums;word-break:break-word} .p i{font-style:normal;color:var(--m);display:block;font-size:.9em} .p button{border:0;border-radius:.35em;padding:.4em .7em;font-size:.95em;font-weight:600;background:var(--k);color:var(--kf);cursor:pointer;margin-left:auto} .p button:hover:not(:disabled),.p button:focus-visible:not(:disabled){background:var(--kh)} .p button:disabled{background:transparent;color:var(--m);cursor:default;font-weight:400} #rate{text-wrap:balance} #bal,#bal1,#rate{display:block;font-size:.75em;line-height:1.35;margin-top:.5em;text-align:right} #bal:empty,#bal1:empty,#rate:empty{display:none} #bal a,#bal1 a{color:var(--f);cursor:pointer;text-decoration:underline;font-weight:600} .meta input.warn{color:var(--w);border-bottom-color:var(--w)} #th:focus-visible,#lk:focus-visible,#lq:focus-visible,#ln:focus-visible{background:var(--p);color:var(--f)} .chtog{width:100%;margin-top:.5em;padding:.5em;border:0;border-radius:.5em;background:transparent;color:var(--m);cursor:pointer;font:inherit;font-size:.7em;font-weight:600;text-transform:uppercase;letter-spacing:.08em;transition:color .15s,background .15s} .chtog:hover{color:var(--f);background:var(--p)} .chtog span{display:inline-block;transition:transform .2s} .chtog[aria-expanded=true] span{transform:rotate(180deg)} #chBox{margin-top:.4em} .chtf{display:flex;gap:.18em;background:var(--p);padding:.18em;border-radius:.35em;margin-bottom:.4em} .chtf button{flex:1;padding:.35em;font:inherit;font-size:.65em;font-weight:600;text-transform:uppercase;letter-spacing:.06em;border:0;border-radius:.35em;background:transparent;color:var(--m);cursor:pointer} .chtf button.on{background:var(--k);color:var(--kf)} .chtf button:disabled{opacity:.35;cursor:default} .chtf .chk{flex:0 0 auto;padding:.35em .6em;border-left:1px solid var(--e);border-radius:0 .35em .35em 0;margin-left:.15em} .chtf .chk:hover{color:var(--f)} #chArt{touch-action:pan-y;position:relative;border:1px solid transparent;border-radius:.5em;overflow:hidden;background:var(--p)} #chArt svg{display:block;width:100%;height:auto} #chArt .hd{position:absolute;top:.4em;left:.55em;font-size:.7em;color:var(--m);font-variant-numeric:tabular-nums;pointer-events:none} #chArt .hd b{color:var(--f);font-size:1.25em;font-weight:600} #chArt .hd .ohlc{display:block;font-size:.9em;opacity:.85} #chArt .msg{padding:2.2em .8em;text-align:center;font-size:.72em;color:var(--m);line-height:1.6} #chNote{display:block;font-size:.65em;color:var(--m);margin-top:.35em;text-align:right;font-variant-numeric:tabular-nums} .panel select.rich{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none} .tkb{display:none;align-items:center;gap:.25em;border:0;background:transparent;padding:0; font:inherit;font-size:.9em;font-weight:600;letter-spacing:.02em;color:var(--f);cursor:pointer} .tkb.on{display:inline-flex} .tkbc{font-size:.7em;color:var(--m)} .tkp{position:absolute;z-index:40;width:min(20em,calc(100vw - 2em));max-height:min(32em,calc(100vh - 5em)); display:flex;flex-direction:column;background:var(--c);border:1px solid var(--e); border-radius:.5em;box-shadow:0 4px 12px #0000001f,0 16px 40px #00000024;overflow:hidden} #tkFind{border:0;border-bottom:1px solid var(--e);background:transparent;color:var(--f); font:inherit;font-size:.8em;padding:.7em .8em;outline:0;flex:none} #tkList{overflow-y:auto;overscroll-behavior:contain} .tkg{padding:.55em .8em .25em;font-size:.62em;letter-spacing:.09em;text-transform:uppercase;color:var(--m)} .tkr{display:flex;align-items:flex-start;gap:.55em;width:100%;box-sizing:border-box; padding:.5em .8em;border:0;background:transparent;color:var(--f);font:inherit; text-align:left;cursor:pointer} .tkr:hover,.tkr.cur{background:var(--fh)} .tkr[aria-disabled="true"]{opacity:.5;cursor:default} .tkr[aria-disabled="true"]:hover{background:transparent} .tkr img,.tkr svg{flex:none;border-radius:50%;margin-top:.1em} .tki{flex:none;width:20px;height:20px;display:flex;align-items:center;justify-content:center} .tkt{min-width:0;flex:1} .tks{display:flex;align-items:baseline;gap:.45em} .tks b{font-size:.85em;font-weight:600} .tkn{font-size:.7em;color:var(--m);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .tkd{font-size:.66em;color:var(--m);line-height:1.45;margin-top:.15em; display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden} .tkmark{flex:none;font-size:.75em;color:var(--m)} .tkempty{padding:1.4em .8em;text-align:center;font-size:.72em;color:var(--m)} @media(pointer:coarse){#th,#lk,#lq,#ln{width:2.4em;height:2.4em} body:has(.tkp:not(.hide))::before{content:"";position:fixed;inset:0;z-index:39; pointer-events:none;background:#0000008c} .tkp{position:fixed;left:0;right:0;bottom:0;top:auto;width:100%;max-width:none; max-height:82vh;border-radius:.75em .75em 0 0;box-shadow:0 -6px 32px #00000047; padding-bottom:env(safe-area-inset-bottom,0)} .tkp::before{content:"";flex:none;width:2.2em;height:.25em;border-radius:.2em; background:var(--e);margin:.5em auto .1em} .tkd{-webkit-line-clamp:1} .tkr{padding:.75em .9em} #tkFind{padding:.85em .9em;font-size:16px}.rcpt,.meta input,.dly input,.lqin{font-size:16px}.lnbtn{padding:.4em .8em}.p button,.tabs button,.o button,.lqbtn{padding:.6em .8em}.chtf button{padding:.5em}}</style> <script>const H=document.documentElement;let LS;try{LS=localStorage}catch{LS={}}if(LS.t?LS.t=='d':matchMedia('(prefers-color-scheme:dark)').matches)H.className='d'</script> <svg id="logo" viewBox="0 0 400 400" aria-hidden="true"><clipPath id="lgf"><rect width="400" height="400"/></clipPath><g clip-path="url(#lgf)"><rect width="400" height="400"/><path d="M-60-20L460-20L460 90L80 310L460 310L460 420L-60 420L-60 310L320 90L-60 90Z"/></g></svg> <div class="card"> <div class="meta"><span id="addr">Not connected</span><label id="slipL">Slippage <input id="slip" type="number" value="0.5" min="0.01" max="10" step="0.1">%</label><button id="lq" title="Liquidity for this pair" aria-label="Toggle liquidity mode" aria-pressed="false"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3.2c3.6 4 6 7.2 6 10.1a6 6 0 0 1-12 0c0-2.9 2.4-6.1 6-10.1z"/></svg></button><button id="ln" title="Launch a coin" aria-label="Toggle launch mode" aria-pressed="false"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="3.5"/></svg></button><button id="lk" title="Copy shareable link" aria-label="Copy shareable link"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2.5"/><path d="M5.5 15H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v.5"/></svg></button><button id="th" title="Toggle theme" aria-label="Toggle theme"></button></div> <div class="tabs" role="tablist"><button id="tabSwap" class="on" role="tab" aria-selected="true">Swap</button><button id="tabSend" role="tab" aria-selected="false">Send</button><button id="tabBook" role="tab" aria-selected="false">Orders</button></div> <div class="panel"><div class="hdr"><small id="payL">You pay</small><span class="pill"><span id="fromIcon"></span><select id="fromSel"></select><button type="button" class="tkb" id="fromPick" aria-haspopup="listbox" aria-expanded="false"><span class="tkbs"></span><span class="tkbc">▾</span></button></span></div><div class="row" id="amtRow"><input id="amt" type="text" inputmode="decimal" placeholder="0.0" aria-label="Amount to pay" autocomplete="off" spellcheck="false"></div><small id="bal"></small></div> <button id="flip" class="flip" title="Flip tokens" aria-label="Flip tokens"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14m-5-5 5 5 5-5"/></svg></button> <div class="panel" id="rcvPanel"><div class="hdr"><small id="rcvHdr">You receive</small><span class="pill"><span id="toIcon"></span><select id="toSel"></select><button type="button" class="tkb" id="toPick" aria-haspopup="listbox" aria-expanded="false"><span class="tkbs"></span><span class="tkbc">▾</span></button></span></div><div class="row" id="outRow"><input id="outAmt" type="text" inputmode="decimal" placeholder="0.0" aria-label="Amount to receive" autocomplete="off" spellcheck="false"></div><small id="bal1" class="hide"></small><small id="rate"></small></div> <div id="
0xacfba7ce…4082·#25,751,071·0x7c9fc97d…15dfce
nv;syncInv();drawChart()}; chTf.appendChild(invBtn); const syncChart=()=>{chSyncOrientation();syncInv();loadChart()}; chTog.onclick=()=>{chOpen=!chOpen;LS.ch=chOpen?"1":"0";syncChartUI()}; const lsecs=v=>{const m=/^(\d+)([mhdw]?)$/.exec((v||"").trim()); return m?+m[1]*(m[2]=="d"?86400:m[2]=="h"?3600:m[2]=="w"?604800:m[2]=="m"?60:1):NaN}; const symAddr=x=>{const m=TOKENS.filter(t=>t.sym.toLowerCase()===x); return m.length===1?m[0].addr.toLowerCase():""}; let lSeq=0; async function applyLink(){ const q=new URLSearchParams(location.hash.slice(1)); if(![...q.keys()].length)return; const my=++lSeq; const at=a=>a?TOKENS.findIndex(t=>t.addr.toLowerCase()===a):-1; const tok=async v=>{if(!v)return "";const x=v.trim().toLowerCase(); if(!/^0x[0-9a-f]{40}$/.test(x))return symAddr(x); if(at(x)<0)try{await addCustomToken(x,0)}catch{} return at(x)<0?"":x}; if(symAddr((q.get("token")||"").trim().toLowerCase())||symAddr((q.get("out")||"").trim().toLowerCase()))pairChosen=true; const both=async()=>[await tok(q.get("token")),await tok(q.get("out"))]; let[fa,ta]=await both(); if(my!==lSeq)return; if((fa&&at(fa)<0)||(ta&&at(ta)<0)){[fa,ta]=await both();if(my!==lSeq)return} const fi=at(fa),oi=at(ta); if(fi>=0||oi>=0)pairChosen=true; rebuild(); if(fi>=0)fromSel.value=fi; if(oi>=0&&oi!==fi)toSel.value=oi; const to=q.get("to"),lk=lsecs(q.get("lock")),want=q.get("tab"),n=q.get("amount"); const t=want==="swap"||want==="send"||want==="book"?want :q.get("out")?"swap":(to||!isNaN(lk))?"send":tab; if(fromSel.value===toSel.value){ const alt=String(TOKENS.findIndex((x,i)=>String(i)!==fromSel.value&&(t==="book"||x.std!=="nft"))); if(alt!=="-1"){if(oi>=0)fromSel.value=alt;else toSel.value=alt} } if(lqMode&&(fi>=0||oi>=0||to||n||!isNaN(lk)))lqSet(false); setTab(t); if(to)rc.value=to; if(!isNaN(lk)){const os=[...dly.options].map(x=>+x.value).sort((a,b)=>a-b); const v=String(os.find(o=>o>=lk)??os[os.length-1]); if(t==="book")bookD=v;else sendD=v; if(t!=="swap")dly.value=v} if(n&&/^\d*\.?\d+$/.test(n)){ if(q.get("exactOut")==="1"&&q.get("out")){outAmt.value=n;setMode("out");fitFont(outAmt)} else{amt.value=n;setMode("in");fitFont(amt)} } refreshBalance();update(); } addEventListener("hashchange",applyLink); const tname=t=>/\s/.test(t.sym)||!symAddr(t.sym.toLowerCase())?t.addr:t.sym; lk.onclick=async()=>{ const f=TOKENS[fromSel.value],t=TOKENS[toSel.value],p=new URLSearchParams(); p.set("token",tname(f)); if(tab==="send"){ const r=rc.value.trim();if(r)p.set("to",r); if(amt.value.trim())p.set("amount",amt.value.trim()); if(+dly.value)p.set("lock",dly.value); }else{ p.set("out",tname(t)); if(tab==="book"){p.set("tab","book");if(+dly.value)p.set("lock",dly.value)} if(mode==="out"&&outAmt.value.trim()){p.set("amount",outAmt.value.trim());p.set("exactOut","1")} else if(amt.value.trim())p.set("amount",amt.value.trim()); } const u=location.origin+location.pathname+"#"+p; try{await navigator.clipboard.writeText(u);stat.textContent="Link copied"} catch{stat.textContent=u} }; tabSwap.onclick=()=>setTab("swap"); tabSend.onclick=()=>setTab("send"); {const TABS=[tabSwap,tabSend,tabBook],NAMES=["swap","send","book"]; for(const el of TABS)el.addEventListener("keydown",e=>{ const i=TABS.indexOf(e.target); const j=e.key==="ArrowRight"?(i+1)%TABS.length:e.key==="ArrowLeft"?(i+TABS.length-1)%TABS.length :e.key==="Home"?0:e.key==="End"?TABS.length-1:-1; if(j<0)return; e.preventDefault();TABS[j].focus();setTab(NAMES[j]); }); const syncStops=()=>TABS.forEach(el=>el.tabIndex=el.classList.contains("on")?0:-1); syncStops();for(const el of TABS)el.addEventListener("click",syncStops); for(const el of TABS)el.addEventListener("keyup",syncStops);} applyLink(); async function doSend(){ if(!sendReady)return; swap.disabled=true; try{ const{to,amount,token}=sendReady; const fresh=await rcvOf(rc.value.trim(),true); if(fresh.toLowerCase()!==to.toLowerCase())throw Error("recipient changed \u2014 check and retry"); if(amount>fromBalance)throw Error("insufficient balance"); await checkWallet(); const dsec=BigInt(dly.value||0); let txReq,batch=null; if(dsec>0n){ const isE=token.addr===ZERO; if(tipCk.checked)await syncTip(); const tip=tipCk.checked?sendTipWei:0n; const val=isE?amount+tip:tip; if(isE&&val>fromBalance)throw Error("insufficient ETH for amount + keeper tip"); const dep=tip>0n ?"0x"+SEL_DEPOSITTIP+encAddr(isE?ZERO:token.addr)+encAddr(to) +encUint(amount)+encUint(dsec)+encUint(tip)+encUint(192n)+encUint(0n) :"0x"+SEL_DEPOSITTO+encAddr(isE?ZERO:token.addr)+encAddr(to) +encUint(isE?0n:amount)+encUint(dsec)+encUint(160n)+encUint(0n); txReq={from:account,to:SLOW,data:dep,value:toHex(val)}; if(!isE){ const aHex=await rpc(C,[{to:token.addr,data:"0x"+SEL_ALLOWANCE+encAddr(account)+encAddr(SLOW)},L]); const cur=BigInt(aHex); if(cur<amount){ const pre=cur>0n?[{to:token.addr,data:"0x"+SEL_APPROVE+encAddr(SLOW)+encUint(0),value:"0x0"}]:[]; const ap={to:token.addr,data:"0x"+SEL_APPROVE+encAddr(SLOW)+encUint(amount),value:"0x0"}; if(await canBatch(account))batch=[...pre,ap,{to:SLOW,data:dep,value:toHex(val)}]; else{stat.textContent="Approve "+token.sym+"..."; for(const c of[...pre,ap]){const at=await rpc(S,[{from:account,...c}]);await waitTx(at)}} }} }else{ txReq=token.addr===ZERO ?{from:account,to,value:toHex(amount)} :{from:account,to:token.addr,data:"0x"+SEL_TRANSFER+encAddr(to)+encUint(amount),value:"0x0"}; } let tx; if(batch){await checkWallet();tx=await sendBatch(account,batch)} else{const ret=await rpc(C,[txReq,L]);if(dsec===0n&&token.addr!==ZERO)okRet(ret);tx=await rpc(S,[txReq])} await settle(tx); amt.value="";fitFont(amt);sendReady=null; refreshBalance();render();loadPos(); }catch(e){err(e);render()} } async function sendRouterFunded(callData,fundedCallData,token,amount,msgValue,verb){ await checkWallet(); let data=callData,batch=null; if(token.addr!==ZERO){ const allowData="0x"+SEL_ALLOWANCE+encAddr(account)+encAddr(ZROUTER); const allowance=BigInt(await rpc(C,[{to:token.addr,data:allowData},L])); if(allowance<amount){ let ready=false; const pi=await permitInfo(token.addr,account); if(pi)try{ stat.textContent="Sign approval..."; const pd=BigInt(Math.floor(Date.now()/1e3)+1800); data=encCalls([await signPermit(token.addr,pi,account,amount,pd),callData]); ready=true; }catch(e){if(isRejection(e))throw e} const selected=TOKENS[fromSel.value]; const walletBal=selected&&selected.addr.toLowerCase()===token.addr.toLowerCase() ?fromBalance:BigInt(await rpc(C,[{to:token.addr,data:"0x"+SEL_BALANCEOF+encAddr(account)},L])); if(!ready&&walletBal>=amount&&await p2Ready(token.addr,account)>=amount)try{ stat.textContent="Sign approval..."; const pd=BigInt(Math.floor(Date.now()/1e3)+1800); data=encCalls([await signPermit2(token.addr,account,amount,pd),fundedCallData||callData]); ready=true; }catch(e){if(isRejection(e))throw e} if(!ready&&await canBatch(account)){ const pre=allowance>0n ?[{to:token.addr,data:"0x"+SEL_APPROVE+encAddr(ZROUTER)+encUint(0n),value:"0x0"}]:[]; batch=[...pre, {to:token.addr,data:"0x"+SEL_APPROVE+encAddr(ZROUTER)+encUint(amount),value:"0x0"}, {to:ZROUTER,data:callData,value:toHex(msgValue)}]; ready=true; } if(!ready){ const approvals=allowance>0n?[0n,amount]:[amount]; for(let i=0;i<approvals.length;i++){ stat.textContent=`Approve ${token.sym}${approvals.length>1?` (${i+1}/${approvals.length})`:""}...`; const h=await rpc(S,[{from:account,to:token.addr, data:"0x"+SEL_APPROVE+encAddr(ZROUTER)+encUint(approvals[i]),value:"0x0"}]); await waitTx(h); } const fresh=BigInt(await rpc(C,[{to:token.addr,data:allowData},L])); if(fresh<amount)throw Error("approval failed"); } } } stat.textContent=verb; let tx; if(batch){await checkWallet();tx=await sendBatch(account,batch)} else{ const req={from:account,to:ZROUTER,data,value:toHex(msgValue)}; await rpc(C,[req,L]);await checkWallet();tx=await rpc(S,[req]); } await settle(tx); } const PUSH_MAGIC="381bcb9ccaaab09ca206deb73834ed7b483c58bb3face9b43e8c7b50193253e2"; const SEL_SAFE_TRANSFER_DATA="b88d4fde",SEL_OWNEROF="6352211e"; async function pushNftOrder(f,t){ const id=nftId.value.trim(); if(!/^\d+$/.test(id))throw Error("name the token id you are selling"); const tokenId=BigInt(id); if(kind.value!=="fixed")throw Error("an NFT is indivisible, so it sells at a fixed price"); const want=parseUnits(outAmt.value.trim()||"0",t.dec); if(!want)throw Error("name a price"); const quote=t.addr===ZERO?WETH:t.addr; if(quote.toLowerCase()===f.addr.toLowerCase())throw Error("pick different tokens"); if((await rpc("eth_getCode",[SB2,L])).length<5)throw Error("fixed orderbook not deployed yet"); let cp=ZERO; const v=rc.value.trim(); if(v){cp=await rcvOf(v,true);if(!cp)throw Error("Private recipient not resolved")} let owner=ZERO; try{owner="0x"+strip0x(await rpc(C,[{to:f.addr,data:"0x"+SEL_OWNEROF+encUint(tokenId)},L])).slice(-40)} catch{throw Error(`${f.sym} #${id} does not exist`)} if(owner.toLowerCase()!==account.toLowerCase())throw Error(`you do not own ${f.sym} #${id}`); const exp=+dly.value?BigInt(Math.floor(Date.now()/1e3)+ +dly.value):0n; const terms=PUSH_MAGIC+encAddr(quote)+encUint(want)+encUint(exp)+encBool(false)+encAddr(cp); const data="0x"+SEL_SAFE_TRANSFER_DATA+encAddr(account)+encAddr(SB2)+encUint(tokenId) +encUint(128n)+encUint(192n)+terms; const req={from:account,to:f.addr,data,value:"0x0"}; await rpc(C,[req,L]); await checkWallet(); await settle(await rpc(S,[req])); } const SEL_CREATE_ETH="0e8e31d3",SEL_CREATE="8e14f9f8",SEL_FLOOR_BID="3d8d260b"; async function bidCollection(f,t,climbing){ if(nftId.value.trim())throw Error("clear the token id to bid on the whole collection"); const ceiling=parseUnits(amt.value.trim()||"0",f.dec); if(!ceiling)throw Error("name the most you will pay in total"); const open=climbing?parseUnits(floorAmt.value.trim()||"0",f.dec):ceiling; if(climbing&&!open)throw Error("set an opening bid"); if(open>ceiling)throw Error("the opening bid exceeds the maximum"); if(ceiling>(1n<<96n)-1n)throw Error("bid amount too large"); const wantRaw=outAmt.value.trim(); if(wantRaw&&!/^\d+$/.test(wantRaw))throw Error("how many tokens? a whole number"); const want=wantRaw?BigInt(wantRaw):1n; if(!want)throw Error("bid for at least one"); if(want>(1n<<128n)-1n)throw Error("too many to bid for at once"); const duration=BigInt(dly.value||0); if(!duration)throw Error("a bid needs a window — pick an expiry"); if((await rpc("eth_getCode",[FLOOR,L])).length<5)throw Error("bid board not deployed yet"); const terms=encAddr(t.addr)+encAddr(f.addr===ZERO?ZERO:f.addr)+encUint(want) +encUint(open)+encUint(ceiling)+encUint(0n)+encUint(duration)+encBool(true) +encUint(288n)+encUint(0n); const data="0x"+SEL_FLOOR_BID+encUint(32n)+terms; if(f.addr===ZERO){ const req={from:account,to:FLOOR,data,value:toHex(ceiling)}; await rpc(C,[req,L]);await checkWallet();await settle(await rpc(S,[req])); return; } const tx={from:account,to:FLOOR,data,value:"0x0"}; const al=BigInt(await rpc(C,[{to:f.addr,data:"0x"+SEL_ALLOWANCE+encAddr(account)+encAddr(FLOOR)},L])); const approvals=[]; if(al<ceiling){ if(al)approvals.push({to:f.addr,data:"0x"+SEL_APPROVE+encAddr(FLOOR)+encUint(0n),value:"0x0"}); approvals.push({to:f.addr,data:"0x"+SEL_APPROVE+encAddr(FLOOR)+encUint(ceiling),value:"0x0"}); } if(approvals.length&&await canBatch(account)){ await checkWallet(); await settle(await sendBatch(account,[...approvals,{to:FLOOR,data,value:"0x0"}])); return; } for(const a of approvals){await checkWallet();await settle(await rpc(S,[{from:account,...a}]))} await rpc(C,[tx,L]);await checkWallet();await settle(await rpc(S,[tx])); } async function buyNftOrder(f,t){ const id=nftId.value.trim(); if(!/^\d+$/.test(id))throw Error("name the token id you want to buy"); const tokenId=BigInt(id); if(kind.value!=="fixed")throw Error("a bid for one token is a fixed offer"); const price=parseUnits(amt.value.trim()||"0",f.dec); if(!price)throw Error("name what you will pay"); if((await rpc("eth_getCode",[SB2,L])).length<5)throw Error("fixed orderbook not deployed yet"); let cp=ZERO; const v=rc.value.trim(); if(v){cp=await rcvOf(v,true);if(!cp)throw Error("Private recipient not resolved")} try{await rpc(C,[{to:t.addr,data:"0x"+SEL_OWNEROF+encUint(tokenId)},L])} catch{throw Error(`${t.sym} #${id} does not exist`)} const exp=+dly.value?BigInt(Math.floor(Date.now()/1e3)+ +dly.value):0n; if(f.addr===ZERO){ const data="0x"+SEL_CREATE_ETH+encAddr(t.addr)+encUint(tokenId)+encBool(false) +encUint(exp)+encBool(true)+encAddr(cp); const req={from:account,to:SB2,data,value:toHex(price)}; await rpc(C,[req,L]);await checkWallet();await settle(await rpc(S,[req])); return; } const data="0x"+SEL_CREATE+encAddr(f.addr)+encUint(price)+encAddr(t.addr)+encUint(tokenId) +encBool(false)+encUint(exp)+encBool(false)+encBool(true)+encAddr(cp); const tx={from:account,to:SB2,data,value:"0x0"}; const al=BigInt(await rpc(C,[{to:f.addr,data:"0x"+SEL_ALLOWANCE+encAddr(account)+encAddr(SB2)},L])); const approvals=[]; if(al<price){ if(al)approvals.push({to:f.addr,data:"0x"+SEL_APPROVE+encAddr(SB2)+encUint(0n),value:"0x0"}); approvals.push({to:f.addr,data:"0x"+SEL_APPROVE+encAddr(SB2)+encUint(price),value:"0x0"}); } if(approvals.length&&await canBatch(account)){ await checkWallet(); await settle(await sendBatch(account,[...approvals,{to:SB2,data,value:"0x0"}])); return; } for(const a of approvals){await checkWallet();await settle(await rpc(S,[{from:account,...a}]))} await rpc(C,[tx,L]);await checkWallet();await settle(await rpc(S,[tx])); } async function placeOrder(){ const f=TOKENS[fromSel.value],t=TOKENS[toSel.value]; swap.disabled=true; try{ if(f.std==="nft"||t.std==="nft"){ if(f.std==="nft")await pushNftOrder(f,t); else if(nftId.value.trim())await buyNftOrder(f,t); else await bidCollection(f,t,kind.value==="floor"); amt.value="";outAmt.value="";nftId.value="";floorAmt.value=""; floorTouched=false; fitFont(amt);fitFont(outAmt);fitFont(floorAmt); stat.textContent=f.std==="nft"?"Listed.":"Bid placed."; loadBook();refreshBalance();render(); return; } const sell=parseUnits(amt.value.trim(),f.dec),want=parseUnits(outAmt.value.trim(),t.dec); const dutch=kind.value==="dutch",bid=kind.value==="floor"; const floor=(dutch||bid)&&floorAmt.value.trim()?parseUnits(floorAmt.value.trim(),bid?f.dec:t.dec):0n; if(dutch&&floor===0n){ try{ if(!confirm(`This lot decays to ZERO.\n\nOnce the window ends, anyone can take all ${trimAmt(sell,f.dec)} ${f.sym} for nothing — and it stays that way until you cancel.\n\nThat is how a "come and take it" launch works. If you meant to keep a reserve, set a Floor total instead.\n\nContinue with a zero floor?`))return; }catch{ stat.textContent="This lot would decay to zero and this browser blocked the confirmation. Set a Floor total, or open the page outside the wallet's in-app browser."; return; } } const board=bid?FLOOR:dutch?DUTCH:SB2; const quoteToken=!dutch&&!bid&&t.addr===ZERO?WETH:t.addr; const [bc,oc]=await Promise.all([rpc("eth_getCode",[board,L]),rpc("eth_getCode",[ORDERBOL,L])]); if(bc.length<5)throw Error(`${bid?"bid board":dutch?"Dutch":"fixed"} orderbook not deployed yet`); if(oc.length<5)throw Error("routed order adapter not deployed yet"); if(!bid&&f.addr===ZERO&&t.addr===WETH)throw Error("pick a non-WETH quote token"); if(!dutch&&!bid&&(f.addr===WETH||f.addr===ZERO)&&quoteToken===WETH)throw Error("pick different tokens"); if(dutch&&floor>want)throw Error("floor exceeds start price"); let cp=ZERO; const v=rc.value.trim(); if(!dutch&&!bid&&v){cp=await rcvOf(v,true);if(!cp)throw Error("Private recipient not resolved")} const pf=fill.value==="1"?1n:0n; const placementDeadline=BigInt(Math.floor(Date.now()/1e3)+1800); let orderData; if(bid){ const duration=BigInt(dly.value||0); if(!duration)throw Error("choose a bid window"); if(!floor)throw Error("set an opening bid"); if(floor>sell)throw Error("opening bid exceeds the maximum"); if(want>(1n<<128n)-1n||sell>(1n<<96n)-1n)throw Error("bid amount too large"); if(t.addr===ZERO)throw Error("a bid buys a token — pick one to bid for"); if((f.addr===ZERO?WETH:f.addr).toLowerCase()===t.addr.toLowerCase()) throw Error("pick different tokens"); orderData="0x"+SEL_ORDER_FLOOR+encAddr(FLOOR)+encAddr(account)+encAddr(account) +encAddr(t.addr)+encAddr(f.addr)+encUint(want)+encUint(floor)+encUint(sell) +encUint(0n)+encUint(duration)+encUint(placementDeadline); }else if(dutch){ const duration=BigInt(dly.value||0); if(!duration)throw Error("choose a decay duration"); if(sell>(1n<<128n)-1n||want>(1n<<96n)-1n||floor>(1n<<96n)-1n)throw Error("Dutch amount too large"); const dutchExpiry=dutchEnds(); orderData="0x"+SEL_ORDER_DUTCH+encAddr(DUTCH)+encAddr(account)+encAddr(account) +encAddr(f.addr)+encAddr(t.addr)+encUint(sell)+encUint(want)+encUint(floor) +encUint(0n)+encUint(duration)+encUint(dutchExpiry)+encUint(placementDeadline); }else{ const exp=+dly.value?BigInt(Math.floor(Date.now()/1e3)+ +dly.value):0n; orderData="0x"+SEL_ORDER_FIXED+encAddr(SB2)+encAddr(account)+encAddr(account) +encAddr(f.addr)+encUint(sell)+encAddr(quoteToken)+encUint(want)+encUint(pf) +encUint(exp)+encAddr(cp)+encUint(placementDeadline); } let direct,funded; if(f.addr===ZERO){ direct=funded=encSnwap(ZERO,0n,account,ZERO,0n,ORDERBOL,orderData); }else{ const checkpoint=encCheckpoint(ORDERBOL,f.addr,account); direct=encCalls([ checkpoint, encSnwap(f.addr,sell,account,ZERO,0n,ORDERBOL,orderData) ]); funded=encCalls([ checkpoint, encSweep(f.addr,sell,ORDERBOL), encSnwap(ZERO,0n,account,ZERO,0n,ORDERBOL,orderData) ]); } await sendRouterFunded(direct,funded,f,sell,f.addr===ZERO?sell:0n, bid?"Placing bid (escrowing the ceiling)...":dutch?"Placing Dutch order...":t.addr===ZERO?"Placing limit order (ETH quote settles as WETH)...":"Placing limit order..."); amt.value="";outAmt.value="";floorAmt.value=""; floorTouched=false; fitFont(amt);fitFont(outAmt);fitFont(floorAmt); loadBook();refreshBalance();render(); }catch(e){err(e);render()} } swap.onclick=async()=>{ if(tab==="book"){if(!account)return connect();return placeOrder()} if(tab==="send"){if(!account)return connect();return doSend()} if(!account) return connect(); if(!last)return; if(Date.now()>last.exp){stat.textContent="Quote expired — refreshing...";updateSoon();return} if(last.impact!==null&&last.impact!==undefined&&last.impact>=IMPACT_CONFIRM){ const pct=(Number(last.impact)/100).toFixed(2); const worse=`about ${trimAmt(last.lossTok,last.lossDec)} ${last.lossSym} ${last.isIn?"less than":"more than"} the market rate`; try{ if(last.impact>=IMPACT_TYPED){ const need=String(Math.floor(Number(last.impact)/100)); if(prompt(`STOP — this trade is priced ${pct}% away from the market.\n\nYou would ${last.isIn?"receive":"pay"} ${worse}.\n\nUsually this means the pool is illiquid or the amount is wrong.\n\nType ${need} to accept this loss:`)!==need)return; }else if(!confirm(`Price impact ${pct}%.\n\nYou will ${last.isIn?"receive":"pay"} ${worse}.\n\nContinue?`))return; }catch{ stat.textContent=`This trade is ${pct}% away from the market and this browser blocked the confirmation. Reduce the amount, or open the page outside the wallet's in-app browser.`; return; } } swap.disabled=true; try{ const q=last; const {callData,fundedCallData,msgValue,amountIn,from,to}=q; const wantsValue=q.wrap>0n?0n:(from.addr===ZERO?amountIn:0n); if(msgValue!==wantsValue)throw Error("bad value"); const rvNow=rc.value.trim(); if(((rvNow?await rcvOf(rvNow,true):"")||"").toLowerCase()!==(q.recipient||"").toLowerCase()) throw Error("recipient changed — check and retry"); await checkWallet(); try{await preflightFills(q.fills,q.tokenIn,q.tokenOut)} catch(e){stat.textContent=e.message;swap.disabled=false;last=null;updateSoon();return} let cd2=callData,batchCalls=null,prep=[]; const spender=q.spender!==undefined?q.spender:(to?null:ZROUTER); const target=to||ZROUTER; const fund=q.wrap>0n?{addr:WETH,sym:"WETH",dec:18}:from; if(q.source===SRC_PRECISION&&q.pool){ const ok=await mc3([{to:PFACTORY,data:"0x"+SEL_ISPOOL+encAddr(q.pool)}]) .then(([r])=>!!r&&BigInt(r)===1n).catch(()=>false); if(!ok)throw Error("that pool is not one the factory made — refusing to send"); if(q.wrap>0n){ const have=BigInt(await rpcRead("eth_getBalance",[account,L])); if(have<q.wrap)throw Error(`Not enough ETH: this needs ${trimAmt(q.wrap,18)} and you hold ${trimAmt(have,18)}.`); prep.push({to:WETH,data:"0xd0e30db0",value:toHex(q.wrap), note:`Wrapping ${trimAmt(q.wrap,18)} ETH — this pool holds WETH`}); } } const prepBatchable=prep.length?await canBatch(account):false; if(prep.length&&!prepBatchable){ for(const c of prep){ stat.textContent=c.note+"..."; const ctx={from:account,to:c.to,data:c.data,value:c.value}; await rpc(C,[ctx,L]);await checkWallet(); await settle(await rpc(S,[ctx])); } prep=[]; } if(fund.addr!==ZERO&&spender&&spender!==ZROUTER){ const allowData="0x"+SEL_ALLOWANCE+encAddr(account)+encAddr(spender); const a=BigInt(await rpc(C,[{to:fund.addr,data:allowData},L])); if(a<amountIn){ if(await canBatch(account)){ const pre=a>0n?[{to:fund.addr,data:"0x"+SEL_APPROVE+encAddr(spender)+encUint(0n),value:"0x0"}]:[]; batchCalls=[...pre,{to:fund.addr,data:"0x"+SEL_APPROVE+encAddr(spender)+encUint(amountIn),value:"0x0"}, {to:target,data:callData,value:toHex(msgValue)}]; }else{ const n=a>0n?2:1; if(a>0n){ stat.textContent=`Approving (1/${n})...`; await waitTx(await rpc(S,[{from:account,to:fund.addr, data:"0x"+SEL_APPROVE+encAddr(spender)+encUint(0)}])); } stat.textContent=n>1?"Approving (2/2)...":"Approving..."; await waitTx(await rpc(S,[{from:account,to:fund.addr, data:"0x"+SEL_APPROVE+encAddr(spender)+encUint(amountIn)}])); if(BigInt(await rpcRead(C,[{to:fund.addr,data:allowData},L]))<amountIn)throw Error("approval failed"); } } }else if(fund.addr!==ZERO&&!to&&!msgValue){ const allowData="0x"+SEL_ALLOWANCE+encAddr(account)+encAddr(ZROUTER); const aHex=await rpc(C,[{to:fund.addr,data:allowData},L]); const a=BigInt(aHex); if(a<amountIn){ let permitted=false; const sameAsQuote=fund===from; const pi=sameAsQuote?await permitInfo(fund.addr,account):null; if(pi){try{ stat.textContent="Sign approval..."; const pd=BigInt(Math.floor(Date.now()/1e3)+1800); cd2=encCalls([await signPermit(fund.addr,pi,account,amountIn,pd),callData]); permitted=true; }catch(e){if(isRejection(e)){stat.textContent="";swap.disabled=false;return}}} const p2Amount=amountIn; const p2Fundable=!!fundedCallData||q.source!==SRC_PRECISION; if(!permitted&&p2Fundable&&sameAsQuote&&fromBalance>=p2Amount&&await p2Ready(fund.addr,account)>=p2Amount){try{ stat.textContent="Sign approval..."; const pd=BigInt(Math.floor(Date.now()/1e3)+1800); const p2Call=await signPermit2(fund.addr,account,p2Amount,pd); cd2=fundedCallData?encCalls([p2Call,fundedCallData]) :encCalls(q.isIn?[p2Call,callData]:[p2Call,callData,encSweep(fund.addr,0n,account)]); permitted=true; }catch(e){if(isRejection(e)){stat.textContent="";swap.disabled=false;return}}} if(!permitted&&await canBatch(account)){ const pre=a>0n?[{to:fund.addr,data:"0x"+SEL_APPROVE+encAddr(ZROUTER)+encUint(0n),value:"0x0"}]:[]; batchCalls=[...pre,{to:fund.addr,data:"0x"+SEL_APPROVE+encAddr(ZROUTER)+encUint(amountIn),value:"0x0"}, {to:target,data:callData,value:toHex(msgValue)}]; permitted=true; } if(!permitted){ const n=a>0n?2:1; if(a>0n){ stat.textContent=`Approving (1/${n})...`; const zeroData="0x"+SEL_APPROVE+encAddr(ZROUTER)+encUint(0); const h1=await rpc(S,[{from:account,to:fund.addr,data:zeroData}]); await waitTx(h1); } stat.textContent=n>1?`Approving (2/2)...`:"Approving..."; const apprData="0x"+SEL_APPROVE+encAddr(ZROUTER)+encUint(amountIn); const h2=await rpc(S,[{from:account,to:fund.addr,data:apprData}]); await waitTx(h2); const nHex=await rpc(C,[{to:fund.addr,data:allowData},L]); if(BigInt(nHex)<amountIn)throw Error("approval failed"); } } } if(prep.length&&batchCalls){ batchCalls=[...prep.map(({to,data,value})=>({to,data,value})),...batchCalls]; prep=[]; } let tx; const mainTx={from:account,to:target,data:cd2,value:toHex(msgValue)}; if(prep.length&&!batchCalls&&await canBatch(account)){ stat.textContent="Swapping..."; await checkWallet(); tx=await sendBatch(account,[...prep.map(({to,data,value})=>({to,data,value})), {to:target,data:cd2,value:toHex(msgValue)}]); }else{ for(const c of prep){ stat.textContent=c.note+"..."; const ctx={from:account,to:c.to,data:c.data,value:c.value}; await rpc(C,[ctx,L]);await checkWallet(); await settle(await rpc(S,[ctx])); } stat.textContent="Swapping..."; if(batchCalls){ await checkWallet(); tx=await sendBatch(account,batchCalls); }else{ await rpc(C,[mainTx,L]); await checkWallet(); tx=await rpc(S,[mainTx]); } } await settle(tx); last=null; chCache={};loadChart(); refreshBalance(); render(); }catch(e){err(e);render()} };</script>
0xacfba7ce…4082·#25,751,070·0x119f7136…fb61fa
cUint(a1) +encUint(minLP)+encAddr(to); const tx={from:account,to:PFACTORY,data,value:native?toHex(a0):"0x0"}; stat.textContent="Creating the band..."; await rpc(C,[tx,L]);await checkWallet();await settle(await rpc(S,[tx])); stat.textContent="Band created."; loadLq();refreshBalance(); }catch(e){err(e);out.textContent="";} finally{--busy;go.disabled=false} } async function lqApprove(token,need){ const al=BigInt(await rpc(C,[{to:token,data:"0x"+SEL_ALLOWANCE+encAddr(account)+encAddr(PFACTORY)},L])); if(al>=need)return; if(al)await settle(await rpc(S,[{from:account,to:token, data:"0x"+SEL_APPROVE+encAddr(PFACTORY)+encUint(0n),value:"0x0"}])); await settle(await rpc(S,[{from:account,to:token, data:"0x"+SEL_APPROVE+encAddr(PFACTORY)+encUint(need),value:"0x0"}])); } lqList.onclick=async ev=>{ if(ev.target.id==="lqCreate"){await lqCreate();return} if(ev.target.id==="lqNewTog"){ const pr=chPair();if(!pr)return; ev.target.outerHTML=lqNewForm(pr[0],pr[1]); lqSyncAmounts(); lqNewPreview(); lqList.querySelector(".lqnew")?.scrollIntoView?.({block:"nearest"}); return} const b=ev.target.closest("[data-act]"); if(!b||busy)return; const p=chPair(); if(!p)return; const [t0,t1]=p,pool=b.dataset.pool; const row=lqRows.find(x=>x.pool.toLowerCase()===pool.toLowerCase()); if(!row)return; const act=b.dataset.act; if(act!=="a"&&!account)return void(stat.textContent="Connect a wallet first."); if(act==="a"){ const box=b.closest(".lqrow").querySelector(".lqadd"); box.classList.toggle("hide"); b.textContent=box.classList.contains("hide")?"Add":"Cancel"; if(!box.classList.contains("hide"))box.querySelector(".lqin").focus(); return; } if(act==="w"){ if(row.shares===0n)return; await doRemove(pool,row.shares,t0,t1); return; } if(act==="ac"){ const box=b.closest(".lqadd"); const ins=box.querySelectorAll(".lqin"); if(isZap(box)){ const z=zapSide(box,t0,t1); if(z.err){stat.textContent=z.err;return} if(!z.amt)return; await doZap(pool,z.tok,z.amt); return; } let a0,a1; try{ a0=parseUnits(ins[0].value.trim(),t0.dec);a1=parseUnits(ins[1].value.trim(),t1.dec); }catch{stat.textContent="Invalid amount";return} if(!a0||!a1)return; await doAdd(pool,a0,a1,t0,t1); } }; /* The docs, behind a footer link rather than a seventh icon: this is a reference, not a mode, and the meta row is already full. */ footDoc.setAttribute("aria-expanded","false"); footDoc.onclick=e=>{ e.preventDefault(); const on=docPanel.classList.contains("hide"); footDoc.setAttribute("aria-expanded",on?"true":"false"); docPanel.classList.toggle("hide",!on); footDoc.textContent=on?"hide":"how it works"; if(on)docPanel.scrollIntoView({behavior:"smooth",block:"nearest"}); }; lq.onclick=()=>lqSet(!lqMode); const PLAUNCH="0x0000002fC8E77585A008Aa45d78A71ad36293aEe"; const SEL_LAUNCH="6a648dc6"; const SEL_LAUNCHART="72cf7bd8"; const SEL_CREATOROF="dea5c2e0"; const LN_MIME={"image/png":0,"image/webp":1,"image/svg+xml":2,"image/gif":3,"image/jpeg":4}; const MIME_NAME=["image/png","image/webp","image/svg+xml","image/gif","image/jpeg","image/avif"]; const LN_MAX_ART=24575; const LN_BUDGET=8192; const LN_MAX_ALLOC=2000; const LN_MIN_POOLED=2000000000000n; const LN_MIN_MCAP=1000000000000n; const lnHex=b=>{let h="";for(const x of b)h+=x.toString(16).padStart(2,"0");return h}; const lnTail=b=>{const h=lnHex(b);return encUint(b.length)+h.padEnd(Math.ceil(h.length/64)*64,"0")}; const encLaunch=(n,y,u,sup,alloc,mcap,own,art,mime)=>{ const T=new TextEncoder(); const dyn=[T.encode(n),T.encode(y),T.encode(u)]; const statics=[encUint(sup),encUint(alloc),encUint(mcap),encAddr(own)]; if(art){dyn.push(art);statics.push(encUint(mime))} const words=statics.length+(art?4:3); let off=words*32,head="",tail=""; const heads=[]; for(const b of dyn){heads.push(encUint(off));off+=lnTail(b).length/2;tail+=lnTail(b)} head=heads.slice(0,3).join("")+statics.slice(0,4).join("") +(art?heads[3]+statics[4]:""); return "0x"+(art?SEL_LAUNCHART:SEL_LAUNCH)+head+tail; }; let lnMode=false,lnArtBytes=null,lnArtMime=0; function lnSet(on){ lnMode=on; if(on&&lqMode)lqSet(false); ln.setAttribute("aria-pressed",on?"true":"false"); ln.classList.toggle("on",on); for(const el of [rate,flip,swap])if(el)el.classList.toggle("hide",on); for(const el of [document.querySelector(".panel"),rcvPanel])if(el)el.classList.toggle("hide",on); document.querySelector(".card").classList.toggle("lnon",on); for(const el of [lnPanel,lnNote,lnGo])el.classList.toggle("hide",!on); rc.value="";rc.classList.remove("bad");rcvEl.textContent=""; rc.title=on?"Who the token belongs to: receives the creator allocation, collects the creator share of trading fees for the life of the token, and may edit its name, symbol and logo. Leave blank and it is you. Worth setting to a multisig if the project has one.":""; rc.placeholder=on?"Creator — defaults to you" :"Recipient (optional) — 0x or .eth/.(g)wei"; stat.textContent="";lnNote.textContent=""; if(on)lnQuote(); } function lnQuote(){ lnGo.textContent=account?"Launch":"Connect Wallet"; const ns=v=>v.replace(/[, ]/g,""); const sup=ns(lnSupply.value),mc=+ns(lnMcap.value),al=+ns(lnAlloc.value); lnNote.textContent=""; if(!(Number(sup)>0)||!(mc>0))return; const pooled=Number(sup)*(1-(al>0?al:0)/100); if(!(pooled>0))return; const px=mc/pooled; const sym=lnSym.value.trim().toUpperCase(); const poolFrac=100/(mc+1); const firstEth=poolFrac*pooled/Number(sup); lnNote.textContent=`1 ETH buys ${firstEth>=10?Math.round(firstEth):firstEth.toFixed(firstEth<1?2:1)}% of all ${sym||"tokens"}` +` \u00b7 opens at 1 ETH \u2248 ${trimNum(1/px)} ${sym||"tokens"}` +(al>0?` \u00b7 creator keeps ${trimNum(Number(sup)*al/100)} (${al}%)`:"") +(al>=10?` \u00b7 that much can sell for most of the ether early buyers put in`:"") +(lnArtBytes?` \u00b7 logo ${(lnArtBytes.length/1024).toFixed(1)} KB`:""); lnNote.style.color=poolFrac>=10?"#c33":""; } const trimNum=n=>n>=1000?Math.round(n).toLocaleString("en-US") :n>=1?n.toLocaleString("en-US",{maximumFractionDigits:4}):String(Number(n.toPrecision(4))); const blobText=b=>b.text?b.text():new Promise((res,rej)=>{ const r=new FileReader();r.onload=()=>res(String(r.result));r.onerror=()=>rej(r.error);r.readAsText(b)}); const blobBytes=async b=>b.arrayBuffer?new Uint8Array(await b.arrayBuffer()):await new Promise((res,rej)=>{ const r=new FileReader();r.onload=()=>res(new Uint8Array(r.result));r.onerror=()=>rej(r.error);r.readAsArrayBuffer(b)}); async function lnPrepare(file){ if(/svg/.test(file.type)){ const b=new TextEncoder().encode((await blobText(file)) .replace(/<\?xml[^>]*\?>/g,"").replace(/<!--[\s\S]*?-->/g,"").replace(/<!DOCTYPE[^>]*>/gi,"") .replace(/>\s+</g,"><").trim()); if(b.length>LN_MAX_ART)throw Error(`That SVG is ${(b.length/1024).toFixed(1)} KB and the on-chain limit is 24 KB.`); return{bytes:b,mime:LN_MIME["image/svg+xml"]}; } const bmp=await createImageBitmap(file); let smallest=null; for(const side of [512,384,256,192,128,96,64]){ for(const [type,q] of [["image/webp",1],["image/png",undefined],["image/webp",0.85],["image/webp",0.6]]){ const sc=Math.min(1,side/Math.max(bmp.width,bmp.height)); const cv=document.createElement("canvas"); cv.width=Math.max(1,Math.round(bmp.width*sc));cv.height=Math.max(1,Math.round(bmp.height*sc)); const cx=cv.getContext("2d");cx.imageSmoothingQuality="high"; cx.drawImage(bmp,0,0,cv.width,cv.height); const blob=await new Promise(r=>cv.toBlob(r,type,q)); if(!blob||blob.type!==type)continue; const bytes=await blobBytes(blob); if(!smallest||bytes.length<smallest.bytes.length)smallest={bytes,mime:LN_MIME[type]}; if(bytes.length<=LN_BUDGET)return{bytes,mime:LN_MIME[type]}; } } if(smallest&&smallest.bytes.length<=LN_MAX_ART)return smallest; throw Error("That image will not compress small enough. Flat colours and simple shapes do far better than photographs."); } lnPick.onclick=()=>lnArt.click(); lnArt.onchange=async()=>{ const f=lnArt.files&&lnArt.files[0]; if(!f)return; lnArtNote.textContent="reading…"; try{ const r=await lnPrepare(f); lnArtBytes=r.bytes;lnArtMime=r.mime; lnArtNote.textContent=`${(r.bytes.length/1024).toFixed(1)} KB`; lnArtNote.classList.add("set"); let b64="";for(const x of r.bytes)b64+=String.fromCharCode(x); lnPrev.src="data:"+MIME_NAME[r.mime]+";base64,"+btoa(b64); lnPrev.classList.add("set"); }catch(e){ lnArtBytes=null;lnArtNote.classList.remove("set");lnPrev.classList.remove("set"); lnArtNote.textContent="none";stat.textContent=e.message||String(e); } lnQuote(); }; const lnGroup=el=>{ const dot=el.value.indexOf("."); const intRaw=(dot<0?el.value:el.value.slice(0,dot)).replace(/\D/g,""); const frac=dot<0?"":"."+el.value.slice(dot+1).replace(/\D/g,""); const digitsBefore=el.value.slice(0,el.selectionStart??el.value.length).replace(/\D/g,"").length; const out=intRaw.replace(/\B(?=(\d{3})+(?!\d))/g,",")+frac; if(out===el.value)return; el.value=out; let seen=0,pos=digitsBefore?out.length:0; if(digitsBefore)for(let i=0;i<out.length;i++){ if(/\d/.test(out[i])&&++seen===digitsBefore){pos=i+1;break} } try{el.setSelectionRange(pos,pos)}catch{} }; lnSupply.oninput=()=>{lnGroup(lnSupply);lnQuote()}; for(const el of [lnName,lnSym,lnMcap,lnAlloc])el.oninput=lnQuote; ln.onclick=()=>lnSet(!lnMode); lnGo.onclick=async()=>{ stat.textContent=""; if(lnGo.disabled)return; if(!account)return void connect(); lnGo.disabled=true; try{ await checkWallet(); const name=lnName.value.trim(),sym=lnSym.value.trim().toUpperCase(); if(!name||!sym)throw Error("A name and a symbol, please."); const utf8=v=>new TextEncoder().encode(v).length; if(utf8(name)>64)throw Error(`That name is ${utf8(name)} bytes and the limit is 64.`); if(utf8(sym)>16)throw Error(`That symbol is ${utf8(sym)} bytes and the limit is 16.`); const supRaw=lnSupply.value.replace(/[, ]/g,""); if(!/^\d+(\.\d+)?$/.test(supRaw)||!(Number(supRaw)>0))throw Error("Supply has to be a positive number."); const supply=parseUnits(supRaw,18); const alloc=Math.round(Number(lnAlloc.value||0)*100); if(!(alloc>=0)||alloc>LN_MAX_ALLOC)throw Error("Allocation runs from 0 to 20%."); const mcap=parseUnits(String(lnMcap.value||"").trim(),18); if(mcap<LN_MIN_MCAP)throw Error("That opening valuation is too small to price."); const pooled=supply-supply*BigInt(alloc)/10000n; if(pooled<LN_MIN_POOLED)throw Error("Too little supply reaches the pool to price a market."); let owner=account; const rv=rc.value.trim(); if(rv){ if(/^0x[0-9a-fA-F]{40}$/.test(rv)){ if(/^0x0{40}$/i.test(rv))throw Error("The creator cannot be the zero address."); owner=rv; }else if(/\.(g?wei|eth)$/i.test(rv)){ rcvEl.textContent="resolving\u2026"; let r="";try{r=await nameFwd(rv.toLowerCase())}catch{r=""} rcvEl.textContent=""; if(!r||/^0x0{40}$/i.test(r))throw Error("That name is not registered."); owner=r; }else throw Error("The creator must be an address, or a .eth / .wei name."); } stat.textContent="Launching\u2026"; const req={from:account,to:PLAUNCH,value:"0x0", data:encLaunch(name,sym,"",supply,BigInt(alloc),mcap,owner,lnArtBytes,lnArtMime)}; const sim=await rpc(C,[req,L]); const candidate=/^0x[0-9a-fA-F]{128}$/.test(sim||"")?"0x"+strip0x(sim).slice(24,64):null; const receipt=await waitTx(await rpc(S,[req])); const topic=kecStr("Launched(address,address,address,uint256,uint256,uint256)"); const lg=(receipt.logs||[]).find(l=>(l.address||"").toLowerCase()===PLAUNCH.toLowerCase() &&(l.topics||[])[0]&&l.topics[0].toLowerCase()===topic.toLowerCase()); let token=lg?"0x"+strip0x(lg.topics[1]).slice(24):null; if(!token&&candidate){ try{ const c=await rpc(C,[{to:PLAUNCH,data:"0x"+SEL_CREATOROF+encAddr(candidate)},L]); if(c&&"0x"+strip0x(c).slice(24)===owner.toLowerCase())token=candidate; }catch{} } if(token){ try{ const idx=await addCustomToken(token); rebuild(); fromSel.value=String(Math.max(0,TOKENS.findIndex(t=>t.addr===ZERO))); fromSel.dataset.prev=fromSel.value; toSel.value=String(idx); toSel.dataset.prev=toSel.value; pairChosen=true; lnSet(false); syncDisabled();syncNftUI(); refreshBalance(); update(); }catch{} } stat.textContent=token?`${sym} is live at ${short(token)}`:`${sym} is live`; lnArtBytes=null;lnArtNote.textContent="none";lnArtNote.classList.remove("set"); lnPrev.classList.remove("set");lnArt.value=""; }catch(e){err(e)} finally{lnGo.disabled=false} }; const SEL_MARKETS="29c21083",SEL_TAPE="29a65241"; const FINE=300,COARSE=14400,TFS=[["5m",300],["1h",3600],["4h",14400],["1d",86400]]; const CH_POOLS=8; const srcFor=tf=>tf>=COARSE?COARSE:FINE; let chKind=LS.ck==="line"?"line":"candle",chInv=false,chInvKey="",chPeriod=300,chBars=null,chSeq=0,chPools=null,chOpen=LS.ch==="1",chCache={}; let chDraw=null; const unf=p=>(p&0xffffff)*2**(p>>>24); const decBar=w=>{if(!w)return null;const f=s=>Number((w>>BigInt(s))&0xffffffffn); return{b:Number(w&0xffffffffn),o:unf(f(32)),h:unf(f(64)),l:unf(f(96)),c:unf(f(128)),v:unf(f(160)),n:Number((w>>192n)&0xffffn)}}; const rollUp=(bars,src,dst)=>{if(dst<=src)return bars.filter(Boolean); const out=[];let g=null,k=null; for(let i=bars.length-1;i>=0;i--){const b=bars[i];if(!b)continue; const key=Math.floor(b.b*src/dst); if(key!==k){if(g)out.push(g);k=key;g={b:key,o:b.o,h:b.h,l:b.l,c:b.c,v:b.v,n:b.n}} else{g.h=Math.max(g.h,b.h);g.l=Math.min(g.l,b.l);g.c=b.c;g.v+=b.v;g.n+=b.n}} if(g)out.push(g);return out.reverse()}; const CH_WICK_SHARE=.01; const mergeTapes=tapes=>{const m=new Map(); for(const bars of tapes)for(const b of bars||[]){if(!b)continue; let e=m.get(b.b); if(!e)m.set(b.b,e={b:b.b,v:0,n:0,ov:0,cv:0,parts:[]}); e.v+=b.v;e.n+=b.n;e.ov+=b.o*b.v;e.cv+=b.c*b.v;e.parts.push(b)} const out=[...m.values()].sort((x,y)=>y.b-x.b); for(const e of out){ const big=e.v>0?e.parts.filter(x=>x.v>=e.v*CH_WICK_SHARE):[]; const src=big.length?big:e.parts; let h=-Infinity,l=Infinity; for(const x of src){if(x.h>h)h=x.h;if(x.l<l)l=x.l} e.h=h;e.l=l; if(e.v>0){e.o=e.ov/e.v;e.c=e.cv/e.v}else{e.o=src[0].o;e.c=src[0].c} delete e.ov;delete e.cv;delete e.parts} return out}; const chPair=()=>{const f=TOKENS[fromSel.value],t=TOKENS[toSel.value]; if(!f||!t||f.addr===t.addr)return null; return f.addr.toLowerCase()<t.addr.toLowerCase()?[f,t]:[t,f]}; async function loadChart(){ chBars=null;chPools=null; const p=chPair(); if(tab!=="swap"||!account||!p){syncChartUI();return} const my=++chSeq,[t0,t1]=p,key=t0.addr+":"+t1.addr; const hit=chCache[key]; if(hit&&Date.now()-hit.at<30000){chBars=hit.bars;chPools=hit.pools;syncChartUI();return} try{ if(PPLENS===ZERO||(await rpc("eth_getCode",[PPLENS,L])).length<5)return syncChartUI(); const [raw]=await mc3([{to:PPLENS,data:"0x"+SEL_MARKETS+encAddr(t0.addr)+encAddr(t1.addr) +encAddr(account)+encUint(0)+encUint(LQ_SCAN)+encUint(0)}]); if(my!==chSeq)return; const found=[]; if(raw){const h=strip0x(raw),n=Number(BigInt("0x"+h.slice(64,128))),base=128; for(let i=0;i<n&&i<LQ_SCAN;i++){const at=k=>BigInt("0x"+h.slice(base+(i*PI_WORDS+k)*64,base+(i*PI_WORDS+k+1)*64)); if(at(PI_HOOK)!==0n||at(PI_LIQ)===0n)continue; found.push({a:"0x"+h.slice(base+(i*PI_WORDS+PI_POOL)*64+24,base+(i*PI_WORDS+PI_POOL+1)*64),liq:at(PI_LIQ)})}} found.sort((x,y)=>y.liq>x.liq?1:y.liq<x.liq?-1:0); const pools=found.slice(0,CH_POOLS).map(x=>x.a); if(!pools.length){chCache[key]={at:Date.now(),bars:null,pools:null};return syncChartUI()} const reqs=[]; for(const a of pools)reqs.push({to:a,data:"0x"+SEL_TAPE+encUint(FINE)+encUint(256)}, {to:a,data:"0x"+SEL_TAPE+encUint(COARSE)+encUint(256)}); const tapes=await mc3Deep(reqs); if(my!==chSeq)return; const dec1Tape=hex=>{if(!hex)return null; const h=strip0x(hex),n=Number(BigInt("0x"+h.slice(64,128))),bars=[]; for(let j=0;j<n;j++)bars.push(decBar(BigInt("0x"+h.slice(128+j*64,128+(j+1)*64)))); return bars}; const decoded=tapes.map(dec1Tape); const fine=[],coarse=[]; for(let i=0;i<pools.length;i++){fine.push(decoded[i*2]);coarse.push(decoded[i*2+1])} const sc=10**(t0.dec-t1.dec)/1e18; const build=set=>{const m=mergeTapes(set); for(const b of m){b.o*=sc;b.h*=sc;b.l*=sc;b.c*=sc}return m.length?m:null}; const f=build(fine); let c=build(coarse); if(f&&c){const lead=rollUp(f,FINE,COARSE).filter(b=>b.b>c[0].b); if(lead.length)c=lead.concat(c)} chBars=f||c?{[FINE]:f,[COARSE]:c}:null; const live=set=>set.filter(d=>d&&d.some(Boolean)).length; chPools=chBars?{[FINE]:live(fine),[COARSE]:live(coarse)}:null; chCache[key]={at:Date.now(),bars:chBars,pools:chPools}; }catch{chBars=null;chPools=null} if(my===chSeq)syncChartUI(); } const chPoolsAt=step=>chPools&&chPools[step]||0; const chLabel=p=>chInv?`${p[0].sym} per ${p[1].sym}`:`${p[1].sym} per ${p[0].sym}`; function syncChartUI(){ const on=tab==="swap"&&!!account&&!!chBars; chTog.classList.toggle("hide",!on); chBox.classList.toggle("hide",!on||!chOpen); chTog.setAttribute("aria-expanded",on&&chOpen?"true":"false"); if(!on||!chOpen)return; const p=chPair(); chNote.textContent=p?chLabel(p):""; drawChart();syncInv(); } const NUMERAIRES=new Set([ "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "0xdac17f958d2ee523a2206206994597c13d831ec7", "0x6b175474e89094c44da98b954eedeac495271d0f", ]); const moneyRank=a=>{a=(a||"").toLowerCase(); return NUMERAIRES.has(a)?2:(a===ZERO||a===WETH.toLowerCase())?1:0}; const chPairKey=p=>p?p[0].addr.toLowerCase()+":"+p[1].addr.toLowerCase():""; const chNaturalInv=p=>!!p&&moneyRank(p[0].addr)>moneyRank(p[1].addr); const CH_SLOTS=Math.floor((340-2-42)/3); const chFinest=()=>chBars&&chBars[FINE]?FINE:COARSE; const chSeries=secs=>{ if(!chBars)return null; const src=srcFor(secs),step=chBars[src]?src:chFinest(),base=chBars[step]; if(!base||!base.length)return null; const per=Math.max(secs,step); let pts=rollUp(base,step,per).slice().reverse(); pts=pts.map(b=>{const{o,c}=b; if(!(o>0)||!(c>0)||!isFinite(o)||!isFinite(c))return null; const h=b.h>0&&isFinite(b.h)?b.h:0,l=b.l>0&&isFinite(b.l)?b.l:Infinity; return{...b,h:Math.max(h,o,c),l:Math.min(l,o,c)}}).filter(Boolean); return pts.length?{pts,per,step}:null; }; const chAutoPeriod=()=>{ if(!chBars)return null; const fit=secs=>{const s=chSeries(secs); if(!s||s.per!==secs)return null; const span=s.pts[s.pts.length-1].b-s.pts[0].b+1; return{n:s.pts.length,ok:s.pts.length>=6&&span<=CH_SLOTS}}; let best=null,bestN=-1; for(const[,secs] of TFS){const f=fit(secs); if(!f)continue; if(f.ok)return secs; if(f.n>bestN){best=secs;bestN=f.n}} return best==null?chFinest():best; }; let chTfKey=""; const chSyncPeriod=()=>{ const k=chPairKey(chPair()); if(k===chTfKey)return; const auto=chAutoPeriod(); if(auto==null)return; chTfKey=k;chPeriod=auto; }; const chSyncTfs=()=>{ const finest=chFinest(); if(chPeriod<finest)chPeriod=finest; for(const c of chTf.children){if(!c.dataset.secs)continue; const secs=Number(c.dataset.secs); c.disabled=!!chBars&&secs<finest; c.title=c.disabled?"This pair has no five-minute tape to draw":""; c.classList.toggle("on",secs===chPeriod)} }; const chSyncOrientation=()=>{ const p=chPair(),k=chPairKey(p); if(k===chInvKey)return; chInvKey=k;chInv=chNaturalInv(p); }; const hudFor=i=>{ if(!chDraw)return; const{pts,num,UP,DN,ch,per,hd}=chDraw; if(!hd)return; if(i<0){const b=pts[pts.length-1]; hd.innerHTML=`<b>${num(b.c)}</b> <span style="color:${ch>=0?UP:DN}">${ch>=0?"+":""}${ch.toFixed(2)}%</span>`; return} const b=pts[i],up=b.c>=b.o,d=new Date(b.b*per*1e3); const when=per>=86400 ?d.toLocaleDateString("en-US",{month:"short",day:"numeric"}) :d.toLocaleTimeString("en-US",{hour:"2-digit",minute:"2-digit",hour12:false}); hd.innerHTML=`<b style="color:${up?UP:DN}">${num(b.c)}</b> <span>${when}</span>` +`<span class="ohlc">H ${num(b.h)} L ${num(b.l)} · ${b.n} trade${b.n===1?"":"s"}</span>`; }; const chPoint=e=>{ if(!chDraw)return; const r=chArt.getBoundingClientRect(); if(!r.width)return; const{pts,W,PL,st,b0}=chDraw; const x=(e.clientX-r.left)/r.width*W; const at=b0+Math.round((x-PL)/st-.5); let i=0,near=Infinity; for(let j=0;j<pts.length;j++){const d=Math.abs(pts[j].b-at); if(d<near){near=d;i=j}} hudFor(i); if(!chDraw.svg)return; let el=chDraw.cross; if(!el){ el=document.createElementNS("http://www.w3.org/2000/svg","line"); el.setAttribute("class","cross"); el.setAttribute("y1","0");el.setAttribute("y2","150"); el.setAttribute("stroke","currentColor");el.setAttribute("stroke-opacity",".35"); el.setAttribute("stroke-width",".8"); chDraw.svg.appendChild(el);chDraw.cross=el; } const cx=chDraw.X(i).toFixed(1); el.setAttribute("x1",cx);el.setAttribute("x2",cx); }; chArt.addEventListener("pointermove",chPoint); chArt.addEventListener("pointerdown",chPoint); chArt.addEventListener("pointerleave",()=>{ if(chDraw&&chDraw.cross){chDraw.cross.remove();chDraw.cross=null} hudFor(-1)}); function drawChart(){ if(!chBars)return; const W=340,H=150,PL=2,PR=42,PT=10,PB=14,VH=22; const pw=W-PL-PR,ph=H-PT-PB-VH; chSyncPeriod();chSyncOrientation();chSyncTfs(); const ser=chSeries(chPeriod); if(!ser){chArt.innerHTML='<div class="msg">No trades in this window.</div>';chDraw=null;return} const per=ser.per; let pts=ser.pts; const newest=pts[pts.length-1].b,cap=Math.floor(pw/3); const kept=pts.filter(b=>newest-b.b<cap); const clipped=kept.length<pts.length; pts=kept; if(chInv)pts=pts.map(b=>({...b,o:1/b.o,h:1/b.l,l:1/b.h,c:1/b.c})); if(!pts.length){chArt.innerHTML='<div class="msg">No trades in this window.</div>';chDraw=null;return} let hi=-Infinity,lo=Infinity,vm=0; for(const b of pts){if(b.h>hi)hi=b.h;if(b.l<lo)lo=b.l;if(b.v>vm)vm=b.v} if(hi===lo){hi*=1.001;lo*=0.999} let sp=hi-lo;hi+=sp*.09;lo-=sp*.09;sp=hi-lo; const n=pts.length,b0=pts[0].b,slots=pts[n-1].b-b0+1; const st=pw/slots,bw=Math.max(1,Math.min(9,st*.68)); const X=i=>PL+st*(pts[i].b-b0+.5),Y=p=>PT+ph*(1-(p-lo)/sp),vt=PT+ph+6,VY=v=>vt+(VH-6)*(1-(vm?v/vm:0)); const num=v=>v>=1000?v.toLocaleString("en-US",{maximumFractionDigits:2}):v>=1?v.toFixed(3):v.toPrecision(3); const UP="#16a34a",DN="#c33"; const p0=chPair(),lbl=p0?chLabel(p0):"Price"; let s=`<svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="none" role="img" aria-label="${lbl}, ${n} bars, ${num(pts[n-1].c)} now, range ${num(lo)} to ${num(hi)}">`; for(let g=0;g<=3;g++){const p=lo+sp*g/3,y=Y(p).toFixed(1); s+=`<line x1="${PL}" y1="${y}" x2="${W-PR}" y2="${y}" stroke="currentColor" stroke-opacity=".1"/>` +`<text x="${W-PR+4}" y="${(Y(p)+2.5).toFixed(1)}" font-size="7" fill="currentColor" fill-opacity=".5">${num(p)}</text>`} for(let i=0;i<n;i++){const b=pts[i],y=VY(b.v); s+=`<rect x="${(X(i)-bw/2).toFixed(1)}" y="${y.toFixed(1)}" width="${bw.toFixed(1)}" height="${Math.max(.4,vt+VH-y).toFixed(1)}" fill="currentColor" fill-opacity=".18"/>`} const rise=pts[n-1].c>=pts[0].o; if(chKind==="line"){ const d=pts.map((b,i)=>`${i?"L":"M"}${X(i).toFixed(1)} ${Y(b.c).toFixed(1)}`).join(" "); const col=rise?UP:DN; s+=`<path d="${d} L${X(n-1).toFixed(1)} ${(PT+ph).toFixed(1)} L${X(0).toFixed(1)} ${(PT+ph).toFixed(1)} Z" fill="${col}" fill-opacity=".1"/>` +`<path d="${d}" fill="none" stroke="${col}" stroke-width="1.2" stroke-linejoin="round"/>` +`<circle cx="${X(n-1).toFixed(1)}" cy="${Y(pts[n-1].c).toFixed(1)}" r="1.8" fill="${col}"/>`; }else{ for(let i=0;i<n;i++){const b=pts[i],col=b.c>=b.o?UP:DN,x=X(i); const t=Y(Math.max(b.o,b.c)),bo=Y(Math.min(b.o,b.c)); s+=`<line x1="${x.toFixed(1)}" y1="${Y(b.h).toFixed(1)}" x2="${x.toFixed(1)}" y2="${Y(b.l).toFixed(1)}" stroke="${col}" stroke-width=".8"/>` +`<rect x="${(x-bw/2).toFixed(1)}" y="${t.toFixed(1)}" width="${bw.toFixed(1)}" height="${Math.max(.8,bo-t).toFixed(1)}" fill="${col}"/>`} } s+="</svg>"; const last=pts[n-1],first=pts[0],ch=((last.c-first.o)/first.o)*100; chArt.innerHTML=s+'<div class="hd"></div>'; chDraw={pts,X,W,PL,st,b0,num,UP,DN,ch,per, hd:chArt.querySelector(".hd"),svg:chArt.querySelector("svg"),cross:null}; hudFor(-1); const took=rel(slots*per); const np=chPoolsAt(ser.step); chNote.textContent=p0?`${chLabel(p0)} · ${np} pool${np===1?"":"s"} · ${clipped?"last ":""}${took}`:""; } for(const[label,secs]of TFS){const b=document.createElement("button"); b.textContent=label;b.dataset.secs=String(secs);if(secs===chPeriod)b.className="on"; b.onclick=()=>{chPeriod=secs;chTfKey=chPairKey(chPair());drawChart()}; chTf.appendChild(b)} const kindBtn=document.createElement("button"); kindBtn.className="chk"; const syncKind=()=>{kindBtn.textContent=chKind==="candle"?"Line":"Candle"; kindBtn.title=`Switch to ${kindBtn.textContent.toLowerCase()} chart`}; syncKind(); kindBtn.onclick=()=>{chKind=chKind==="candle"?"line":"candle";LS.ck=chKind;syncKind();drawChart()}; chTf.appendChild(kindBtn); const invBtn=document.createElement("button"); invBtn.className="chk"; invBtn.textContent="⇅"; const syncInv=()=>{const p=chPair(); invBtn.title=p?`Price ${chInv?p[1].sym:p[0].sym} instead — currently ${chInv?p[0].sym:p[1].sym} per ${chInv?p[1].sym:p[0].sym}`:"Invert the price"; invBtn.setAttribute("aria-pressed",chInv?"true":"false")}; syncInv(); invBtn.onclick=()=>{chInv=!chI
0xacfba7ce…4082·#25,751,069·0xcb87410e…f055b1
for="lqFee">Fee<select id="lqFee">` +LQ_FEES.map(([v,l,h])=>`<option value="${v}"${v===3000?" selected":""}>${l} — ${h}</option>`).join("") +`</select></label>` +`<div class="lqout" id="lqRangeOut"></div>` +`<div class="lqout" id="lqPv"></div>` +`<div class="lqout" id="lqOwnNote"></div>` +`<div class="lqout" id="lqGas"></div>` +`<button class="primary" id="lqCreate" disabled>Create band</button></div>`; } const pxToSqrt=(px,d0,d1)=>{ const raw=px*10**(d1-d0); if(!isFinite(raw)||raw<=0)return 0n; return BigInt(Math.round(Math.sqrt(raw)*1e9))*10n**9n; }; const lqMarket=(t0,t1,lo,hi,fee,owner)=>encAddr(t0)+encAddr(t1)+encUint(lo)+encUint(hi) +encUint(fee)+encAddr(ZERO)+encAddr(owner)+encUint(0n); const lqSeedPrice=(sl,sh,in0,in1)=>{ const L=Number(sl),H=Number(sh),x=Number(in0),y=Number(in1),W=1e18; const g=s=>(x*s*H*(s-L))/((H-s)*W*y*W); let lo=L,hi=H; for(let i=0;i<90;i++){const m=(lo+hi)/2;if(g(m)<1)lo=m;else hi=m} const s=(lo+hi)/2; if(!(s>L&&s<H))return 0n; return BigInt(Math.round(s)); }; const lqBandFor=(px,mult,a0,a1)=>{ if(a0&&!a1)return[px,px*mult]; if(a1&&!a0)return[px/mult,px]; return[px/mult,px*mult]; }; const LQ_MIN_LIQUIDITY=1000n,LQ_MIN_RESOLUTION=1000000n,LQ_WAD=10n**18n, LQ_MAX_LIQUIDITY=(1n<<128n)-1n,LQ_MAX_SQRT=10n**36n,LQ_SEED_TOLERANCE=100n; const lqUp=(a,b,c)=>(a*b+c-1n)/c; const LQ_NOSEED={ok:false,lp:0n,used0:0n,used1:0n}; function lqSeedQuote(sl,sh,s,a0,a1){ if(!(sl>0n)||!(sh>sl)||sh>LQ_MAX_SQRT||s<sl||s>sh)return LQ_NOSEED; const f0=()=>a0*s/(sh-s)*sh/LQ_WAD,f1=()=>a1*LQ_WAD/(s-sl); let total; if(s===sl)total=f0(); else if(s===sh)total=f1(); else{const x=f0(),y=f1();total=x<y?x:y} if(total===0n||total>LQ_MAX_LIQUIDITY)return LQ_NOSEED; let used0=lqUp(total,(sh-s)*LQ_WAD,sh*s),used1=lqUp(total,s-sl,LQ_WAD); const v0=total*LQ_WAD/sh,v1=total*sl/LQ_WAD; if(v0<LQ_MIN_RESOLUTION||v1<LQ_MIN_RESOLUTION)return LQ_NOSEED; let x=v0+used0,y=v1+used1; const maxX=y*LQ_WAD*LQ_WAD/(sl*sl); if(x>maxX){used0=maxX>v0?maxX-v0:0n;x=v0+used0} const maxY=x*sh*sh/(LQ_WAD*LQ_WAD); if(y>maxY){used1=maxY>v1?maxY-v1:0n;y=v1+used1} if(used0>a0||used1>a1||total<=LQ_MIN_LIQUIDITY)return LQ_NOSEED; const wantSq=s*s,gotSq=x===0n?0n:y*LQ_WAD*LQ_WAD/x,slack=wantSq/LQ_SEED_TOLERANCE; if(gotSq<wantSq-slack||gotSq>wantSq+slack)return LQ_NOSEED; return {ok:true,lp:total-LQ_MIN_LIQUIDITY,used0,used1}; } // The seed postconditions `lqSeedQuote` does NOT model, because the LENS does // not either - and the two are pinned to one fixture, so folding these in would // break the pin that makes the mirror worth trusting. Hence a separate layer. // // `_addLiquidity` refuses on three further grounds once the amounts settle: // 1. RESERVES PAST uint128. Checked BEFORE the two bound corrections, which // only lower the amounts - so against pre-correction figures, recomputed // here rather than read off the corrected result. // 2. A PRICE THE BAND EXCLUDES. The tolerance test is against the REQUESTED // price; a seed at a bound can land within 1% of it and still sit a // rounding step outside the band itself. // 3. NO EXECUTABLE TRADE EITHER WAY. Inside the band is a property of the // floored price; whether an integer swap exists is one of the reserves. // Such a pool is inert and immutable, so refusing to create it is the // only remedy. // Each printed "Seeds X + Y" and let the wallet deliver the revert. // // For the bands THIS FORM makes: `lqMarket` passes a zero hook and a zero // creator share, so surcharge and creator cut vanish and `gross`, `net` and // `kept` collapse into one figure. const LQ_MAX128=(1n<<128n)-1n,LQ_FEE_DENOM=1000000n; const lqInBand=(sl,sh,vX,vY)=>{ if(vX<=0n)return false; const ratio=vY*LQ_WAD*LQ_WAD/vX,hi=sh+1n; return ratio>=sl*sl&&ratio<hi*hi; }; // Smallest priced amount whose output does not floor to zero, then the input // that survives the fee rounding - inverted in the order the swap applies it, // and replayed forward with the same floors. function lqExecutable(sl,sh,fee,augIn,augOut,rIn,rOut,zeroForOne){ if(augOut<=1n)return false; let need=lqUp(augIn,1n,augOut-1n); if(need===0n)need=1n; if(need>LQ_MAX128)return false; const net=lqUp(need,LQ_FEE_DENOM,LQ_FEE_DENOM-fee); if(net>LQ_MAX128)return false; const priced=net*(LQ_FEE_DENOM-fee)/LQ_FEE_DENOM; if(priced===0n)return false; if(net>LQ_MAX128-rIn)return false; const out=priced*augOut/(augIn+priced); if(out===0n||out>rOut)return false; return zeroForOne?lqInBand(sl,sh,augIn+net,augOut-out):lqInBand(sl,sh,augOut-out,augIn+net); } const lqTradeable=(sl,sh,fee,x,y,r0,r1)=> (r1!==0n&&lqExecutable(sl,sh,fee,x,y,r0,r1,true)) ||(r0!==0n&&lqExecutable(sl,sh,fee,y,x,r1,r0,false)); function lqSeedAdmits(sl,sh,s,fee,q){ if(!q||!q.ok)return false; const total=q.lp+LQ_MIN_LIQUIDITY; if(lqUp(total,(sh-s)*LQ_WAD,sh*s)>LQ_MAX128)return false; if(lqUp(total,s-sl,LQ_WAD)>LQ_MAX128)return false; const x=total*LQ_WAD/sh+q.used0,y=total*sl/LQ_WAD+q.used1; if(!lqInBand(sl,sh,x,y))return false; return lqTradeable(sl,sh,fee,x,y,q.used0,q.used1); } const lqBandFits=(sl,sh,sp,fee,in0,in1)=> lqSeedAdmits(sl,sh,sp,fee,lqSeedQuote(sl,sh,sp,in0,in1)); const LQ_WIDTHS=[1000,100,10,2]; function lqFullWidth(d0,d1,a0,a1,px,fee){ for(const mult of LQ_WIDTHS){ const [lo,hi]=lqBandFor(px,mult,a0,a1); const sl=pxToSqrt(lo,d0,d1),sh=pxToSqrt(hi,d0,d1); if(!sl||!sh||sh<=sl)continue; const sp=a0&&!a1?sl:a1&&!a0?sh:lqSeedPrice(sl,sh,a0,a1); if(sp&&lqBandFits(sl,sh,sp,fee,a0,a1))return mult; } return null; } async function loadLq(){ const p=chPair(); const my=++lqSeq; if(!p){lqList.innerHTML=`<div class="lqempty">Pick two different tokens.</div>`;lqSub.textContent="";lqRows=[];return}lqSyncAmounts(); const [t0,t1]=p; lqSub.textContent=`${t0.sym} / ${t1.sym}`; lqList.innerHTML=`<div class="lqempty">Loading…</div>`; try{ const who=account||ZERO; const [raw,c]=await mc3([ {to:PPLENS,data:"0x"+SEL_MARKETS+encAddr(t0.addr)+encAddr(t1.addr) +encAddr(who)+encUint(0)+encUint(LQ_SCAN)+encUint(0)}, {to:PFACTORY,data:"0x"+SEL_PAIR_COUNT+encAddr(t0.addr)+encAddr(t1.addr)}, ]); if(my!==lqSeq)return; let total=0;try{if(c)total=Number(BigInt(c))}catch{} const rows=[]; if(raw){const h=strip0x(raw),n=Number(BigInt("0x"+h.slice(64,128))),base=128; for(let i=0;i<n&&i<LQ_SCAN;i++){ const at=k=>BigInt("0x"+h.slice(base+(i*PI_WORDS+k)*64,base+(i*PI_WORDS+k+1)*64)); rows.push({pool:"0x"+h.slice(base+(i*PI_WORDS+PI_POOL)*64+24,base+(i*PI_WORDS+PI_POOL+1)*64), sl:at(PI_SL),sh:at(PI_SH),fee:at(PI_FEE),r0:at(PI_R0),r1:at(PI_R1),px:at(PI_PX), liq:at(PI_LIQ),hook:"0x"+h.slice(base+(i*PI_WORDS+PI_HOOK)*64+24,base+(i*PI_WORDS+PI_HOOK+1)*64), shares:0n,mine0:0n,mine1:0n})}} if(account&&rows.length){ const bs=await mc3Deep(rows.map(r=>({to:r.pool,data:"0x"+SEL_BALANCEOF+encAddr(account)}))); if(my!==lqSeq)return; rows.forEach((r,i)=>{ if(!bs[i]||r.liq===0n)return; const s=dec1(bs[i],0); r.shares=s;r.mine0=s*r.r0/r.liq;r.mine1=s*r.r1/r.liq; }); } if(my!==lqSeq)return; if(!rows.length){ lqList.innerHTML=`<div class="lqempty">No Precision band for this pair yet.</div>`+lqNewForm(t0,t1);lqSyncAmounts(); lqRows=[]; lqNewPreview(); return} rows.sort((a,b)=>(b.shares>0n)-(a.shares>0n)||(b.liq>a.liq?1:b.liq<a.liq?-1:0)); const shown=rows.slice(0,LQ_SHOW); if(total>shown.length)lqSub.textContent+=` · showing ${shown.length} of ${total}` +(total>LQ_SCAN?` (first ${LQ_SCAN} scanned)`:""); lqRows=shown; lqList.innerHTML=shown.map(o=>lqRow(o,t0,t1)).join("") +`<button class="lqbtn lqnewtog" id="lqNewTog">Create a new band</button>`; lqSyncAmounts(); }catch(e){ if(my!==lqSeq)return; lqRows=[]; lqList.innerHTML=`<div class="lqempty">Could not read pools.</div>`;lqSyncAmounts(); } } const SEL_REMOVE="e39b0eb5",SEL_ADDEXACT="cc0025e4",SEL_PREVIEW_REMOVE="a2eaee07", SEL_PREVIEW_ADD="e03ec807"; const dec1=(h,i)=>BigInt("0x"+strip0x(h).slice(i*64,(i+1)*64)); async function previewRemove(pool,shares){ const [r]=await mc3([{to:PLQLENS,data:"0x"+SEL_PREVIEW_REMOVE+encAddr(pool)+encUint(shares)}]); if(!r)return null; return dec1(r,0)===1n?{a0:dec1(r,1),a1:dec1(r,2)}:null; } async function previewAdd(pool,a0,a1){ const [r]=await mc3([{to:PLQLENS,data:"0x"+SEL_PREVIEW_ADD+encAddr(pool)+encUint(a0)+encUint(a1)}]); if(!r)return null; return dec1(r,0)===1n?{lp:dec1(r,1),used0:dec1(r,2),used1:dec1(r,3),ref0:dec1(r,4),ref1:dec1(r,5)}:null; } async function lqTo(){ const v=rc.value.trim(); if(!v)return account; const to=await rcvOf(v).catch(()=>""); if(!to){rc.classList.add("bad"); stat.textContent="Recipient must be an address or a .wei / .gwei / .eth name that resolves."; return null} rc.classList.remove("bad"); return to; } let lqToSeq=0; async function lqShowTo(){ const my=++lqToSeq,v=rc.value.trim(); if(!v){rc.classList.remove("bad");rcvEl.textContent="";return} rcvEl.textContent="resolving..."; const to=await rcvOf(v).catch(()=>""); if(my!==lqToSeq)return; rc.classList.toggle("bad",!to); rcvEl.textContent=to||""; } async function previewZap(pool,tokenIn,amountIn){ const [r]=await mc3([{to:PLQLENS,data:"0x"+SEL_PREVIEW_ZAP+encAddr(pool)+encAddr(tokenIn)+encUint(amountIn)}]); if(!r)return null; return dec1(r,0)===1n?{portion:dec1(r,1),lp:dec1(r,2)}:null; } const lessSlip=v=>{const b=BigInt(Math.round(Math.min(10,Math.max(.01,+slip.value||.5))*100)); return v-(v*b)/10000n}; async function ensureAllowance(token,spender,need){ if(token===ZERO||need===0n)return; const [r]=await mc3([{to:token,data:"0x"+SEL_ALLOWANCE+encAddr(account)+encAddr(spender)}]); if(r&&dec1(r,0)>=need)return; stat.textContent="Approving…"; if(r&&dec1(r,0)>0n)await waitTx(await rpc(S,[{from:account,to:token, data:"0x"+SEL_APPROVE+encAddr(spender)+encUint(0),value:"0x0"}])); await waitTx(await rpc(S,[{from:account,to:token, data:"0x"+SEL_APPROVE+encAddr(spender)+encUint(need),value:"0x0"}])); } async function factoryPool(pool){ const [r]=await mc3([{to:PFACTORY,data:"0x"+SEL_ISPOOL+encAddr(pool)}]).catch(()=>[null]); return !!r&&dec1(r,0)===1n; } /* THE ESCAPE HATCH, SURFACED. `removeLiquidity` demands exact movement on BOTH sides, so one token that pauses, blacklists the holder, or starts taking a transfer fee takes the whole withdrawal down with it - the healthy side included. The pool carries `removeLiquidityLossy` for that and the page could not reach it. Tried only AFTER the strict exit is refused by an `eth_call`, so it cannot silently downgrade a withdrawal that would have settled in full. Both sides are attempted first; abandoning one is offered only when that fails, because it FORFEITS that side's claim to the remaining holders - the caller's decision, not the page's. MINIMUMS OF ZERO, deliberately: the lossy path bounds what LEAVES the pool, below the pro-rata claim on a short pool, so the strict bound would revert on exactly the pools this escapes. Safe here in a way it would not be on a swap - nothing touches the price, the burn is the caller's own shares, and the payout is pro-rata on the surviving balance, so no ordering pays. The confirmation states the loss instead. */ const SEL_REMOVE_LOSSY="0cc55f06"; const encLossy=(shares,to,take0,take1)=>"0x"+SEL_REMOVE_LOSSY+encUint(shares) +encUint(0n)+encUint(0n)+encAddr(to)+encBool(take0)+encBool(take1); async function lossyExit(pool,shares,t0,t1){ const attempt=async(take0,take1)=>{ const req={from:account,to:pool,value:"0x0",data:encLossy(shares,account,take0,take1)}; try{await rpc(C,[req,L]);return req}catch{return null} }; const both=await attempt(true,true); if(both)return{req:both,note:`This pool cannot pay out in full — one of its` +` tokens is short, or takes a fee on transfer.\n\nYou would receive your share` +` of what it can still pay, on both sides, which is less than the amounts` +` shown.\n\nContinue?`}; for(const [take0,take1,keep,drop] of [[true,false,t0,t1],[false,true,t1,t0]]){ const req=await attempt(take0,take1); if(!req)continue; return{req,note:`The ${drop.sym} side of this pool cannot be transferred.\n\n` +`You can still leave through ${keep.sym} alone — but doing so GIVES UP your` +` ${drop.sym} claim permanently, to the holders who remain.\n\nContinue?`}; } return null; } async function doRemove(pool,shares,t0,t1){ const pv=await previewRemove(pool,shares); if(!pv){stat.textContent="This amount redeems nothing — the pool would refuse it.";return} if(!await factoryPool(pool)){stat.textContent="Not a Precision pool — refusing to send.";return} ++busy; try{ await checkWallet(); stat.textContent="Withdrawing…"; let req={from:account,to:pool,value:"0x0", data:"0x"+SEL_REMOVE+encUint(shares)+encUint(lessSlip(pv.a0))+encUint(lessSlip(pv.a1))+encAddr(account)}; try{await rpc(C,[req,L])} catch(strict){ const alt=await lossyExit(pool,shares,t0,t1); if(!alt)throw strict; let go=false; try{go=confirm(alt.note)} catch{go=false} if(!go){stat.textContent="Withdrawal cancelled.";return} req=alt.req; stat.textContent="Withdrawing what the pool can still pay…"; } await settle(await rpc(S,[req])); await loadLq();refreshBalance(); }catch(e){err(e)} finally{--busy} } async function doAdd(pool,a0,a1,t0,t1){ const to=await lqTo(); if(!to)return; const pv=await previewAdd(pool,a0,a1); if(!pv){stat.textContent="The pool would refuse this deposit.";return} if(!await factoryPool(pool)){stat.textContent="Not a Precision pool — refusing to send.";return} ++busy; try{ await checkWallet(); await ensureAllowance(t0.addr,pool,a0); await ensureAllowance(t1.addr,pool,a1); stat.textContent="Adding…"; const value=t0.addr===ZERO?a0:0n; const req={from:account,to:pool,value:toHex(value), data:"0x"+SEL_ADDEXACT+encUint(0)+encUint(a0)+encUint(a1)+encUint(lessSlip(pv.lp))+encAddr(to)}; await rpc(C,[req,L]); await settle(await rpc(S,[req])); await loadLq();refreshBalance(); }catch(e){err(e)} finally{--busy} } async function doZap(pool,tok,amountIn){ const to=await lqTo(); if(!to)return; const pv=await previewZap(pool,tok.addr,amountIn); if(!pv||pv.lp===0n){stat.textContent="The pool would refuse this deposit.";return} if(!await factoryPool(pool)){stat.textContent="Not a Precision pool — refusing to send.";return} ++busy; try{ await checkWallet(); const minLp=lessSlip(pv.lp); const zap="0x"+SEL_ZAPIN+encAddr(pool)+encAddr(tok.addr)+encUint(amountIn) +encUint(pv.portion)+encUint(minLp)+encAddr(to)+encAddr(account); const leg=encSnwap(tok.addr,amountIn,to,pool,minLp,PROUTE,zap); let data,value; if(tok.addr===ZERO){data=leg;value=toHex(amountIn)} else{ stat.textContent="Approving…"; await ensureAllowance(tok.addr,ZROUTER,amountIn); data=encCalls([ encSnwap(ZERO,0n,account,ZERO,0n,PROUTE, "0x"+SEL_PCHECKPOINT+encAddr(tok.addr)+strip0x(keccak(hexToBytes(zap)))+encAddr(account)), leg]); value="0x0"; } stat.textContent="Zapping…"; const req={from:account,to:ZROUTER,value,data}; await rpc(C,[req,L]); await settle(await rpc(S,[req])); await loadLq();refreshBalance(); }catch(e){err(e)} finally{--busy} } let lqPvT,lqPvSeq=0; async function lqPreview(box){ const pool=box.closest(".lqrow")?.dataset.pool||""; const row=lqRows.find(x=>x.pool.toLowerCase()===pool.toLowerCase()); const p=chPair(); const out=box.querySelector(".lqpv"),go=box.querySelector('[data-act="ac"]'); if(!row||!p){out.textContent="";go.disabled=true;return} const [t0,t1]=p,ins=box.querySelectorAll(".lqin"); let a0,a1; try{ a0=ins[0].value.trim()?parseUnits(ins[0].value.trim(),t0.dec):0n; a1=ins[1].value.trim()?parseUnits(ins[1].value.trim(),t1.dec):0n; }catch{out.textContent="Invalid amount.";go.disabled=true;return} if(isZap(box)){ const z=zapSide(box,t0,t1); if(z.err){out.textContent=z.err;go.disabled=true;return} if(!z.amt){out.textContent=`Enter an amount of ${t0.sym} or ${t1.sym}.`;go.disabled=true;return} const mz=++lqPvSeq; out.textContent="Checking…";go.disabled=true; const zp=await previewZap(row.pool,z.tok.addr,z.amt).catch(()=>null); if(mz!==lqPvSeq)return; if(!zp||zp.lp===0n){out.textContent="The pool would refuse this deposit.";go.disabled=true;return} out.textContent=`swaps ${trimAmt(zp.portion,z.tok.dec)} of ${trimAmt(z.amt,z.tok.dec)} ${z.tok.sym}` +` for ${t1.sym}, then deposits both · mints ~${trimAmt(zp.lp,18)} LP`; go.disabled=false;return; } if(!a0||!a1){ out.textContent="Enter both amounts.";go.disabled=true;return} const my=++lqPvSeq; out.textContent="Checking…";go.disabled=true; const pv=await previewAdd(row.pool,a0,a1).catch(()=>null); if(my!==lqPvSeq)return; if(!pv){out.textContent="The pool would refuse this deposit.";go.disabled=true;return} const ref=(pv.ref0>0n?` · refunds ${trimAmt(pv.ref0,t0.dec)} ${t0.sym}`:"") +(pv.ref1>0n?` · refunds ${trimAmt(pv.ref1,t1.dec)} ${t1.sym}`:""); out.textContent=`deposits ${trimAmt(pv.used0,t0.dec)} ${t0.sym} + ${trimAmt(pv.used1,t1.dec)} ${t1.sym}${ref}`; go.disabled=false; } const isZap=box=>!!box.querySelector(".lqz")?.checked; function zapSide(box,t0,t1){ const ins=box.querySelectorAll(".lqin"); const v0=ins[0].value.trim(),v1=ins[1].value.trim(); if(v0&&v1)return{err:`One side only — clear the ${t1.sym} field, or turn one-sided off.`}; if(!v0&&!v1)return{err:`Enter an amount of ${t0.sym} or ${t1.sym}.`}; const tok=v0?t0:t1; try{return{tok,amt:parseUnits(v0||v1,tok.dec)}}catch{return{err:"Invalid amount."}} } function lqMirror(box,src){ if(isZap(box))return; const pool=box.closest(".lqrow")?.dataset.pool||""; const row=lqRows.find(x=>x.pool.toLowerCase()===pool.toLowerCase()); const p=chPair(); if(!row||!p||!row.r0||!row.r1)return; const [t0,t1]=p,ins=box.querySelectorAll(".lqin"); const i=src===ins[0]?0:src===ins[1]?1:-1; if(i<0)return; const other=ins[1-i],v=src.value.trim(); if(!v){other.value="";return} try{ const raw=parseUnits(v,i===0?t0.dec:t1.dec); other.value=formatUnits(i===0?raw*row.r1/row.r0:raw*row.r0/row.r1,i===0?t1.dec:t0.dec); }catch{} } lqList.addEventListener("input",e=>{ if(!e.target.classList.contains("lqin"))return; const box=e.target.closest(".lqadd"); if(box){lqMirror(box,e.target);clearTimeout(lqPvT);lqPvT=setTimeout(()=>lqPreview(box),300);return} if(e.target.closest(".lqnew")){clearTimeout(lqPvT);lqPvT=setTimeout(lqNewPreview,300)} }); lqList.addEventListener("change",e=>{ if(e.target.id==="lqFee"||e.target.id==="lqRange")return lqNewPreview(); if(!e.target.classList.contains("lqz"))return; const box=e.target.closest(".lqadd"),ins=box.querySelectorAll(".lqin"); const on=e.target.checked; if(!on)lqMirror(box,ins[0]); box.querySelector('[data-act="ac"]').textContent=on?"Zap in":"Add liquidity"; lqPreview(box); }); let lqNewSeq=0; let lqPvT2; const lqPreviewSoon=()=>{clearTimeout(lqPvT2);lqPvT2=setTimeout(lqNewPreview,320)}; async function lqNewPreview(){ const box=lqList.querySelector(".lqnew"); if(!box)return; const out=box.querySelector("#lqPv"),go=box.querySelector("#lqCreate"); const bandOut=box.querySelector("#lqRangeOut"); const my=++lqNewSeq; go.disabled=true; const pair=chPair(); if(!pair){out.textContent="";return} const [t0,t1]=pair; const range=box.querySelector("#lqRange").value; box.querySelector("#lqCustom").classList.toggle("hide",range!=="custom"); const payTok=TOKENS[fromSel.value]; const payIs0=!!payTok&&payTok.addr.toLowerCase()===t0.addr.toLowerCase(); const el0=payIs0?amt:outAmt,el1=payIs0?outAmt:amt; let a0=0n,a1=0n; try{ const v0=el0.value.trim(),v1=el1.value.trim(); a0=v0?parseUnits(v0,t0.dec):0n;a1=v1?parseUnits(v1,t1.dec):0n; }catch{out.textContent="Check the amounts.";bandOut.textContent="";return} const oneSided=(a0&&!a1)?0:(a1&&!a0)?1:-1; box.querySelector("#lqPxRow").classList.toggle("hide",oneSided<0); if(!a0&&!a1){ bandOut.textContent=""; out.textContent=`Enter how much ${t0.sym} and ${t1.sym} to deposit — the ratio sets the opening price.`; return} const pxIn=box.querySelector("#lqPx").value.trim(); const px=oneSided<0 ?(Number(a1)/10**t1.dec)/(Number(a0)/10**t0.dec) :(pxIn?+pxIn:NaN); if(!(px>0)){ bandOut.textContent=""; const only=oneSided===0?t0.sym:t1.sym; out.textContent=`${/^[AEIOU]/i.test(only)?"An":"A"} ${only}-only band has no ratio to` +` read a price from — name one.`; return} // Read before the width search, not after it: the fee is one of the inputs to // whether a band is executable at all, so the search has to know it. const fee=BigInt(box.querySelector("#lqFee").value); let lo,hi; if(range==="custom"){ const n=id=>{const v=box.querySelector("#"+id).value.trim();return v?+v:NaN}; lo=n("lqLo");hi=n("lqHi"); if(!(lo>0)||!(hi>0)){bandOut.textContent="";out.textContent="Name both prices.";return} if(lo>=hi){out.textContent="The low price must be below the high one.";bandOut.textContent="";return} if(oneSided<0&&(px<lo||px>hi)){ out.textContent=`These amounts imply ${fmtPx(px)}, which is outside that band.`;return} }else{ let mult=+range; if(range==="full"){ const found=lqFullWidth(t0.dec,t1.dec,a0,a1,px,fee); if(!found){ bandOut.textContent=""; out.textContent="Even the narrowest range needs more than this deposit.";return} mult=found; } [lo,hi]=lqBandFor(px,mult,a0,a1); } const sl=pxToSqrt(lo,t0.dec,t1.dec),sh=pxToSqrt(hi,t0.dec,t1.dec); if(!sl||!sh||sh<=sl){out.textContent="Check the prices.";bandOut.textContent="";return} const sp=oneSided===0?sl:oneSided===1?sh:lqSeedPrice(sl,sh,a0,a1); if(!sp){out.textContent="Those amounts cannot open a band that wide.";return} const openAt=bandPrice(sp,t0.dec,t1.dec); bandOut.innerHTML=lqBar(lo,hi,openAt)+`<span></span>`; bandOut.querySelector("span").textContent= `${fmtPx(lo)} – ${fmtPx(hi)} · opens at ${fmtPx(openAt)} ${t1.sym} per ${t0.sym}` +(oneSided===0?` · ${t0.sym} only`:oneSided===1?` · ${t1.sym} only`:""); box.querySelector("#lqOwnNote").textContent= `Unowned, like any AMM pool: anyone may add, and no creator fee can ever be` +` charged. Prices are ${t1.sym} per ${t0.sym}.`; lqGasNote(box); const market=lqMarket(t0.addr,t1.addr,sl,sh,fee,ZERO); box.dataset.sl=sl;box.dataset.sh=sh;box.dataset.sp=sp;box.dataset.fee=fee; box.dataset.a0=a0;box.dataset.a1=a1;box.dataset.lp=0; const q=lqSeedQuote(sl,sh,sp,a0,a1); if(!q.ok){out.textContent="This band cannot open at that price with those amounts.";return} // The mirror answers for the LENS; these are the pool's own remaining // refusals. Saying so here is the whole point of previewing at all - without // it the page printed the seed it was about to send and let the wallet // deliver the revert. if(!lqSeedAdmits(sl,sh,sp,fee,q)){ out.textContent="A band this size could not be traded once opened —" +" widen the range, or deposit more.";return} out.textContent=`Seeds ${trimAmt(q.used0,t0.dec)} ${t0.sym} + ${trimAmt(q.used1,t1.dec)} ${t1.sym}` +((q.used0<a0||q.used1<a1)?" — the rest is returned":""); box.dataset.lp=q.lp; try{ const [addr]=await mc3([{to:PFACTORY,data:"0x"+SEL_POOLFOR+market}]); if(my!==lqNewSeq)return; const pool="0x"+strip0x(addr).slice(24); const [sup]=await mc3([{to:pool,data:"0x"+SEL_TOTALSUPPLY}]).catch(()=>[null]); if(my!==lqNewSeq)return; if(sup&&strip0x(sup).length>=64&&dec1(sup,0)>0n){ out.textContent="That exact band already exists — add to it from the list above."; return} go.disabled=!account; }catch{ if(my!==lqNewSeq)return; out.textContent="Could not reach the factory."; } } const LQ_CREATE_GAS=4700000n; async function lqGasNote(box){ const el=box.querySelector("#lqGas"); if(!el)return; el.textContent=`Creating deploys the pool contract — about ` +`${(Number(LQ_CREATE_GAS)/1e6).toFixed(1)}M gas.`; try{ const wei=BigInt(await rpc("eth_gasPrice",[]))*LQ_CREATE_GAS; el.textContent=`Creating deploys the pool contract — about ` +`${(Number(LQ_CREATE_GAS)/1e6).toFixed(1)}M gas, roughly ` +`${trimAmt(wei,18)} ETH at the moment. Adding to a band that already ` +`exists costs a fraction of it, so check the fee your wallet offers.`; }catch{} } async function lqCreate(){ const box=lqList.querySelector(".lqnew"); if(!box||busy||!account)return; const pair=chPair(); if(!pair)return; const [t0,t1]=pair; const go=box.querySelector("#lqCreate"),out=box.querySelector("#lqPv"); const sl=BigInt(box.dataset.sl||0),sh=BigInt(box.dataset.sh||0),sp=BigInt(box.dataset.sp||0); const a0=BigInt(box.dataset.a0||0),a1=BigInt(box.dataset.a1||0),fee=BigInt(box.dataset.fee||0); if(!sl||!sh||!sp){out.textContent="Preview the band first.";return} const to=await lqTo(); if(!to)return; ++busy;go.disabled=true; try{ const bps=BigInt(Math.round(Math.min(10,Math.max(.01,+slip.value||.5))*100)); const lp=BigInt(box.dataset.lp||0); const minLP=lp>0n?lp*(10000n-bps)/10000n:0n; const market=lqMarket(t0.addr,t1.addr,sl,sh,fee,ZERO); const native=t0.addr===ZERO; if(!native&&a0)await lqApprove(t0.addr,a0); if(a1)await lqApprove(t1.addr,a1); const data="0x"+SEL_CREATE_SEED+market+encUint(sp)+encUint(a0)+en
0xacfba7ce…4082·#25,751,068·0xfc5845ef…cdfbb8
!row.nA&&row.tA.toLowerCase()===WETH.toLowerCase()&&wantsUnwrap(); const floorish=b.dataset.d==="1"||b.dataset.f==="1"; const sel=floorish ?(native?SEL_DUTCH_CANCEL_UNWRAP:SEL_DUTCH_CANCEL) :(native&&b.dataset.v==="1"?SEL_CANCEL_UNWRAP:SEL_CANCELORD); const data="0x"+sel+encUint(id); const lapsed=row.exp&&BigInt(Math.floor(Date.now()/1e3))>row.exp; stat.textContent=`${lapsed?"Reclaiming":"Cancelling"} — the ${b.dataset.f==="1"?"unspent escrow":"unfilled remainder"} returns as ${native&&(floorish||b.dataset.v==="1")?"ETH":"its escrow token"}`; const tx={from:account,to,data,value:"0x0"}; await rpc(C,[tx,L]); await settle(await rpc(S,[tx])); loadBook();refreshBalance();return; } const r=row; let payB=r.aB,getA=r.aA; if(r.pf&&!r.nA&&!r.nB&&!r.dutch&&!r.floor&&r.v2&&r.maker.toLowerCase()!==account.toLowerCase()){ const dB=r.dB??18,dA=r.dA??18,sB=r.sB||"tokens",sA=r.sA||"tokens"; let have=0n; try{ have=r.tB===ZERO||r.tB.toLowerCase()===WETH.toLowerCase() ?BigInt(await rpc("eth_getBalance",[account,L])) +(r.tB===ZERO?0n:BigInt(await rpc(C,[{to:WETH,data:"0x"+SEL_BALANCEOF+encAddr(account)},L]))) :BigInt(await rpc(C,[{to:r.tB,data:"0x"+SEL_BALANCEOF+encAddr(account)},L])); }catch{} const suggest=have&&have<r.aB?have:r.aB; const answer=prompt( `Fill how much of this order?\n\nIt sells ${trimAmt(r.aA,dA)} ${sA} for ${trimAmt(r.aB,dB)} ${sB} in total, ` +`and can be filled in part.\n\nEnter the ${sB} you want to spend:`, trimAmt(suggest,dB)); if(answer===null)return; let asked; try{asked=parseUnits(String(answer).trim(),dB)}catch{throw Error("that is not an amount")} if(!asked)throw Error("enter an amount above zero"); if(asked>r.aB)throw Error(`that is more than the order needs — it costs ${trimAmt(r.aB,dB)} ${sB} in full`); if(asked<r.aB){ const q=await rpc(C,[{to:r.board,data:"0x"+SEL_QUOTEFILL+encUint(id)+encUint(asked)},L]); const h=strip0x(q); if(h.length<128)throw Error("the board would not quote that amount"); getA=BigInt("0x"+h.slice(0,64)); payB=BigInt("0x"+h.slice(64,128)); if(!getA)throw Error("that amount buys nothing — try a larger one"); stat.textContent=`Filling ${trimAmt(payB,dB)} ${sB} of ${trimAmt(r.aB,dB)} for ${trimAmt(getA,dA)} ${sA}...`; } } const nativeOut=!r.nA&&r.tA.toLowerCase()===WETH.toLowerCase()&&wantsUnwrap(); const wethLeg=!r.nB&&r.tB.toLowerCase()===WETH.toLowerCase()&&payB>0n; const plan=wethLeg?await planWethPayment(payB):null; if(plan&&plan.rail==="short") throw Error(`Not enough ether to fill: it costs ${trimAmt(payB,18)} and you hold ` +`${trimAmt(plan.w,18)} WETH plus ${trimAmt(plan.e,18)} ETH. ` +(r.pf ?`This button takes the whole order — to buy part of it, use the Swap tab, which fills this order alongside the pools.` :`This order is all-or-nothing, so it cannot be filled in smaller pieces.`)); if(plan&&plan.wrap>0n){ stat.textContent=`Wrapping ${trimAmt(plan.wrap,18)} ETH to cover the rest...`; const wtx={from:account,to:WETH,data:"0xd0e30db0",value:toHex(plan.wrap)}; await rpc(C,[wtx,L]);await checkWallet();await settle(await rpc(S,[wtx])); } const nativeIn=plan?.rail==="eth"; const routeIn=nativeIn?ZERO:r.tB; const routeOut=nativeOut?ZERO:r.tA; const canonicalNative=routeIn.toLowerCase()===routeOut.toLowerCase() ||(routeOut===ZERO&&routeIn.toLowerCase()===WETH.toLowerCase()); if(!r.nB)await assertFunded(routeIn,payB, known(routeIn)||(routeIn===ZERO?{sym:"ETH",dec:18}:{sym:r.sB||"TOKEN",dec:r.dB??18})); if(!r.nA&&!r.nB&&r.cp===ZERO&&!canonicalNative){ if((await rpc("eth_getCode",[SWAPBOL,L])).length<5)throw Error("orderbook executor not deployed yet"); await preflightLeg(r,routeIn,routeOut,payB); const pay=payB,fillPlan=encFillPlan([ {id:r.id,board:r.board,pay,get:getA,part:false} ],routeIn,routeOut,account,account,dl); const planCall=encSnwap(pay?routeIn:ZERO,pay,account,routeOut,getA,SWAPBOL,fillPlan); const checkpoint=routeIn===ZERO?null:encCheckpoint(SWAPBOL,routeIn,account); const direct=checkpoint?encCalls([checkpoint,planCall]):planCall; const funded=routeIn===ZERO||!pay?direct:encCalls([ checkpoint, encSweep(routeIn,pay,SWAPBOL), encSnwap(ZERO,0n,account,routeOut,getA,SWAPBOL,fillPlan) ]); const meta=known(routeIn)||{addr:routeIn,sym:r.sB||"TOKEN",dec:r.dB??18}; await sendRouterFunded(direct,funded,meta,pay,routeIn===ZERO?pay:0n,"Filling order..."); loadBook();refreshBalance();return; } const data=r.dutch ?"0x"+SEL_DUTCH_FILL+encUint(id)+encUint(r.nA?0n:getA)+encAddr(account)+encUint(payB) :r.v2 ?"0x"+(nativeIn?SEL_FILL2_ETH:nativeOut?SEL_FILL2_UNWRAP:SEL_FILL2) +encUint(id)+encUint(dl)+(nativeIn?"":encUint(payB))+encUint(r.nA?0n:getA)+encAddr(ZERO) :"0x"+SEL_FILL1+encUint(id)+encUint(dl); const payNative=(nativeIn&&!r.dutch&&r.v2)||(r.dutch&&r.tB===ZERO); const wrap=nativeIn&&!payNative&&payB ?{to:WETH,data:"0xd0e30db0",value:toHex(payB)}:null; const tx={from:account,to,data,value:payNative?toHex(payB):"0x0"}; const approvals=[]; if(r.nB){ let ap=ZERO; try{ap="0x"+strip0x(await rpc(C,[{to:r.tB,data:"0x081812fc"+encUint(payB)},L])).slice(-40)}catch{} if(ap.toLowerCase()!==to.toLowerCase()){ approvals.push({to:r.tB,data:"0x"+SEL_APPROVE+encAddr(to)+encUint(payB),value:"0x0"}); } }else if(r.tB!==ZERO&&payB&&!(nativeIn&&!r.dutch&&r.v2)){ const al=BigInt(await rpc(C,[{to:r.tB,data:"0x"+SEL_ALLOWANCE+encAddr(account)+encAddr(to)},L])); if(al<payB){ if(al)approvals.push({to:r.tB,data:"0x"+SEL_APPROVE+encAddr(to)+encUint(0n),value:"0x0"}); approvals.push({to:r.tB,data:"0x"+SEL_APPROVE+encAddr(to)+encUint(payB),value:"0x0"}); } } const revoke=r.dutch&&!r.nB&&r.tB!==ZERO ?{to:r.tB,data:"0x"+SEL_APPROVE+encAddr(to)+encUint(0n),value:"0x0"}:null; if((wrap||approvals.length||revoke)&&await canBatch(account)){ stat.textContent="Preparing and filling atomically..."; await settle(await sendBatch(account,[ ...(wrap?[wrap]:[]),...approvals,{to,data,value:tx.value},...(revoke?[revoke]:[]) ])); loadBook();refreshBalance();return; } if(wrap){ stat.textContent="Wrapping ETH..."; await waitTx(await rpc(S,[{from:account,...wrap}])); } for(let i=0;i<approvals.length;i++){ stat.textContent=`Approve${r.nB?" NFT":""}${approvals.length>1?` (${i+1}/${approvals.length})`:""}...`; await waitTx(await rpc(S,[{from:account,...approvals[i]}])); } await rpc(C,[tx,L]); await settle(await rpc(S,[tx])); if(revoke)try{ stat.textContent="Filled · revoking quote allowance..."; await waitTx(await rpc(S,[{from:account,...revoke}])); stat.textContent="Done"; }catch{stat.textContent="Filled · quote allowance remains; revoke it in your wallet"} loadBook();refreshBalance(); }catch(x){err(x);b.disabled=false} finally{--busy} }); const SLOW="0x000000000000888741B254d37e1b27128AfEAaBC"; const SEL_DEPOSITTO="94eeaec9",SEL_CLAIM="379607f5",SEL_REVERSE="97d15425",SEL_WITHDRAWFROM="d4fdc309"; const SEL_UNLOCK="6198e339",SEL_CLAWBACK="fcc36bc9",SEL_GUARDIAN="0633b14a"; const GRACE=2592000n; const SLOW_GATE="0xb8B546b93a82f4Aa6f0345142dF5679B659ef3D4"; const SEL_DEPOSITTIP="75f92e42",SEL_TIPS="a5c68c59",SEL_REFUNDTIP="d27e1e72"; const TIP_GAS=180000n; let sendTipWei=0n,tipSeq=0; async function syncTip(){ const my=++tipSeq; const on=tab==="send"&&+dly.value>0; tipL.classList.toggle("hide",!on); if(!on||!tipCk.checked){sendTipWei=0n;tipNote.textContent="";return} tipNote.textContent="estimating…"; let wei=0n; try{wei=BigInt(await rpc("eth_gasPrice",[]))*TIP_GAS}catch{} if(my!==tipSeq)return; sendTipWei=wei>0n?wei:10n**15n/2n; tipNote.textContent="tip ≈ "+trimAmt(sendTipWei,18)+" ETH"; render(); } const SEL_OUT="d40d4bc6",SEL_IN="e3993ee7",SEL_PENDING="6577b86a"; const idTok=i=>"0x"+(i&((1n<<160n)-1n)).toString(16).padStart(40,"0"); const idDelay=i=>i>>160n; const w=(h,n)=>BigInt("0x"+(strip0x(h).slice(n*64,n*64+64)||"0")); const metaCache=new Map(); async function tokMeta(a){ a=a.toLowerCase(); if(a===ZERO)return{sym:"ETH",dec:18}; const k=TOKENS.find(t=>t.addr.toLowerCase()===a); if(k)return k; if(metaCache.has(a))return metaCache.get(a); let m={sym:a.slice(0,6)+"…",dec:18}; try{const[sr,dr]=await Promise.all([rpc(C,[{to:a,data:"0x95d89b41"},L]),rpc(C,[{to:a,data:"0x313ce567"},L])]); const d=Number(BigInt(dr)); if(Number.isInteger(d)&&d>=0&&d<=36)m={sym:decodeString(sr).trim().replace(/[<>&"'`]/g,"").slice(0,10)||m.sym,dec:d}; }catch{} metaCache.set(a,m);return m; } const rel=s=>{const a=Math.abs(s),u=[[86400,"d"],[3600,"h"],[60,"m"]]; for(const[n,l]of u)if(a>=n)return Math.floor(a/n)+l;return a+"s"}; let posSeq=0,posShown=25,slowGuard=false; const POSPAGE=25; async function loadPos(){ if(!account){pos.innerHTML="";tabSend.textContent="Send";slowGuard=false;return} const my=++posSeq; let outIds=[],inIds=[]; try{ const[oh,ih,gd]=await Promise.all([ rpc(C,[{to:SLOW,data:"0x"+SEL_OUT+encAddr(account)},L]), rpc(C,[{to:SLOW,data:"0x"+SEL_IN+encAddr(account)},L]), rpc(C,[{to:SLOW,data:"0x"+SEL_GUARDIAN+encAddr(account)},L]).catch(()=>"0x")]); outIds=decUintArr(oh);inIds=decUintArr(ih); if(my===posSeq)slowGuard=w(gd,0)!==0n; }catch{return} if(my!==posSeq)return; const dirs=new Map(); for(const[t,d]of[...outIds.map(i=>[i,"out"]),...inIds.map(i=>[i,"in"])]){ const k=t.toString();dirs.set(k,(dirs.get(k)||"")+d)} const allKeys=[...dirs.keys()].sort((a,b)=>a.length-b.length||(a<b?1:-1)); const keys=allKeys.slice(0,posShown); const more=allKeys.length-keys.length; const ps=[]; for(let i=0;i<keys.length;i+=10){ ps.push(...await Promise.all(keys.slice(i,i+10).map(k=> rpc(C,[{to:SLOW,data:"0x"+SEL_PENDING+encUint(BigInt(k))},L]).catch(()=>0)))); if(my!==posSeq)return; } if(my!==posSeq)return; const rows=[],settled=[]; keys.forEach((k,i)=>{const p=ps[i];if(!p)return; const ts=w(p,0); if(ts===0n){if(dirs.get(k).includes("out"))settled.push(k);return} rows.push({tid:BigInt(k),dir:dirs.get(k),ts,id:w(p,3),amount:w(p,4)})}); const stale=[]; for(let i=0;i<settled.length;i+=10){ const ts=await Promise.all(settled.slice(i,i+10).map(k=> rpc(C,[{to:SLOW_GATE,data:"0x"+SEL_TIPS+encUint(BigInt(k))},L]).catch(()=>0))); if(my!==posSeq)return; ts.forEach((t,j)=>{if(t&&w(t,0)>0n&&idTok(w(t,1))===account.toLowerCase()) stale.push({tid:settled[i+j],amount:w(t,0)})}); } const now=BigInt(Math.floor(Date.now()/1e3)); const ready=rows.filter(r=>r.dir.includes("in")&&now>=r.ts+idDelay(r.id)).length; tabSend.textContent=ready?`Send (${ready}${more?"+":""})`:"Send"; if(tab!=="send"||!(rows.length||stale.length)){pos.innerHTML="";return} rows.sort((a,b)=>Number((a.ts+idDelay(a.id))-(b.ts+idDelay(b.id)))); const metas=await Promise.all(rows.map(r=>tokMeta(idTok(r.id)))); if(my!==posSeq)return; let html=`<div class="ph">Time-locked</div>`; rows.forEach((r,n)=>{ const m=metas[n],exp=r.ts+idDelay(r.id),left=Number(exp-now); const ripe=now>=exp; const amt=`${trimAmt(r.amount,m.dec)} ${m.sym}`; const io=r.dir.includes("in"),oo=r.dir.includes("out"); const held=`data-t="${r.tid}" data-i="${r.id}" data-m="${r.amount}"`; const cb=oo&&ripe&&now>=exp+GRACE; let act,note; if(cb)note="Unclaimed past grace · recoverable"; else if(ripe)note=io?"Ready to claim":"Matured · recipient can claim, you in "+rel(Number(exp+GRACE-now)); else note=(io&&!oo?"Arrives in ":"Reversible · sends in ")+rel(left); if(io&&ripe)act=`<button data-a="c" data-t="${r.tid}">${slowGuard?"Unlock":"Claim"}</button>`; else if(oo&&!ripe)act=`<button data-a="r" ${held}>Reverse</button>`; else if(cb)act=`<button data-a="k" ${held}>Clawback</button>`; else act=`<button disabled>${io?"In":"Out"}</button>`; html+=`<div class="p"><span><b>${oo&&io?"↺":oo?"→":"←"} ${amt}</b><i>${note}</i></span>${act}</div>`; }); stale.forEach(s=>{html+=`<div class="p"><span><b>↩ ${trimAmt(s.amount,18)} ETH</b>` +`<i>Keeper tip on a settled transfer · unspent</i></span>` +`<button data-a="t" data-t="${s.tid}">Reclaim tip</button></div>`}); if(more)html+=`<div class="p" style="justify-content:center"><button data-a="m">Show ${Math.min(more,POSPAGE)} more of ${more}</button></div>`; pos.innerHTML=html; } pos.addEventListener("click",async e=>{ const m=e.target.closest('button[data-a="m"]'); if(m){posShown+=POSPAGE;m.disabled=true;loadPos();return} const b=e.target.closest("button[data-t]"); if(!b)return; b.disabled=true; ++busy; try{ await checkWallet(); const a=b.dataset.a; if(a==="t"){if(!await refundTipBestEffort(b.dataset.t,"Tip"))throw Error("the tip is no longer refundable"); refreshBalance();loadPos();return} const back=a==="r"||a==="k"; let data="0x"+(a==="r"?SEL_REVERSE:a==="k"?SEL_CLAWBACK:slowGuard?SEL_UNLOCK:SEL_CLAIM)+encUint(BigInt(b.dataset.t)); if(back&&!slowGuard)data=encCalls([data,"0x"+SEL_WITHDRAWFROM+encAddr(account)+encAddr(account) +encUint(BigInt(b.dataset.i))+encUint(BigInt(b.dataset.m))]); const txReq={from:account,to:SLOW,data,value:"0x0"}; await rpc(C,[txReq,L]); await settle(await rpc(S,[txReq])); if(back)await refundTipBestEffort(b.dataset.t,a==="r"?"Reversed":"Recovered"); refreshBalance();loadPos(); }catch(x){err(x);b.disabled=false} finally{--busy} }); async function refundTipBestEffort(tid,verb="Reversed"){ try{ const t=await rpc(C,[{to:SLOW_GATE,data:"0x"+SEL_TIPS+encUint(BigInt(tid))},L]); if(w(t,0)===0n)return false; const req={from:account,to:SLOW_GATE,data:"0x"+SEL_REFUNDTIP+encUint(BigInt(tid)),value:"0x0"}; await rpc(C,[req,L]); stat.textContent=verb+" · reclaiming keeper tip..."; await settle(await rpc(S,[req])); stat.textContent=verb+" · keeper tip refunded"; return true; }catch{return false} } const rcErr=v=>isAddr(v) ?(/^0x0{40}$/i.test(v)?"Recipient cannot be the zero address" :!ckAddr(v)?"That address fails its checksum — a character is wrong. Paste it again.":"") :!/\.(g?wei|eth)$/i.test(v)?"Recipient must be an address or a .wei / .gwei / .eth name" :!nameOk(v.toLowerCase())?"That name uses characters this page cannot resolve.":""; const rcvOf=async(v,fresh)=>{ if(isAddr(v))return /^0x0{40}$/i.test(v)||!ckAddr(v)?"":v; if(!/\.(g?wei|eth)$/i.test(v))return ""; const n=v.toLowerCase(); if(!nameOk(n))return ""; let a=""; try{a=fresh?await nameFwd(n):await nameFwdCached(n)} catch(e){if(e&&e.offchain)throw e} return !isAddr(a)||/^0x0{40}$/i.test(a)?"":a; }; async function updateSend(){ const my=++sSeq; sendReady=null; const f=TOKENS[fromSel.value]; const v=rc.value.trim(); let amount=0n,amtErr=""; const q=amt.value.trim(); try{amount=parseUnits(q,f.dec)} catch(e){if(q&&!/\.$/.test(q))amtErr=/decimals/.test(e.message)?`${f.sym} has ${f.dec} decimals`:"Invalid amount"} if(!v){rc.classList.remove("bad");rcvEl.textContent="";stat.textContent=amtErr;render();return} const bad=rcErr(v); if(bad){rc.classList.add("bad");rcvEl.textContent=""; stat.textContent=bad;render();return} if(!/^0x/.test(v))rcvEl.textContent="resolving..."; let to="",toErr=""; try{to=await rcvOf(v)}catch(e){toErr=e&&e.offchain?e.message:""} if(my!==sSeq)return; if(!to){rc.classList.add("bad");rcvEl.textContent=""; stat.textContent=toErr||"Name not registered";render();return} rc.classList.remove("bad"); rcvEl.textContent=to; stat.textContent=amtErr; if(amount>0n)sendReady={to,amount,token:f,label:/^0x/.test(v)?to.slice(0,6)+"\u2026"+to.slice(-4):v}; render(); } const syncNftUI=()=>{ const anyNft=tab==="book"&&(isNft(fromSel.value)||isNft(toSel.value)); nftIdL.classList.toggle("hide",!anyNft); if(!anyNft)nftId.value=""; nftId.placeholder=isNft(fromSel.value)?"Which one you are selling":"Any from collection"; syncOrderType(); showFloor(); }; const setTab=t=>{ if(tab==="send")sendD=dly.value;else if(tab==="book")bookD=dly.value; if(t!=="swap"&&lqMode)lqSet(false); if(t!=="swap"&&lnMode)lnSet(false); lq.classList.toggle("hide",t!=="swap"); ln.classList.toggle("hide",t!=="swap"); if(t!==tab){rc.value="";rc.classList.remove("bad");rcvEl.textContent=""} tab=t;last=null;sendReady=null;stat.textContent="";posShown=POSPAGE; const send=t==="send",bk=t==="book"; tabSwap.classList.toggle("on",t==="swap"); tabSend.classList.toggle("on",send); tabBook.classList.toggle("on",bk); for(const[el,on]of[[tabSwap,t==="swap"],[tabSend,send],[tabBook,bk]])el.setAttribute("aria-selected",on?"true":"false"); for(const el of[rcvPanel,flip])el.classList.toggle("hide",send); if(!bk)for(const[sel,other]of[[fromSel,toSel],[toSel,fromSel]]){ if(!isNft(sel.value))continue; const alt=TOKENS.findIndex((t,i)=>t.std!=="nft"&&String(i)!==other.value); if(alt>=0)sel.value=String(alt); } if(!send&&fromSel.value===toSel.value){ const alt=TOKENS.findIndex((t,i)=>String(i)!==fromSel.value&&(bk||t.std!=="nft")); if(alt>=0)toSel.value=String(alt); } syncDisabled(); syncNftUI(); slipL.classList.toggle("hide",t!=="swap"); dlyL.classList.toggle("hide",t==="swap"); kindL.classList.toggle("hide",!bk); ethModeL.classList.toggle("hide",!bk); dlyL.firstChild.nodeValue=bk?"Expires":"Time lock"; dly.options[0].textContent=bk?"Never":"Instant"; if(send)dly.value=sendD;else if(bk)dly.value=bookD; payL.textContent=send?"Amount":bk?"You sell":"You pay"; rcvHdr.textContent=bk&&kind.value==="dutch"?"Start ask total":bk?"You want":"You receive"; rc.placeholder=send?"Recipient \u2014 0x or .eth/.(g)wei" :bk?"Private to (optional) \u2014 0x or .eth/.(g)wei" :"Recipient (optional) \u2014 0x or .eth/.(g)wei"; syncOrderType();syncChart();syncTip(); loadPos();loadBook(); outAmt.value="";mode="in"; update(); }; dly.onchange=()=>{if(tab==="book")bookD=dly.value;else sendD=dly.value;syncTip();render()}; tipCk.onchange=()=>{syncTip();render()}; fill.onchange=render; // The rate line spells this out - "rests at X for 1 day and returns to you" - // so without a redraw the sentence describes the previous choice. dRest.onchange=render; const syncOrderType=()=>{ const dutch=tab==="book"&&kind.value==="dutch"; const bid=tab==="book"&&kind.value==="floor"; const sched=dutch||bid; const nftLeg=tab==="book"&&(isNft(fromSel.value)||isNft(toSel.value)); dRestL.classList.toggle("hide",!dutch); const collectionBid=nftLeg&&isNft(toSel.value)&&!nftId.value.trim(); floorL.classList.toggle("hide",!sched); floorL.firstChild.nodeValue=bid?"Opening bid":"Floor total"; if(tab==="book"){ dlyL.firstChild.nodeValue=dutch?"Decays over":bid||collectionBid?"Bid window":"Expires"; dly.options[0].textContent=sched||collectionBid?"—":"Never"; } fillL.classList.toggle("hide",tab!=="book"||sched||nftLeg); rc.classList.toggle("hide",sched); rcvEl.classList.toggle("hide",sched); if((sched||collectionBid)&&+dly.value===0){dly.value="86400";if(tab==="book")bookD=dly.value} if(tab==="book"){ payL.textContent=bid?"You escrow (max)":"You sell"; rcvHdr.textContent=bid?"You want to buy":dutch?"Start ask total":"You want"; } }; kind.onchange=()=>{syncOrderType();seedFloor();render()}; let floorTouched=false; const dutchEnds=()=>{ const rest=BigInt(dRest.value||0); if(!rest)return 0n; return BigInt(Math.floor(Date.now()/1e3))+BigInt(dly.value||0)+rest; }; const seedFloor=()=>{ if(floorTouched||kind.value!=="dutch")return; const t=TOKENS[toSel.value]; const raw=outAmt.value.trim(); if(!raw||!t)return; let start;try{start=parseUnits(raw,t.dec)}catch{return} if(!start)return; floorAmt.value=trimAmt(start/2n,t.dec); fitFont(floorAmt); }; floorAmt.addEventListener("input",()=>{ floorTouched=true; fitFont(floorAmt);render(); }); tabBook.onclick=()=>setTab("book"); // Balances refreshed only after your OWN transaction, so anything arriving // from elsewhere left Max and "Insufficient balance" reading a dead figure. setInterval(()=>{if(account&&!document.hidden&&!busy){refreshBalance();loadPos();if(tab==="book")loadBook()}},3e4); setInterval(()=>{ if(tab!=="swap"||!account||document.hidden||!last||swap.disabled||busy)return; if(Date.now()>=last.exp-5000&&autoQ++<AUTO_Q_MAX)update(1); },5e3); // Both loops above stand down while hidden and the requote budget is spent, // not refilled, so a backgrounded tab came back stale with no loop to fix it. document.addEventListener("visibilitychange",()=>{ if(document.hidden||!account||busy)return; autoQ=0; refreshBalance();loadPos(); if(tab==="book")loadBook(); if(tab==="swap"&&!swap.disabled&&last&&Date.now()>=last.exp-5000)update(); }); let lqSeq=0,lqRows=[]; const SEL_ISPOOL="5b16ebb7",SEL_POOLFOR="83bd1387",SEL_TOTALSUPPLY="18160ddd",SEL_CREATE_SEED="7163352a"; const bandPrice=(sqrtP,d0,d1)=>{const r=Number(sqrtP)/1e18;return r*r*10**(d0-d1)}; const lqBar=(lo,hi,at)=>{ if(!(lo>0)||!(hi>lo)||!(at>0))return ""; const f=(Math.log(at)-Math.log(lo))/(Math.log(hi)-Math.log(lo)); const out=f<0||f>1; const pct=(Math.max(0,Math.min(1,f))*100).toFixed(1); return `<div class="lqbar${out?" out":""}" style="--at:${pct}%" aria-hidden="true"><b></b><i></i></div>`; }; const fmtPx=v=>!isFinite(v)||v<=0?"—":v>=1000?v.toLocaleString(undefined,{maximumFractionDigits:0}) :v>=1?v.toFixed(4):v.toPrecision(3); const lqSyncAmounts=()=>{ const show=!lqMode||!!lqList.querySelector(".lqnew"); for(const el of [amtRow,outRow])if(el)el.classList.toggle("hide",!show); }; function lqSet(on){ lqMode=on; if(on&&lnMode)lnSet(false); lq.setAttribute("aria-pressed",on?"true":"false"); lq.classList.toggle("on",on); for(const el of [rate,flip,swap]) if(el)el.classList.toggle("hide",on); rc.value="";rc.classList.remove("bad");rcvEl.textContent=""; rc.placeholder=on?"LP shares to (optional) — 0x or .eth/.(g)wei" :"Recipient (optional) — 0x or .eth/.(g)wei"; if(on){amt.value="";outAmt.value="";fitFont(amt);fitFont(outAmt)} for(const el of [amtRow,outRow])if(el)el.classList.toggle("hide",on); rcvHdr.textContent=on?"and":"You receive"; payL.textContent=on?"Liquidity for":"You pay"; lqPanel.classList.toggle("hide",!on); refreshBalance1(); if(on)loadLq();else{lqRows=[];lqList.innerHTML=""} lqSyncAmounts(); } const fullBand=(lo,hi)=>lo>0&&isFinite(hi)&&hi/lo>=9.9e5; function lqRow(o,t0,t1){ const px=bandPrice(o.px,t0.dec,t1.dec),lo=bandPrice(o.sl,t0.dec,t1.dec),hi=bandPrice(o.sh,t0.dec,t1.dec); const inRange=px>=lo&&px<=hi&&o.liq>0n; const mine=o.shares>0n; const p=`data-pool="${o.pool}"`; return `<div class="lqrow${inRange?"":" out"}" ${p}>` +`<div class="lqband"><b>${fmtPx(lo)} – ${fmtPx(hi)}</b> <span class="lqfee">${(Number(o.fee)/10000).toFixed(2)}%</span>` +(o.hook!==ZERO?` <span class="lqtag" title="Hooked: may add a surcharge, and cannot be used in a clamped route">hook</span>`:"") +(fullBand(lo,hi) ?` <span class="lqtag" title="The widest band this page creates: 1000x either side of the opening price, a millionfold span. Price is effectively always inside it, so the position behaves like an ordinary constant-product pool. It is still bounded - no band here is infinite.">full range</span>`:"") +(inRange?"":` <span class="lqtag">out of range</span>`)+`</div>` +lqBar(lo,hi,px) +`<div class="lqmeta">${fmtPx(px)} ${t1.sym} per ${t0.sym}` +` · holds ${trimAmt(o.r0,t0.dec)} ${t0.sym} + ${trimAmt(o.r1,t1.dec)} ${t1.sym}` +` <button class="lqbtn" data-act="a" ${p}>Add</button></div>` +(mine?`<div class="lqmine">yours: ${trimAmt(o.mine0,t0.dec)} ${t0.sym} + ${trimAmt(o.mine1,t1.dec)} ${t1.sym}` +` <button class="lqbtn" data-act="w" ${p}>Withdraw all</button></div>`:"") +`<div class="lqadd hide">` +(o.liq>0n ?`<label class="lqzap"><input type="checkbox" class="lqz"> One-sided — deposit just ${t0.sym} or just ${t1.sym}</label>`:"") +`<input class="lqin" data-side="0" inputmode="decimal" autocomplete="off" spellcheck="false" placeholder="0.0 ${t0.sym}" aria-label="${t0.sym} to deposit">` +`<input class="lqin" data-side="1" inputmode="decimal" autocomplete="off" spellcheck="false" placeholder="0.0 ${t1.sym}" aria-label="${t1.sym} to deposit">` +`<div class="lqpv"></div>` +`<button class="lqbtn" data-act="ac" ${p} disabled>Add liquidity</button></div>` +`</div>`; } const LQ_FEES=[[500,"0.05%","pairs that track each other"], [3000,"0.30%","most pairs"],[10000,"1.00%","thin or volatile"]]; const LQ_RANGES=[["full","Full range"],["100","Wide — 100×"],["10","10×"], ["2","2×"],["1.25","Tight — 25%"],["custom","Custom prices"]]; function lqNewForm(t0,t1){ return `<div class="lqnew" data-t0="${t0.addr}" data-t1="${t1.addr}">` +`<label class="dly" for="lqRange">Range<select id="lqRange">` +LQ_RANGES.map(([v,l])=>`<option value="${v}"${v==="full"?" selected":""}>${l}</option>`).join("") +`</select></label>` +`<label class="dly hide" id="lqCustom">Prices<span class="r">` +`<input id="lqLo" inputmode="decimal" autocomplete="off" spellcheck="false" placeholder="Low" aria-label="Lowest price in the band">` +`<input id="lqHi" inputmode="decimal" autocomplete="off" spellcheck="false" placeholder="High" aria-label="Highest price in the band"></span></label>` +`<label class="dly hide" id="lqPxRow" for="lqPx">Opening price` +`<input id="lqPx" inputmode="decimal" autocomplete="off" spellcheck="false" placeholder="${t1.sym} per ${t0.sym}" aria-label="Opening price"></label>` +`<label class="dly"
0xacfba7ce…4082·#25,751,066·0x192e9eea…a7e3fb
let floorSeq=0; nftId.addEventListener("input",()=>{syncOrderType();showFloor();render()}); async function showFloor(){ const el=document.getElementById("floorNote");if(!el)return; const side=isNft(fromSel.value)?fromSel.value:isNft(toSel.value)?toSel.value:null; const live=tab==="book"&&side!==null&&!nftId.value.trim(); if(!live){el.classList.add("hide");el.textContent="";return} const seq=++floorSeq,t=TOKENS[side]; el.classList.remove("hide");el.textContent="Checking floor bids…"; const rows=await collectionFloor(t.addr).catch(()=>[]); if(seq!==floorSeq)return; if(!rows.length){el.textContent="No standing bids on this collection.";return} const best=rows[0],q=known(best.quote); el.textContent=`${rows.length} collection bid${rows.length>1?"s":""} · best pays ` +`${trimAmt(best.proceeds,q?q.dec:18)} ${q?q.sym:"?"} for any ${t.sym}`; } function decBidRows(hex){ const out=[]; try{ const h=strip0x(hex),bytes=h.length/2; const word=b=>{if(b<0||b+32>bytes)throw 0;return BigInt("0x"+h.slice(b*2,(b+32)*2))}; const num=b=>{const v=word(b);if(v>0xffffffffffffffffn)throw 0;return Number(v)}; const arr=num(0); if(arr<64||arr+32>bytes)throw 0; const n=num(arr),base=arr+32; if(n>256||base+n*32>bytes)throw 0; for(let i=0;i<n;i++){ try{ const row=base+num(base+i*32); if(row+17*32>bytes)throw 0; const f=k=>word(row+k*32); const txt=k=>{const p=row+num(row+k*32),len=num(p); if(p+32+len>bytes)throw 0; return safeSym(new TextDecoder().decode(hexToBytes(h.slice((p+32)*2,(p+32+Math.min(len,1024))*2))))}; const dec=k=>{const v=num(row+k*32);return v>0&&v<=37?v-1:null}; out.push({ id:f(0), bidder:"0x"+h.slice((row+1*32+12)*2,(row+2*32)*2), token:"0x"+h.slice((row+2*32+12)*2,(row+3*32)*2), quote:"0x"+h.slice((row+3*32+12)*2,(row+4*32)*2), isNFT:f(4)!==0n, anyId:f(5)!==0n, remaining:f(7), initial:f(8), price:f(9), proceeds:f(10), expiry:f(12), tDec:dec(13),qDec:dec(14), tSym:txt(15),qSym:txt(16), }); }catch{continue} } return out; }catch{return out} } async function collectionFloor(collection){ if(FLOORVIEW===ZERO||FLOOR===ZERO)return[]; const data="0x"+SEL_COLL_BIDS+encAddr(FLOOR)+encAddr(collection)+encAddr(ZERO) +encUint(0)+encUint(16)+encUint(256); const raw=await rpc(C,[{to:FLOORVIEW,data},L]).catch(()=>null); if(!raw)return[]; const rows=decBidRows(raw); return rows.filter(r=>r.anyId&&r.remaining>0n) .sort((a,b)=>{const l=b.proceeds*a.remaining,r=a.proceeds*b.remaining;return l>r?1:l<r?-1:0}); } async function floorCandidates(tokenIn,tokenOut,block){ const bin=tokenIn===ZERO?WETH:tokenIn,bout=tokenOut===ZERO?WETH:tokenOut; const data="0x"+SEL_FLOOR_CANDS+encAddr(FLOOR)+encAddr(bin)+encAddr(bout) +encUint(0)+encUint(64)+encUint(512); const raw=await rpc(C,[{to:FLOORVIEW,data},block]).catch(()=>null); if(!raw)return[]; const out=[]; for(const b of decBidRows(raw)){ if(b.isNFT||b.remaining<=0n||b.initial<=0n||b.price<=0n||b.proceeds<=0n)continue; if(b.token.toLowerCase()!==bin.toLowerCase())continue; if(b.quote.toLowerCase()!==bout.toLowerCase())continue; out.push({ id:b.id,board:FLOOR,floor:1,v2:0,dutch:0, maker:b.bidder,pf:1,exp:b.expiry,nA:0,nB:0,cp:ZERO, tA:b.quote,aA:b.proceeds,sA:b.qSym,dA:b.qDec, tB:b.token,aB:b.remaining,sB:b.tSym,dB:b.tDec, price:b.price,initial:b.initial, }); } return out; } function floorGet(o,pay){return o.price*pay/o.initial} function floorPay(o,want){ if(want<=0n)return 0n; const g=(want*o.initial+o.price-1n)/o.price; return g>o.aB?o.aB:g; } function rowGet(o,pay){return o.floor?floorGet(o,pay):o.aA*pay/o.aB} function decViewPage(hex){ const h=strip0x(hex),bytes=h.length/2; const word=b=>{if(b<0||b+32>bytes)throw Error("bad lens page");return BigInt("0x"+h.slice(b*2,(b+32)*2))}; const addr=(b)=>"0x"+h.slice((b+12)*2,(b+32)*2); const arr=Number(word(0)),next=word(32); if(!Number.isSafeInteger(arr)||arr<64||arr+32>bytes)throw Error("bad lens offset"); const n=Number(word(arr)),head=arr+32; if(!Number.isSafeInteger(n)||n>256||head+n*32>bytes)throw Error("bad lens length"); const text=(base,k)=>{const off=Number(word(base+k*32)),p=base+off,len=Number(word(p)); if(!Number.isSafeInteger(off)||!Number.isSafeInteger(len)||p+32+len>bytes)throw Error("bad lens string"); const cap=Math.min(len,1024),a=new Uint8Array(cap); for(let i=0;i<cap;i++)a[i]=parseInt(h.slice((p+32+i)*2,(p+33+i)*2),16); return new TextDecoder().decode(a)}; const rows=[]; const dec=v=>{const d=Number(v);return Number.isInteger(d)&&d>=0&&d<=36?d:null}; for(let i=0;i<n;i++){ try{ const off=Number(word(head+i*32)),b=head+off; if(!Number.isSafeInteger(off)||b+16*32>bytes)throw Error("bad lens row"); rows.push({id:word(b),maker:addr(b+32),pf:word(b+64)===1n,exp:word(b+96), nA:word(b+128)===1n,nB:word(b+160)===1n,cp:addr(b+192), tA:addr(b+224),aA:word(b+256),sA:safeSym(text(b,9)),dA:dec(word(b+320)), tB:addr(b+352),aB:word(b+384),sB:safeSym(text(b,13)),dB:dec(word(b+448)),board:addr(b+480)}); }catch{continue} } return{rows,next}; } const betterRate=(a,b)=>a.aA*b.aB>b.aA*a.aB; const MAX_BOOK_LEGS=32,AON_PAIR_SEEDS=24; function planBookExactIn(rows,amountIn,ammOut,ammIn){ if(!amountIn||!ammIn)return null; let c=rows.filter(o=>!o.nA&&!o.nB&&o.aA&&(o.aB||o.dutch)&&(o.pf||o.aB<=amountIn) &&!(account&&o.maker&&o.maker.toLowerCase()===account.toLowerCase())); const candidateCapped=c.length>256; const aonCount=c.reduce((n,o)=>n+!o.pf,0); const aonBounded=aonCount>=3; c.sort((a,b)=>betterRate(a,b)?-1:betterRate(b,a)?1:0); if(c.length>256)c.length=256; const build=(seed=-1,seed2=-1)=>{ let rem=amountIn,bin=0n,bout=0n,fills=[],legCapped=false; const take=o=>{ if(o.aA*ammIn<=ammOut*o.aB)return; if(!o.pf&&o.aB>rem)return; if(fills.length>=MAX_BOOK_LEGS){legCapped=true;return} if(o.aB===0n){ fills.push({id:o.id,board:o.board,pay:0n,get:o.aA,part:false}); bout+=o.aA;return; } const pay=o.aB<rem?o.aB:rem; if(!pay)return; const get=o.pf?(pay===o.aB?o.aA:rowGet(o,pay)):o.aA; if(!get)return; fills.push({id:o.id,board:o.board,pay,get,part:o.pf&&pay<o.aB}); rem-=pay;bin+=pay;bout+=get; }; if(seed>=0)take(c[seed]); if(seed2>=0)take(c[seed2]); for(let i=0;i<c.length;i++)if(i!==seed&&i!==seed2)take(c[i]); return{fills,bookIn:bin,bookOut:bout,ammIn:rem,score:bout+rem*ammOut/ammIn, bounded:candidateCapped||aonBounded||legCapped}; }; let best=build(-1); for(let i=0;i<c.length;i++)if(!c[i].pf){ const p=build(i);if(p.score>best.score)best=p; } const aon=[];for(let i=0;i<c.length&&aon.length<AON_PAIR_SEEDS;i++)if(!c[i].pf)aon.push(i); for(let i=0;i<aon.length;i++)for(let j=i+1;j<aon.length;j++){ const p=build(aon[i],aon[j]);if(p.score>best.score)best=p; } return best.fills.length?best:null; } const cheaperRate=(a,b)=>a.aB*b.aA<b.aB*a.aA; function planBookExactOut(rows,amountOut,ammIn,ammOut){ if(!amountOut||!ammOut)return null; let c=rows.filter(o=>!o.nA&&!o.nB&&o.aA&&(o.aB||o.dutch)&&(o.pf||o.aA<=amountOut) &&!(account&&o.maker&&o.maker.toLowerCase()===account.toLowerCase())); const candidateCapped=c.length>256; const aonCount=c.reduce((n,o)=>n+!o.pf,0); const aonBounded=aonCount>=3; c.sort((a,b)=>cheaperRate(a,b)?-1:cheaperRate(b,a)?1:0); if(c.length>256)c.length=256; const build=(seed=-1,seed2=-1)=>{ let left=amountOut,bin=0n,bout=0n,fills=[],legCapped=false; const take=o=>{ if(o.aB*ammOut>=ammIn*o.aA)return; if(!o.pf&&o.aA>left)return; if(fills.length>=MAX_BOOK_LEGS){legCapped=true;return} const want=o.aA<left?o.aA:left; let pay,get; if(o.aB===0n){pay=0n;get=want} else if(want===o.aA){pay=o.aB;get=o.aA} else if(o.floor){ pay=floorPay(o,want); get=floorGet(o,pay); } else{ pay=(want*o.aB+o.aA-1n)/o.aA; if(pay>o.aB)pay=o.aB; get=pay*o.aA/o.aB; } if(!get)return; fills.push({id:o.id,board:o.board,pay,get,part:want!==o.aA}); bin+=pay;bout+=get;left-=get<left?get:left; }; if(seed>=0)take(c[seed]); if(seed2>=0)take(c[seed2]); for(let i=0;i<c.length&&left;i++)if(i!==seed&&i!==seed2)take(c[i]); return{fills,bookIn:bin,bookOut:bout,ammOut:left,score:bin+left*ammIn/ammOut, bounded:candidateCapped||aonBounded||legCapped}; }; let best=build(-1); for(let i=0;i<c.length;i++)if(!c[i].pf){ const p=build(i);if(p.score<best.score)best=p; } const aon=[];for(let i=0;i<c.length&&aon.length<AON_PAIR_SEEDS;i++)if(!c[i].pf)aon.push(i); for(let i=0;i<aon.length;i++)for(let j=i+1;j<aon.length;j++){ const p=build(aon[i],aon[j]);if(p.score<best.score)best=p; } return best.fills.length?best:null; } function encFillPlan(fills,tokenIn,tokenOut,recipient,refundTo,deadline){ let tail=encUint(fills.length); for(const f of fills)tail+=encUint(f.id)+encAddr(f.board)+encUint(f.pay)+encUint(f.get)+encBool(f.part); return"0x"+SEL_FILLPLAN+encAddr(tokenIn)+encAddr(tokenOut)+encAddr(recipient)+encAddr(refundTo) +encUint(deadline)+encUint(192)+tail; } function encFillPlanAndSwap(fills,tokenIn,tokenOut,recipient,refundTo,deadline,ammIn,ammData){ let ft=encUint(fills.length); for(const f of fills)ft+=encUint(f.id)+encAddr(f.board)+encUint(f.pay)+encUint(f.get)+encBool(f.part); const d=strip0x(ammData||"0x"),dt=encUint(d.length/2)+d.padEnd(Math.ceil(d.length/64)*64,"0"); return"0x"+SEL_FILLPLAN_SWAP+encAddr(tokenIn)+encAddr(tokenOut)+encAddr(recipient)+encAddr(refundTo) +encUint(deadline)+encUint(256)+encUint(ammIn)+encUint(256+ft.length/2)+ft+dt; } function encSnwap(tokenIn,amountIn,recipient,tokenOut,minOut,executor,data){ const d=strip0x(data),body=encUint(d.length/2)+d.padEnd(Math.ceil(d.length/64)*64,"0"); return"0x"+SEL_SNWAP+encAddr(tokenIn)+encUint(amountIn)+encAddr(recipient)+encAddr(tokenOut) +encUint(minOut)+encAddr(executor)+encUint(224)+body; } const encCheckpoint=(executor,token,recipient)=>encSnwap( ZERO,0n,recipient,ZERO,0n,executor,"0x"+SEL_CHECKPOINT+encAddr(token) ); const encSweep=(token,amount,to)=>"0x"+SEL_SWEEP+encAddr(token)+encUint(0)+encUint(amount)+encAddr(to); function encPermit2Hybrid(ammData,fills,tokenIn,tokenOut,recipient,refundTo,deadline,bookIn,bookOut){ const bookData=encFillPlan(fills,tokenIn,tokenOut,recipient,refundTo,deadline); const steps=[encCheckpoint(SWAPBOL,tokenIn,refundTo)]; if(bookIn)steps.push(encSweep(tokenIn,bookIn,SWAPBOL)); steps.push(encSnwap(ZERO,0n,recipient,tokenOut,bookOut,SWAPBOL,bookData)); if(ammData&&ammData!=="0x")steps.push(ammData); steps.push(encSweep(tokenIn,0n,refundTo)); return encCalls(steps); } async function swapCandidates(tokenIn,tokenOut,block){ const key=tokenIn.toLowerCase()+":"+tokenOut.toLowerCase(),old=candCache[key]; if(old&&Date.now()-old.at<15000&&old.block===block){ old.rows.incomplete=old.incomplete;old.rows.unavailable=old.unavailable; return old.rows; } const snapshotAt=Date.now(); const boardIn=tokenIn===ZERO?WETH:tokenIn; const boardOut=tokenOut===ZERO?WETH:tokenOut; const [lc,le]=await Promise.all([ rpc("eth_getCode",[SBVIEW,block]),rpc("eth_getCode",[SWAPBOL,block]) ]); if(lc.length<5||le.length<5){ const rows=[];rows.unavailable=true;return rows; } const specs=[...BOARDS]; if(DUTCH!==ZERO&&(await rpc("eth_getCode",[DUTCH,block])).length>4){ specs.push({a:DUTCH,dutch:1,ti:tokenIn}); if(tokenIn===ZERO)specs.push({a:DUTCH,dutch:1,ti:WETH}); } const groups=await Promise.all(specs.map(async b=>{ let cursor=0n,out=[],turns=0,incomplete=false,failed=false; try{do{ const ti=b.dutch?b.ti:boardIn,to=boardOut; const data="0x"+(b.dutch?SEL_DUTCH_CANDS:SEL_CANDS)+encAddr(b.a) +(b.dutch?"":encBool(b.v2))+encAddr(ti)+encAddr(to)+encAddr(SWAPBOL) +encUint(cursor)+encUint(512); const p=decViewPage(await rpc(C,[{to:SBVIEW,data},block])); out.push(...p.rows.map(r=>({...r,v2:b.v2||0,dutch:b.dutch||0}))); cursor=p.next; ++turns; }while(cursor&&turns<64)}catch{failed=true;incomplete=true} incomplete=incomplete||!!cursor; return{out,incomplete,failed}; })); let rows=groups.flatMap(g=>g.out); if(tokenIn===ZERO&&tokenOut.toLowerCase()===WETH.toLowerCase()){ rows=rows.filter(r=>r.dutch&&r.tA.toLowerCase()===WETH.toLowerCase()&&r.tB===ZERO); } else if(await floorRoutable(block)){ rows=rows.concat(await floorCandidates(tokenIn,tokenOut,block).catch(()=>[])); } const incomplete=groups.some(g=>g.incomplete); const unavailable=groups.every(g=>g.failed&&!g.out.length); rows.incomplete=incomplete; rows.unavailable=unavailable; const current=candCache[key],bn=BigInt(block),cn=current?BigInt(current.block):-1n; if(!current||bn>cn||(bn===cn&&snapshotAt>=current.at)){ candCache[key]={at:snapshotAt,block,rows,incomplete,unavailable}; } for(const k in candCache)if(k!==key&&snapshotAt-candCache[k].at>60000)delete candCache[k]; return rows; } async function lensBookRows(my){ if((await rpc("eth_getCode",[SBVIEW,L])).length<5)return null; const specs=[...BOARDS]; if(DUTCH!==ZERO&&(await rpc("eth_getCode",[DUTCH,L])).length>4)specs.push({a:DUTCH,dutch:1}); const pages=await Promise.all(specs.map(async b=>{ let cursor=0n,out=[],turns=0; do{ const data="0x"+(b.dutch?SEL_RECENT_DUTCH:SEL_RECENT)+encAddr(b.a) +(b.dutch?"":encBool(b.v2))+encUint(cursor)+encUint(80)+encUint(256); const p=decViewPage(await rpc(C,[{to:SBVIEW,data},L])); out.push(...p.rows.map(r=>({...r,v2:b.v2||0,dutch:b.dutch||0}))); cursor=p.next; }while(cursor&&out.length<160&&++turns<64&&my===bookSeq); return out; })); const rows=pages.flat(); if(FLOOR!==ZERO&&FLOORVIEW!==ZERO){ await floorRoutable(L); try{ const raw=await rpc(C,[{to:FLOORVIEW, data:"0x"+SEL_RECENT_FLOOR+encAddr(FLOOR)+encUint(0)+encUint(80)+encUint(256)},L]); for(const b of decBidRows(raw)){ if(b.remaining<=0n)continue; rows.push({ id:b.id,board:FLOOR,floor:1,v2:0,dutch:0, maker:b.bidder,pf:b.isNFT?0:1,exp:b.expiry, nA:0,nB:0,cp:ZERO, tA:b.quote,aA:b.proceeds,sA:b.qSym,dA:b.qDec, tB:b.token,aB:b.remaining,sB:b.tSym,dB:b.isNFT?0:b.tDec, price:b.price,initial:b.initial,anyId:b.anyId,isNFT:b.isNFT?1:0, }); } }catch{} } return rows; } async function loadBook(){ if(!account||tab!=="book"){book.innerHTML="";bookRows=[];syncBook(0);return} const my=++bookSeq; const live=[]; for(const b of BOARDS){ let code="0x"; try{code=await rpc("eth_getCode",[b.a,L])}catch{} if(code.length>4)live.push(b); } if(my!==bookSeq)return; if(!live.length){ bookRows=[];bookLegs=[]; book.innerHTML='<h6><span>Orderbook</span></h6><div class="o"><span class="l"><i>No Swapboard deployed on this network yet.</i></span></div>'; syncBook(0);book.classList.remove("hide");return} let rows=null; try{rows=await lensBookRows(my)}catch{rows=null} if(my!==bookSeq)return; if(!rows)rows=[]; for(const b of rows.length?[]:live){ let total=0; try{total=Number(BigInt(await rpc(C,[{to:b.a,data:"0x"+SEL_NEXTID},L])))}catch{continue} if(!total)continue; const hi=total,lo=Math.max(0,hi-40); const ids=[];for(let i=hi-1;i>=lo;--i)ids.push(i); let raw=""; try{raw=await rpc(C,[{to:b.a,data:"0x"+SEL_GETORDERS+encUint(32)+encUint(ids.length)+ids.map(i=>encUint(i)).join("")},L])}catch{continue} if(my!==bookSeq)return; const h=strip0x(raw);if(!h)continue; const w=WORDS(b.v2),n=Number(BigInt("0x"+h.slice(64,128))); const at=(r,k)=>BigInt("0x"+h.slice(128+(r*w+k)*64,128+(r*w+k+1)*64)); const ad=(r,k)=>"0x"+h.slice(128+(r*w+k)*64+24,128+(r*w+k+1)*64); for(let r=0;r<n&&r<ids.length;++r){ if(at(r,1)!==1n)continue; const o=b.v2 ?{id:ids[r],board:b.a,v2:1,maker:ad(r,0),pf:at(r,2)===1n,exp:at(r,3), nA:at(r,4)===1n,nB:at(r,5)===1n,cp:ad(r,6), tA:ad(r,7),aA:at(r,8),tB:ad(r,9),aB:at(r,10)} :{id:ids[r],board:b.a,v2:0,maker:ad(r,0),pf:0,exp:0n,nA:0,nB:0, cp:ZERO,tA:ad(r,2),aA:at(r,3),tB:ad(r,4),aB:at(r,5)}; if(o.exp&&BigInt(Math.floor(Date.now()/1e3))>o.exp &&o.maker.toLowerCase()!==account.toLowerCase())continue; if(o.cp!==ZERO &&o.cp.toLowerCase()!==account.toLowerCase() &&o.maker.toLowerCase()!==account.toLowerCase())continue; rows.push(o); } } if(my!==bookSeq)return; const cutoff=BigInt(Math.floor(Date.now()/1e3)); rows=rows.filter(o=>{ const mine=o.maker.toLowerCase()===account.toLowerCase(); if(o.exp&&cutoff>o.exp&&!mine)return false; return o.cp===ZERO ||o.cp.toLowerCase()===account.toLowerCase() ||o.maker.toLowerCase()===account.toLowerCase(); }); const dutchRows=rows.filter(r=>r.dutch); const bidRows=rows.filter(r=>r.floor); const fillRows=rows.filter(r=>!r.dutch&&!r.floor&&r.v2&&r.pf&&!r.nA&&!r.nB); if(dutchRows.length||bidRows.length||fillRows.length){ const reqs=[ ...dutchRows.map(r=>({to:DUTCH,data:"0x"+SEL_DUTCH_LISTING+encUint(r.id)})), ...bidRows.map(r=>({to:FLOOR,data:"0x"+SEL_BIDS+encUint(r.id)})), ...fillRows.map(r=>({to:r.board,data:"0x"+SEL_INITIAL_B+encUint(r.id)})), ]; const raw=await mc3Deep(reqs); if(my!==bookSeq)return; let k=0; for(const r of dutchRows){const x=raw[k++];if(!x)continue; const h=strip0x(x),w=n=>BigInt("0x"+h.slice(n*64,(n+1)*64)); if(h.length>=640)Object.assign(r,{ start:w(2),duration:w(3),startPrice:w(5),endPrice:w(7),initial:w(8),remaining:w(9)}); } for(const r of bidRows){const x=raw[k++];if(!x)continue; const h=strip0x(x),w=n=>BigInt("0x"+h.slice(n*64,(n+1)*64)); if(h.length>=11*64)Object.assign(r,{start:w(2),duration:w(3),startPrice:w(5),endPrice:w(7)}); } for(const r of fillRows){const x=raw[k++];if(!x)continue; const h=strip0x(x);if(h.length<64)continue; const init=BigInt("0x"+h.slice(0,64)); if(init>r.aB)r.filled=(init-r.aB)*10000n/init; } } if(!rows.length){bookRows=[];bookLegs=[];book.innerHTML="";syncBook(0);return} await loadLogos(rows.flatMap(r=>[r.tA,r.tB])); if(my!==bookSeq)return; const legs=await Promise.all(rows.map(r=>Promise.all([ leg(r.tA,r.aA,r.nA,r.sA!=null&&r.dA!=null?{sym:r.sA,dec:r.dA}:null), leg(r.tB,r.aB,r.nB,r.sB!=null&&r.dB!=null?{sym:r.sB,dec:r.dB}:null) ]))); if(my!==bookSeq)return; bookRows=rows;bookLegs=legs; paintBook(); } let bookPairOnly=LS.bkp!=="0",bookType="all",bookLegs=[]; const sameAsset=(a,b)=>{ const w=x=>{x=(x||"").toLowerCase();return x===ZERO.toLowerCase()?WETH.toLowerCase():x}; return w(a)===w(b); }; const rowInPair=r=>{ const f=TOKENS[fromSel.value]?.addr,t=TOKENS[toSel.value]?.addr; if(!f||!t)return true; return (sameAsset(r.tA,f)&&sameAsset(r.tB,t))||(sameAsset(r.tA,t)&&sameAsset(r.tB,f)); }; const rowIsType=r=>bookType==="all" ||(bookType==="dutch"&&r.dutch) ||(bookType==="bid"&&r.floor) ||(bookType==="nft"&&(r.nA||r.nB||r.isNFT)) ||(bookType==="limit"&&!r.dutch&&!r.floor); function paintBook(){ const rows=bookRows,legs=bookLegs; if(!rows.length){book.innerHTML="";syncBook(0);return} let own="",rest="",nOwn=0,nRest=0,hidden=0; const now=BigInt(Math.floor(Date.now()/1e3)); rows.forEach((r,i)=>{ const[a,bg]=legs[i]||[]; if(!a||!bg)return; const mine=r.maker.toLowerCase()===account.toLowerCase(); const expired=!!(r.exp&&now>r.exp); const tags=(expired?'<span class="tg w" title="This order has lapsed and can no longer be filled, but its escrow is still held by the board. Reclaim returns it to you.">expired</span>':"") +(r.nA||r.nB||r.isNFT?'<span class="tg">NFT</span>':"") +(r.v2&&!r.pf&&!r.nA&&!r.nB?'<span class="tg">AON</span>':"") +(r.dutch?'<span class="tg">Dutch</span>':"") +(r.floor?'<span class="tg">Bid</span>':"") +(r.floor&&r.isNFT&&r.anyId?'<span class="tg">any id</span>':"") +(r.filled!==undefined&&r.filled>0n?`<span class="tg">${(Number(r.filled)/100).toFixed(0)}% filled</span>`:"") +(r.cp!==ZERO?'<span class="tg">private</span>':"") +(!a.ok||!bg.ok?'<a class="tg w" href="'+ZLIST_URL+'" target="_blank" rel="noopener"' +' title="Not on the staked token list. Its symbol and decimals are read straight from the contract' +' and nothing has vouched for it - check the address before filling. Stake on zList to get a real' +' token listed.">unverified</a>':""); let when=expired?" · lapsed "+rel(Number(now-r.exp))+" ago — escrow still held" :r.exp?" · "+rel(Number(r.exp-now))+" left":""; if(!expired&&r.dutch&&r.duration){ const end=r.start+r.duration; const floor=r.initial?(r.endPrice*r.remaining+r.initial-1n)/r.initial:0n; when=now>=end?` · floor reached: ${trimAmt(floor,r.dB)} ${r.sB}` :` · floor ${trimAmt(floor,r.dB)} ${r.sB} in ${rel(Number(end-now))}`; } if(!expired&&r.floor&&r.duration){ const end=r.start+r.duration; when=now>=end?" · window closed" :` · climbs to ${trimAmt(r.initial?r.endPrice*r.aB/r.initial:r.endPrice,r.dA)} ${r.sA} in ${rel(Number(end-now))}`; } const dset=`data-i="${r.id}" data-b="${r.board}" data-v="${r.v2}" data-d="${r.dutch||0}" data-f="${r.floor||0}"`; const takeable=!r.floor||(!r.isNFT&&floorReady); const act=mine?`<button data-x="c" ${dset}>${expired?"Reclaim":"Cancel"}</button>` :takeable?`<button data-x="f" ${dset}>${r.floor?"Sell into":"Fill"}</button>` :`<button disabled>${r.isNFT?"NFT bid":"Not routable"}</button>`; const card=r.board.toLowerCase()!==SB1.toLowerCase(); const row=`<div class="ow"><div class="o${card?" ins":""}"${card?` data-k="${r.board}:${r.id}"`:""}>` +`<span class="l">${a.ic}${bg.ic}<span><b>${a.txt} → ${bg.txt}</b>` +`<i>${a.link}${tags}${when}</i></span></span>${act}</div><div class="ic"></div></div>`; if(mine){own+=row;++nOwn;return} if(!(bookPairOnly?rowInPair(r):true)||!rowIsType(r)){++hidden;return} rest+=row;++nRest; }); let html=""; if(nOwn)html+=`<h6><span>Your orders</span><span>${nOwn}</span></h6>`+own; html+=bookFilters(hidden); if(nRest)html+=rest; else html+=`<div class="lqempty">${emptyBook()}${hidden?` ${hidden} hidden by the filters.`:""}</div>`; book.innerHTML=html; syncBook(nOwn+nRest,rows.length); } const cardCache={}; async function loadCard(board,id){ const k=board.toLowerCase()+":"+id; if(cardCache[k])return cardCache[k]; const raw=await rpc(C,[{to:board,data:"0x"+SEL_TOKENURI+encUint(id)},L]); const uri=decodeString(raw); const i=uri.indexOf("base64,"); if(i<0)throw Error("card is not inline"); const j=JSON.parse(atob(uri.slice(i+7))); const out={attrs:Array.isArray(j.attributes)?j.attributes:[],img:safeUrl(j.image), name:safeSym(j.name).slice(0,40)}; cardCache[k]=out;return out; } function cardHtml(c){ const rows=c.attrs.slice(0,10).map(a=>{ const t=safeSym(a&&a.trait_type).slice(0,20); let v=a&&a.value; if(a&&a.display_type==="date"&&+v)v=new Date(+v*1e3).toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"}); else if(a&&a.display_type==="number")v=String(v); return`<dt>${t}</dt><dd>${safeSym(v).slice(0,28)}</dd>`}).join(""); return(rows?`<dl>${rows}</dl>`:"")+(c.img?`<img src="${c.img}" alt="" loading="lazy">`:""); } async function inspect(el,k){ const box=el.parentNode.querySelector(".ic"); if(!box)return; if(box.innerHTML){box.innerHTML="";return} const at=k.lastIndexOf(":"),board=k.slice(0,at),id=BigInt(k.slice(at+1)); box.textContent="Reading the receipt\u2026"; try{box.innerHTML=cardHtml(await loadCard(board,id))||"No card for this order."} catch{box.textContent="This receipt could not be read."} } const BOOK_TYPES=[["all","All"],["limit","Limit"],["dutch","Dutch"],["bid","Bids"],["nft","NFT"]]; const emptyBook=()=>{ const ty=BOOK_TYPES.find(([v])=>v===bookType); const what=bookType==="all"?"orders":`${ty?ty[1]:bookType} orders`; const where=bookPairOnly ?` on ${TOKENS[fromSel.value]?.sym||"?"}/${TOKENS[toSel.value]?.sym||"?"}`:""; return `No ${what.toLowerCase()}${where} from anyone else.`; }; function bookFilters(hidden){ const f=TOKENS[fromSel.value]?.sym||"?",t=TOKENS[toSel.value]?.sym||"?"; const chip=(on,attr,val,label,title)=>`<button class="bkc${on?" on":""}" ` +`data-${attr}="${val}"${title?` title="${title}"`:""}>${label}</button>`; return `<h6><span>Orderbook</span><span>${hidden?hidden+" hidden":""}</span></h6>` +`<div class="bkf">` +chip(bookPairOnly,"bf","1",`${f}/${t}`,"Only orders on the pair selected above") +chip(!bookPairOnly,"bf","0","All pairs","Every order on every board") +`</div><div class="bkf">` +BOOK_TYPES.map(([v,l])=>chip(bookType===v,"bt",v,l)).join("") +`</div>`; } let bookShown=0; function syncBook(n,total){ bookShown=n; total=total===undefined?n:total; const on=tab==="book"&&total>0; bkTog.classList.toggle("hide",!on); bkTog.firstChild.nodeValue=total ?`Orderbook (${n<total?`${n} of ${total}`:total}) `:"Orderbook "; bkTog.setAttribute("aria-expanded",on&&bkOpen?"true":"false"); book.classList.toggle("hide",!on||!bkOpen); } bkTog.onclick=()=>{bkOpen=!bkOpen;LS.bk=bkOpen?"1":"0";syncBook(bookShown,bookRows.length)}; book.addEventListener("click",async e=>{ const fc=e.target.closest("[data-bf]"),tc=e.target.closest("[data-bt]"); if(fc){bookPairOnly=fc.dataset.bf==="1";LS.bkp=bookPairOnly?"1":"0";paintBook();return} if(tc){bookType=tc.dataset.bt;paintBook();return} const b=e.target.closest("button[data-i]"); if(!b){ const row=e.target.closest(".o.ins"); if(row&&!e.target.closest("a"))inspect(row,row.dataset.k); return; } b.disabled=true; ++busy; try{ await checkWallet(); const id=BigInt(b.dataset.i),to=b.dataset.b,dl=BigInt(Math.floor(Date.now()/1e3)+1800); const row=bookRows.find(x=>String(x.id)===b.dataset.i&&x.board.toLowerCase()===to.toLowerCase()); if(!row)throw Error("order changed — refresh and retry"); if(b.dataset.x==="c"){ const native=
0xacfba7ce…4082·#25,751,063·0x01d306cc…89abe9
cv||acct||ZERO,acct||ZERO,quoteBlock).catch(()=>null); if(my!==seq)return; if(hq)pick(hq); } let tail=null; for(const i of tailJobs){const y=dec[i];if(!y)continue; const v=isIn?y.best.amountOut:y.best.amountIn; if(v&&(!tail||(isIn?v>tail.best.amountOut:v<tail.best.amountIn)))tail=y; } if(my!==seq)return; const v4q=await v4Job; let standalone=null; let v4c=null; if(v4q&&v4q.amountOut>0n){ const v4Min=v4q.amountOut-(v4q.amountOut*slipBps)/10000n; const p=v4q.pool; v4c={best:{source:4,amountIn:v4q.amountIn,amountOut:v4q.amountOut,feeBps:BigInt(p.fee)/100n}, callData:"0x"+SEL_V4SWAP+encPoolKey(p)+encBool(p.zeroForOne)+encUint(v4q.amountIn) +encUint(v4Min)+encAddr(rcv||acct||ZERO)+encUint(deadline), fundedCallData:null,amountLimit:v4Min,msgValue:f.addr===ZERO?swapAmt:0n, to:V4PORT,spender:f.addr===ZERO?null:V4PORT}; if(!r){r=v4c;standalone=v4c} } const pq=await preJob; if(my!==seq)return; let pc=null; if(pq&&pq.out>0n){ const pMin=pq.out-(pq.out*slipBps)/10000n; const pNative=pq.tokenIn===ZERO; const pWethIn=pNative&&(pq.unwrap||0n)>0n; const pTo=rcv||acct||ZERO; const pFund=pWethIn?WETH:pq.tokenIn; const routeData=pWethIn ?encPRouteWeth([pq.pool],pq.tokenOut,swapAmt,pMin,pTo) :encPRoute([pq.pool],pq.tokenIn,pq.tokenOut,swapAmt,pMin,pTo); const routedLeg=encSnwap(pFund,swapAmt,pTo,pq.tokenOut,pMin,PROUTE,routeData); const checkpointLeg=encSnwap(ZERO,0n,acct||ZERO,ZERO,0n,PROUTE, "0x"+SEL_PCHECKPOINT+encAddr(pFund)+strip0x(keccak(hexToBytes(routeData)))+encAddr(acct||ZERO)); const pCall=(pNative&&!pWethIn)?routedLeg:encCalls([checkpointLeg,routedLeg]); const pFunded=(pNative&&!pWethIn)?null:encCalls([ checkpointLeg, encSweep(pFund,swapAmt,PROUTE), encSnwap(ZERO,0n,pTo,pq.tokenOut,pMin,PROUTE,routeData), ]); pc={best:{source:SRC_PRECISION,amountIn:swapAmt,amountOut:pq.out,feeBps:pq.fee/100n}, callData:pCall, fundedCallData:pFunded,amountLimit:pMin,msgValue:(pNative&&!pWethIn)?swapAmt:0n, wrap:pq.wrap||0n,outDiffers:!!pq.outDiffers,poolOut:pq.tokenOut, to:null,spender:(pNative&&!pWethIn)?null:ZROUTER,pool:pq.pool}; if(!r){r=pc;standalone=pc} } if(!r&&curveOnlyOut)throw Error(`exact-output is unavailable for this pair — enter an amount to PAY instead`); if(!r)throw Error("bad quote"); if(r.best.amountIn<=0n||r.best.amountOut<=0n||(!isIn&&r.amountLimit<r.best.amountIn))throw Error("bad quote"); let hybrid=null,bookIncomplete=false,bookUnavailable=false,plannerBounded=false; if(acct&&!standalone){ const candidates=await candJob; bookIncomplete=!!candidates.incomplete; bookUnavailable=!!candidates.unavailable; if(my!==seq)return; const quoteRem=async(x,eo)=>{ const qs=[],groups=[],add=(sel,data,u,v,S,sb)=>{ qs.push({data:"0x"+sel+data,u,v,S,sb});return qs.length-1}; const z0=encBool(eo)+encAddr(f.addr)+encAddr(t.addr)+encUint(x)+encUint(0n)+encUint(deadline); const zs=encBool(eo)+encAddr(f.addr)+encAddr(t.addr)+encUint(x)+encUint(slipBps)+encUint(deadline); groups.push([add("e453166e",a+rf+z0,4,9),add("e453166e",a+rf+zs,4,9)]); groups.push([add("4c464f59",a+z0,8,13),add("4c464f59",a+zs,8,13)]); if(!eo){ const w=slipBps*3n,ss=w<150n?150n:w>500n?500n:w; const ps=encAddr(f.addr)+encAddr(t.addr)+encUint(x)+encUint(ss)+encUint(deadline); for(const sel of["892af013","85f86a90"])groups.push([add(sel,a+ps,4,8,1,ss)]); } const raw=await mc3Deep(qs.map(q=>({to:ZQUOTER,data:q.data})),quoteBlock); const ds=qs.map((q,i)=>{try{return raw[i]?decQ(raw[i],q.sb||slipBps,eo,q.u,q.v,q.S):null}catch{return null}}); let best=null; for(const g of groups){ const y=g.length>1?merge(ds[g[0]],ds[g[1]]):ds[g[0]]; if(!y)continue; if(eo&&y.sources&&y.sources.includes(SRC_CURVE))continue; const v=eo?y.best.amountIn:y.best.amountOut; if(v&&(!best||(eo?v<best.best.amountIn:v>best.best.amountOut)))best=y; } return best; }; const gasValue=async wei=>{ const token=isIn?t.addr:f.addr; if(token===ZERO||token.toLowerCase()===WETH.toLowerCase())return wei; const eo=!isIn,ti=isIn?ZERO:token,to=isIn?token:ZERO; const z=encBool(eo)+encAddr(ti)+encAddr(to)+encUint(wei)+encUint(0n)+encUint(deadline); try{ const raw=await rpc(C,[{to:ZQUOTER,data:"0xe453166e"+a+rf+z},quoteBlock]); const q=decQ(raw,0n,eo,4,9); return isIn?q.best.amountOut:q.best.amountIn; }catch{return null} }; const tailDelta=swapAmt-tailAmt; const seedOut=isIn&&tail&&r.best.amountOut>tail.best.amountOut ?r.best.amountOut-tail.best.amountOut:r.best.amountOut; const seedIn=!isIn&&tail&&r.best.amountIn>tail.best.amountIn ?r.best.amountIn-tail.best.amountIn:r.best.amountIn; let p=isIn ?planBookExactIn(candidates,swapAmt,seedOut,tail&&seedOut!==r.best.amountOut?tailDelta:r.best.amountIn) :planBookExactOut(candidates,swapAmt,seedIn,tail&&seedIn!==r.best.amountIn?tailDelta:r.best.amountOut); plannerBounded=!!(p&&p.bounded); let amm=null; for(let round=0;p&&(isIn?p.ammIn:p.ammOut)&&round<2;round++){ const rem=isIn?p.ammIn:p.ammOut; amm=await quoteRem(rem,!isIn).catch(()=>null); if(!amm)break; const next=isIn ?planBookExactIn(candidates,swapAmt,amm.best.amountOut,rem) :planBookExactOut(candidates,swapAmt,amm.best.amountIn,rem); if(!next){p=null;break} const stable=(isIn?next.ammIn===p.ammIn:next.ammOut===p.ammOut); p=next;plannerBounded=plannerBounded||!!p.bounded;if(stable)break; } if(p){ const rem=isIn?p.ammIn:p.ammOut; amm=rem?await quoteRem(rem,!isIn).catch(()=>null):null; if(!rem||amm){ const recipient=rcv||acct; const ammBudget=amm?(isIn?rem:amm.amountLimit):0n; const totalBudget=p.bookIn+ammBudget; const expectedIn=p.bookIn+(amm?amm.best.amountIn:0n); const expectedOut=p.bookOut+(amm?amm.best.amountOut:0n); const saving=isIn ?(expectedOut>r.best.amountOut?expectedOut-r.best.amountOut:0n) :(expectedIn<r.best.amountIn?r.best.amountIn-expectedIn:0n); let gasFloor=null; if(saving){ const gasPrice=await gasPriceJob; if(gasPrice){ const extraWei=gasPrice*(120000n+105000n*BigInt(p.fills.length)); gasFloor=await gasValue(extraWei); } } const edgeBps=5n+2n*BigInt(p.fills.length); const fallbackWin=isIn ?expectedOut*10000n>r.best.amountOut*(10000n+edgeBps) :expectedIn*(10000n+edgeBps)<r.best.amountIn*10000n; const wins=saving>0n&&(gasFloor===null?fallbackWin:saving*10n>gasFloor*12n); if(my!==seq)return; if(wins){ const fp=encFillPlanAndSwap( p.fills,f.addr,t.addr,recipient,acct,deadline,ammBudget,amm?amm.callData:"0x" ); const minOut=isIn?p.bookOut+(amm?amm.amountLimit:0n):swapAmt; const snwapIn=totalBudget===0n?ZERO:f.addr; const planCall=encSnwap(snwapIn,totalBudget,recipient,t.addr,minOut,SWAPBOL,fp); const callData=f.addr===ZERO?planCall:encCalls([ encCheckpoint(SWAPBOL,f.addr,acct),planCall ]); let fundedCallData=null; if(f.addr!==ZERO){ fundedCallData=encPermit2Hybrid( amm?amm.callData:"0x",p.fills,f.addr,t.addr,recipient,acct,deadline,p.bookIn,p.bookOut ); } r={...r,best:{...r.best,amountIn:expectedIn,amountOut:expectedOut},callData, fills:p.fills, fundedCallData,amountLimit:isIn?minOut:totalBudget,msgValue:f.addr===ZERO?totalBudget:0n, slipBps:amm&&amm.slipBps}; hybrid=isIn ?{bookIn:p.bookIn,bookOut:p.bookOut,ammIn:p.ammIn} :{bookIn:p.bookIn,bookOut:p.bookOut,ammOut:p.ammOut}; plannerBounded=plannerBounded||!!p.bounded; } } } } for(const c of [v4c,pc]) if(c&&c!==r&&c.best.amountOut>r.best.amountOut){r=c;hybrid=null} const finalIn=r.best.amountIn; const finalOut=r.best.amountOut; if(isIn){outAmt.value=trimAmt(finalOut,t.dec);fitFont(outAmt)} else{amt.value=trimAmt(finalIn,f.dec);fitFont(amt)} const rx=(+formatUnits(finalOut,t.dec))/(+formatUnits(finalIn,f.dec)); const s=r.best.source,raw=SOURCES[s]||"?"; const src=hybrid?"Orderbook + AMM":(s===3||s===4||s===SRC_PRECISION)&&r.best.feeBps?`${raw}\u00a0${Number(r.best.feeBps)/100}%`:raw; rate.textContent=isFinite(rx)?`1 ${f.sym} ≈ ${rx>=1?rx.toLocaleString("en-US",{maximumFractionDigits:4}):rx.toPrecision(4)} ${t.sym} · ${src}`:""; // The band holds the OTHER form of ether, so what lands is not the token named // on the panel. As a clause at the tail of the rate line it sat past the fee, // the minimum and the impact - facts away from the amount it contradicts. It // belongs on the heading over that amount. const paysSym=r.poolOut===ZERO?"ETH":"WETH"; rcvHdr.textContent=r.outDiffers?`You receive \u2014 as ${paysSym}`:"You receive"; if(r.outDiffers&&rate.textContent)rate.textContent+=` \u00b7 pays ${paysSym}`; rate.title=""; if(isIn)rate.textContent+=` · Min ${trimAmt(r.amountLimit,t.dec)} ${t.sym}`; else{ const exactMax=formatUnits(r.amountLimit,f.dec); rate.textContent+=` · Max ${maxAmt(r.amountLimit,f.dec)} ${f.sym}`; rate.title=`Exact maximum: ${exactMax} ${f.sym}`; } if(r.slipBps>slipBps)rate.textContent+=` · Split — bound widened to ${Number(r.slipBps)/100}%`; if(bookIncomplete)rate.textContent+=" · Book scan capped"; if(plannerBounded)rate.textContent+=" · Planner heuristic/capped"; if(bookUnavailable)rate.textContent+=" · AMM only (books offline)"; if(pq&&pq.capped)rate.textContent+=` · Precision scan capped (${LQ_SCAN})`; const refQ=iRef>=0&&dec[iRef]?dec[iRef].best:null; const imp=refQ?impactBps(isIn?finalOut:finalIn,isIn?refQ.amountOut:refQ.amountIn,!isIn):null; const mkt=refQ?(isIn?refQ.amountOut:refQ.amountIn)*REF_N:0n; const lossTok=imp!==null&&mkt?(isIn?(mkt>finalOut?mkt-finalOut:0n):(finalIn>mkt?finalIn-mkt:0n)):0n; if(imp!==null&&imp>=IMPACT_HIDE){ const pctTxt=(Number(imp)/100).toFixed(2); rate.textContent+=` · Impact ${pctTxt}%`; if(imp>=IMPACT_WARN)stat.textContent=`High price impact: ${pctTxt}% — about ${trimAmt(lossTok,isIn?t.dec:f.dec)} ${isIn?t.sym:f.sym} worse than the market rate.`; } const inApprove=isIn?swapAmt:r.msgValue||r.amountLimit; if(acct)last={callData:r.callData,fundedCallData:r.fundedCallData,msgValue:r.msgValue, to:r.to,spender:r.spender, amountIn:inApprove,from:f,recipient:rcv||"",exp:Date.now()+QUOTE_TTL,impact:imp, lossTok,lossSym:isIn?t.sym:f.sym,lossDec:isIn?t.dec:f.dec, fills:r.fills||null,tokenIn:f.addr,tokenOut:t.addr, source:r.best.source, wrap:r.wrap||0n,pool:r.pool||null, mkt,got:isIn?finalOut:finalIn,isIn}; quoting=false; render(); }catch(e){if(my===seq){ quoting=false; if(advancePastDeadPair()){loadChart();update();return} outEl.value="";fitFont(outEl);render(); stat.textContent=e?.noWallet?NO_WALLET :mcGaveUp>gaveUp0||(mcFailed>failed0&&mcOk===ok0) ?"Your RPC could not complete the quote — some venues were never asked. Try again, or switch networks in your wallet and back." :"No route: "+(e.message||e)}} } let debounceT=0; const showAddr=async()=>{const a=account,short=a.slice(0,6)+"..."+a.slice(-4); addr.textContent=short;addr.title=a+" - click to disconnect";addr.classList.add("on"); addr.setAttribute("role","button");addr.tabIndex=0; const n=await nameRev(a);if(n&&a===account)addr.textContent=n}; async function disconnect(){ sessionStorage.setItem("dc","1"); try{await rpc("wallet_revokePermissions",[{eth_accounts:{}}])}catch{} location.reload(); } addr.onclick=()=>{if(account)disconnect()}; addr.onkeydown=e=>{if(account&&(e.key==="Enter"||e.key===" ")){e.preventDefault();disconnect()}}; const AUTO_Q_MAX=5; const updateSoon=()=>{clearTimeout(debounceT);debounceT=setTimeout(()=>{debounceT=0;update()},250)}; const setMode=m=>{mode=m;const o=m==="in"?outAmt:amt;o.value="";fitFont(o);last=null;sendReady=null;render()}; amt.addEventListener("input",()=>{ if(lqMode){fitFont(amt);lqPreviewSoon();return} if(tab==="book"){fitFont(amt);render();return}setMode("in");fitFont(amt);updateSoon()}); outAmt.addEventListener("input",()=>{ if(lqMode){fitFont(outAmt);lqPreviewSoon();return} if(tab==="book"){fitFont(outAmt);seedFloor();render();return}setMode("out");fitFont(outAmt);updateSoon()}); const slipWarn=()=>slip.classList.toggle("warn",+slip.value>=3); if(LS.slip&&+LS.slip>=.01&&+LS.slip<=10)slip.value=LS.slip; slipWarn(); slip.addEventListener("input",()=>{last=null;slipWarn();render();updateSoon()}); rc.addEventListener("input",()=>{ if(lnMode)return; if(lqMode){clearTimeout(lqToT);lqToT=setTimeout(lqShowTo,300);return} last=null;sendReady=null;render();updateSoon()}); let lqToT; // No field sits in a <form>, so Enter did nothing. It never signs against a // figure unseen: mid-debounce it flushes the pending quote and stops there. const onEnter=e=>{ if(e.key!=="Enter")return; const go=lnMode?lnGo:swap; if(!go||go.classList.contains("hide"))return; e.preventDefault(); if(debounceT){clearTimeout(debounceT);debounceT=0;update();return} if(!go.disabled)go.click(); }; for(const el of[amt,outAmt,rc,slip,floorAmt,nftId,lnName,lnSym,lnSupply,lnMcap,lnAlloc]) if(el)el.addEventListener("keydown",onEnter); slip.addEventListener("change",()=>{slip.value=Math.min(10,Math.max(.01,+slip.value||.5));slipWarn();LS.slip=slip.value}); for(const sel of [fromSel,toSel]){ sel.dataset.prev=sel.value; sel.addEventListener("change",async()=>{ if(sel.value!=="__custom"){ sel.dataset.prev=sel.value; sel.dataset.addr=TOKENS[sel.value]?.addr||""; pairChosen=true; syncDisabled(sel); syncNftUI(); if(sel===fromSel)refreshBalance();else refreshBalance1(); loadChart(); if(tab==="book"&&bookRows.length)paintBook(); update(); return; } last=null;render(); const prev=sel.dataset.prev; let a; try{a=(prompt("Paste token address:")||"").trim()} catch{sel.value=prev;syncDisabled(); stat.textContent="This browser blocked the address prompt — add the token with a #token=0x… link instead."; return} if(!a){sel.value=prev;update();return} stat.textContent="Loading token..."; try{ const idx=await addCustomToken(a); rebuild(); const otherSel=sel===fromSel?toSel:fromSel; if(String(idx)===otherSel.value){otherSel.value=prev;otherSel.dataset.prev=prev} sel.value=String(idx); sel.dataset.prev=sel.value; pairChosen=true; stat.textContent=""; syncDisabled(sel);syncNftUI(); refreshBalance(); update(); }catch(e){ const msg="Couldn't load token: "+(e.message||e); sel.value=prev; syncDisabled();syncNftUI();refreshBalance();update(); stat.textContent=msg; } }); } flip.onclick=()=>{pairChosen=true;const a=fromSel.value,b=toSel.value,v=outAmt.value.replace(/,/g,"");fromSel.value=b;toSel.value=a;if(v&&!isNaN(+v))amt.value=v;setMode("in");fitFont(amt);syncDisabled();syncNftUI();refreshBalance();loadChart();update()}; const sameAcct=(a,b)=>!!a&&!!b&&a.toLowerCase()===b.toLowerCase(); const wireWallet=eth=>{ if(!eth||wireWallet.done)return; wireWallet.done=true; eth.on?.("chainChanged",cid=>{if(connecting||(+cid===1&&account))return;location.reload()}); eth.on?.("accountsChanged",accs=>{if(connecting||sameAcct(accs?.[0],account))return;location.reload()}); (async()=>{ if(sessionStorage.getItem("dc"))return; try{ const accs=await rpc("eth_accounts",[]); const cid=await rpc(I,[]); if(accs[0]&&+cid===1){ account=accs[0]; showAddr(); const keep2=pairAddrs(); await loadTokenList(); adoptList(keep2); refreshBalance();update();loadPos();syncChart(); } }catch{} })(); (async()=>{ const self=selfFromUrl()||ZSWAP_SELF; if(!self)return; try{ if(+(await rpc(I,[]))!==CHAIN_ID)return; const ret=strip0x(await rpc(C,[{to:self,data:"0x"+SEL_LATEST},L])||""); if(ret.length!==64)return; let tip="0x"+ret.slice(24); const me=self.toLowerCase(); if(BigInt(tip)===0n||tip===me)return; const rd=async(to,sel)=>{const h=strip0x(await rpc(C,[{to,data:"0x"+sel},L])||"");return h.length===64?h:""}; const now=Date.now()/1000; for(let i=0;i<4&&tip!==me;i++){ const pv=await rd(tip,SEL_PREV); if(!pv)break; const prev="0x"+pv.slice(24); if(BigInt(prev)===0n)break; const at=await rd(prev,SEL_SUCCAT); const born=at?Number(BigInt("0x"+at)):0; if(!born||now>=born+MATURITY)break; tip=prev; } if(tip===me)return; const h=(location.hostname||"").split("."); const url=/^0x[0-9a-fA-F]{40}$/.test(h[0]||"") ?`${location.protocol}//${[tip,...h.slice(1)].join(".")}/` :`https://${tip}.w4eth.io/`; footAddr.innerHTML+=` · <a href="${url}" target="_blank" rel="noopener" ` +`title="A newer version exists on chain. This page is unchanged and always will be; the link opens the successor.">newer →</a>`; }catch{} })(); }; wireWallet(window.ethereum); if(!window.ethereum)window.addEventListener("ethereum#initialized",()=>wireWallet(window.ethereum),{once:true}); const SEL_TRANSFER="a9059cbb"; const SB2="0x000000dA7bb4B2A9E3e80e9A4D4157E26CA6189b"; const SB1="0x000000fF3D7A2d373615141d7489Ca66683DbecF"; const SBVIEW="0x000000E0b25449F32f7D9259aC449bA88E78dFCE"; const SWAPBOL="0x00000087A6dc5071779Ed1F8274A39230768B976"; const DUTCH="0x000000a213b430D14Bae6062c176289B05e04489"; const ORDERBOL="0x000000e6c7a12C80525ee74e7434aAb919447D95"; const FLOOR="0x00000080198137F790DA4C52bb902cf87c276748"; const FLOORVIEW="0x0000004E376e9dB5D9EC28E6711E1a64997C6ba7"; const SEL_NEXTID="2a58b330",SEL_GETORDERS="03652027",SEL_CANCELORD="514fcac7"; const SEL_FILL1="c37dfc5b",SEL_FILL2="9d136c7f",SEL_FILL2_UNWRAP="3987baf4",SEL_FILL2_ETH="6f608bab"; const SEL_RECENT="6a9849c1",SEL_RECENT_DUTCH="98035c9a",SEL_DUTCH_FILL="ae7a8260",SEL_DUTCH_CANCEL="40e58ee5"; const SEL_CANCEL_UNWRAP="21dd76f9",SEL_DUTCH_CANCEL_UNWRAP="8382de65"; const SEL_DUTCH_LISTING="de74e57b"; const SEL_CANDS="5f452988",SEL_DUTCH_CANDS="eb33e466",SEL_FILLPLAN="c277f67c"; const SEL_FILLPLAN_SWAP="9090c8e5",SEL_SNWAP="5f3bd1c8"; const SEL_COLL_BIDS="16bb24eb"; const SEL_FLOOR_CANDS="c587d927",SEL_RECENT_FLOOR="a5edd13d"; const SEL_ORDER_FLOOR="23e93357"; const SEL_BOL_FLOOR="b732d224"; let floorReady=null; async function floorRoutable(block){ if(FLOOR===ZERO||FLOORVIEW===ZERO)return false; if(floorReady!==null)return floorReady; try{ const h=await rpc(C,[{to:SWAPBOL,data:"0x"+SEL_BOL_FLOOR},block||L]); floorReady=("0x"+strip0x(h).slice(-40)).toLowerCase()===FLOOR.toLowerCase(); }catch{return false} return floorReady; } const SEL_BIDS="4423c5f1"; const SEL_INITIAL_B="878ea250",SEL_TOKENURI="c87b56dd",SEL_LOGOOF="6ce273ac"; const SEL_QUOTEFILL="f325beda"; async function preflightLeg(row,routeIn,routeOut,pay){ if(!row.floor)return preflightAsk(row); if(!await floorRoutable(L))throw Error("this executor cannot route bids yet"); const want=routeIn===ZERO?WETH:routeIn,gets=routeOut===ZERO?WETH:routeOut; let h; try{h=await rpc(C,[{to:FLOOR,data:"0x"+SEL_BIDS+encUint(row.id)},L])} catch{throw Error("bid could not be read — refresh and retry")} const at=i=>strip0x(h).slice(i*64,(i+1)*64); if(strip0x(h).length<11*64)throw Error("bid could not be read — refresh and retry"); const addr=i=>"0x"+at(i).slice(24); if(/^0x0{40}$/i.test(addr(0)))throw Error("bid is closed — refresh and retry"); if(BigInt("0x"+at(1)))throw Error("NFT bids are not routable from here"); if(addr(4).toLowerCase()!==want.toLowerCase() ||addr(6).toLowerCase()!==gets.toLowerCase()) throw Error("bid changed — refresh and retry"); if(BigInt("0x"+at(10))<pay)throw Error("bid partly taken — refresh and retry"); } async function preflightFills(fills,tokenIn,tokenOut){ if(!fills||!fills.length)return; const inA=(tokenIn===ZERO?WETH:tokenIn).toLowerCase(); const outA=(tokenOut===ZERO?WETH:tokenOut).toLowerCase(); const reads=fills.map(f=>f.board.toLowerCase()===FLOOR.toLowerCase() ?{to:FLOOR,data:"0x"+SEL_BIDS+encUint(f.id)} :f.board.toLowerCase()===DUTCH.toLowerCase() ?{to:DUTCH,data:"0x"+SEL_DUTCH_LISTING+encUint(f.id)} :{to:f.board,data:"0x"+SEL_GETORDERS+encUint(32)+encUint(1)+encUint(f.id)}); const res=await mc3Deep(reads); const stale=()=>{throw Error("an order in this route just changed — refreshing")}; for(let i=0;i<fills.length;i++){ const f=fills[i],raw=res[i]; if(!raw)stale(); const s=strip0x(raw),at=k=>BigInt("0x"+s.slice(k*64,(k+1)*64)); if(f.board.toLowerCase()===FLOOR.toLowerCase()){ if(s.length<11*64)stale(); const ad=k=>"0x"+s.slice(k*64+24,(k+1)*64); if(/^0x0{40}$/i.test(ad(0))||at(1))stale(); if(ad(4).toLowerCase()!==inA||ad(6).toLowerCase()!==outA)stale(); if(at(10)<f.pay)stale(); }else if(f.board.toLowerCase()===DUTCH.toLowerCase()){ if(s.length<640||at(9)===0n)stale(); }else{ const wds=WORDS(f.board.toLowerCase()===SB2.toLowerCase()?1:0); if(s.length<(2+wds)*64)stale(); const w=k=>at(2+k),ad=k=>"0x"+s.slice((2+k)*64+24,(3+k)*64); if(w(1)!==1n)stale(); const v2=wds===11,iA=v2?7:2,iB=v2?9:4; if(ad(iA).toLowerCase()!==outA||ad(iB).toLowerCase()!==inA)stale(); if(w(iB+1)<f.pay)stale(); } } } async function preflightAsk(row){ if(row.dutch){ let h; try{h=await rpc(C,[{to:DUTCH,data:"0x"+SEL_DUTCH_LISTING+encUint(row.id)},L])} catch{throw Error("listing could not be read — refresh and retry")} const s=strip0x(h); if(s.length<640)throw Error("listing could not be read — refresh and retry"); if(BigInt("0x"+s.slice(9*64,10*64))===0n)throw Error("listing is closed — refresh and retry"); return; } let h; try{h=await rpc(C,[{to:row.board, data:"0x"+SEL_GETORDERS+encUint(32)+encUint(1)+encUint(row.id)},L])} catch{throw Error("order could not be read — refresh and retry")} const s=strip0x(h),wds=WORDS(row.v2); if(s.length<(2+wds)*64)throw Error("order could not be read — refresh and retry"); const at=k=>BigInt("0x"+s.slice(128+k*64,128+(k+1)*64)); const ad=k=>"0x"+s.slice(128+k*64+24,128+(k+1)*64); if(at(1)!==1n)throw Error("order is no longer active — refresh and retry"); const iA=row.v2?7:2,iB=row.v2?9:4; if(ad(iA).toLowerCase()!==row.tA.toLowerCase() ||ad(iB).toLowerCase()!==row.tB.toLowerCase()) throw Error("order changed — refresh and retry"); if(at(iA+1)!==row.aA||at(iB+1)!==row.aB)throw Error("order changed — refresh and retry"); } const SEL_ORDER_FIXED="bcdb7936",SEL_ORDER_DUTCH="bbd94e1a",SEL_CHECKPOINT="a972985e"; const BOARDS=[{a:SB2,v2:1},{a:SB1,v2:0}]; const WORDS=b=>b?11:6; let bookSeq=0,bookRows=[],candCache={},bkOpen=LS.bk!=="0"; let busy=0; if(LS.wm==="weth"||LS.wm==="auto")ethMode.value=LS.wm; ethMode.onchange=()=>{LS.wm=ethMode.value}; const wantsUnwrap=()=>ethMode.value!=="weth"; const planWethPayment=async need=>{ let w=0n,e=0n; try{ const[wh,eh]=await Promise.all([ rpc(C,[{to:WETH,data:"0x"+SEL_BALANCEOF+encAddr(account)},L]), rpc("eth_getBalance",[account,L])]); w=BigInt(wh);e=BigInt(eh); }catch{return{rail:"weth",wrap:0n,w,e}} if(w>=need)return{rail:"weth",wrap:0n,w,e}; if(e>=need)return{rail:"eth",wrap:0n,w,e}; let gas=0n; try{gas=BigInt(await rpc("eth_gasPrice",[]))*400000n}catch{} const spendable=e>gas?e-gas:0n; if(w+spendable>=need)return{rail:"weth",wrap:need-w,w,e}; return{rail:"short",wrap:0n,w,e}; }; const assertFunded=async(token,need,meta)=>{ if(!need)return; let have; try{ have=token===ZERO ?BigInt(await rpc("eth_getBalance",[account,L])) :BigInt(await rpc(C,[{to:token,data:"0x"+SEL_BALANCEOF+encAddr(account)},L])); }catch{return} if(have>=need)return; const m=meta||known(token)||{sym:token===ZERO?"ETH":"tokens",dec:18}; throw Error(`Not enough ${m.sym} to fill: it costs ${trimAmt(need,m.dec)} ` +`and you hold ${trimAmt(have,m.dec)}.`); }; const known=a=>TOKENS.find(t=>t.addr.toLowerCase()===a.toLowerCase()); const sm=i=>i.replace('width="20" height="20"','width="15" height="15"'); const escan=(a,id)=>`<a href="https://etherscan.io/${id!=null?`nft/${a}/${id}`:`token/${a}`}" target="_blank" rel="noreferrer">${a.slice(0,6)}…${a.slice(-4)} ↗</a>`; const SEL_CONTRACTURI="e8a3d485"; const okDataImg=u=>typeof u==="string"&&u.length<=60000 &&/^data:image\/[a-z.+-]{2,20};base64,[A-Za-z0-9+/=]+$/i.test(u); const imgFromContractURI=raw=>{ const P="data:application/json;base64,"; const u=String(raw||"").trim(); if(!u.startsWith(P)||u.length>400000)return ""; let txt; try{ const bin=atob(u.slice(P.length)); txt=new TextDecoder().decode(Uint8Array.from(bin,c=>c.charCodeAt(0))); }catch{return ""} let j;try{j=JSON.parse(txt)}catch{return ""} return okDataImg(j&&j.image)?j.image:""; }; const logoCache={}; async function loadLogos(addrs){ const want=[...new Set(addrs.map(a=>a.toLowerCase()))] .filter(a=>a!==ZERO&&!known(a)&&logoCache[a]===undefined); if(!want.length)return; let raw=[]; if(TOKENLIST!==ZERO)raw=await mc3Deep(want.map(a=>({to:TOKENLIST,data:"0x"+SEL_LOGOOF+encAddr(a)}))).catch(()=>[]); want.forEach((a,i)=>{let u="";try{if(raw[i])u=safeUrl(decodeString(raw[i]))}catch{}logoCache[a]=u}); const self=want.filter(a=>!logoCache[a]); if(!self.length)return; const cu=await mc3Deep(self.map(a=>({to:a,data:"0x"+SEL_CONTRACTURI}))).catch(()=>[]); self.forEach((a,i)=>{try{ const u=imgFromContractURI(cu[i]?decodeString(cu[i]):""); if(u)logoCache[a]=u; }catch{}}); } async function leg(addr,amt,isNft,meta){ const k=known(addr); if(isNft)return{ic:genIcon("#"),txt:`#${amt}`,ok:0,link:escan(addr,amt)}; const raw=meta&&meta.sym?meta:await tokMeta(addr),m={...raw,sym:safeSym(raw.sym)}; const lg=k?"":logoCache[addr.toLowerCase()]; const ic=k?sm(k.icon):lg?`<img src="${lg}" width="15" height="15">`:genIcon(m.sym); return{ic,txt:`${trimAmt(amt,m.dec)} ${k?safeSym(k.sym):'“'+m.sym+'”'}`,ok:!!k,link:addr===ZERO?"ETH":escan(addr)}; }
0xacfba7ce…4082·#25,751,062·0xcc239395…c39e2b
; TOKENS.forEach((t,i)=>{const o=`<option value="${i}">${t.sym}</option>`; if(t.std==="nft")nft+=o;else ft+=o}); return ft +(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 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 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;return} if(!sell||!want){swap.textContent=bid?"Place bid":"Place order";swap.disabled=true;return} if(bid){ if(t.addr===ZERO){swap.textContent="Pick a token to bid for";swap.disabled=true;return} if((f.addr===ZERO?WETH:f.addr).toLowerCase()===t.addr.toLowerCase()){ swap.textContent="Pick different token";swap.disabled=true;return} if(!floor){swap.textContent="Set an opening bid";swap.disabled=true;return} if(floor>sell){swap.textContent="Opening exceeds maximum";swap.disabled=true;return} if(+dly.value===0){swap.textContent="Choose a bid window";swap.disabled=true;return} if(sell>fromBalance){swap.textContent="Insufficient balance";swap.disabled=true;return} swap.textContent=`Bid ${trimAmt(floor,f.dec)} → ${trimAmt(sell,f.dec)} ${f.sym} for ${trimAmt(want,t.dec)} ${t.sym}`; swap.disabled=false;return; } if(f.addr===ZERO&&t.addr===WETH){swap.textContent="Pick different token";swap.disabled=true;return} if(!dutch&&f.addr===WETH&&t.addr===ZERO){swap.textContent="Pick different token";swap.disabled=true;return} if(dutch&&floor>want){swap.textContent="Floor exceeds start";swap.disabled=true;return} if(dutch&&+dly.value===0){swap.textContent="Choose decay duration";swap.disabled=true;return} if(sell>fromBalance){swap.textContent="Insufficient balance";swap.disabled=true;return} swap.textContent=dutch ?`Dutch ${trimAmt(sell,f.dec)} ${f.sym} · ${trimAmt(want,t.dec)} → ${trimAmt(floor,t.dec)} ${t.sym}` :`Place ${fill.value==="1"?"":"all-or-nothing "}order \u2014 ${trimAmt(sell,f.dec)} ${f.sym} \u2192 ${trimAmt(want,t.dec)} ${t.addr===ZERO?"WETH":t.sym}`; { const qs=t.addr===ZERO?"ETH":t.sym; rate.textContent=dutch ?`Falls from ${trimAmt(want,t.dec)} to ${trimAmt(floor,t.dec)} ${qs} over ${rel(+dly.value)}, then rests at ${trimAmt(floor,t.dec)} ${qs} ` +(+dRest.value?`for ${rel(+dRest.value)} and returns to you`:`until you cancel`) :`${fill.value==="1"?"Fills in any amount":"Fills all at once"} at ${trimAmt(want,t.dec)} ${t.addr===ZERO?"WETH":t.sym} for ${trimAmt(sell,f.dec)} ${f.sym}` +(+dly.value?`, expiring in ${rel(+dly.value)}`:", and never expires"); } swap.disabled=false;return; } if(tab==="send"){ if(!sendReady){ let n=0n;try{n=parseUnits(amt.value.trim(),TOKENS[fromSel.value].dec)}catch{} swap.textContent=n>0n?"Name a recipient":"Enter an amount"; swap.disabled=true;return} if(sendReady.amount>fromBalance){swap.textContent="Insufficient balance";swap.disabled=true;return} const a=`${trimAmt(sendReady.amount,sendReady.token.dec)} ${sendReady.token.sym}`,dv=+dly.value; if(dv&&sendTipWei>0n&&sendReady.token.addr===ZERO &&sendReady.amount+sendTipWei>fromBalance){ swap.textContent="Insufficient ETH for amount + keeper tip";swap.disabled=true;return} swap.textContent=(dv?`Lock ${a} for ${rel(dv)}`:`Send ${a}`)+` \u2192 ${sendReady.label}`; swap.disabled=false;return; } if(quoting){swap.textContent="Quoting…";swap.disabled=true;return} if(!last){swap.textContent="Swap";swap.disabled=true;return} if(last.amountIn>fromBalance){swap.textContent="Insufficient balance";swap.disabled=true;return} swap.textContent="Swap";swap.disabled=false; }; syncIcons(); render(); const isRejection=e=>e?.code===4001||/user\s+reject|user\s+denied/i.test(e?.message||""); async function connect(){ if(!window.ethereum){stat.textContent=NO_WALLET;return} if(connecting)return; connecting=true; // Waits on the wallet prompt and the whole token list, while the button still // read "Connect Wallet" and a second click vanished into the guard above. swap.textContent="Connecting…";swap.disabled=true; lnGo.textContent="Connecting…";lnGo.disabled=true; try{ let accs; try{accs=await rpc("eth_requestAccounts",[])} catch(e){err(e);return} if(!accs[0])return; let chainId=await rpc(I,[]); if(+chainId!==1){ try{await rpc("wallet_switchEthereumChain",[{chainId:"0x1"}])} catch(e){stat.textContent=isRejection(e)?"":"Switch to Ethereum mainnet.";return} chainId=await rpc(I,[]); if(+chainId!==1){stat.textContent="Switch to Ethereum mainnet.";return} } sessionStorage.removeItem("dc"); account=accs[0]; const keepPair=pairAddrs(); await loadTokenList(); adoptList(keepPair); showAddr(); refreshBalance(); update(); loadPos(); syncChart(); if(lnMode)lnQuote(); }finally{connecting=false;swap.disabled=false; lnGo.disabled=false;lnGo.textContent=account?"Launch":"Connect Wallet";render()} } const lqReserveGas=()=>lqList.querySelector(".lqnew")?LQ_CREATE_GAS+300000n:400000n; const lqFillMax=(tok,max)=>{ const box=lqList.querySelector(".lqadd:not(.hide)"); const pair=chPair(); if(!box||!pair)return false; const side=pair[0].addr.toLowerCase()===tok.addr.toLowerCase()?"0":"1"; const el=box.querySelector(`.lqin[data-side="${side}"]`); if(!el)return false; el.value=formatUnits(max,tok.dec); el.dispatchEvent(new Event("input",{bubbles:true})); return true; }; bal1.addEventListener("click",async e=>{ if(e.target.tagName!=="A")return; e.preventDefault(); const t=TOKENS[toSel.value]; if(!t)return; let reserve=0n; if(t.addr===ZERO){ try{reserve=BigInt(await rpc("eth_gasPrice",[]))*lqReserveGas()}catch{} } const max=toBalance>reserve?toBalance-reserve:0n; if(lqFillMax(t,max))return; outAmt.value=formatUnits(max,t.dec); fitFont(outAmt); lqPreviewSoon(); }); bal.addEventListener("click",async e=>{ if(e.target.tagName!=="A")return; e.preventDefault(); const f=TOKENS[fromSel.value]; if(!f)return; let reserve=0n; if(f.addr===ZERO){ try{reserve=BigInt(await rpc("eth_gasPrice",[])) *(lqMode?lqReserveGas():tab==="swap"?900000n:tab==="book"?350000n :+dly.value?(tipCk.checked?200000n+TIP_GAS:200000n):30000n)}catch{} } const max=fromBalance>reserve?fromBalance-reserve:0n; if(lqMode&&lqFillMax(f,max))return; amt.value=formatUnits(max,f.dec); fitFont(amt); if(lqMode)return; setMode("in"); update(); }); let seq=0; const REVERTS={ "1ab7da6b":"That took too long and the deadline passed, so nothing was placed. Try again — you were not charged for an order.", "98c2c368":"The order was refused as malformed. Check the amounts and the expiry.", "7939f424":"The token transfer was refused — usually a missing or spent approval. Try again and approve first.", "90b8ec18":"The token transfer was refused by the token itself.", "3e3f8f73":"The approval was refused by the token.", "b12d13eb":"Ether could not be sent to the recipient — a contract there may not accept it.", "ab143c06":"That call re-entered and was stopped.", }; const explain=e=>{ if(e?.noWallet)return NO_WALLET; const s=String(e?.data?.data||e?.data||e?.message||e||""); const m=/0x([0-9a-fA-F]{8})\b/.exec(s); const hit=m&&REVERTS[m[1].toLowerCase()]; return hit||("Error: "+s.slice(0,110)); }; const err=e=>stat.textContent=isRejection(e)?"":explain(e); const settle=async tx=>{ if(!tx){stat.textContent="Done";return} if(!/^0x[0-9a-f]{64}$/i.test(tx))throw Error("bad tx"); stat.innerHTML=txLink(tx,"Sent");await waitTx(tx);stat.innerHTML=txLink(tx,"Done")}; const txLink=(h,label)=>`${label} <a href="https://etherscan.io/tx/${h}" target="_blank" rel="noreferrer">${h.slice(0,10)}…${h.slice(-6)}</a>`; let autoQ=0; async function update(auto){ if(!auto)autoQ=0; syncIcons(); if(lqMode)return loadLq(); if(tab==="book")return render(); if(tab==="send")return updateSend(); const my=++seq; quoting=false; // The heading carries a fact about the QUOTE - "as WETH" - so it has to be // dropped with the quote, or it outlives the trade it was true of. last=null; rate.textContent=""; if(!lqMode&&!lnMode)rcvHdr.textContent="You receive"; render(); const f=TOKENS[fromSel.value], t=TOKENS[toSel.value]; if(!f||!t)return; if(f.addr===t.addr){stat.textContent="Pick different tokens.";return} stat.textContent=""; const isIn=mode==="in"; const inEl=isIn?amt:outAmt, outEl=isIn?outAmt:amt, inTok=isIn?f:t; outEl.value=""; fitFont(outEl); if(!inEl.value.trim())return; let swapAmt; try{swapAmt=parseUnits(inEl.value.trim(),inTok.dec)} catch(e){const q=inEl.value.trim(); if(q&&!/\.$/.test(q))stat.textContent=/decimals/.test(e.message)?`${inTok.sym} has ${inTok.dec} decimals`:"Invalid amount"; return} if(swapAmt<=0n)return; const rv=rc.value.trim(); let rcv=""; if(rv){ const fail=m=>{rc.classList.add("bad");rcvEl.textContent=""; outEl.value="";fitFont(outEl);stat.textContent=m}; const bad=rcErr(rv); if(bad){fail(bad);return} if(isAddr(rv))rcv=rv; else{ rcvEl.textContent="resolving..."; try{rcv=await rcvOf(rv)} catch(e){if(my!==seq)return;fail(e&&e.offchain?e.message:"Name not registered");return} if(my!==seq)return; if(!rcv){fail("Name not registered");return} } rcvEl.textContent=rcv; }else rcvEl.textContent=""; rc.classList.remove("bad"); if(!rv&&f.addr===WETH&&t.addr===ZERO){ outEl.value=inEl.value;fitFont(outEl); rate.textContent=`1 ${f.sym} = 1 ${t.sym}`; last={callData:"0x2e1a7d4d"+encUint(swapAmt),msgValue:0n,amountIn:swapAmt,from:f,to:WETH,recipient:"",exp:Date.now()+QUOTE_TTL}; render();return; } if(!rv&&f.addr===ZERO&&t.addr===WETH){ outEl.value=inEl.value;fitFont(outEl); rate.textContent=`1 ${f.sym} = 1 ${t.sym}`; last={callData:"0xd0e30db0",msgValue:swapAmt,amountIn:swapAmt,from:f,to:WETH,recipient:"",exp:Date.now()+QUOTE_TTL}; render();return; } const pct=Math.min(10,Math.max(.01,+slip.value||.5)); const slipBps=BigInt(Math.round(pct*100)); const deadline=BigInt(Math.floor(Date.now()/1e3)+1800); outEl.value="...";fitFont(outEl);quoting=true;render(); const acct=account; const gaveUp0=mcGaveUp,ok0=mcOk,failed0=mcFailed; try{ const a=encAddr(rcv||acct||ZERO),rf=encAddr(acct||ZERO),tl=encBool(!isIn)+encAddr(f.addr)+encAddr(t.addr)+encUint(swapAmt)+encUint(slipBps)+encUint(deadline); const _rq=[]; const Q=(x,d,u,v,S,sb,mv)=>{_rq.push({to:ZQUOTER,data:"0x"+x+d,u,v,S,sb,mv});return _rq.length-1}; let r=null; const candKey=f.addr.toLowerCase()+":"+t.addr.toLowerCase(); const cachedCandidates=candCache[candKey]; const quoteBlock=cachedCandidates&&Date.now()-cachedCandidates.at<15000 ?cachedCandidates.block:await rpc("eth_blockNumber",[]); const candJob=acct?swapCandidates(f.addr,t.addr,quoteBlock).catch(()=>{ const x=[];x.unavailable=true;return x }):Promise.resolve([]); const gasPriceJob=acct?rpc("eth_gasPrice",[]).then(BigInt).catch(()=>0n):Promise.resolve(0n); const v4Job=isIn?quoteV4Pools(f,t,swapAmt,quoteBlock).catch(()=>null):Promise.resolve(null); const preJob=isIn?quotePrecision(f,t,swapAmt,PROUTE,quoteBlock).catch(()=>null):Promise.resolve(null); const SRC_CURVE=5; let curveOnlyOut=false; const exactOutSafe=y=>{if(isIn||!y.sources||!y.sources.includes(SRC_CURVE))return true;curveOnlyOut=true;return false}; const pick=y=>{if(!y)return;if(!exactOutSafe(y))return;const v=isIn?y.best.amountOut:y.best.amountIn;if(!v)return; if(!r||(isIn?v>r.best.amountOut:v<r.best.amountIn))r=y}; const tl0=encBool(!isIn)+encAddr(f.addr)+encAddr(t.addr)+encUint(swapAmt)+encUint(0n)+encUint(deadline); const jobs=[ [Q("e7798987",a+tl,0,4,0,0,6)], [Q("e453166e",a+rf+tl0,4,9),Q("e453166e",a+rf+tl,4,9)], [Q("4c464f59",a+tl0,8,13),Q("4c464f59",a+tl,8,13)]]; let splitSlip=0n; if(isIn){ const w=slipBps*3n,SS=w<150n?150n:w>500n?500n:w; splitSlip=SS; const ps=encAddr(f.addr)+encAddr(t.addr)+encUint(swapAmt)+encUint(SS)+encUint(deadline); for(const x of["892af013","85f86a90"])jobs.push([Q(x,a+ps,4,8,1,SS)]); } const tailStep=swapAmt/100n||1n,tailAmt=swapAmt>tailStep?swapAmt-tailStep:0n; const tailJobs=[]; if(tailAmt){ const z=encBool(!isIn)+encAddr(f.addr)+encAddr(t.addr)+encUint(tailAmt)+encUint(0n)+encUint(deadline); tailJobs.push(Q("e453166e",a+rf+z,4,9),Q("4c464f59",a+z,8,13)); if(isIn)for(const x of["892af013","85f86a90"]){ const ps=encAddr(f.addr)+encAddr(t.addr)+encUint(tailAmt)+encUint(splitSlip)+encUint(deadline); tailJobs.push(Q(x,a+ps,4,8,1,splitSlip)); } } const refAmt=swapAmt/REF_N; const iRef=refAmt>0n ?Q("e453166e",a+rf+encBool(!isIn)+encAddr(f.addr)+encAddr(t.addr)+encUint(refAmt)+encUint(0n)+encUint(deadline),4,9) :-1; const mcRaw=await mc3Deep(_rq.map(q=>({to:q.to,data:q.data})),quoteBlock); if(my!==seq)return; const dec=_rq.map((q,i)=>{try{return mcRaw[i]?decQ(mcRaw[i],q.sb||slipBps,!isIn,q.u,q.v,q.S,q.mv):null}catch{return null}}); for(const g of jobs)pick(g.length>1?merge(dec[g[0]],dec[g[1]]):dec[g[0]]); if(isIn&&!(dec[jobs[1][0]]||dec[jobs[1][1]]||dec[jobs[2][0]]||dec[jobs[2][1]])){ const hq=await hubRoute(f,t,swapAmt,slipBps,deadline,r
0xacfba7ce…4082·#25,751,061·0x167b9eb7…128d6c
:tA, unwrap:fA===ZERO&&f.addr!==ZERO?amountIn:0n, wrap:fA!==ZERO&&f.addr===ZERO?amountIn:0n, outDiffers:(tA===ZERO)!==(t.addr===ZERO)}; } const SEL_BBS="e7798987"; const HUBS=()=>[WETH,...NUMERAIRES,"0x2260fac5e5542a773aa44fbcfedf7c193bc2c599","0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0"]; async function hubRoute(f,t,amt,sb,dl,to,rf,block){ const nf=(f.addr===ZERO?WETH:f.addr).toLowerCase(),nt=(t.addr===ZERO?WETH:t.addr).toLowerCase(); const H=HUBS().filter(h=>h.toLowerCase()!==nf&&h.toLowerCase()!==nt); if(!H.length)return null; const E=(x,i,o,n)=>({to:ZQUOTER,data:"0x"+SEL_BBS+encAddr(x)+encBool(false)+encAddr(i)+encAddr(o)+encUint(n)+encUint(sb)+encUint(dl)}); const D=h=>{try{return decQ(h,sb,false,0,4,0,6)}catch{return null}}; const L=[],r1=await mc3Deep(H.map(h=>E(ZROUTER,f.addr,h,amt)),block); for(let i=0;i<H.length;i++){const a=r1[i]&&D(r1[i]);if(a&&a.amountLimit>0n)L.push({h:H[i],a})} if(!L.length)return null; const r2=await mc3Deep(L.map(x=>E(to,x.h,t.addr,x.a.amountLimit)),block); let B=null; for(let i=0;i<L.length;i++){const b=r2[i]&&D(r2[i]); if(b&&b.best.amountOut>0n&&(!B||b.best.amountOut>B.b.best.amountOut))B={...L[i],b}} if(!B)return null; const c=[B.a.callData,B.b.callData,encSweep(B.h,0n,rf),encSweep(ZERO,0n,rf)]; return{best:{source:B.b.best.source,feeBps:B.b.best.feeBps,amountIn:amt,amountOut:B.b.best.amountOut}, callData:encCalls(c),amountLimit:B.b.amountLimit, msgValue:f.addr===ZERO?amt:0n,spender:f.addr===ZERO?null:ZROUTER}; } async function quoteV4Pools(f,t,amountIn,block){ const pools=v4PoolsFor(f,t); if(!pools.length||amountIn<=0n)return null; const rs=(await mc3Deep(pools.map(p=>({to:V4LENS, data:"0x"+SEL_V4QUOTE+encBool(false)+encAddr(f.addr)+encAddr(t.addr) +encUint(p.fee)+encUint(p.ts)+encAddr(p.hooks)+encUint(amountIn)})),block)) .map(h=>{try{const x=strip0x(h||"");return x.length>=128?BigInt("0x"+x.slice(64,128)):0n}catch{return 0n}}); let best=null; for(let i=0;i<pools.length;i++)if(rs[i]>0n&&(!best||rs[i]>best.amountOut)) best={pool:pools[i],amountIn,amountOut:rs[i]}; return best; } const setListNote=t=>{ const el=document.getElementById("listNote"); if(!el)return; el.textContent=t||""; el.classList.toggle("hide",!t)}; const setListSource=w=>setListNote(w?"Built-in token list — "+w:""); let rankFellBack=false; const rankedIds=async()=>{ rankFellBack=false; try{ const ids=decUintArr(await rpc(C,[{to:ZLISTLENS,data:"0x"+SEL_RANKEDIDS},L])); if(ids.length)return ids; }catch{} try{ const ids=decUintArr(await rpc(C,[{to:TOKENLIST,data:"0x"+SEL_RANKEDIDS},L])); if(ids.length)rankFellBack=true; return ids; }catch{return[]} }; async function loadTokenList(){ if(TOKENLIST===ZERO)return setListSource("registry address not set"); try{ const ids=await rankedIds(); if(!ids.length)return setListSource("registry is empty"); const raw=await mc3Deep(ids.slice(0,64).map(id=>({to:TOKENLIST,data:"0x"+SEL_JSON+encUint(id)}))); const rows=raw.map(h=>{try{return h?decStr(h):null}catch{return null}}); const next=[]; for(const row of rows){ if(!row)continue; let t;try{t=JSON.parse(row)}catch{continue} if(t.k!=="eip155")continue; const std=t.p==="Native"||t.p==="ERC-20"?"ft":t.p==="ERC-721"?"nft":""; if(!std)continue; if(!/^0x[0-9a-f]{40}$/i.test(t.a||""))continue; if(!Number.isInteger(t.d)||t.d<0||t.d>36)continue; if(next.some(x=>x.addr.toLowerCase()===t.a.toLowerCase()))continue; const sym=safeSym(t.s).slice(0,12); const logo=safeUrl(t.l); next.push({sym,addr:t.a.toLowerCase(),dec:t.d,std,icon:logo?`<img src="${logo}" width="20" height="20">`:genIcon(sym), name:String(t.n??"").slice(0,64),desc:String(t.desc??"").slice(0,240), v4:parseV4Pools(t.e,t.a)}); } next.sort((a,b)=>(a.std==="nft")-(b.std==="nft")); if(!next.some(t=>t.std==="ft"))return setListSource("registry returned nothing swappable"); const mine=TOKENS.filter(t=>t.cust&&!next.some(x=>x.addr===t.addr.toLowerCase())); const anchors=BUILTIN.filter(b=>(b.addr===ZERO||b.addr===WETH)&&!next.some(x=>x.addr===b.addr)); TOKENS.splice(0,TOKENS.length,...next,...anchors,...mine); setListNote(rankFellBack?"Ranked by listing order — conviction unavailable":""); }catch(e){setListSource("could not be reached")} } const strip0x=h=>h.startsWith("0x")?h.slice(2):h; const pad32=h=>strip0x(h).padStart(64,"0"); const encUint=v=>pad32(BigInt(v).toString(16)); const encAddr=a=>pad32(a.toLowerCase()); const isAddr=a=>/^0x[0-9a-fA-F]{40}$/.test(a||""); const decStr=hex=>{const h=strip0x(hex);const n=parseInt(h.slice(64,128),16); return new TextDecoder().decode(hexToBytes(h.slice(128,128+n*2)))}; const decUintArr=hex=>{const h=strip0x(hex);const n=parseInt(h.slice(64,128),16);const o=[]; for(let i=0;i<n;i++)o.push(BigInt("0x"+h.slice(128+i*64,192+i*64)));return o}; const encBool=b=>pad32(b?"1":"0"); const toHex=v=>"0x"+BigInt(v).toString(16); const parseUnits=(s,dec)=>{ const t=s.trim().replace(/,/g,""); if(!/^(?:\d+|\d*\.\d+|\d+\.)$/.test(t))throw Error("invalid amount"); const[ip,fp=""]=t.split("."); if(fp.length>dec)throw Error("too many decimals"); return BigInt((ip||"0")+fp.padEnd(dec,"0")); }; const formatUnits=(v,dec)=>{ if(!dec)return BigInt(v).toString(); const s=BigInt(v).toString().padStart(dec+1,"0"); const ip=s.slice(0,-dec); const fp=s.slice(-dec).replace(/0+$/,""); return fp?`${ip}.${fp}`:ip; }; const trimAmt=(v,dec)=>{const s=formatUnits(v,dec),i=s.indexOf(".");if(i<0)return s;const r=s.slice(0,i+7).replace(/\.?0+$/,"");return r==="0"&&v?s:r}; const maxAmt=(v,dec)=>{v=BigInt(v);if(dec<=6)return formatUnits(v,dec); const u=10n**BigInt(dec-6);return formatUnits((v+u-1n)/u,6)}; const fitFont=el=>{ const l=el.value.length,s=l>18?".9em":l>14?"1.1em":l>10?"1.25em":"1.65em"; if(el.style.fontSize!==s)el.style.fontSize=s; }; const decQ=(hex,slip,eo,u,v,S,mv)=>{ const h=strip0x(hex),num=i=>BigInt("0x"+h.slice(i*64,(i+1)*64)),E=()=>{throw Error("bad quote")}; if(h.length<(v+2)*64)E(); const o=Number(num(v))*2;if(o+64>h.length)E(); const n=Number(BigInt("0x"+h.slice(o,o+64)));if(!n||o+64+n*2>h.length)E(); const g=num(u+3),t=g>0n,k=S?(num(3)>0n?0:u):t?u:0,B=10000n,ai=S?num(2)+num(u+2):num(2),ao=S?num(3)+g:t?g:num(3),q=eo?ai:ao; const src=[];for(let w=0;w<=u;w+=4)if(num(w+2)>0n||num(w+3)>0n)src.push(Number(num(w))); return{best:{source:Number(num(k)),feeBps:num(k+1),amountIn:ai,amountOut:ao},sources:src,callData:"0x"+h.slice(o+64,o+64+n*2),amountLimit:eo?(q*(B+slip)+B-1n)/B:q*(B-slip)/B,msgValue:num(mv||v+1),slipBps:slip}; }; const merge=(est,exe)=>{if(!est)return exe;if(!exe)return null;return{...exe,best:est.best}}; const NO_WALLET="No wallet detected."; const rpc=(method,params)=>window.ethereum ?window.ethereum.request({method,params}) :Promise.reject(Object.assign(Error(NO_WALLET),{noWallet:true})); const rpcRead=async(method,params,tries=4)=>{ let last; for(let i=0;i<tries;i++){ try{return await rpc(method,params)} catch(e){last=e;await new Promise(r=>setTimeout(r,300*(i+1)))} } throw last; }; const okRet=h=>{h=strip0x(h);if(h&&BigInt("0x"+h)===0n)throw Error("token returned false")}; const waitTx=async hash=>{ const end=Date.now()+600000; let misses=0,wait=1500; while(Date.now()<end){ try{ const r=await rpc("eth_getTransactionReceipt",[hash]); misses=0; if(r){ if(r.status!=="0x1")throw Error("tx reverted: "+hash); return r; } wait=Math.min(wait+500,5000); }catch(e){ if(/tx reverted/.test(e.message||""))throw e; if(++misses>2)wait=Math.min(wait*2,15000); } await new Promise(res=>setTimeout(res,wait)); } throw Error("tx not mined in time"); }; const checkWallet=async()=>{if(!account)throw Error("Connect a wallet first.");const[c,a]=await Promise.all([rpc(I,[]),rpc("eth_accounts",[])]);if(+c!==1||a[0]?.toLowerCase()!==account.toLowerCase())throw Error("wallet changed")}; const hexToBytes=h=>{const s=strip0x(h);const o=new Uint8Array(s.length/2);for(let i=0;i<o.length;i++)o[i]=parseInt(s.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=n=>{let nd="0x"+"0".repeat(64); if(n)for(const l of n.split(".").reverse())nd=keccak(new Uint8Array([...hexToBytes(nd),...hexToBytes(keccak(new TextEncoder().encode(l)))])); return nd}; const encStr=v=>{const b=new TextEncoder().encode(v);let h=""; for(const x of b)h+=x.toString(16).padStart(2,"0"); return pad32("20")+encUint(b.length)+h.padEnd(Math.ceil(h.length/64)*64,"0")}; const retAddr=h=>{const s=strip0x(h||""); if(s.length!==64||/[^0-9a-fA-F]/.test(s)||/[^0]/.test(s.slice(0,24)))return""; const a="0x"+s.slice(24).toLowerCase(); return /^0x0{40}$/.test(a)?"":a}; const nameOk=n=>/^[\x21-\x7e]+$/.test(n)&&n.split(".").every(l=>l.length>0); const nsFwd=async(reg,n)=>{const id=await rpc(C,[{to:reg,data:"0x"+SEL_CID+encStr(n)},L]); if(strip0x(id||"").length!==64)return""; return retAddr(await rpc(C,[{to:reg,data:"0x"+SEL_RES+strip0x(id)},L]))}; 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 encBytes=h=>{h=strip0x(h);return encUint(h.length/2)+h.padEnd(Math.ceil(h.length/64)*64,"0")}; const encResolve=(dns,inner)=>{const a=encBytes(dns); return pad32("40")+encUint(64+a.length/2)+a+encBytes(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=> rpc(C,[{to:ENSREG,data:"0x"+SEL_RSLV+pad32(nd)},L]).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 s=await rpc(C,[{to:r,data:supports(SEL_ERESOLVE)},L]); return strip0x(s||"").length===64&&BigInt(s)===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 rpc(C,[{to:f.r,data:inner},L])); if(!await ensWild(f.r))return""; try{return retAddr(decBytes(await rpc(C,[{to:f.r, data:"0x"+SEL_ERESOLVE+encResolve(dnsEnc(n),inner)},L])))} 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 rpc(C,[{to:r,data:"0x"+SEL_ENAME+pad32(nd)},L])).trim()}catch{return""}}; const nameFwd=n=>/\.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=>rpc(C,[c,L]).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()===a.toLowerCase()?n:""}; return await agrees(str(w))||await agrees(str(g)) ||await agrees(await ensRevName(nd,retAddr(er)))||""}; const decodeString=hex=>{ const h=strip0x(hex); 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; return new TextDecoder().decode(hexToBytes(h.slice(off+64,off+64+len))); }; const kecStr=v=>keccak(new TextEncoder().encode(v)); const ckAddr=a=>{const s=strip0x(a); if(s===s.toLowerCase()||s===s.toUpperCase())return true; const h=strip0x(kecStr(s.toLowerCase())); for(let i=0;i<40;i++){const c=s[i]; if(c>="a"&&c<="f"&&parseInt(h[i],16)>=8)return false; if(c>="A"&&c<="F"&&parseInt(h[i],16)<8)return false} return true}; const domainSep=(name,ver,tok)=>keccak(hexToBytes( kecStr(EIP712DOM)+pad32(kecStr(name))+pad32(kecStr(ver))+encUint(CHAIN_ID)+encAddr(tok))); const permitInfo=async(tok,owner)=>{ try{ const[ds,th,nc,nm]=await mc3Deep([ {to:tok,data:"0x"+SEL_DS},{to:tok,data:"0x"+SEL_PERMIT_TH}, {to:tok,data:"0x"+SEL_NONCES+encAddr(owner)},{to:tok,data:"0x"+SEL_NAME}]); if(!ds||ds==="0x")return null; if(th&&strip0x(th).length>=64&&strip0x(th).slice(0,64).toLowerCase()!==TH_2612)return null; if(!nc||!nm)return null; const nonce=BigInt(nc); const name=decodeString(nm); for(const ver of ["1","2"])if(domainSep(name,ver,tok).toLowerCase()===ds.toLowerCase()) return{name,ver,nonce}; }catch{} 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=6; 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); } }; const REF_N=100n; 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"; // A quote in flight and no amount at all left the button identical: "Swap", // greyed. Declared here, not in `update`, because `render` runs first. 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=""
0xacfba7ce…4082·#25,751,060·0x199a9105…8116ed
"> <div class="hdr lnhdr"><small>Launch a coin</small><small id="lnSub">one transaction, no ether needed</small></div> <label class="dly">Name<input id="lnName" maxlength="64" placeholder="Zero Cat" autocomplete="off" spellcheck="false"></label> <label class="dly">Symbol<input id="lnSym" maxlength="16" placeholder="ZCAT" autocomplete="off" spellcheck="false"></label> <label class="dly">Supply<input id="lnSupply" inputmode="decimal" value="1,000,000,000" autocomplete="off" spellcheck="false"></label> <div class="lngap">Market</div> <label class="dly" title="The pool holds NO ether at launch - this is its virtual ether reserve, and it is the only thing deciding where on the curve your market opens. It sets the opening price and how hard that price is to move, which are the same knob: roughly this much buying moves the market halfway. Small numbers mean the first buyer takes a huge share of the supply.">Starting market cap<span class="lnu"><input id="lnMcap" inputmode="decimal" value="30" autocomplete="off" spellcheck="false">ETH</span></label> <label class="dly" title="The percentage of total supply paid to the creator at launch. Everything else is seeded into the pool. Capped at 20%.">Creator keeps<span class="lnu"><input id="lnAlloc" inputmode="decimal" value="0" autocomplete="off" spellcheck="false">%</span></label> <div class="lngap">Optional</div> <label class="dly" id="lnArtRow" title="Held on-chain as contract code, not as a link. Nothing to pin and no gateway to go dark. Editable later, so a rough mark now costs nothing.">Logo<span><img id="lnPrev" alt=""><small id="lnArtNote">none</small><button type="button" id="lnPick" class="lnbtn">Choose</button></span></label> <input id="lnArt" type="file" accept="image/png,image/jpeg,image/webp,image/gif,image/svg+xml" class="hide"> </div> <input id="rc" class="rcpt" placeholder="Recipient (optional) — 0x or .eth/.(g)wei" spellcheck="false" autocomplete="off"> <small id="rcvEl"></small> <small id="lnNote" class="hide" role="status" aria-live="polite"></small> <button id="lnGo" class="primary hide">Launch</button> <label class="dly hide" id="kindL">Order type<select id="kind"><option value="fixed">Fixed limit</option><option value="dutch">Dutch decay</option><option value="floor">Climbing bid</option></select></label> <label class="dly hide" id="dlyL">SLOW<select id="dly"><option value="0">Instant</option><option value="3600">1 hour</option><option value="86400">1 day</option><option value="259200">3 days</option><option value="604800">7 days</option></select></label> <label class="dly hide" id="tipL" title="A time-locked transfer does not move on its own: once the lock expires somebody has to send the claim transaction. Tick this and the page posts a small ETH tip alongside the deposit, which pays a keeper to claim it for the recipient the moment it matures. The tip is refundable — if you reverse the transfer, the ether comes back to you.">Auto-claim<span class="tipr"><small id="tipNote"></small><input id="tipCk" type="checkbox" aria-describedby="tipNote"></span></label> <label class="dly hide" id="fillL">Fill<select id="fill"><option value="1">Any amount</option><option value="0">All or nothing</option></select></label> <label class="dly hide" id="floorL">Floor total<input id="floorAmt" inputmode="decimal" placeholder="0.0"></label> <label class="dly hide" id="dRestL" title="A Dutch lot has TWO clocks. It decays to your floor over the window above, and then - unless it is given an end - it sits there fillable forever, until you spend gas cancelling. This is how long it rests at the floor before the escrow simply comes back to you.">Rest at floor<select id="dRest"><option value="0">Forever</option><option value="86400">1 day</option><option value="604800">1 week</option><option value="2592000">30 days</option></select></label> <label class="dly hide" id="nftIdL">Token ID<input id="nftId" inputmode="numeric" placeholder="Any from collection" spellcheck="false" autocomplete="off"></label> <label class="dly hide" id="ethModeL" title="Applies when YOU fill an order from the book, or cancel one of yours and the escrow comes back wrapped. It does NOT change what an order you place pays you: boards are denominated in WETH, and a maker's proceeds are transferred as WETH with no unwrap available - so a limit order asking for ETH is an order asking for WETH, and fills as such. Paying is not a setting either: the page spends whichever of your ETH and WETH covers it, and wraps the difference if only the two together do.">Fills &amp; cancels pay<select id="ethMode"><option value="auto">ETH</option><option value="weth">WETH</option></select></label> <small id="floorNote" class="hide" role="status" aria-live="polite"></small> <button id="swap" class="primary">Connect Wallet</button> <div id="listNote" class="hide" role="status" aria-live="polite"></div> <div id="tkPanel" class="tkp hide"><input id="tkFind" type="text" placeholder="Search name, symbol or address" autocomplete="off" spellcheck="false" aria-label="Filter tokens"><div id="tkList" role="listbox"></div></div> <div id="stat" role="status" aria-live="polite"></div> <button id="chTog" class="chtog hide" aria-expanded="false">Chart <span>▾</span></button> <div id="chBox" class="hide"><div class="chtf" id="chTf"></div><div id="chArt"></div><small id="chNote"></small></div> <div id="pos"></div> <button id="bkTog" class="chtog hide" aria-expanded="true">Orderbook <span>▾</span></button> <div id="book"></div> <div id="docPanel" class="docs hide"> <details><summary>Swapping</summary> <p>Every quote is compared across Uniswap v2, v3 and v4, Curve, Sushi and our own Precision pools, and the best one wins. One route, one signature.</p> <p><b>Slippage</b> is the worst price you will accept &mdash; move past it and the trade reverts rather than filling badly. <b>Price impact</b> is how far your own size moves the price; the page warns as it grows and will not let a very bad one through quietly.</p> <p>ETH and WETH are interchangeable here. The page wraps or unwraps whichever way the pool needs, inside the same transaction.</p></details> <details><summary>Sending</summary> <p>A plain transfer, or a <b>SLOW</b> one: time-locked, and reversible by you at any point until it matures. Useful for large amounts and for addresses you have not paid before.</p> <p>A locked transfer does not deliver itself &mdash; someone must send the claim once it matures. Tick <b>auto-claim</b> and the page posts a small tip so a keeper does that for the recipient. The tip is refundable: reverse the transfer and it comes back.</p></details> <details><summary>Orders</summary> <p><b>Fixed limit</b> &mdash; a price you name. It rests until someone fills it or you cancel.</p> <p><b>Dutch decay</b> &mdash; starts above your floor and falls to it over a window you choose. It has two clocks: the decay, then how long it rests at the floor before the escrow returns to you. Set that to <em>Forever</em> and it waits until you cancel.</p> <p><b>Climbing bid</b> &mdash; the mirror: a bid that rises toward a ceiling you name.</p> <p>Orders are escrowed on chain when placed, so a fill cannot fail for want of funds, and cancelling returns the escrow. Boards are denominated in WETH &mdash; an order asking for ETH is asking for WETH, and pays out as such.</p></details> <details><summary>Liquidity <em>the droplet</em></summary> <p>Precision pools are <b>bands</b>, not endless curves. A position earns fees only while the price is inside its band, and holds one asset or the other at the edges. A wider band is safer and earns less; a narrow one is the opposite.</p> <p>The bar on each row shows where the price sits in that band. <em>Out of range</em> means it is earning nothing right now.</p></details> <details><summary>Launching a coin <em>the coin</em></summary> <p>One transaction, and no ether needed aside from gas. A fixed supply is minted, your allocation is paid to you immediately, and <b>everything else is seeded into a fresh ETH pool</b> &mdash; one-sided, so the pool opens holding only your token.</p> <p><b>Starting market cap</b> is the pool's virtual ether reserve. It sets the opening price and how hard that price is to move, which are the same dial: roughly that much buying moves the market halfway. Small numbers mean the first buyer takes a large share of the supply, which is why the form says what one ether would buy.</p> <p><b>Creator keeps</b> is a share of the token SUPPLY, minted straight to you when the coin launches &mdash; tokens, not ether, and not a fee on trading. Keep 10% of a billion and 100,000,000 land in your wallet at once, unlocked, with nothing vesting.</p> <p>Whatever you keep does not go into the pool, and selling it later pulls ether back out of that pool &mdash; so a modest-looking percentage can be a claim on most of what early buyers put in. Anyone deciding whether to buy your coin can work that out too.</p> <p>A <b>logo</b> is stored as contract code on Ethereum &mdash; no IPFS, no pinning service, nothing to go dark. Upload anything; the page compresses it and shows the size before you sign. You can replace it later.</p></details> <details><summary>Fees</summary> <p>A launched market charges <b>1%</b> per swap. Half of that stays in the pool as reserves, which raises the floor for every holder. The other half is collectable, and splits <b>80% creator, 10% protocol, 10% tithe</b>.</p> <p>The <b>tithe</b> is burned to Ethereum itself. The ether goes to the BETH burner, which destroys it and mints a BETH receipt recording that it happened. Nobody can spend it and it cannot be redeemed &mdash; the ether no longer exists. What it buys is a smaller ether supply, which accrues to everyone holding ether rather than to us.</p> <p>The token side of collected fees is burned outright too &mdash; a launched coin's supply only ever falls.</p></details> <details><summary>What this page is</summary> <p>There is no server. This page is a contract on Ethereum and its bytes cannot be changed by anyone, us included &mdash; a new version is a new address, not an edit. It talks to your wallet and to Ethereum, and to nothing else.</p> <p>Your keys never leave your wallet. Nothing here custodies your funds; every trade settles from your address to a contract you can read.</p> <p>The token list is <b>zList</b>, ranked on chain by conviction staking. Anything not on it can still be traded by pasting its address.</p></details> </div> <footer id="foot"><span id="footV"></span><span id="footAddr"></span><span>&middot; <a id="footDoc" href="#" role="button">how it works</a></span><span>&middot; <a id="footList" target="_blank" rel="noopener" title="Token order here comes from zList, ranked by onchain conviction staking. Stake to move a token up the list - or to get one listed at all.">token list</a></span></footer> </div> <script>const IC0='<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">', ICMOON=IC0+'<path d="M20.5 14.6A8.6 8.6 0 0 1 9.4 3.5a8.6 8.6 0 1 0 11.1 11.1z"/></svg>', ICSUN=IC0+'<circle cx="12" cy="12" r="4.2"/><path d="M12 2v2m0 16v2M4.2 4.2l1.5 1.5m12.6 12.6 1.5 1.5M2 12h2m16 0h2M4.2 19.8l1.5-1.5M18.3 5.7l1.5-1.5"/></svg>'; const ic=()=>{const d=H.className=='d';th.innerHTML=d?ICSUN:ICMOON;tc.content=d?'#101010':'#fafafa'};ic(); const ZSWAP_VERSION="0.1"; const ZSWAP_SELF=""; const ZSWAP_PREVIOUS=""; const ZLIST_URL="https://0x0000006d936ba3653b8854490e16e782cd32a9a8.w4eth.io/"; const selfFromUrl=()=>{ const h=(location.hostname||"").split("."); if(!/^0x[0-9a-fA-F]{40}$/.test(h[0]||""))return ""; if(/^\d+$/.test(h[1]||"")&&h[1]!=="1")return ""; return h[0]; }; const short=a=>a.slice(0,6)+"\u2026"+a.slice(-4); (()=>{ const self=selfFromUrl()||ZSWAP_SELF; footV.textContent=`zSwap v${ZSWAP_VERSION}`; footList.href=ZLIST_URL; if(self){ footAddr.innerHTML=`\u00b7 <a href="https://etherscan.io/address/${self}#code" target="_blank" rel="noopener" ` +`title="This exact build, on chain. Its bytes cannot change; a newer version is a different address.">${short(self)}</a>`; if(ZSWAP_PREVIOUS)footAddr.innerHTML+=` \u00b7 <a href="https://etherscan.io/address/${ZSWAP_PREVIOUS}#code" ` +`target="_blank" rel="noopener" title="The version this one succeeded. It still serves its own page, unchanged.">prev</a>`; } })(); th.onclick=()=>{H.classList.toggle('d');LS.t=H.className=='d'?'d':'l';ic()}; const ZQUOTER="0xc7a03f9ed2be5feea18ce93e12f4f05c98287c16"; const WNS="0x0000000000696760E15f265e828DB644A0c242EB"; const GNS="0x9D51D507BC7264d4fE8Ad1cf7Fe191933A0a81d6"; const ENSREG="0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e"; const ZROUTER="0x000000000000FB114709235f1ccBFfb925F600e4"; const PFACTORY="0x000000Eb27B557aB426d9E99cFd54EC455799e81"; const PLQLENS="0x000000956bf20A41C54BaE4a4b6F5C8A166DAB4E"; const PPLENS="0x000000Bad3a2fa57ed74fa06000573ccddF6B7fB"; const PI_WORDS=19,PI_POOL=0,PI_LIQ=9,PI_HOOK=10,PI_SL=3,PI_SH=4, PI_FEE=5,PI_R0=6,PI_R1=7,PI_PX=8; const CHAIN_ID=1; const ZERO="0x0000000000000000000000000000000000000000"; const WETH="0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"; const TOKENLIST="0x0000006013dF75A31678B786061C2B54bf531524"; const ZLISTLENS="0x000000cEa3AB048d59473F3fb116A8D7F1abd247"; const SEL_RANKEDIDS="df7ca268"; const SEL_JSON="74e18e96"; const SEL_CID="fb021939",SEL_RES="4f896d4f",SEL_REV="9af8b7aa"; const SEL_RSLV="0178b8bf",SEL_EADDR="3b3b57de",SEL_ENAME="691f3431"; const SEL_ERESOLVE="9061b923"; const NAME_OFFCHAIN="That name keeps its records off chain, and this page reads only on-chain state."; const SEL_ALLOWANCE="dd62ed3e"; const SEL_LATEST="52bfe789",SEL_PREV="247dfaa8",SEL_SUCCAT="451aae60"; const MATURITY=259200; const SEL_DS="3644e515",SEL_NONCES="7ecebe00",SEL_NAME="06fdde03",SEL_PERMIT_TH="30adf81f"; const TH_2612="6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9"; const SEL_RPERMIT="7ac2ff7b",SEL_MULTICALL="ac9650d8",SEL_SWEEP="cb019b84"; const PERMIT2="0x000000000022D473030F116dDEE9F6B43aC78BA3"; const SEL_P2TF="09d31579"; const EIP712DOM="EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"; const SEL_APPROVE="095ea7b3"; const SEL_BALANCEOF="70a08231"; const V4PORT="0x000000dfb53Fa7f1c486470034741d5BCBE14BE9"; const V4LENS="0x000000c3aE1692983941495162A4AAB40660E65F"; const SEL_V4QUOTE="d500463c",SEL_V4SWAP="48e6f730"; const V4POOL_KEY="0x95a932c205571d4d1ca72715c642a2eca21dde79ffc28ff11509681f9383385f"; const C="eth_call",S="eth_sendTransaction",L="latest",I="eth_chainId"; const SOURCES=["UniV2","Sushi","zAMM","UniV3","UniV4","Curve","Lido","Wrap","Precision"]; const SRC_PRECISION=8; const ETH_ICON=`<svg width="20" height="20" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" r="16" fill="#627EEA"/><g fill="#FFF" fill-rule="nonzero"><path fill-opacity=".602" d="M16.498 4v8.87l7.497 3.35z"/><path d="M16.498 4L9 16.22l7.498-3.35z"/><path fill-opacity=".602" d="M16.498 21.968v6.027L24 17.616z"/><path d="M16.498 27.995v-6.028L9 17.616z"/><path fill-opacity=".2" d="M16.498 20.573l7.497-4.353-7.497-3.348z"/><path fill-opacity=".602" d="M9 16.22l7.498 4.353v-7.701z"/></g></g></svg>`; const USDC_ICON=`<svg width="20" height="20" viewBox="0 0 32 32"><g fill="none"><circle fill="#2775CA" cx="16" cy="16" r="16"/><g fill="#FFF"><path d="M20.022 18.124c0-2.124-1.28-2.852-3.84-3.156-1.828-.243-2.193-.728-2.193-1.578 0-.85.61-1.396 1.828-1.396 1.097 0 1.707.364 2.011 1.275a.458.458 0 00.427.303h.975a.416.416 0 00.427-.425v-.06a3.04 3.04 0 00-2.743-2.489V9.142c0-.243-.183-.425-.487-.486h-.915c-.243 0-.426.182-.487.486v1.396c-1.829.242-2.986 1.456-2.986 2.974 0 2.002 1.218 2.791 3.778 3.095 1.707.303 2.255.668 2.255 1.639 0 .97-.853 1.638-2.011 1.638-1.585 0-2.133-.667-2.316-1.578-.06-.242-.244-.364-.427-.364h-1.036a.416.416 0 00-.426.425v.06c.243 1.518 1.219 2.61 3.23 2.914v1.457c0 .242.183.425.487.485h.915c.243 0 .426-.182.487-.485V21.34c1.829-.303 3.047-1.578 3.047-3.217z"/><path d="M12.892 24.497c-4.754-1.7-7.192-6.98-5.424-11.653.914-2.55 2.925-4.491 5.424-5.402.244-.121.365-.303.365-.607v-.85c0-.242-.121-.424-.365-.485-.061 0-.183 0-.244.06a10.895 10.895 0 00-7.13 13.717c1.096 3.4 3.717 6.01 7.13 7.102.244.121.488 0 .548-.243.061-.06.061-.122.061-.243v-.85c0-.182-.182-.424-.365-.546zm6.46-18.936c-.244-.122-.488 0-.548.242-.061.061-.061.122-.061.243v.85c0 .243.182.485.365.607 4.754 1.7 7.192 6.98 5.424 11.653-.914 2.55-2.925 4.491-5.424 5.402-.244.121-.365.303-.365.607v.85c0 .242.121.424.365.485.061 0 .183 0 .244-.06a10.895 10.895 0 007.13-13.717c-1.096-3.46-3.778-6.07-7.13-7.162z"/></g></g></svg>`; const USDT_ICON=`<svg width="20" height="20" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" r="16" fill="#26A17B"/><path fill="#FFF" d="M17.922 17.383v-.002c-.11.008-.677.042-1.942.042-1.01 0-1.721-.03-1.971-.042v.003c-3.888-.171-6.79-.848-6.79-1.658 0-.809 2.902-1.486 6.79-1.66v2.644c.254.018.982.061 1.988.061 1.207 0 1.812-.05 1.925-.06v-2.643c3.88.173 6.775.85 6.775 1.658 0 .81-2.895 1.485-6.775 1.657m0-3.59v-2.366h5.414V7.819H8.595v3.608h5.414v2.365c-4.4.202-7.709 1.074-7.709 2.118 0 1.044 3.309 1.915 7.709 2.118v7.582h3.913v-7.584c4.393-.202 7.694-1.073 7.694-2.116 0-1.043-3.301-1.914-7.694-2.117"/></g></svg>`; const WBTC_ICON=`<svg width="20" height="20" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" r="16" fill="#F7931A"/><path fill="#FFF" fill-rule="nonzero" d="M23.189 14.02c.314-2.096-1.283-3.223-3.465-3.975l.708-2.84-1.728-.43-.69 2.765c-.454-.114-.92-.22-1.385-.326l.695-2.783L15.596 6l-.708 2.839c-.376-.086-.746-.17-1.104-.26l.002-.009-2.384-.595-.46 1.846s1.283.294 1.256.312c.7.175.826.638.805 1.006l-.806 3.235c.048.012.11.03.18.057l-.183-.045-1.13 4.532c-.086.212-.303.531-.793.41.018.025-1.256-.313-1.256-.313l-.858 1.978 2.25.561c.418.105.828.215 1.231.318l-.715 2.872 1.727.43.708-2.84c.472.127.93.245 1.378.357l-.706 2.828 1.728.43.715-2.866c2.948.558 5.164.333 6.097-2.333.752-2.146-.037-3.385-1.588-4.192 1.13-.26 1.98-1.003 2.207-2.538zm-3.95 5.538c-.533 2.147-4.148.986-5.32.695l.95-3.805c1.172.293 4.929.872 4.37 3.11zm.535-5.569c-.487 1.953-3.495.96-4.47.717l.86-3.45c.975.243 4.118.696 3.61 2.733z"/></g></svg>`; const WSTETH_ICON=`<svg width="20" height="20" viewBox="0 0 32 32"><g fill="none"><circle fill="#00A3FF" cx="16" cy="16" r="16"/><path d="M9.437 14.864l-.181.275c-2.048 3.097-1.603 7.253 1.034 9.824 1.561 1.521 3.622 2.353 5.683 2.353 0 0 0 0-6.536-12.452z" fill="#FFF"/><path opacity=".6" d="M15.997 18.611l-6.56-3.747c6.56 12.452 6.56 12.452 6.56 12.452 0-2.683 0-5.623 0-8.705z" fill="#FFF"/><path opacity=".6" d="M22.563 14.864l.181.275c2.048 3.097 1.603 7.253-1.034 9.824-1.561 1.521-3.622 2.353-5.683 2.353 0 0 0 0 6.536-12.452z" fill="#FFF"/><path opacity=".2" d="M16.003 18.611l6.56-3.747c-6.56 12.452-6.56 12.452-6.56 12.452 0-2.683 0-5.623 0-8.705z" fill="#FFF"/><path opacity=".2" d="M16.004 10.239v6.459l5.654-3.23-5.654-3.229z" fill="#FFF"/><path opacity=".6" d="M16.005 10.239l-5.655 3.229 5.655 3.23v-6.46z" fill="#FFF"/><path d="M16.005 4.805l-5.655 8.668 5.655-3.233V4.805z" fill="#FFF"/><path opacity=".6" d="M16.004 10.238l5.658 3.23-5.658-8.674v5.444z" fill="#FFF"/></g></svg>`; const RETH_ICON=`<svg width="20" height="20" viewBox="0 0 32 32"><circle cx="16" cy="16" r="16" fill="#ED5A37"/><path d="M16 5.75L9.89 15.85 16 19.45l6.08-3.6z" fill="#FFF"/><path d="M16 20.6l-6.11-3.59L16 25.58l6.08-8.57z" fill="#FFF"/></svg>`; const BOLD_ICON=`<svg width="20" height="20" viewBox="0 0 32 32" fill="none"><circle cx="16" cy="16" r="16" fill="#63D77D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M12.1719 4.56641H8.58203V26.1016H15.7617V25.2422C16.8398 25.793 18.0586 26.1055 19.3555 26.1055C23.7148 26.1055 27.25 22.5703 27.25 18.207C27.25 13.8438 23.7148 10.3086 19.3555 10.3086C18.0586 10.3086 16.8398 10.6211 15.7617 11.1719V4.56641H12.1719ZM15.7617 11.1719C13.207 12.4805 11.457 15.1406 11.457 18.207C11.457 21.2734 13.207 23.9336 15.7617 25.2422V11.1719Z" fill="#1C1D4F"/></svg>`; const TOKENS=[ {sym:"ETH", addr:ZERO, dec:18, icon:ETH_ICON}, {sym:"WETH", addr:WETH, dec:18, icon:ETH_ICON}, {sym:"wstETH",addr:"0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0",dec:18,icon:WSTETH_ICON}, {sym:"rETH", addr:"0xae78736cd615f374d3085123a210448e74fc6393",dec:18,icon:RETH_ICON}, {sym:"WBTC", addr:"0x2260fac5e5542a773aa44fbcfedf7c193bc2c599",dec:8, icon:WBTC_ICON}, {sym:"USDC", addr:"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",dec:6, icon:USDC_ICON}, {sym:"USDT", addr:"0xdac17f958d2ee523a2206206994597c13d831ec7",dec:6, icon:USDT_ICON}, {sym:"BOLD", addr:"0x6440f144b7e50d6a8439336510312d2f54beb01d",dec:18,icon:BOLD_ICON}, ]; const BUILTIN=TOKENS.slice(); function parseV4Pools(extras,tokenAddr){ if(!Array.isArray(extras)||!/^0x[0-9a-f]{40}$/i.test(tokenAddr||""))return[]; const row=extras.find(x=>x&&typeof x.v==="string"&&String(x.k).toLowerCase()===V4POOL_KEY); if(!row)return[]; const out=[]; for(const spec of row.v.split(";")){ const p=spec.trim().split(":"); if(p.length!==5||p[0]!=="v1")continue; const other=p[1]==="0"?ZERO:p[1]; const hooks=p[4]==="0"?ZERO:p[4]; if(!/^0x[0-9a-f]{40}$/i.test(other)||!/^0x[0-9a-f]{40}$/i.test(hooks))continue; if(!/^\d{1,7}$/.test(p[2])||!/^\d{1,7}$/.test(p[3]))continue; const fee=Number(p[2]),ts=Number(p[3]); if(!Number.isInteger(fee)||!Number.isInteger(ts)||ts<1)continue; if(other.toLowerCase()===tokenAddr.toLowerCase())continue; const a=tokenAddr.toLowerCase(),b=other.toLowerCase(); const zeroFirst=BigInt(a)<BigInt(b); out.push({c0:zeroFirst?tokenAddr:other,c1:zeroFirst?other:tokenAddr,fee,ts,hooks}); if(out.length===8)break; } return out; } function v4PoolsFor(f,t){ const fa=f.addr.toLowerCase(),ta=t.addr.toLowerCase(); const seen=new Set(),out=[]; for(const tok of [f,t])for(const p of (tok.v4||[])){ const c0=p.c0.toLowerCase(),c1=p.c1.toLowerCase(); if(!((c0===fa&&c1===ta)||(c0===ta&&c1===fa)))continue; const id=`${c0}:${c1}:${p.fee}:${p.ts}:${p.hooks.toLowerCase()}`; if(seen.has(id))continue; seen.add(id); out.push({...p,zeroForOne:c0===fa}); } return out; } const encPoolKey=p=>encAddr(p.c0)+encAddr(p.c1)+encUint(p.fee)+encUint(p.ts)+encAddr(p.hooks); const SEL_QUOTE_BEST="2adaa389",SEL_EFFFEE="ef66de32"; const PROUTE="0x0000007Be74558A1F8c9045301c6F44C8eD0c9eB"; const SEL_PROUTE="5d6498e1",SEL_PCHECKPOINT="0b7c6c6c"; const SEL_PROUTEWETH="78fd78b0"; const SEL_ZAPIN="c98c2c0b"; const SEL_PREVIEW_ZAP="e7cddab0"; const encPRouteWeth=(pools,tokenOut,amountIn,minOut,to)=> "0x"+SEL_PROUTEWETH+encUint(160)+encAddr(tokenOut)+encUint(amountIn) +encUint(minOut)+encAddr(to)+encUint(pools.length)+pools.map(encAddr).join(""); const encPRoute=(pools,tokenIn,tokenOut,amountIn,minOut,to)=> "0x"+SEL_PROUTE+encUint(192)+encAddr(tokenIn)+encAddr(tokenOut)+encUint(amountIn) +encUint(minOut)+encAddr(to)+encUint(BigInt(pools.length))+pools.map(encAddr).join(""); const SEL_PAIR_COUNT="355da246"; const LQ_SCAN=128; const LQ_SHOW=24; async function quotePrecision(f,t,amountIn,sender,block){ if(PPLENS===ZERO||amountIn<=0n)return null; const nat=a=>a.toLowerCase()===WETH.toLowerCase()?ZERO:a; const vars=[]; const addVar=(a,b)=>{ const [x,y]=a.toLowerCase()<b.toLowerCase()?[a,b]:[b,a]; if(!vars.some(v=>v.c0===x&&v.c1===y&&v.tokenIn===a))vars.push({c0:x,c1:y,tokenIn:a}); }; const forms=a=>a===ZERO?[ZERO,WETH] :a.toLowerCase()===WETH.toLowerCase()?[WETH,ZERO]:[a]; for(const a of forms(f.addr))for(const b of forms(t.addr)){ if(nat(a)===nat(b))continue; addVar(a,b); } const reqs=[]; for(const v of vars)reqs.push( {to:PPLENS,data:"0x"+SEL_QUOTE_BEST+encAddr(v.c0)+encAddr(v.c1) +encAddr(sender)+encAddr(v.tokenIn)+encUint(amountIn)+encUint(0n)+encUint(BigInt(LQ_SCAN))}, {to:PFACTORY,data:"0x"+SEL_PAIR_COUNT+encAddr(v.c0)+encAddr(v.c1)}); const res=await mc3Deep(reqs,block); let pool="",out=0n,n=0,fA=f.addr,won=null; for(let i=0;i<vars.length;i++){ const q=res[i*2],c=res[i*2+1]; if(!q)continue; const h=strip0x(q); if(h.length<128)continue; const p2="0x"+h.slice(24,64),o2=BigInt("0x"+h.slice(64,128)); if(o2<=0n||/^0x0{40}$/i.test(p2))continue; if(o2<=out)continue; pool=p2;out=o2;fA=vars[i].tokenIn;won=vars[i]; try{if(c)n=Number(BigInt(c))}catch{n=0} } if(!pool||!won)return null; const tA=won.c0===fA?won.c1:won.c0; let fee=0n; try{const [fx]=await mc3Deep([{to:PPLENS, data:"0x"+SEL_EFFFEE+encAddr(pool)+encAddr(sender)+encAddr(fA)+encUint(amountIn)}],block); if(fx)fee=BigInt(fx)}catch{} return {pool,out,fee,capped:n>LQ_SCAN,tokenIn:fA,tokenOut
0xacfba7ce…4082·#25,751,058·0x26666b28…b8adc9
<!doctype html> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>zSwap</title> <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 400' width='400' height='400'%3E%3Crect width='400' height='400' fill='%23000'/%3E%3CclipPath id='frame'%3E%3Crect width='400' height='400'/%3E%3C/clipPath%3E%3Cg clip-path='url(%23frame)'%3E%3Cpath d='M-60-20L460-20L460 90L80 310L460 310L460 420L-60 420L-60 310L320 90L-60 90Z' fill='white'/%3E%3C/g%3E%3C/svg%3E" type="image/svg+xml"> <meta name="theme-color" id="tc" content="#fafafa"> <style>:root{color-scheme:light;--b:#fafafa;--c:#fff;--p:#f6f6f6;--f:#000;--m:#6b6b6b;--n:#666;--e:#ddd;--k:#000;--kf:#fff;--kh:#333;--fh:#ebebeb;--e2:#ececec;--w:#c33;--s:0 1px 3px #0000000a,0 8px 24px #0000000f} .d{color-scheme:dark;--b:#101010;--c:#181818;--p:#242424;--f:#eee;--m:#999;--n:#999;--e:#3d3d3d;--k:#fff;--kf:#000;--kh:#ddd;--fh:#303030;--e2:#2c2c2c;--w:#e57373;--s:0 1px 2px #0000004d,0 12px 32px #00000052} :focus-visible{outline:2px solid var(--f);outline-offset:2px} @media(prefers-reduced-motion:reduce){*{transition:none!important}.flip:hover{transform:none}} body{background:var(--b);color:var(--f);font-family:system-ui,sans-serif;min-height:100dvh;margin:0;padding:1em .85em;display:flex;flex-direction:column;box-sizing:border-box} .card{border:1px solid var(--e);border-radius:.5em;padding:.9em;background:var(--c);max-width:22em;width:100%;margin:auto;box-shadow:var(--s);box-sizing:border-box} #addr{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} #addr.on{cursor:pointer} #addr.on:hover{color:var(--f);text-decoration:line-through} #logo{position:fixed;top:.85em;left:.85em;width:1.75em;height:1.75em; pointer-events:none;z-index:1} #logo rect{fill:#000} #logo path{fill:#fff} @media(max-width:32em){#logo{display:none}} .meta{display:grid;grid-template-columns:minmax(0,1fr) auto auto auto auto auto;align-items:center;gap:.4em;font-size:.85em;color:var(--n);margin-bottom:.75em} #slipL{display:inline-flex;align-items:baseline;gap:.15em;font-size:.72em;color:var(--m); letter-spacing:.04em;white-space:nowrap;margin-right:.15em} .meta input{width:3em;font-size:.9em;padding:.15em .1em;border:0;border-bottom:1px solid var(--e2);transition:border-color .15s;border-radius:0;background:transparent;text-align:right;outline:0;color:var(--f);-moz-appearance:textfield} .meta input::-webkit-outer-spin-button,.meta input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0} #slip{font-size:1.2em;width:2.4em;font-weight:600;font-variant-numeric:tabular-nums} #th,#lk,#lq,#ln{display:inline-flex;align-items:center;justify-content:center;border:0;border-radius:.35em;background:transparent;cursor:pointer;color:var(--m);opacity:.8;width:1.8em;height:1.8em;line-height:1;transition:color .15s,opacity .15s,background .15s} #th:hover,#lk:hover,#lq:hover,#ln:hover,#lq.on,#ln.on{opacity:1} #th:hover,#lk:hover,#lq:hover,#ln:hover{background:var(--p);color:var(--f)} .meta input:hover{border-bottom-color:var(--m)} .meta input:focus{border-bottom-color:currentcolor} .panel{background:var(--p);border:1px solid transparent;border-radius:.5em;padding:.82em .85em;margin:.32em 0;transition:border-color .15s} .panel:hover,.dly:hover,.rcpt:hover,.lqin:hover{border-color:var(--e)} .panel:focus-within,.dly:focus-within,.rcpt:focus-within,.lqin:focus-within{border-color:var(--f)} .panel small{color:var(--m);font-size:.8em} .hdr small{text-transform:uppercase;letter-spacing:.08em;font-size:.7em} .row{display:flex;gap:.4em;align-items:center;margin-top:.35em} .panel input{font-size:1.65em;font-weight:500;letter-spacing:-.02em;border:0;background:transparent;color:var(--f);outline:none;padding:0;flex:1;min-width:0;width:100%;font-variant-numeric:tabular-nums;transition:font-size .1s} .panel input::placeholder{color:var(--m);opacity:.32} .pill{display:inline-flex;align-items:center;gap:.4em;cursor:pointer;transition:opacity .15s} .pill:hover,.pill:focus-within{opacity:.6} .panel select,.dly select{appearance:none;-webkit-appearance:none;border:0;background:transparent url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5' viewBox='0 0 8 5'%3E%3Cpath d='M0 0h8L4 5z' fill='%23999'/%3E%3C/svg%3E") no-repeat right 0 center;padding:0 .85em 0 0;max-width:8em;font-size:.9em;font-weight:600;color:var(--f);letter-spacing:.02em;cursor:pointer;text-overflow:ellipsis} .panel select:focus{outline:0} svg{vertical-align:middle;flex-shrink:0} .flip{display:flex;align-items:center;justify-content:center;margin:-.62em auto;width:2em;height:2em;border-radius:50%;border:3px solid var(--c);background:var(--c);cursor:pointer;color:var(--m);padding:0;box-shadow:0 1px 2px #0000000f;transition:background .25s,color .25s,transform .25s} .flip:hover{background:var(--fh);color:var(--f);transform:rotate(180deg)} .primary{width:100%;box-sizing:border-box;border:1px solid transparent;min-height:3.1em;padding:.9em;overflow-wrap:anywhere;line-height:1.3;font-size:1em;border-radius:.5em;background:var(--k);color:var(--kf);cursor:pointer;margin-top:.75em;font-weight:600;text-transform:uppercase;letter-spacing:.08em;transition:background .15s,border-color .15s} .primary:hover:not(:disabled){background:var(--kh)} .primary:active:not(:disabled),.lqbtn:active:not(:disabled),.o button:active:not(:disabled),.p button:active:not(:disabled),.tabs button:active,.chtog:active,.bkc:active,.lnbtn:active{transform:translateY(1px)} .primary:disabled{background:var(--p);border-color:transparent;color:var(--m);cursor:not-allowed} .rcpt{width:100%;box-sizing:border-box;margin-top:.6em;padding:.66em .85em;border:1px solid transparent;border-radius:.5em;background:var(--p);transition:border-color .15s;color:var(--f);font-size:.75em;outline:0;font-family:inherit} #lq.on,#ln.on{background:var(--k);color:var(--kf)} .lqrow{padding:.62em .7em;background:var(--p);border:1px solid transparent;border-radius:.5em;margin:.35em 0} .lqrow.out{opacity:.55} .lqband{display:flex;align-items:center;gap:.4em;font-size:.82em} .lqfee{color:var(--m);font-size:.72em} .lqtag{font-size:.62em;padding:.1em .4em;border-radius:.35em;border:1px solid var(--e);color:var(--m)} .lqmeta{font-size:.7em;color:var(--m);margin-top:.25em;display:flex;align-items:center;gap:.4em} .lqmeta>.lqbtn,.lqmine>.lqbtn{margin-left:auto;min-width:7em} .lqmine{font-size:.7em;margin-top:.25em;display:flex;align-items:center;gap:.4em} .lqempty{padding:.9em .7em;font-size:.75em;color:var(--m);text-align:center} .lqnewtog{width:100%;margin-top:.55em} .lqnew{margin-top:.5em;display:flex;flex-direction:column;gap:.35em;text-align:left} .lqnew .dly{margin-top:.4em;gap:.6em} .lqnew .dly>select{flex:1;min-width:0;max-width:none;text-align:right;text-align-last:right} .lqnew .dly .r{display:flex;gap:.4em;align-items:center} .lqnew .dly .r input{width:6.5em} .lqout{font-size:.72em;color:var(--m);line-height:1.55;min-height:1.1em} .lqbar{position:relative;height:3px;border-radius:2px;background:var(--e);margin:.45em 0 .3em} .lqbar>i{position:absolute;top:-2.5px;width:3px;height:8px;border-radius:1px;background:var(--f); left:var(--at,50%);transform:translateX(-1.5px)} .lqbar.out>i{background:var(--m)} .lqbar>b{position:absolute;top:0;height:3px;border-radius:2px;background:var(--m);opacity:.45; left:0;right:0} .lqnew .primary{margin-top:.5em;min-height:2.6em;font-size:.85em} #listNote{max-width:22em;margin:.4em auto 0;font-size:11px;opacity:.75;text-align:center} .lqbtn{font-size:.9em;padding:.3em .7em;border-radius:.5em;cursor:pointer;border:1px solid var(--e);background:transparent;color:var(--f);font-family:inherit;font-weight:600; white-space:nowrap;flex:0 0 auto;text-align:center;transition:background .15s,color .15s,border-color .15s} .lqbtn:hover:not(:disabled),.lqbtn:focus-visible:not(:disabled){background:var(--k);color:var(--kf);border-color:var(--k)} .lqbtn:disabled{opacity:.4;cursor:not-allowed} .lqadd{margin-top:.45em;display:flex;flex-direction:column;gap:.35em} .lqin{width:100%;box-sizing:border-box;padding:.5em .65em;font:inherit;font-size:.75em;border:1px solid transparent;border-radius:.5em;background:var(--p);transition:border-color .15s;color:var(--f);outline:0} .lqzap{display:flex;align-items:center;gap:.45em;font-size:.7em;color:var(--m);cursor:pointer;margin-bottom:.1em} .lqzap input{margin:0;cursor:pointer;accent-color:var(--f)} .lqpv{font-size:.7em;opacity:.7;min-height:1.2em;line-height:1.5} .lqadd .lqbtn{margin-left:0;align-self:stretch;font-size:.75em;padding:.5em .8em} .rcpt.bad{border-color:var(--w)} #rcvEl{display:block;font-size:.7em;color:var(--m);margin-top:.35em;word-break:break-all;text-align:center} #rcvEl:empty{display:none} #stat{font-size:.8em;color:var(--n);word-break:break-all;margin-top:.75em;text-align:center} #stat:not(:empty){margin-bottom:.5em} #stat a{color:var(--f);text-decoration:underline} .hdr{display:flex;justify-content:space-between;align-items:center;gap:.5em} .tabs{display:flex;gap:.18em;margin-bottom:.75em;background:var(--p);padding:.18em;border-radius:.5em} .tabs button{flex:1;padding:.5em;font-size:.75em;font-weight:600;text-transform:uppercase;letter-spacing:.08em;border:0;border-radius:.35em;background:transparent;color:var(--m);cursor:pointer;transition:background .15s,color .15s} .tabs button.on{background:var(--k);color:var(--kf)} .tabs button:hover:not(.on),.tabs button:focus-visible:not(.on){background:var(--fh);color:var(--f)} .tabs button.on:hover,.tabs button.on:focus-visible{background:var(--kh)} .hide{display:none!important} #book{margin-top:.4em;max-height:17em;overflow-y:auto;overscroll-behavior:contain} #book:empty{display:none} #book h6{font-size:.7em;text-transform:uppercase;letter-spacing:.08em;color:var(--m);margin:.6em 0 .3em;font-weight:600;display:flex;justify-content:space-between} .o{display:flex;justify-content:space-between;align-items:center;flex-wrap:nowrap;gap:.5em;padding:.5em .7em;background:var(--p);border-radius:.5em;margin-bottom:.3em;font-size:.75em} .o .l{display:flex;align-items:center;gap:.45em;min-width:0;flex:1 1 auto} .o .l>span{min-width:0;flex:1 1 auto} .o svg{flex-shrink:0} .o svg+svg{margin-left:-.5em} .o b{font-weight:600;font-variant-numeric:tabular-nums;display:block} .o i{font-style:normal;color:var(--m);display:block;font-size:.9em;overflow-wrap:break-word} .o i a{word-break:break-all} .o a{color:var(--m)} .o button{border:1px solid var(--e);border-radius:.5em;padding:.35em .7em;font-size:.95em;font-weight:600;background:transparent;color:var(--f);cursor:pointer;flex:0 0 auto;transition:background .15s,color .15s,border-color .15s} .o button:hover:not(:disabled),.o button:focus-visible:not(:disabled){background:var(--k);color:var(--kf);border-color:var(--k)} #foot{display:flex;justify-content:center;gap:.5em;flex-wrap:wrap;margin-top:1.1em;font-size:.68em;color:var(--m);letter-spacing:.03em} #foot span:empty{display:none} #foot a{color:var(--m);text-decoration:none;border-bottom:1px solid var(--e)} #foot a:hover{color:var(--f)} .o button:disabled{background:transparent;border-color:transparent;color:var(--m);cursor:default;font-weight:400} .bkf{display:flex;flex-wrap:wrap;gap:.3em;margin:.35em 0 .45em} .bkc{font-size:.68em;padding:.35em .6em;border-radius:.35em;border:0; background:transparent;color:var(--m);cursor:pointer;font-family:inherit;font-weight:600; white-space:nowrap;transition:background .15s,color .15s} .bkc:hover:not(.on),.bkc:focus-visible:not(.on){color:var(--f);background:var(--fh)} .bkc.on{background:var(--k);color:var(--kf)} .bkc.on:hover{background:var(--kh)} .ow{margin-bottom:.3em} .ow .o{margin-bottom:0} .o.ins{cursor:pointer} .o.ins:hover{background:var(--fh)} .ic:empty{display:none} .ic{padding:.5em .7em;background:var(--p);border:0;border-top:1px solid var(--e2);border-radius:0 0 .5em .5em;font-size:.72em;color:var(--n)} .ic img{display:block;width:100%;max-width:15em;margin:.5em auto 0;border-radius:.35em} .ic dl{display:grid;grid-template-columns:auto 1fr;gap:.15em .6em;margin:0} .ic dt{color:var(--m)} .ic dd{margin:0;color:var(--f);font-variant-numeric:tabular-nums;text-align:right} .tg{display:inline-block;font-size:.85em;padding:.05em .35em;border-radius:.35em;background:var(--e2);color:var(--n);margin-left:.3em} .tg.w{background:transparent;border:0;color:var(--w);font-weight:600; text-transform:uppercase;font-size:.62em;letter-spacing:.1em;padding:0; vertical-align:.1em;line-height:1;text-decoration:none;margin-left:.5em} .o a.tg.w{color:var(--w)} .dly{display:flex;justify-content:space-between;align-items:center;gap:.6em;font-size:.75em;color:var(--n);margin-top:.55em;padding:.55em .8em;background:var(--p);border:1px solid transparent;border-radius:.5em;transition:border-color .15s} .dly select{font-size:1em} #lnArtNote{color:var(--n);font-variant-numeric:tabular-nums} #lnArtNote.set{color:var(--f)} .lnbtn{font:inherit;font-size:1em;color:var(--f);background:transparent;border:1px solid var(--e);border-radius:.35em;padding:.2em .6em;cursor:pointer} .lnbtn:hover{background:var(--e)} #lnArtRow{gap:.6em} #lnArtRow>span{display:flex;align-items:center;gap:.6em} #lnPrev{width:1.6em;height:1.6em;border-radius:.35em;object-fit:cover;display:none} #lnPrev.set{display:block} .lnon #chTog,.lnon #chBox,.lnon #bkTog,.lnon #book,.lnon #pos{display:none!important} /* DOCS. The page already explains itself - 21 `title` tooltips, ~2.7 KB of it - and every word is unreachable on a phone, because there is no hover. This is the same explanation in a place a thumb can get to. Native <details> so it costs no script and collapses to a scannable list of headings. */ .docs{margin-top:.5em} .docs details{border-top:1px solid var(--e)} .docs details:last-child{border-bottom:1px solid var(--e)} .docs summary{cursor:pointer;list-style:none;padding:.7em .2em;font-size:.78em;font-weight:600; letter-spacing:.02em;color:var(--f);display:flex;justify-content:space-between;align-items:center} .docs summary::-webkit-details-marker{display:none} .docs summary::after{content:"+";color:var(--m);font-weight:400} .docs details[open] summary::after{content:"\2013"} .docs p{margin:0 0 .7em;padding:0 .2em;font-size:.72em;line-height:1.6;color:var(--n)} .docs b{color:var(--f);font-weight:600} .docs em{font-style:normal;color:var(--m)} .lngap{font-size:.6em;text-transform:uppercase;letter-spacing:.1em;color:var(--m);margin:.9em .2em .1em} .lnhdr{margin-bottom:.2em} #lnSub{color:var(--m);text-transform:none;letter-spacing:0} #lnNote,#floorNote{display:block;font-size:.7em;line-height:1.45;color:var(--m);text-align:center;text-wrap:balance;margin:.55em .2em 0} #lnNote:empty,#floorNote:empty{display:none} .lnu{display:flex;align-items:baseline;gap:.3em;color:var(--m);flex:0 1 auto;min-width:0} .lnu input{width:7em;flex:0 1 7em;min-width:0} .tipr{display:flex;align-items:center;gap:.5em} .tipr small{color:var(--m);font-size:.95em} .dly input{width:9em;flex:0 1 9em;min-width:0;border:0;border-bottom:1px solid var(--e2);transition:border-color .15s;background:transparent;color:var(--f);text-align:right;outline:0;font:inherit;font-variant-numeric:tabular-nums} .dly input[type=checkbox]{width:auto;border:0;cursor:pointer;accent-color:var(--f)} .dly input:hover{border-bottom-color:var(--e)} .dly input:focus{border-bottom-color:var(--f)} #pos{margin-top:.4em} #pos:empty{display:none} .ph{margin:.9em 0 .45em;font-size:.9em;color:var(--f);font-weight:700} .p{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:.5em;padding:.55em .7em;border:0;background:var(--p);border-radius:.5em;margin-bottom:.35em;font-size:.75em} .p span{flex:1;min-width:8em} .p b{display:block;font-weight:600;font-variant-numeric:tabular-nums;word-break:break-word} .p i{font-style:normal;color:var(--m);display:block;font-size:.9em} .p button{border:0;border-radius:.35em;padding:.4em .7em;font-size:.95em;font-weight:600;background:var(--k);color:var(--kf);cursor:pointer;margin-left:auto} .p button:hover:not(:disabled),.p button:focus-visible:not(:disabled){background:var(--kh)} .p button:disabled{background:transparent;color:var(--m);cursor:default;font-weight:400} #rate{text-wrap:balance} #bal,#bal1,#rate{display:block;font-size:.75em;line-height:1.35;margin-top:.5em;text-align:right} #bal:empty,#bal1:empty,#rate:empty{display:none} #bal a,#bal1 a{color:var(--f);cursor:pointer;text-decoration:underline;font-weight:600} .meta input.warn{color:var(--w);border-bottom-color:var(--w)} #th:focus-visible,#lk:focus-visible,#lq:focus-visible,#ln:focus-visible{background:var(--p);color:var(--f)} .chtog{width:100%;margin-top:.5em;padding:.5em;border:0;border-radius:.5em;background:transparent;color:var(--m);cursor:pointer;font:inherit;font-size:.7em;font-weight:600;text-transform:uppercase;letter-spacing:.08em;transition:color .15s,background .15s} .chtog:hover{color:var(--f);background:var(--p)} .chtog span{display:inline-block;transition:transform .2s} .chtog[aria-expanded=true] span{transform:rotate(180deg)} #chBox{margin-top:.4em} .chtf{display:flex;gap:.18em;background:var(--p);padding:.18em;border-radius:.35em;margin-bottom:.4em} .chtf button{flex:1;padding:.35em;font:inherit;font-size:.65em;font-weight:600;text-transform:uppercase;letter-spacing:.06em;border:0;border-radius:.35em;background:transparent;color:var(--m);cursor:pointer} .chtf button.on{background:var(--k);color:var(--kf)} .chtf button:disabled{opacity:.35;cursor:default} .chtf .chk{flex:0 0 auto;padding:.35em .6em;border-left:1px solid var(--e);border-radius:0 .35em .35em 0;margin-left:.15em} .chtf .chk:hover{color:var(--f)} #chArt{touch-action:pan-y;position:relative;border:1px solid transparent;border-radius:.5em;overflow:hidden;background:var(--p)} #chArt svg{display:block;width:100%;height:auto} #chArt .hd{position:absolute;top:.4em;left:.55em;font-size:.7em;color:var(--m);font-variant-numeric:tabular-nums;pointer-events:none} #chArt .hd b{color:var(--f);font-size:1.25em;font-weight:600} #chArt .hd .ohlc{display:block;font-size:.9em;opacity:.85} #chArt .msg{padding:2.2em .8em;text-align:center;font-size:.72em;color:var(--m);line-height:1.6} #chNote{display:block;font-size:.65em;color:var(--m);margin-top:.35em;text-align:right;font-variant-numeric:tabular-nums} .panel select.rich{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none} .tkb{display:none;align-items:center;gap:.25em;border:0;background:transparent;padding:0; font:inherit;font-size:.9em;font-weight:600;letter-spacing:.02em;color:var(--f);cursor:pointer} .tkb.on{display:inline-flex} .tkbc{font-size:.7em;color:var(--m)} .tkp{position:absolute;z-index:40;width:min(20em,calc(100vw - 2em));max-height:min(32em,calc(100vh - 5em)); display:flex;flex-direction:column;background:var(--c);border:1px solid var(--e); border-radius:.5em;box-shadow:0 4px 12px #0000001f,0 16px 40px #00000024;overflow:hidden} #tkFind{border:0;border-bottom:1px solid var(--e);background:transparent;color:var(--f); font:inherit;font-size:.8em;padding:.7em .8em;outline:0;flex:none} #tkList{overflow-y:auto;overscroll-behavior:contain} .tkg{padding:.55em .8em .25em;font-size:.62em;letter-spacing:.09em;text-transform:uppercase;color:var(--m)} .tkr{display:flex;align-items:flex-start;gap:.55em;width:100%;box-sizing:border-box; padding:.5em .8em;border:0;background:transparent;color:var(--f);font:inherit; text-align:left;cursor:pointer} .tkr:hover,.tkr.cur{background:var(--fh)} .tkr[aria-disabled="true"]{opacity:.5;cursor:default} .tkr[aria-disabled="true"]:hover{background:transparent} .tkr img,.tkr svg{flex:none;border-radius:50%;margin-top:.1em} .tki{flex:none;width:20px;height:20px;display:flex;align-items:center;justify-content:center} .tkt{min-width:0;flex:1} .tks{display:flex;align-items:baseline;gap:.45em} .tks b{font-size:.85em;font-weight:600} .tkn{font-size:.7em;color:var(--m);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .tkd{font-size:.66em;color:var(--m);line-height:1.45;margin-top:.15em; display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden} .tkmark{flex:none;font-size:.75em;color:var(--m)} .tkempty{padding:1.4em .8em;text-align:center;font-size:.72em;color:var(--m)} @media(pointer:coarse){#th,#lk,#lq,#ln{width:2.4em;height:2.4em} body:has(.tkp:not(.hide))::before{content:"";position:fixed;inset:0;z-index:39; pointer-events:none;background:#0000008c} .tkp{position:fixed;left:0;right:0;bottom:0;top:auto;width:100%;max-width:none; max-height:82vh;border-radius:.75em .75em 0 0;box-shadow:0 -6px 32px #00000047; padding-bottom:env(safe-area-inset-bottom,0)} .tkp::before{content:"";flex:none;width:2.2em;height:.25em;border-radius:.2em; background:var(--e);margin:.5em auto .1em} .tkd{-webkit-line-clamp:1} .tkr{padding:.75em .9em} #tkFind{padding:.85em .9em;font-size:16px}.rcpt,.meta input,.dly input,.lqin{font-size:16px}.lnbtn{padding:.4em .8em}.p button,.tabs button,.o button,.lqbtn{padding:.6em .8em}.chtf button{padding:.5em}}</style> <script>const H=document.documentElement;let LS;try{LS=localStorage}catch{LS={}}if(LS.t?LS.t=='d':matchMedia('(prefers-color-scheme:dark)').matches)H.className='d'</script> <svg id="logo" viewBox="0 0 400 400" aria-hidden="true"><clipPath id="lgf"><rect width="400" height="400"/></clipPath><g clip-path="url(#lgf)"><rect width="400" height="400"/><path d="M-60-20L460-20L460 90L80 310L460 310L460 420L-60 420L-60 310L320 90L-60 90Z"/></g></svg> <div class="card"> <div class="meta"><span id="addr">Not connected</span><label id="slipL">Slippage <input id="slip" type="number" value="0.5" min="0.01" max="10" step="0.1">%</label><button id="lq" title="Liquidity for this pair" aria-label="Toggle liquidity mode" aria-pressed="false"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3.2c3.6 4 6 7.2 6 10.1a6 6 0 0 1-12 0c0-2.9 2.4-6.1 6-10.1z"/></svg></button><button id="ln" title="Launch a coin" aria-label="Toggle launch mode" aria-pressed="false"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="3.5"/></svg></button><button id="lk" title="Copy shareable link" aria-label="Copy shareable link"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2.5"/><path d="M5.5 15H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v.5"/></svg></button><button id="th" title="Toggle theme" aria-label="Toggle theme"></button></div> <div class="tabs" role="tablist"><button id="tabSwap" class="on" role="tab" aria-selected="true">Swap</button><button id="tabSend" role="tab" aria-selected="false">Send</button><button id="tabBook" role="tab" aria-selected="false">Orders</button></div> <div class="panel"><div class="hdr"><small id="payL">You pay</small><span class="pill"><span id="fromIcon"></span><select id="fromSel"></select><button type="button" class="tkb" id="fromPick" aria-haspopup="listbox" aria-expanded="false"><span class="tkbs"></span><span class="tkbc">▾</span></button></span></div><div class="row" id="amtRow"><input id="amt" type="text" inputmode="decimal" placeholder="0.0" aria-label="Amount to pay" autocomplete="off" spellcheck="false"></div><small id="bal"></small></div> <button id="flip" class="flip" title="Flip tokens" aria-label="Flip tokens"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14m-5-5 5 5 5-5"/></svg></button> <div class="panel" id="rcvPanel"><div class="hdr"><small id="rcvHdr">You receive</small><span class="pill"><span id="toIcon"></span><select id="toSel"></select><button type="button" class="tkb" id="toPick" aria-haspopup="listbox" aria-expanded="false"><span class="tkbs"></span><span class="tkbc">▾</span></button></span></div><div class="row" id="outRow"><input id="outAmt" type="text" inputmode="decimal" placeholder="0.0" aria-label="Amount to receive" autocomplete="off" spellcheck="false"></div><small id="bal1" class="hide"></small><small id="rate"></small></div> <div id="lqPanel" class="hide"><div class="hdr"><small>Liquidity</small><small id="lqSub"></small></div><div id="lqList"></div></div> <div id="lnPanel" class="hide
{\u00000123456789abcdefbtn{\u00000123456789abcdefbtn{\u00000123456789abcdefbtndata:application/json;base64,ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef`ghijklmnopqrstuvwxyz0123456789-_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef`ghijklmnopqrstuvwxyz0123456789-_
{\u00000123456789abcdefbtn{\u00000123456789abcdefbtn{\u00000123456789abcdefbtndata:application/json;base64,ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef`ghijklmnopqrstuvwxyz0123456789-_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef`ghijklmnopqrstuvwxyz0123456789-_
1-50 of 76