0x68575b07…67c7·#25,913,252·view on Etherscan
en(wasOpen);
syncBook(nOwn+nRest,rows.length);
book.scrollTop=wasAt;
}
const pxOf=(r,a,bg)=>{
const v=unitPx(r);
return v?`<span class="px">${sigAmt(v,18,4)} ${bg.sym}/${a.sym}</span> \u00b7 `:"";
};
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;capObj(cardCache,CARD_MAX);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">`:"");
}
let inspectSeq=0;
async function inspect(el,k){
const box=el.parentNode.querySelector(".ic");
if(!box)return;
if(box.innerHTML){box.innerHTML="";delete box.dataset.job;el.setAttribute("aria-expanded","false");return}
el.setAttribute("aria-expanded","true");
const at=k.lastIndexOf(":"),board=k.slice(0,at),id=BigInt(k.slice(at+1));
const job=String(++inspectSeq);box.dataset.job=job;
box.textContent="Reading the receipt\u2026";
let html;
try{html=cardHtml(await loadCard(board,id))||"No card for this order."}
catch{html=""}
if(box.dataset.job!==job)return;
if(html)box.innerHTML=html;else 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 type="button" class="bkc${on?" on":""}" `
+`aria-pressed="${on?"true":"false"}" data-${attr}="${val}"${title?` title="${title}"`:""}>${label}</button>`;
const note=[hidden?hidden+" hidden":"",bookTrunc?"scan capped":""].filter(Boolean).join(" \u00b7 ");
return `<div class="bkh"><h6><span>Orderbook</span><span title="${bookTrunc?"More orders exist than this scan returned \u2014 the newest are shown first.":""}">${note}</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></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("keydown",e=>{
if(e.key!=="Enter"&&e.key!==" ")return;
const row=e.target.closest(".o.ins");
if(!row||e.target!==row)return;
e.preventDefault();inspect(row,row.dataset.k);
});
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;LS.bkt=bookType;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 sendTx([tx]));
loadBook();refreshBalance();return;
}
if(b.dataset.x==="w"){
const refund=row.tA;
const native=refund.toLowerCase()===WETH.toLowerCase()&&wantsUnwrap();
const dec=row.dutch?(row.dA??18):(row.dB??18),sym=row.dutch?(row.sA||"tokens"):(row.sB||"tokens");
const held=row.dutch?row.aA:row.aB;
const answer=prompt(`Shrink this ${row.dutch?"lot":"bid"}.\n\nIt is escrowed for ${trimAmt(held,dec)} ${sym}.\n\nEnter the amount to stop ${row.dutch?"selling":"bidding for"} — the escrow behind it returns${native?" as ETH":""}. The rest stays live:`,trimAmt(held,dec));
if(answer===null){b.disabled=false;return}
let asked;
try{asked=parseUnits(String(answer).trim(),dec)}catch{throw Error("that is not an amount")}
if(!asked)throw Error("enter an amount above zero");
if(asked>=held)throw Error(`that is the whole ${row.dutch?"lot":"bid"} — use Cancel instead`);
const tx={from:account,to,data:"0x"+SEL_WITHDRAW+encUint(id)+encUint(asked)+encBool(native),value:"0x0"};
stat.textContent=`Shrinking — the escrow behind ${trimAmt(asked,dec)} ${sym} returns${native?" as ETH":""}...`;
await rpc(C,[tx,L]);
await settle(await sendTx([tx]));
loadBook();refreshBalance();return;
}
if(b.dataset.x==="r"){
const sym=row.sB||"tokens",dec=row.dB??18;
const answer=prompt(`Reprice this order without losing its place on the book.\n\nIt sells ${trimAmt(row.aA,row.dA??18)} ${row.sA||"tokens"} for ${trimAmt(row.aB,dec)} ${sym}.\n\nEnter the NEW total ${sym} you want:`,trimAmt(row.aB,dec));
if(answer===null){b.disabled=false;return}
let asked;
try{asked=parseUnits(String(answer).trim(),dec)}catch{throw Error("that is not an amount")}
if(!asked)throw Error("enter an amount above zero");
if(asked===row.aB)throw Error("that is the same price");
const tx={from:account,to,data:"0x"+SEL_REPLACE+encUint(id)+encUint(row.aA)+encUint(asked)+encUint(row.exp||0n),value:"0x0"};
stat.textContent="Repricing...";
await rpc(C,[tx,L]);
await settle(await sendTx([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 sendTx([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 sendTx([{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 sendTx([{from:account,...approvals[i]}]));
}
await rpc(C,[tx,L]);
await settle(await sendTx([tx]));
if(revoke)try{
stat.textContent="Filled · revoking quote allowance...";
await waitTx(await sendTx([{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},ok=0;
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};ok=1}
}catch{}
if(ok){metaCache.set(a,m);capMap(metaCache,META_MAX)}
return m;
}
async function warmMeta(addrs){
const want=[...new Set(addrs.map(a=>(a||"").toLowerCase()))]
.filter(a=>/^0x[0-9a-f]{40}$/.test(a)&&a!==ZERO&&!known(a)&&!metaCache.has(a));
if(!want.length)return;
const res=await mc3Deep(want.flatMap(a=>[{to:a,data:"0x95d89b41"},{to:a,data:"0x313ce567"}])).catch(()=>[]);
want.forEach((a,i)=>{
const sr=res[i*2],dr=res[i*2+1];
if(!sr||!dr)return;
let d;try{d=Number(BigInt(dr))}catch{return}
if(!Number.isInteger(d)||d<0||d>36)return;
let sym="";try{sym=decodeString(sr).trim().replace(/[<>&"'`]/g,"").slice(0,10)}catch{}
metaCache.set(a,{sym:sym||a.slice(0,6)+"…",dec:d});
});
capMap(metaCache,META_MAX);
}
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=slowGuard?"Unclaimed past grace · unlock credits SLOW; withdraw needs guardian approval":"Unclaimed past grace · recoverable";
else if(ripe)note=io?(slowGuard?"Matured · unlock credits SLOW; withdraw needs guardian approval":"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;Promise.resolve(loadPos()).finally(()=>{if(m.isConnected)m.disabled=false});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 sendTx([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 sendTx([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=/^unclear comma/.test(e.message)?e.message:/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=isWns(TOKENS[fromSel.value]?.addr)||isWns(TOKENS[toSel.value]?.addr)
?(isNft(fromSel.value)?"Which .wei name you are selling":"Any, or a .wei name")
:isNft(fromSel.value)?"Which one you are selling":"Any from collection";
syncOrderType();
showFloor();
};
let syncStops=()=>{};
function chainUi(){
lq.classList.toggle("hide",tab!=="swap"||PROUTE===ZERO);
ln.classList.toggle("hide",tab!=="swap"||PLAUNCH===ZERO);
wn.classList.toggle("hide",tab!=="swap"||CHAIN_ID!==1);
pv.classList.toggle("hide",tab!=="swap"||CHAIN_ID!==1);
tabBook.classList.toggle("hide",SB2===ZERO);
if(lqMode&&PROUTE===ZERO)lqSet(false);
if(lnMode&&PLAUNCH===ZERO)lnSet(false);
if(wnMode&&CHAIN_ID!==1)wnSet(false);
if(pvMode&&CHAIN_ID!==1)pvSet(false);
if(tab==="book"&&SB2===ZERO)setTab("swap");
}
const setTab=t=>{
if(t==="book"&&SB2===ZERO)t="swap";
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);
if(t!=="swap"&&wnMode)wnSet(false);
if(t!=="swap"&&pvMode)pvSet(false);
lq.classList.toggle("hide",t!=="swap"||PROUTE===ZERO);
ln.classList.toggle("hide",t!=="swap"||PLAUNCH===ZERO);
wn.classList.toggle("hide",t