0xacfba7ce…4082·#25,751,066·view on Etherscan
let floorSeq=0;
nftId.addEventListener("input",()=>{syncOrderType();showFloor();render()});
async function showFloor(){
const el=document.getElementById("floorNote");if(!el)return;
const side=isNft(fromSel.value)?fromSel.value:isNft(toSel.value)?toSel.value:null;
const live=tab==="book"&&side!==null&&!nftId.value.trim();
if(!live){el.classList.add("hide");el.textContent="";return}
const seq=++floorSeq,t=TOKENS[side];
el.classList.remove("hide");el.textContent="Checking floor bids…";
const rows=await collectionFloor(t.addr).catch(()=>[]);
if(seq!==floorSeq)return;
if(!rows.length){el.textContent="No standing bids on this collection.";return}
const best=rows[0],q=known(best.quote);
el.textContent=`${rows.length} collection bid${rows.length>1?"s":""} · best pays `
+`${trimAmt(best.proceeds,q?q.dec:18)} ${q?q.sym:"?"} for any ${t.sym}`;
}
function decBidRows(hex){
const out=[];
try{
const h=strip0x(hex),bytes=h.length/2;
const word=b=>{if(b<0||b+32>bytes)throw 0;return BigInt("0x"+h.slice(b*2,(b+32)*2))};
const num=b=>{const v=word(b);if(v>0xffffffffffffffffn)throw 0;return Number(v)};
const arr=num(0);
if(arr<64||arr+32>bytes)throw 0;
const n=num(arr),base=arr+32;
if(n>256||base+n*32>bytes)throw 0;
for(let i=0;i<n;i++){
try{
const row=base+num(base+i*32);
if(row+17*32>bytes)throw 0;
const f=k=>word(row+k*32);
const txt=k=>{const p=row+num(row+k*32),len=num(p);
if(p+32+len>bytes)throw 0;
return safeSym(new TextDecoder().decode(hexToBytes(h.slice((p+32)*2,(p+32+Math.min(len,1024))*2))))};
const dec=k=>{const v=num(row+k*32);return v>0&&v<=37?v-1:null};
out.push({
id:f(0),
bidder:"0x"+h.slice((row+1*32+12)*2,(row+2*32)*2),
token:"0x"+h.slice((row+2*32+12)*2,(row+3*32)*2),
quote:"0x"+h.slice((row+3*32+12)*2,(row+4*32)*2),
isNFT:f(4)!==0n,
anyId:f(5)!==0n,
remaining:f(7),
initial:f(8),
price:f(9),
proceeds:f(10),
expiry:f(12),
tDec:dec(13),qDec:dec(14),
tSym:txt(15),qSym:txt(16),
});
}catch{continue}
}
return out;
}catch{return out}
}
async function collectionFloor(collection){
if(FLOORVIEW===ZERO||FLOOR===ZERO)return[];
const data="0x"+SEL_COLL_BIDS+encAddr(FLOOR)+encAddr(collection)+encAddr(ZERO)
+encUint(0)+encUint(16)+encUint(256);
const raw=await rpc(C,[{to:FLOORVIEW,data},L]).catch(()=>null);
if(!raw)return[];
const rows=decBidRows(raw);
return rows.filter(r=>r.anyId&&r.remaining>0n)
.sort((a,b)=>{const l=b.proceeds*a.remaining,r=a.proceeds*b.remaining;return l>r?1:l<r?-1:0});
}
async function floorCandidates(tokenIn,tokenOut,block){
const bin=tokenIn===ZERO?WETH:tokenIn,bout=tokenOut===ZERO?WETH:tokenOut;
const data="0x"+SEL_FLOOR_CANDS+encAddr(FLOOR)+encAddr(bin)+encAddr(bout)
+encUint(0)+encUint(64)+encUint(512);
const raw=await rpc(C,[{to:FLOORVIEW,data},block]).catch(()=>null);
if(!raw)return[];
const out=[];
for(const b of decBidRows(raw)){
if(b.isNFT||b.remaining<=0n||b.initial<=0n||b.price<=0n||b.proceeds<=0n)continue;
if(b.token.toLowerCase()!==bin.toLowerCase())continue;
if(b.quote.toLowerCase()!==bout.toLowerCase())continue;
out.push({
id:b.id,board:FLOOR,floor:1,v2:0,dutch:0,
maker:b.bidder,pf:1,exp:b.expiry,nA:0,nB:0,cp:ZERO,
tA:b.quote,aA:b.proceeds,sA:b.qSym,dA:b.qDec,
tB:b.token,aB:b.remaining,sB:b.tSym,dB:b.tDec,
price:b.price,initial:b.initial,
});
}
return out;
}
function floorGet(o,pay){return o.price*pay/o.initial}
function floorPay(o,want){
if(want<=0n)return 0n;
const g=(want*o.initial+o.price-1n)/o.price;
return g>o.aB?o.aB:g;
}
function rowGet(o,pay){return o.floor?floorGet(o,pay):o.aA*pay/o.aB}
function decViewPage(hex){
const h=strip0x(hex),bytes=h.length/2;
const word=b=>{if(b<0||b+32>bytes)throw Error("bad lens page");return BigInt("0x"+h.slice(b*2,(b+32)*2))};
const addr=(b)=>"0x"+h.slice((b+12)*2,(b+32)*2);
const arr=Number(word(0)),next=word(32);
if(!Number.isSafeInteger(arr)||arr<64||arr+32>bytes)throw Error("bad lens offset");
const n=Number(word(arr)),head=arr+32;
if(!Number.isSafeInteger(n)||n>256||head+n*32>bytes)throw Error("bad lens length");
const text=(base,k)=>{const off=Number(word(base+k*32)),p=base+off,len=Number(word(p));
if(!Number.isSafeInteger(off)||!Number.isSafeInteger(len)||p+32+len>bytes)throw Error("bad lens string");
const cap=Math.min(len,1024),a=new Uint8Array(cap);
for(let i=0;i<cap;i++)a[i]=parseInt(h.slice((p+32+i)*2,(p+33+i)*2),16);
return new TextDecoder().decode(a)};
const rows=[];
const dec=v=>{const d=Number(v);return Number.isInteger(d)&&d>=0&&d<=36?d:null};
for(let i=0;i<n;i++){
try{
const off=Number(word(head+i*32)),b=head+off;
if(!Number.isSafeInteger(off)||b+16*32>bytes)throw Error("bad lens row");
rows.push({id:word(b),maker:addr(b+32),pf:word(b+64)===1n,exp:word(b+96),
nA:word(b+128)===1n,nB:word(b+160)===1n,cp:addr(b+192),
tA:addr(b+224),aA:word(b+256),sA:safeSym(text(b,9)),dA:dec(word(b+320)),
tB:addr(b+352),aB:word(b+384),sB:safeSym(text(b,13)),dB:dec(word(b+448)),board:addr(b+480)});
}catch{continue}
}
return{rows,next};
}
const betterRate=(a,b)=>a.aA*b.aB>b.aA*a.aB;
const MAX_BOOK_LEGS=32,AON_PAIR_SEEDS=24;
function planBookExactIn(rows,amountIn,ammOut,ammIn){
if(!amountIn||!ammIn)return null;
let c=rows.filter(o=>!o.nA&&!o.nB&&o.aA&&(o.aB||o.dutch)&&(o.pf||o.aB<=amountIn)
&&!(account&&o.maker&&o.maker.toLowerCase()===account.toLowerCase()));
const candidateCapped=c.length>256;
const aonCount=c.reduce((n,o)=>n+!o.pf,0);
const aonBounded=aonCount>=3;
c.sort((a,b)=>betterRate(a,b)?-1:betterRate(b,a)?1:0);
if(c.length>256)c.length=256;
const build=(seed=-1,seed2=-1)=>{
let rem=amountIn,bin=0n,bout=0n,fills=[],legCapped=false;
const take=o=>{
if(o.aA*ammIn<=ammOut*o.aB)return;
if(!o.pf&&o.aB>rem)return;
if(fills.length>=MAX_BOOK_LEGS){legCapped=true;return}
if(o.aB===0n){
fills.push({id:o.id,board:o.board,pay:0n,get:o.aA,part:false});
bout+=o.aA;return;
}
const pay=o.aB<rem?o.aB:rem;
if(!pay)return;
const get=o.pf?(pay===o.aB?o.aA:rowGet(o,pay)):o.aA;
if(!get)return;
fills.push({id:o.id,board:o.board,pay,get,part:o.pf&&pay<o.aB});
rem-=pay;bin+=pay;bout+=get;
};
if(seed>=0)take(c[seed]);
if(seed2>=0)take(c[seed2]);
for(let i=0;i<c.length;i++)if(i!==seed&&i!==seed2)take(c[i]);
return{fills,bookIn:bin,bookOut:bout,ammIn:rem,score:bout+rem*ammOut/ammIn,
bounded:candidateCapped||aonBounded||legCapped};
};
let best=build(-1);
for(let i=0;i<c.length;i++)if(!c[i].pf){
const p=build(i);if(p.score>best.score)best=p;
}
const aon=[];for(let i=0;i<c.length&&aon.length<AON_PAIR_SEEDS;i++)if(!c[i].pf)aon.push(i);
for(let i=0;i<aon.length;i++)for(let j=i+1;j<aon.length;j++){
const p=build(aon[i],aon[j]);if(p.score>best.score)best=p;
}
return best.fills.length?best:null;
}
const cheaperRate=(a,b)=>a.aB*b.aA<b.aB*a.aA;
function planBookExactOut(rows,amountOut,ammIn,ammOut){
if(!amountOut||!ammOut)return null;
let c=rows.filter(o=>!o.nA&&!o.nB&&o.aA&&(o.aB||o.dutch)&&(o.pf||o.aA<=amountOut)
&&!(account&&o.maker&&o.maker.toLowerCase()===account.toLowerCase()));
const candidateCapped=c.length>256;
const aonCount=c.reduce((n,o)=>n+!o.pf,0);
const aonBounded=aonCount>=3;
c.sort((a,b)=>cheaperRate(a,b)?-1:cheaperRate(b,a)?1:0);
if(c.length>256)c.length=256;
const build=(seed=-1,seed2=-1)=>{
let left=amountOut,bin=0n,bout=0n,fills=[],legCapped=false;
const take=o=>{
if(o.aB*ammOut>=ammIn*o.aA)return;
if(!o.pf&&o.aA>left)return;
if(fills.length>=MAX_BOOK_LEGS){legCapped=true;return}
const want=o.aA<left?o.aA:left;
let pay,get;
if(o.aB===0n){pay=0n;get=want}
else if(want===o.aA){pay=o.aB;get=o.aA}
else if(o.floor){
pay=floorPay(o,want);
get=floorGet(o,pay);
}
else{
pay=(want*o.aB+o.aA-1n)/o.aA;
if(pay>o.aB)pay=o.aB;
get=pay*o.aA/o.aB;
}
if(!get)return;
fills.push({id:o.id,board:o.board,pay,get,part:want!==o.aA});
bin+=pay;bout+=get;left-=get<left?get:left;
};
if(seed>=0)take(c[seed]);
if(seed2>=0)take(c[seed2]);
for(let i=0;i<c.length&&left;i++)if(i!==seed&&i!==seed2)take(c[i]);
return{fills,bookIn:bin,bookOut:bout,ammOut:left,score:bin+left*ammIn/ammOut,
bounded:candidateCapped||aonBounded||legCapped};
};
let best=build(-1);
for(let i=0;i<c.length;i++)if(!c[i].pf){
const p=build(i);if(p.score<best.score)best=p;
}
const aon=[];for(let i=0;i<c.length&&aon.length<AON_PAIR_SEEDS;i++)if(!c[i].pf)aon.push(i);
for(let i=0;i<aon.length;i++)for(let j=i+1;j<aon.length;j++){
const p=build(aon[i],aon[j]);if(p.score<best.score)best=p;
}
return best.fills.length?best:null;
}
function encFillPlan(fills,tokenIn,tokenOut,recipient,refundTo,deadline){
let tail=encUint(fills.length);
for(const f of fills)tail+=encUint(f.id)+encAddr(f.board)+encUint(f.pay)+encUint(f.get)+encBool(f.part);
return"0x"+SEL_FILLPLAN+encAddr(tokenIn)+encAddr(tokenOut)+encAddr(recipient)+encAddr(refundTo)
+encUint(deadline)+encUint(192)+tail;
}
function encFillPlanAndSwap(fills,tokenIn,tokenOut,recipient,refundTo,deadline,ammIn,ammData){
let ft=encUint(fills.length);
for(const f of fills)ft+=encUint(f.id)+encAddr(f.board)+encUint(f.pay)+encUint(f.get)+encBool(f.part);
const d=strip0x(ammData||"0x"),dt=encUint(d.length/2)+d.padEnd(Math.ceil(d.length/64)*64,"0");
return"0x"+SEL_FILLPLAN_SWAP+encAddr(tokenIn)+encAddr(tokenOut)+encAddr(recipient)+encAddr(refundTo)
+encUint(deadline)+encUint(256)+encUint(ammIn)+encUint(256+ft.length/2)+ft+dt;
}
function encSnwap(tokenIn,amountIn,recipient,tokenOut,minOut,executor,data){
const d=strip0x(data),body=encUint(d.length/2)+d.padEnd(Math.ceil(d.length/64)*64,"0");
return"0x"+SEL_SNWAP+encAddr(tokenIn)+encUint(amountIn)+encAddr(recipient)+encAddr(tokenOut)
+encUint(minOut)+encAddr(executor)+encUint(224)+body;
}
const encCheckpoint=(executor,token,recipient)=>encSnwap(
ZERO,0n,recipient,ZERO,0n,executor,"0x"+SEL_CHECKPOINT+encAddr(token)
);
const encSweep=(token,amount,to)=>"0x"+SEL_SWEEP+encAddr(token)+encUint(0)+encUint(amount)+encAddr(to);
function encPermit2Hybrid(ammData,fills,tokenIn,tokenOut,recipient,refundTo,deadline,bookIn,bookOut){
const bookData=encFillPlan(fills,tokenIn,tokenOut,recipient,refundTo,deadline);
const steps=[encCheckpoint(SWAPBOL,tokenIn,refundTo)];
if(bookIn)steps.push(encSweep(tokenIn,bookIn,SWAPBOL));
steps.push(encSnwap(ZERO,0n,recipient,tokenOut,bookOut,SWAPBOL,bookData));
if(ammData&&ammData!=="0x")steps.push(ammData);
steps.push(encSweep(tokenIn,0n,refundTo));
return encCalls(steps);
}
async function swapCandidates(tokenIn,tokenOut,block){
const key=tokenIn.toLowerCase()+":"+tokenOut.toLowerCase(),old=candCache[key];
if(old&&Date.now()-old.at<15000&&old.block===block){
old.rows.incomplete=old.incomplete;old.rows.unavailable=old.unavailable;
return old.rows;
}
const snapshotAt=Date.now();
const boardIn=tokenIn===ZERO?WETH:tokenIn;
const boardOut=tokenOut===ZERO?WETH:tokenOut;
const [lc,le]=await Promise.all([
rpc("eth_getCode",[SBVIEW,block]),rpc("eth_getCode",[SWAPBOL,block])
]);
if(lc.length<5||le.length<5){
const rows=[];rows.unavailable=true;return rows;
}
const specs=[...BOARDS];
if(DUTCH!==ZERO&&(await rpc("eth_getCode",[DUTCH,block])).length>4){
specs.push({a:DUTCH,dutch:1,ti:tokenIn});
if(tokenIn===ZERO)specs.push({a:DUTCH,dutch:1,ti:WETH});
}
const groups=await Promise.all(specs.map(async b=>{
let cursor=0n,out=[],turns=0,incomplete=false,failed=false;
try{do{
const ti=b.dutch?b.ti:boardIn,to=boardOut;
const data="0x"+(b.dutch?SEL_DUTCH_CANDS:SEL_CANDS)+encAddr(b.a)
+(b.dutch?"":encBool(b.v2))+encAddr(ti)+encAddr(to)+encAddr(SWAPBOL)
+encUint(cursor)+encUint(512);
const p=decViewPage(await rpc(C,[{to:SBVIEW,data},block]));
out.push(...p.rows.map(r=>({...r,v2:b.v2||0,dutch:b.dutch||0})));
cursor=p.next;
++turns;
}while(cursor&&turns<64)}catch{failed=true;incomplete=true}
incomplete=incomplete||!!cursor;
return{out,incomplete,failed};
}));
let rows=groups.flatMap(g=>g.out);
if(tokenIn===ZERO&&tokenOut.toLowerCase()===WETH.toLowerCase()){
rows=rows.filter(r=>r.dutch&&r.tA.toLowerCase()===WETH.toLowerCase()&&r.tB===ZERO);
}
else if(await floorRoutable(block)){
rows=rows.concat(await floorCandidates(tokenIn,tokenOut,block).catch(()=>[]));
}
const incomplete=groups.some(g=>g.incomplete);
const unavailable=groups.every(g=>g.failed&&!g.out.length);
rows.incomplete=incomplete;
rows.unavailable=unavailable;
const current=candCache[key],bn=BigInt(block),cn=current?BigInt(current.block):-1n;
if(!current||bn>cn||(bn===cn&&snapshotAt>=current.at)){
candCache[key]={at:snapshotAt,block,rows,incomplete,unavailable};
}
for(const k in candCache)if(k!==key&&snapshotAt-candCache[k].at>60000)delete candCache[k];
return rows;
}
async function lensBookRows(my){
if((await rpc("eth_getCode",[SBVIEW,L])).length<5)return null;
const specs=[...BOARDS];
if(DUTCH!==ZERO&&(await rpc("eth_getCode",[DUTCH,L])).length>4)specs.push({a:DUTCH,dutch:1});
const pages=await Promise.all(specs.map(async b=>{
let cursor=0n,out=[],turns=0;
do{
const data="0x"+(b.dutch?SEL_RECENT_DUTCH:SEL_RECENT)+encAddr(b.a)
+(b.dutch?"":encBool(b.v2))+encUint(cursor)+encUint(80)+encUint(256);
const p=decViewPage(await rpc(C,[{to:SBVIEW,data},L]));
out.push(...p.rows.map(r=>({...r,v2:b.v2||0,dutch:b.dutch||0})));
cursor=p.next;
}while(cursor&&out.length<160&&++turns<64&&my===bookSeq);
return out;
}));
const rows=pages.flat();
if(FLOOR!==ZERO&&FLOORVIEW!==ZERO){
await floorRoutable(L);
try{
const raw=await rpc(C,[{to:FLOORVIEW,
data:"0x"+SEL_RECENT_FLOOR+encAddr(FLOOR)+encUint(0)+encUint(80)+encUint(256)},L]);
for(const b of decBidRows(raw)){
if(b.remaining<=0n)continue;
rows.push({
id:b.id,board:FLOOR,floor:1,v2:0,dutch:0,
maker:b.bidder,pf:b.isNFT?0:1,exp:b.expiry,
nA:0,nB:0,cp:ZERO,
tA:b.quote,aA:b.proceeds,sA:b.qSym,dA:b.qDec,
tB:b.token,aB:b.remaining,sB:b.tSym,dB:b.isNFT?0:b.tDec,
price:b.price,initial:b.initial,anyId:b.anyId,isNFT:b.isNFT?1:0,
});
}
}catch{}
}
return rows;
}
async function loadBook(){
if(!account||tab!=="book"){book.innerHTML="";bookRows=[];syncBook(0);return}
const my=++bookSeq;
const live=[];
for(const b of BOARDS){
let code="0x";
try{code=await rpc("eth_getCode",[b.a,L])}catch{}
if(code.length>4)live.push(b);
}
if(my!==bookSeq)return;
if(!live.length){
bookRows=[];bookLegs=[];
book.innerHTML='<h6><span>Orderbook</span></h6><div class="o"><span class="l"><i>No Swapboard deployed on this network yet.</i></span></div>';
syncBook(0);book.classList.remove("hide");return}
let rows=null;
try{rows=await lensBookRows(my)}catch{rows=null}
if(my!==bookSeq)return;
if(!rows)rows=[];
for(const b of rows.length?[]:live){
let total=0;
try{total=Number(BigInt(await rpc(C,[{to:b.a,data:"0x"+SEL_NEXTID},L])))}catch{continue}
if(!total)continue;
const hi=total,lo=Math.max(0,hi-40);
const ids=[];for(let i=hi-1;i>=lo;--i)ids.push(i);
let raw="";
try{raw=await rpc(C,[{to:b.a,data:"0x"+SEL_GETORDERS+encUint(32)+encUint(ids.length)+ids.map(i=>encUint(i)).join("")},L])}catch{continue}
if(my!==bookSeq)return;
const h=strip0x(raw);if(!h)continue;
const w=WORDS(b.v2),n=Number(BigInt("0x"+h.slice(64,128)));
const at=(r,k)=>BigInt("0x"+h.slice(128+(r*w+k)*64,128+(r*w+k+1)*64));
const ad=(r,k)=>"0x"+h.slice(128+(r*w+k)*64+24,128+(r*w+k+1)*64);
for(let r=0;r<n&&r<ids.length;++r){
if(at(r,1)!==1n)continue;
const o=b.v2
?{id:ids[r],board:b.a,v2:1,maker:ad(r,0),pf:at(r,2)===1n,exp:at(r,3),
nA:at(r,4)===1n,nB:at(r,5)===1n,cp:ad(r,6),
tA:ad(r,7),aA:at(r,8),tB:ad(r,9),aB:at(r,10)}
:{id:ids[r],board:b.a,v2:0,maker:ad(r,0),pf:0,exp:0n,nA:0,nB:0,
cp:ZERO,tA:ad(r,2),aA:at(r,3),tB:ad(r,4),aB:at(r,5)};
if(o.exp&&BigInt(Math.floor(Date.now()/1e3))>o.exp
&&o.maker.toLowerCase()!==account.toLowerCase())continue;
if(o.cp!==ZERO
&&o.cp.toLowerCase()!==account.toLowerCase()
&&o.maker.toLowerCase()!==account.toLowerCase())continue;
rows.push(o);
}
}
if(my!==bookSeq)return;
const cutoff=BigInt(Math.floor(Date.now()/1e3));
rows=rows.filter(o=>{
const mine=o.maker.toLowerCase()===account.toLowerCase();
if(o.exp&&cutoff>o.exp&&!mine)return false;
return o.cp===ZERO
||o.cp.toLowerCase()===account.toLowerCase()
||o.maker.toLowerCase()===account.toLowerCase();
});
const dutchRows=rows.filter(r=>r.dutch);
const bidRows=rows.filter(r=>r.floor);
const fillRows=rows.filter(r=>!r.dutch&&!r.floor&&r.v2&&r.pf&&!r.nA&&!r.nB);
if(dutchRows.length||bidRows.length||fillRows.length){
const reqs=[
...dutchRows.map(r=>({to:DUTCH,data:"0x"+SEL_DUTCH_LISTING+encUint(r.id)})),
...bidRows.map(r=>({to:FLOOR,data:"0x"+SEL_BIDS+encUint(r.id)})),
...fillRows.map(r=>({to:r.board,data:"0x"+SEL_INITIAL_B+encUint(r.id)})),
];
const raw=await mc3Deep(reqs);
if(my!==bookSeq)return;
let k=0;
for(const r of dutchRows){const x=raw[k++];if(!x)continue;
const h=strip0x(x),w=n=>BigInt("0x"+h.slice(n*64,(n+1)*64));
if(h.length>=640)Object.assign(r,{
start:w(2),duration:w(3),startPrice:w(5),endPrice:w(7),initial:w(8),remaining:w(9)});
}
for(const r of bidRows){const x=raw[k++];if(!x)continue;
const h=strip0x(x),w=n=>BigInt("0x"+h.slice(n*64,(n+1)*64));
if(h.length>=11*64)Object.assign(r,{start:w(2),duration:w(3),startPrice:w(5),endPrice:w(7)});
}
for(const r of fillRows){const x=raw[k++];if(!x)continue;
const h=strip0x(x);if(h.length<64)continue;
const init=BigInt("0x"+h.slice(0,64));
if(init>r.aB)r.filled=(init-r.aB)*10000n/init;
}
}
if(!rows.length){bookRows=[];bookLegs=[];book.innerHTML="";syncBook(0);return}
await loadLogos(rows.flatMap(r=>[r.tA,r.tB]));
if(my!==bookSeq)return;
const legs=await Promise.all(rows.map(r=>Promise.all([
leg(r.tA,r.aA,r.nA,r.sA!=null&&r.dA!=null?{sym:r.sA,dec:r.dA}:null),
leg(r.tB,r.aB,r.nB,r.sB!=null&&r.dB!=null?{sym:r.sB,dec:r.dB}:null)
])));
if(my!==bookSeq)return;
bookRows=rows;bookLegs=legs;
paintBook();
}
let bookPairOnly=LS.bkp!=="0",bookType="all",bookLegs=[];
const sameAsset=(a,b)=>{
const w=x=>{x=(x||"").toLowerCase();return x===ZERO.toLowerCase()?WETH.toLowerCase():x};
return w(a)===w(b);
};
const rowInPair=r=>{
const f=TOKENS[fromSel.value]?.addr,t=TOKENS[toSel.value]?.addr;
if(!f||!t)return true;
return (sameAsset(r.tA,f)&&sameAsset(r.tB,t))||(sameAsset(r.tA,t)&&sameAsset(r.tB,f));
};
const rowIsType=r=>bookType==="all"
||(bookType==="dutch"&&r.dutch)
||(bookType==="bid"&&r.floor)
||(bookType==="nft"&&(r.nA||r.nB||r.isNFT))
||(bookType==="limit"&&!r.dutch&&!r.floor);
function paintBook(){
const rows=bookRows,legs=bookLegs;
if(!rows.length){book.innerHTML="";syncBook(0);return}
let own="",rest="",nOwn=0,nRest=0,hidden=0;
const now=BigInt(Math.floor(Date.now()/1e3));
rows.forEach((r,i)=>{
const[a,bg]=legs[i]||[];
if(!a||!bg)return;
const mine=r.maker.toLowerCase()===account.toLowerCase();
const expired=!!(r.exp&&now>r.exp);
const tags=(expired?'<span class="tg w" title="This order has lapsed and can no longer be filled, but its escrow is still held by the board. Reclaim returns it to you.">expired</span>':"")
+(r.nA||r.nB||r.isNFT?'<span class="tg">NFT</span>':"")
+(r.v2&&!r.pf&&!r.nA&&!r.nB?'<span class="tg">AON</span>':"")
+(r.dutch?'<span class="tg">Dutch</span>':"")
+(r.floor?'<span class="tg">Bid</span>':"")
+(r.floor&&r.isNFT&&r.anyId?'<span class="tg">any id</span>':"")
+(r.filled!==undefined&&r.filled>0n?`<span class="tg">${(Number(r.filled)/100).toFixed(0)}% filled</span>`:"")
+(r.cp!==ZERO?'<span class="tg">private</span>':"")
+(!a.ok||!bg.ok?'<a class="tg w" href="'+ZLIST_URL+'" target="_blank" rel="noopener"'
+' title="Not on the staked token list. Its symbol and decimals are read straight from the contract'
+' and nothing has vouched for it - check the address before filling. Stake on zList to get a real'
+' token listed.">unverified</a>':"");
let when=expired?" · lapsed "+rel(Number(now-r.exp))+" ago — escrow still held"
:r.exp?" · "+rel(Number(r.exp-now))+" left":"";
if(!expired&&r.dutch&&r.duration){
const end=r.start+r.duration;
const floor=r.initial?(r.endPrice*r.remaining+r.initial-1n)/r.initial:0n;
when=now>=end?` · floor reached: ${trimAmt(floor,r.dB)} ${r.sB}`
:` · floor ${trimAmt(floor,r.dB)} ${r.sB} in ${rel(Number(end-now))}`;
}
if(!expired&&r.floor&&r.duration){
const end=r.start+r.duration;
when=now>=end?" · window closed"
:` · climbs to ${trimAmt(r.initial?r.endPrice*r.aB/r.initial:r.endPrice,r.dA)} ${r.sA} in ${rel(Number(end-now))}`;
}
const dset=`data-i="${r.id}" data-b="${r.board}" data-v="${r.v2}" data-d="${r.dutch||0}" data-f="${r.floor||0}"`;
const takeable=!r.floor||(!r.isNFT&&floorReady);
const act=mine?`<button data-x="c" ${dset}>${expired?"Reclaim":"Cancel"}</button>`
:takeable?`<button data-x="f" ${dset}>${r.floor?"Sell into":"Fill"}</button>`
:`<button disabled>${r.isNFT?"NFT bid":"Not routable"}</button>`;
const card=r.board.toLowerCase()!==SB1.toLowerCase();
const row=`<div class="ow"><div class="o${card?" ins":""}"${card?` data-k="${r.board}:${r.id}"`:""}>`
+`<span class="l">${a.ic}${bg.ic}<span><b>${a.txt} → ${bg.txt}</b>`
+`<i>${a.link}${tags}${when}</i></span></span>${act}</div><div class="ic"></div></div>`;
if(mine){own+=row;++nOwn;return}
if(!(bookPairOnly?rowInPair(r):true)||!rowIsType(r)){++hidden;return}
rest+=row;++nRest;
});
let html="";
if(nOwn)html+=`<h6><span>Your orders</span><span>${nOwn}</span></h6>`+own;
html+=bookFilters(hidden);
if(nRest)html+=rest;
else html+=`<div class="lqempty">${emptyBook()}${hidden?` ${hidden} hidden by the filters.`:""}</div>`;
book.innerHTML=html;
syncBook(nOwn+nRest,rows.length);
}
const cardCache={};
async function loadCard(board,id){
const k=board.toLowerCase()+":"+id;
if(cardCache[k])return cardCache[k];
const raw=await rpc(C,[{to:board,data:"0x"+SEL_TOKENURI+encUint(id)},L]);
const uri=decodeString(raw);
const i=uri.indexOf("base64,");
if(i<0)throw Error("card is not inline");
const j=JSON.parse(atob(uri.slice(i+7)));
const out={attrs:Array.isArray(j.attributes)?j.attributes:[],img:safeUrl(j.image),
name:safeSym(j.name).slice(0,40)};
cardCache[k]=out;return out;
}
function cardHtml(c){
const rows=c.attrs.slice(0,10).map(a=>{
const t=safeSym(a&&a.trait_type).slice(0,20);
let v=a&&a.value;
if(a&&a.display_type==="date"&&+v)v=new Date(+v*1e3).toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"});
else if(a&&a.display_type==="number")v=String(v);
return`<dt>${t}</dt><dd>${safeSym(v).slice(0,28)}</dd>`}).join("");
return(rows?`<dl>${rows}</dl>`:"")+(c.img?`<img src="${c.img}" alt="" loading="lazy">`:"");
}
async function inspect(el,k){
const box=el.parentNode.querySelector(".ic");
if(!box)return;
if(box.innerHTML){box.innerHTML="";return}
const at=k.lastIndexOf(":"),board=k.slice(0,at),id=BigInt(k.slice(at+1));
box.textContent="Reading the receipt\u2026";
try{box.innerHTML=cardHtml(await loadCard(board,id))||"No card for this order."}
catch{box.textContent="This receipt could not be read."}
}
const BOOK_TYPES=[["all","All"],["limit","Limit"],["dutch","Dutch"],["bid","Bids"],["nft","NFT"]];
const emptyBook=()=>{
const ty=BOOK_TYPES.find(([v])=>v===bookType);
const what=bookType==="all"?"orders":`${ty?ty[1]:bookType} orders`;
const where=bookPairOnly
?` on ${TOKENS[fromSel.value]?.sym||"?"}/${TOKENS[toSel.value]?.sym||"?"}`:"";
return `No ${what.toLowerCase()}${where} from anyone else.`;
};
function bookFilters(hidden){
const f=TOKENS[fromSel.value]?.sym||"?",t=TOKENS[toSel.value]?.sym||"?";
const chip=(on,attr,val,label,title)=>`<button class="bkc${on?" on":""}" `
+`data-${attr}="${val}"${title?` title="${title}"`:""}>${label}</button>`;
return `<h6><span>Orderbook</span><span>${hidden?hidden+" hidden":""}</span></h6>`
+`<div class="bkf">`
+chip(bookPairOnly,"bf","1",`${f}/${t}`,"Only orders on the pair selected above")
+chip(!bookPairOnly,"bf","0","All pairs","Every order on every board")
+`</div><div class="bkf">`
+BOOK_TYPES.map(([v,l])=>chip(bookType===v,"bt",v,l)).join("")
+`</div>`;
}
let bookShown=0;
function syncBook(n,total){
bookShown=n;
total=total===undefined?n:total;
const on=tab==="book"&&total>0;
bkTog.classList.toggle("hide",!on);
bkTog.firstChild.nodeValue=total
?`Orderbook (${n<total?`${n} of ${total}`:total}) `:"Orderbook ";
bkTog.setAttribute("aria-expanded",on&&bkOpen?"true":"false");
book.classList.toggle("hide",!on||!bkOpen);
}
bkTog.onclick=()=>{bkOpen=!bkOpen;LS.bk=bkOpen?"1":"0";syncBook(bookShown,bookRows.length)};
book.addEventListener("click",async e=>{
const fc=e.target.closest("[data-bf]"),tc=e.target.closest("[data-bt]");
if(fc){bookPairOnly=fc.dataset.bf==="1";LS.bkp=bookPairOnly?"1":"0";paintBook();return}
if(tc){bookType=tc.dataset.bt;paintBook();return}
const b=e.target.closest("button[data-i]");
if(!b){
const row=e.target.closest(".o.ins");
if(row&&!e.target.closest("a"))inspect(row,row.dataset.k);
return;
}
b.disabled=true;
++busy;
try{
await checkWallet();
const id=BigInt(b.dataset.i),to=b.dataset.b,dl=BigInt(Math.floor(Date.now()/1e3)+1800);
const row=bookRows.find(x=>String(x.id)===b.dataset.i&&x.board.toLowerCase()===to.toLowerCase());
if(!row)throw Error("order changed — refresh and retry");
if(b.dataset.x==="c"){
const native=