memoscan
← all memos

Memo 0xfc5845ef…cdfbb8 on Ethereum

0xacfba7ce…4082·#25,751,068·view on Etherscan
!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"