0xacfba7ce…4082·#25,784,655·view on Etherscan
ymbol and logo. Leave blank and it is you. Worth setting to a multisig if the project has one.":"";
rc.placeholder=on?"Creator — defaults to you"
:"Recipient (optional) — 0x or .eth/.(g)wei";
stat.textContent="";lnNote.textContent="";
if(on)lnQuote();
}
function lnQuote(){
lnGo.textContent=account?"Launch":"Connect Wallet";
const ns=v=>v.replace(/[, ]/g,"");
const sup=ns(lnSupply.value),mc=+ns(lnMcap.value),al=+ns(lnAlloc.value);
lnNote.textContent="";
if(!(Number(sup)>0)||!(mc>0))return;
const pooled=Number(sup)*(1-(al>0?al:0)/100);
if(!(pooled>0))return;
const px=mc/pooled;
const sym=lnSym.value.trim().toUpperCase();
const poolFrac=100/(mc+1);
const firstEth=poolFrac*pooled/Number(sup);
lnNote.textContent=`1 ETH buys ${firstEth>=10?Math.round(firstEth):firstEth.toFixed(firstEth<1?2:1)}% of all ${sym||"tokens"}`
+` \u00b7 opens at 1 ETH \u2248 ${trimNum(1/px)} ${sym||"tokens"}`
+(al>0?` \u00b7 creator keeps ${trimNum(Number(sup)*al/100)} (${al}%)`:"")
+(al>=10?` \u00b7 that much can sell for most of the ether early buyers put in`:"")
+(lnArtBytes?` \u00b7 logo ${(lnArtBytes.length/1024).toFixed(1)} KB`:"");
lnNote.style.color=poolFrac>=10?"#c33":"";
}
const trimNum=n=>n>=1000?Math.round(n).toLocaleString("en-US")
:n>=1?n.toLocaleString("en-US",{maximumFractionDigits:4}):String(Number(n.toPrecision(4)));
const blobText=b=>b.text?b.text():new Promise((res,rej)=>{
const r=new FileReader();r.onload=()=>res(String(r.result));r.onerror=()=>rej(r.error);r.readAsText(b)});
const blobBytes=async b=>b.arrayBuffer?new Uint8Array(await b.arrayBuffer()):await new Promise((res,rej)=>{
const r=new FileReader();r.onload=()=>res(new Uint8Array(r.result));r.onerror=()=>rej(r.error);r.readAsArrayBuffer(b)});
async function lnPrepare(file){
if(/svg/.test(file.type)){
const b=new TextEncoder().encode((await blobText(file))
.replace(/<\?xml[^>]*\?>/g,"").replace(/<!--[\s\S]*?-->/g,"").replace(/<!DOCTYPE[^>]*>/gi,"")
.replace(/>\s+</g,"><").trim());
if(b.length>LN_MAX_ART)throw Error(`That SVG is ${(b.length/1024).toFixed(1)} KB and the on-chain limit is 24 KB.`);
return{bytes:b,mime:LN_MIME["image/svg+xml"]};
}
const bmp=await createImageBitmap(file);
let smallest=null;
for(const side of [512,384,256,192,128,96,64]){
for(const [type,q] of [["image/webp",1],["image/png",undefined],["image/webp",0.85],["image/webp",0.6]]){
const sc=Math.min(1,side/Math.max(bmp.width,bmp.height));
const cv=document.createElement("canvas");
cv.width=Math.max(1,Math.round(bmp.width*sc));cv.height=Math.max(1,Math.round(bmp.height*sc));
const cx=cv.getContext("2d");cx.imageSmoothingQuality="high";
cx.drawImage(bmp,0,0,cv.width,cv.height);
const blob=await new Promise(r=>cv.toBlob(r,type,q));
if(!blob||blob.type!==type)continue;
const bytes=await blobBytes(blob);
if(!smallest||bytes.length<smallest.bytes.length)smallest={bytes,mime:LN_MIME[type]};
if(bytes.length<=LN_BUDGET)return{bytes,mime:LN_MIME[type]};
}
}
if(smallest&&smallest.bytes.length<=LN_MAX_ART)return smallest;
throw Error("That image will not compress small enough. Flat colours and simple shapes do far better than photographs.");
}
lnPick.onclick=()=>lnArt.click();
lnArt.onchange=async()=>{
const f=lnArt.files&&lnArt.files[0];
if(!f)return;
lnArtNote.textContent="reading…";
try{
const r=await lnPrepare(f);
lnArtBytes=r.bytes;lnArtMime=r.mime;
lnArtNote.textContent=`${(r.bytes.length/1024).toFixed(1)} KB`;
lnArtNote.classList.add("set");
let b64="";for(const x of r.bytes)b64+=String.fromCharCode(x);
lnPrev.src="data:"+MIME_NAME[r.mime]+";base64,"+btoa(b64);
lnPrev.classList.add("set");
}catch(e){
lnArtBytes=null;lnArtNote.classList.remove("set");lnPrev.classList.remove("set");
lnArtNote.textContent="none";stat.textContent=e.message||String(e);
}
lnQuote();
};
const lnGroup=el=>{
const dot=el.value.indexOf(".");
const intRaw=(dot<0?el.value:el.value.slice(0,dot)).replace(/\D/g,"");
const frac=dot<0?"":"."+el.value.slice(dot+1).replace(/\D/g,"");
const digitsBefore=el.value.slice(0,el.selectionStart??el.value.length).replace(/\D/g,"").length;
const out=intRaw.replace(/\B(?=(\d{3})+(?!\d))/g,",")+frac;
if(out===el.value)return;
el.value=out;
let seen=0,pos=digitsBefore?out.length:0;
if(digitsBefore)for(let i=0;i<out.length;i++){
if(/\d/.test(out[i])&&++seen===digitsBefore){pos=i+1;break}
}
try{el.setSelectionRange(pos,pos)}catch{}
};
lnSupply.oninput=()=>{lnGroup(lnSupply);lnQuote()};
for(const el of [lnName,lnSym,lnMcap,lnAlloc])el.oninput=lnQuote;
ln.onclick=()=>lnSet(!lnMode);
lnGo.onclick=async()=>{
stat.textContent="";
if(lnGo.disabled)return;
if(!account)return void connect();
lnGo.disabled=true;
try{
await checkWallet();
const name=lnName.value.trim(),sym=lnSym.value.trim().toUpperCase();
if(!name||!sym)throw Error("A name and a symbol, please.");
const utf8=v=>new TextEncoder().encode(v).length;
if(utf8(name)>64)throw Error(`That name is ${utf8(name)} bytes and the limit is 64.`);
if(utf8(sym)>16)throw Error(`That symbol is ${utf8(sym)} bytes and the limit is 16.`);
const supRaw=lnSupply.value.replace(/[, ]/g,"");
if(!/^\d+(\.\d+)?$/.test(supRaw)||!(Number(supRaw)>0))throw Error("Supply has to be a positive number.");
const supply=parseUnits(supRaw,18);
const alloc=Math.round(Number(lnAlloc.value||0)*100);
if(!(alloc>=0)||alloc>LN_MAX_ALLOC)throw Error("Allocation runs from 0 to 20%.");
const mcap=parseUnits(String(lnMcap.value||"").trim(),18);
if(mcap<LN_MIN_MCAP)throw Error("That opening valuation is too small to price.");
const pooled=supply-supply*BigInt(alloc)/10000n;
if(pooled<LN_MIN_POOLED)throw Error("Too little supply reaches the pool to price a market.");
let owner=account;
const rv=rc.value.trim();
if(rv){
if(/^0x[0-9a-fA-F]{40}$/.test(rv)){
if(/^0x0{40}$/i.test(rv))throw Error("The creator cannot be the zero address.");
owner=rv;
}else if(/\.(g?wei|eth)$/i.test(rv)){
rcvEl.textContent="resolving\u2026";
let r="";try{r=await nameFwd(rv.toLowerCase())}catch{r=""}
rcvEl.textContent="";
if(!r||/^0x0{40}$/i.test(r))throw Error("That name is not registered.");
owner=r;
}else throw Error("The creator must be an address, or a .eth / .wei name.");
}
stat.textContent="Launching\u2026";
const req={from:account,to:PLAUNCH,value:"0x0",
data:encLaunch(name,sym,"",supply,BigInt(alloc),mcap,owner,lnArtBytes,lnArtMime)};
const sim=await rpc(C,[req,L]);
const candidate=/^0x[0-9a-fA-F]{128}$/.test(sim||"")?"0x"+strip0x(sim).slice(24,64):null;
const receipt=await waitTx(await rpc(S,[req]));
const topic=kecStr("Launched(address,address,address,uint256,uint256,uint256)");
const lg=(receipt.logs||[]).find(l=>(l.address||"").toLowerCase()===PLAUNCH.toLowerCase()
&&(l.topics||[])[0]&&l.topics[0].toLowerCase()===topic.toLowerCase());
let token=lg?"0x"+strip0x(lg.topics[1]).slice(24):null;
if(!token&&candidate){
try{
const c=await rpc(C,[{to:PLAUNCH,data:"0x"+SEL_CREATOROF+encAddr(candidate)},L]);
if(c&&"0x"+strip0x(c).slice(24)===owner.toLowerCase())token=candidate;
}catch{}
}
if(token){
try{
const idx=await addCustomToken(token);
TOKENS[idx].launched=1;
rebuild();
fromSel.value=String(Math.max(0,TOKENS.findIndex(t=>t.addr===ZERO)));
fromSel.dataset.prev=fromSel.value;
toSel.value=String(idx);
toSel.dataset.prev=toSel.value;
pairChosen=true;
lnSet(false);
syncDisabled();syncNftUI();
refreshBalance();
update();
}catch{}
}
stat.textContent=token?`${sym} is live at ${short(token)}`:`${sym} is live`;
lnArtBytes=null;lnArtNote.textContent="none";lnArtNote.classList.remove("set");
lnPrev.classList.remove("set");lnArt.value="";
}catch(e){err(e)}
finally{lnGo.disabled=false}
};
const SEL_MARKETS="29c21083",SEL_TAPE="29a65241";
const SEL_BYCREATOR="7d78be2b",SEL_TOKEN1="d21220a7",SEL_BYCREATOR_N="aa5e6b5b",SEL_RESERVE0="443cb4bc";
const LAUNCHED=[];
const LAUNCH_SHOW=24;
const LAUNCH_SCAN=256;
const FINE=300,COARSE=14400,TFS=[["5m",300],["1h",3600],["4h",14400],["1d",86400]];
const CH_POOLS=8;
const srcFor=tf=>tf>=COARSE?COARSE:FINE;
let chFloor=0,chSupply=0,chFloorKey="";
let chKind=LS.ck==="line"?"line":"candle",chInv=false,chInvKey="",chPeriod=300,chBars=null,chSeq=0,chPools=null,chOpen=LS.ch==="1",chCache={};
let chDraw=null;
const unf=p=>(p&0xffffff)*2**(p>>>24);
const decBar=w=>{if(!w)return null;const f=s=>Number((w>>BigInt(s))&0xffffffffn);
return{b:Number(w&0xffffffffn),o:unf(f(32)),h:unf(f(64)),l:unf(f(96)),c:unf(f(128)),v:unf(f(160)),n:Number((w>>192n)&0xffffn)}};
const rollUp=(bars,src,dst)=>{if(dst<=src)return bars.filter(Boolean);
const out=[];let g=null,k=null;
for(let i=bars.length-1;i>=0;i--){const b=bars[i];if(!b)continue;
const key=Math.floor(b.b*src/dst);
if(key!==k){if(g)out.push(g);k=key;g={b:key,o:b.o,h:b.h,l:b.l,c:b.c,v:b.v,n:b.n}}
else{g.h=Math.max(g.h,b.h);g.l=Math.min(g.l,b.l);g.c=b.c;g.v+=b.v;g.n+=b.n}}
if(g)out.push(g);return out.reverse()};
const CH_WICK_SHARE=.01;
const mergeTapes=tapes=>{const m=new Map();
for(const bars of tapes)for(const b of bars||[]){if(!b)continue;
let e=m.get(b.b);
if(!e)m.set(b.b,e={b:b.b,v:0,n:0,ov:0,cv:0,parts:[]});
e.v+=b.v;e.n+=b.n;e.ov+=b.o*b.v;e.cv+=b.c*b.v;e.parts.push(b)}
const out=[...m.values()].sort((x,y)=>y.b-x.b);
for(const e of out){
const big=e.v>0?e.parts.filter(x=>x.v>=e.v*CH_WICK_SHARE):[];
const src=big.length?big:e.parts;
let h=-Infinity,l=Infinity;
for(const x of src){if(x.h>h)h=x.h;if(x.l<l)l=x.l}
e.h=h;e.l=l;
if(e.v>0){e.o=e.ov/e.v;e.c=e.cv/e.v}else{e.o=src[0].o;e.c=src[0].c}
delete e.ov;delete e.cv;delete e.parts}
return out};
const chPair=()=>{const f=TOKENS[fromSel.value],t=TOKENS[toSel.value];
if(!f||!t||f.addr===t.addr)return null;
return f.addr.toLowerCase()<t.addr.toLowerCase()?[f,t]:[t,f]};
let chLoading="";
async function loadChart(){
const p0=chPair();
const k0=chPairKey(p0);
if(k0&&chLoading===k0)return;
chBars=null;chPools=null;
const p=p0;
if(tab!=="swap"||!p){syncChartUI();return}
const my=++chSeq,key=k0;
const hit=chCache[key];
if(hit&&Date.now()-hit.at<30000){chBars=hit.bars;chPools=hit.pools;syncChartUI();return}
chLoading=key;
try{
if(PPLENS===ZERO||(await rpc("eth_getCode",[PPLENS,L])).length<5)return syncChartUI();
const marketsFor=async pair=>{
const [raw]=await mc3([{to:PPLENS,data:"0x"+SEL_MARKETS+encAddr(pair[0].addr)+encAddr(pair[1].addr)
+encAddr(ZERO)+encUint(0)+encUint(LQ_SCAN)+encUint(0)}]);
const found=[];
if(raw){const h=strip0x(raw),n=Number(BigInt("0x"+h.slice(64,128))),base=128;
for(let i=0;i<n&&i<LQ_SCAN;i++){const at=k=>BigInt("0x"+h.slice(base+(i*PI_WORDS+k)*64,base+(i*PI_WORDS+k+1)*64));
if(at(PI_HOOK)!==0n||at(PI_LIQ)===0n)continue;
found.push({a:"0x"+h.slice(base+(i*PI_WORDS+PI_POOL)*64+24,base+(i*PI_WORDS+PI_POOL+1)*64),liq:at(PI_LIQ)})}}
found.sort((x,y)=>y.liq>x.liq?1:y.liq<x.liq?-1:0);
return found.slice(0,CH_POOLS).map(x=>x.a);
};
let eff=p,pools=await marketsFor(p);
if(my!==chSeq)return;
if(!pools.length){
const alt=chOtherForm(p);
if(alt){
const altPools=await marketsFor(alt);
if(my!==chSeq)return;
if(altPools.length){eff=alt;pools=altPools}
}
}
const [t0,t1]=eff;
if(!pools.length){chCache[key]={at:Date.now(),bars:null,pools:null,pair:null};return syncChartUI()}
const reqs=[];
for(const a of pools)reqs.push({to:a,data:"0x"+SEL_TAPE+encUint(FINE)+encUint(256)},
{to:a,data:"0x"+SEL_TAPE+encUint(COARSE)+encUint(256)});
const tapes=await mc3Deep(reqs);
if(my!==chSeq)return;
const dec1Tape=hex=>{if(!hex)return null;
const h=strip0x(hex),n=Number(BigInt("0x"+h.slice(64,128))),bars=[];
for(let j=0;j<n;j++)bars.push(decBar(BigInt("0x"+h.slice(128+j*64,128+(j+1)*64))));
return bars};
const decoded=tapes.map(dec1Tape);
const fine=[],coarse=[];
for(let i=0;i<pools.length;i++){fine.push(decoded[i*2]);coarse.push(decoded[i*2+1])}
const sc=10**(t0.dec-t1.dec)/1e18;
const build=set=>{const m=mergeTapes(set);
for(const b of m){b.o*=sc;b.h*=sc;b.l*=sc;b.c*=sc}return m.length?m:null};
const f=build(fine);
let c=build(coarse);
if(f&&c){const lead=rollUp(f,FINE,COARSE).filter(b=>b.b>c[0].b);
if(lead.length)c=lead.concat(c)}
chBars=f||c?{[FINE]:f,[COARSE]:c}:null;
const live=set=>set.filter(d=>d&&d.some(Boolean)).length;
chPools=chBars?{[FINE]:live(fine),[COARSE]:live(coarse)}:null;
chCache[key]={at:Date.now(),bars:chBars,pools:chPools,pair:eff===p?null:eff};
}catch{chBars=null;chPools=null}
finally{if(chLoading===k0)chLoading=""}
if(my===chSeq)syncChartUI();
}
const chPoolsAt=step=>chPools&&chPools[step]||0;
const chLabel=p=>chInv?`${p[0].sym} per ${p[1].sym}`:`${p[1].sym} per ${p[0].sym}`;
async function chFloorFor(p){
const key=chPairKey(p);
if(key===chFloorKey)return;
chFloorKey=key;chFloor=0;chSupply=0;
const tok=p&&p[1]&&p[1].launched?p[1].addr:"";
if(!tok)return;
try{
const [fp,sup]=await mc3Deep([{to:PLAUNCH,data:"0x"+SEL_FLOORPRICE+encAddr(tok)},
{to:tok,data:"0x"+SEL_TOTALSUPPLY}]);
if(key!==chFloorKey)return;
chFloor=fp?+formatUnits(BigInt(fp),18):0;
chSupply=sup?+formatUnits(BigInt(sup),18):0;
}catch{}
}
function syncChartUI(){
const on=tab==="swap"&&!!chBars;
chTog.classList.toggle("hide",!on);
chBox.classList.toggle("hide",!on||!chOpen);
chTog.setAttribute("aria-expanded",on&&chOpen?"true":"false");
if(!on||!chOpen)return;
const p=chEff();
chNote.textContent=p?chLabel(p):"";
drawChart();syncInv();
}
const NUMERAIRES=new Set([
"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"0xdac17f958d2ee523a2206206994597c13d831ec7",
"0x6b175474e89094c44da98b954eedeac495271d0f",
]);
const moneyRank=a=>{a=(a||"").toLowerCase();
return NUMERAIRES.has(a)?2:(a===ZERO||a===WETH.toLowerCase())?1:0};
const chPairKey=p=>p?p[0].addr.toLowerCase()+":"+p[1].addr.toLowerCase():"";
const chNaturalInv=p=>!!p&&moneyRank(p[0].addr)>moneyRank(p[1].addr);
const chOtherForm=p=>{
const eth=p.filter(x=>isEthLike(x.addr));
if(eth.length!==1)return null;
const cur=eth[0],rest=p[0]===cur?p[1]:p[0];
const sub=TOKENS.find(x=>x.addr.toLowerCase()===(cur.addr===ZERO?WETH:ZERO).toLowerCase());
if(!sub)return null;
return sub.addr.toLowerCase()<rest.addr.toLowerCase()?[sub,rest]:[rest,sub];
};
const chEff=()=>{const p=chPair();if(!p)return p;
const hit=chCache[chPairKey(p)];
return (hit&&hit.pair)||p;};
const CH_SLOTS=Math.floor((340-2-42)/3);
const chFinest=()=>chBars&&chBars[FINE]?FINE:COARSE;
const chSeries=secs=>{
if(!chBars)return null;
const src=srcFor(secs),step=chBars[src]?src:chFinest(),base=chBars[step];
if(!base||!base.length)return null;
const per=Math.max(secs,step);
let pts=rollUp(base,step,per).slice().reverse();
pts=pts.map(b=>{const{o,c}=b;
if(!(o>0)||!(c>0)||!isFinite(o)||!isFinite(c))return null;
const h=b.h>0&&isFinite(b.h)?b.h:0,l=b.l>0&&isFinite(b.l)?b.l:Infinity;
return{...b,h:Math.max(h,o,c),l:Math.min(l,o,c)}}).filter(Boolean);
return pts.length?{pts,per,step}:null;
};
const chAutoPeriod=()=>{
if(!chBars)return null;
const fit=secs=>{const s=chSeries(secs);
if(!s||s.per!==secs)return null;
const span=s.pts[s.pts.length-1].b-s.pts[0].b+1;
return{n:s.pts.length,ok:s.pts.length>=6&&span<=CH_SLOTS}};
let best=null,bestN=-1;
for(const[,secs] of TFS){const f=fit(secs);
if(!f)continue;
if(f.ok)return secs;
if(f.n>bestN){best=secs;bestN=f.n}}
return best==null?chFinest():best;
};
let chTfKey="";
const chSyncPeriod=()=>{
const k=chPairKey(chEff());
if(k===chTfKey)return;
const auto=chAutoPeriod();
if(auto==null)return;
chTfKey=k;chPeriod=auto;
};
const chSyncTfs=()=>{
const finest=chFinest();
if(chPeriod<finest)chPeriod=finest;
for(const c of chTf.children){if(!c.dataset.secs)continue;
const secs=Number(c.dataset.secs);
c.disabled=!!chBars&&secs<finest;
c.title=c.disabled?"This pair has no five-minute tape to draw":"";
c.classList.toggle("on",secs===chPeriod)}
};
const chSyncOrientation=()=>{
const p=chEff(),k=chPairKey(p);
if(k===chInvKey)return;
chInvKey=k;chInv=chNaturalInv(p);
};
const hudFor=i=>{
if(!chDraw)return;
const{pts,num,UP,DN,ch,per,hd}=chDraw;
if(!hd)return;
if(i<0){const b=pts[pts.length-1];
let note="",flip="";
if(chInv&&isFinite(ch)&&ch>-100){
const other=(1/(1+ch/100)-1)*100;
const pr=chEff&&chEff();
const base=pr&&pr[1]?pr[1].sym:"the token";
note=` title="${base} is ${other>=0?"+":""}${other.toFixed(2)}% over this window. `
+`This chart prices the pair the other way up, so its line moves the opposite way."`;
flip=` <span class="chflip">${safeSym(base)} ${other>=0?"+":""}${other.toFixed(2)}%</span>`;
}
hd.innerHTML=`<b>${num(b.c)}</b> <span style="color:${ch>=0?UP:DN}"${note}>${ch>=0?"+":""}${ch.toFixed(2)}%</span>${flip}`;
return}
const b=pts[i],up=b.c>=b.o,d=new Date(b.b*per*1e3);
const when=per>=86400
?d.toLocaleDateString("en-US",{month:"short",day:"numeric"})
:d.toLocaleTimeString("en-US",{hour:"2-digit",minute:"2-digit",hour12:false});
hd.innerHTML=`<b style="color:${up?UP:DN}">${num(b.c)}</b> <span>${when}</span>`
+`<span class="ohlc">H ${num(b.h)} L ${num(b.l)} · ${b.n} trade${b.n===1?"":"s"}</span>`;
};
const chPoint=e=>{
if(!chDraw)return;
const r=chArt.getBoundingClientRect();
if(!r.width)return;
const{pts,W,PL,st,b0}=chDraw;
const x=(e.clientX-r.left)/r.width*W;
const at=b0+Math.round((x-PL)/st-.5);
let i=0,near=Infinity;
for(let j=0;j<pts.length;j++){const d=Math.abs(pts[j].b-at);
if(d<near){near=d;i=j}}
hudFor(i);
if(!chDraw.svg)return;
let el=chDraw.cross;
if(!el){
el=document.createElementNS("http://www.w3.org/2000/svg","line");
el.setAttribute("class","cross");
el.setAttribute("y1","0");el.setAttribute("y2","150");
el.setAttribute("stroke","currentColor");el.setAttribute("stroke-opacity",".35");
el.setAttribute("stroke-width",".8");
chDraw.svg.appendChild(el);chDraw.cross=el;
}
const cx=chDraw.X(i).toFixed(1);
el.setAttribute("x1",cx);el.setAttribute("x2",cx);
};
chArt.addEventListener("pointermove",chPoint);
chArt.addEventListener("pointerdown",chPoint);
chArt.addEventListener("pointerleave",()=>{
if(chDraw&&chDraw.cross){chDraw.cross.remove();chDraw.cross=null}
hudFor(-1)});
function drawChart(){
if(!chBars)return;
const W=340,H=150,PL=2,PR=42,PT=10,PB=14,VH=22;
const pw=W-PL-PR,ph=H-PT-PB-VH;
chSyncPeriod();chSyncOrientation();chSyncTfs();
const ser=chSeries(chPeriod);
if(!ser){chArt.innerHTML='<div class="msg">No trades in this window.</div>';chDraw=null;return}
const per=ser.per;
let pts=ser.pts;
const newest=pts[pts.length-1].b,cap=Math.floor(pw/3);
const kept=pts.filter(b=>newest-b.b<cap);
const clipped=kept.length<pts.length;
pts=kept;
if(chInv)pts=pts.map(b=>({...b,o:1/b.o,h:1/b.l,l:1/b.h,c:1/b.c}));
if(!pts.length){chArt.innerHTML='<div class="msg">No trades in this window.</div>';chDraw=null;return}
let hi=-Infinity,lo=Infinity,vm=0;
for(const b of pts){if(b.v>vm)vm=b.v}
if(chKind==="line"){
for(const b of pts){if(b.c>hi)hi=b.c;if(b.c<lo)lo=b.c}
}else{
let bh=-Infinity,bl=Infinity;
for(const b of pts){const t=Math.max(b.o,b.c),u=Math.min(b.o,b.c);
if(t>bh)bh=t;if(u<bl)bl=u}
const span=bh>bl?bh-bl:bh*.1;
const cutHi=Math.min(bh+span*CH_WICK_CAP,bh*2),
cutLo=Math.max(bl-span*CH_WICK_CAP,bl/2);
for(const b of pts){
const h=Math.min(b.h,cutHi),l=Math.max(b.l,cutLo);
if(h>hi)hi=h;if(l<lo)lo=l}
}
if(hi===lo){hi*=1.001;lo*=0.999}
const rawLo=lo;
let sp=hi-lo;hi+=sp*.09;
lo-=Math.min(sp*.09,rawLo>0?rawLo*.1:0);
if(lo<=0)lo=rawLo>0?rawLo*.9:0;
sp=hi-lo;
const n=pts.length,b0=pts[0].b,slots=pts[n-1].b-b0+1;
const st=pw/slots,bw=Math.max(1,Math.min(9,st*.68));
const X=i=>PL+st*(pts[i].b-b0+.5),Y=p=>PT+ph*(1-(p-lo)/sp),vt=PT+ph+6,VY=v=>vt+(VH-6)*(1-(vm?v/vm:0));
const num=v=>v>=1000?v.toLocaleString("en-US",{maximumFractionDigits:2}):v>=1?v.toFixed(3):fmtPx(v);
const UP="#16a34a",DN="#c33";
const p0=chEff(),lbl=p0?chLabel(p0):"Price";
let s=`<svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="none" role="img" aria-label="${lbl}, ${n} bars, ${num(pts[n-1].c)} now, range ${num(lo)} to ${num(hi)}">`;
for(let g=0;g<=3;g++){const p=lo+sp*g/3,y=Y(p).toFixed(1);
s+=`<line x1="${PL}" y1="${y}" x2="${W-PR}" y2="${y}" stroke="currentColor" stroke-opacity=".1"/>`
+`<text x="${W-PR+4}" y="${(Y(p)+2.5).toFixed(1)}" font-size="7" fill="currentColor" fill-opacity=".5">${p>0?num(p):"0"}</text>`}
for(let i=0;i<n;i++){const b=pts[i],y=VY(b.v);
s+=`<rect x="${(X(i)-bw/2).toFixed(1)}" y="${y.toFixed(1)}" width="${bw.toFixed(1)}" height="${Math.max(.4,vt+VH-y).toFixed(1)}" fill="currentColor" fill-opacity=".18"/>`}
const fl=chFloor>0?(chInv?chFloor:1/chFloor):0;
if(fl>0&&fl>=lo&&fl<=hi){
const fy=Y(fl).toFixed(1);
s+=`<line x1="${PL}" y1="${fy}" x2="${W-PR}" y2="${fy}" stroke="currentColor" stroke-opacity=".45" stroke-width=".7" stroke-dasharray="3 2"/>`
+`<text x="${PL+3}" y="${(Y(fl)-2.5).toFixed(1)}" font-size="6.5" fill="currentColor" fill-opacity=".6">floor</text>`;
}
const rise=pts[n-1].c>=pts[0].o;
if(chKind==="line"){
const d=pts.map((b,i)=>`${i?"L":"M"}${X(i).toFixed(1)} ${Y(b.c).toFixed(1)}`).join(" ");
const col=rise?UP:DN;
s+=`<path d="${d} L${X(n-1).toFixed(1)} ${(PT+ph).toFixed(1)} L${X(0).toFixed(1)} ${(PT+ph).toFixed(1)} Z" fill="${col}" fill-opacity=".1"/>`
+`<path d="${d}" fill="none" stroke="${col}" stroke-width="1.2" stroke-linejoin="round"/>`
+`<circle cx="${X(n-1).toFixed(1)}" cy="${Y(pts[n-1].c).toFixed(1)}" r="1.8" fill="${col}"/>`;
}else{
for(let i=0;i<n;i++){const b=pts[i],col=b.c>=b.o?UP:DN,x=X(i);
const t=Y(Math.max(b.o,b.c)),bo=Y(Math.min(b.o,b.c));
s+=`<line x1="${x.toFixed(1)}" y1="${Y(b.h).toFixed(1)}" x2="${x.toFixed(1)}" y2="${Y(b.l).toFixed(1)}" stroke="${col}" stroke-width=".8"/>`
+`<rect x="${(x-bw/2).toFixed(1)}" y="${t.toFixed(1)}" width="${bw.toFixed(1)}" height="${Math.max(.8,bo-t).toFixed(1)}" fill="${col}"/>`}
}
s+="</svg>";
const last=pts[n-1],first=pts[0],ch=((last.c-first.o)/first.o)*100;
chArt.innerHTML=s+'<div class="hd"></div>';
chDraw={pts,X,W,PL,st,b0,num,UP,DN,ch,per,
hd:chArt.querySelector(".hd"),svg:chArt.querySelector("svg"),cross:null};
hudFor(-1);
const took=rel(Math.max(per,(slots-1)*per));
const np=chPoolsAt(ser.step);
const fine=(chBars&&chBars[chFinest()])||null;
const newestBar=fine?fine.find(b=>b&&b.c>0):null;
const latest=newestBar?newestBar.c:pts[n-1].c;
const lastPx=pts[n-1].c;
const canon=latest>0?1/latest:0;
const mcap=chSupply>0&&canon>0?chSupply*canon:0;
chNote.textContent=p0?[chLabel(p0),
mcap>0?`${mcap>=1000?Math.round(mcap).toLocaleString("en-US"):mcap.toFixed(mcap>=1?1:3)} ETH mcap${
ethUsd>0?` (${fmtUsd(mcap*ethUsd)})`:""}`:"",
`${np} pool${np===1?"":"s"}`,`${clipped?"last ":""}${took}`].filter(Boolean).join(" · "):"";
if(p0){const was=chFloor+":"+chSupply;
chFloorFor(p0).then(()=>{if(chFloor+":"+chSupply!==was)drawChart()});}
}
for(const[label,secs]of TFS){const b=document.createElement("button");
b.textContent=label;b.dataset.secs=String(secs);if(secs===chPeriod)b.className="on";
b.onclick=()=>{chPeriod=secs;chTfKey=chPairKey(chEff());drawChart()};
chTf.appendChild(b)}
const kindBtn=document.createElement("button");
kindBtn.className="chk";
const syncKind=()=>{kindBtn.textContent=chKind==="candle"?"Line":"Candle";
kindBtn.title=`Switch to ${kindBtn.textContent.toLowerCase()} chart`};
syncKind();
kindBtn.onclick=()=>{chKind=chKind==="candle"?"line":"candle";LS.ck=chKind;syncKind();drawChart()};
chTf.appendChild(kindBtn);
const invBtn=document.createElement("button");
invBtn.className="chk";
invBtn.textContent="⇅";
const syncInv=()=>{const p=chEff();
invBtn.title=p?`Price ${chInv?p[1].sym:p[0].sym} instead — currently ${chInv?p[0].sym:p[1].sym} per ${chInv?p[1].sym:p[0].sym}`:"Invert the price";
invBtn.setAttribute("aria-pressed",chInv?"true":"false")};
syncInv();
invBtn.onclick=()=>{chInv=!chInv;syncInv();drawChart()};
chTf.appendChild(invBtn);
const syncChart=()=>{chSyncOrientation();syncInv();loadChart()};
chTog.onclick=()=>{chOpen=!chOpen;LS.ch=chOpen?"1":"0";syncChartUI()};
const lsecs=v=>{const m=/^(\d+)([mhdw]?)$/.exec((v||"").trim());
return m?+m[1]*(m[2]=="d"?86400:m[2]=="h"?3600:m[2]=="w"?604800:m[2]=="m"?60:1):NaN};
const symAddr=x=>{const m=TOKENS.filter(t=>t.sym.toLowerCase()===x);
return m.length===1?m[0].addr.toLowerCase():""};
let lSeq=0;
async function applyLink(){
const q=new URLSearchParams(location.hash.slice(1));
if(![...q.keys()].length)return;
const my=++lSeq;
const at=a=>a?TOKENS.findIndex(t=>t.addr.toLowerCase()===a):-1;
const tok=async v=>{if(!v)return "";const x=v.trim().toLowerCase();
if(!/^0x[0-9a-f]{