memoscan
← all memos

Memo 0x41f41867…815aeb on Ethereum

0xacfba7ce…4082·#25,784,652·view on Etherscan
[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