memoscan
← all memos

Memo 0x69b20bc5…4f28f4 on Ethereum

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]{