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"