memoscan
← all memos

Memo 0x119f7136…fb61fa on Ethereum

0xacfba7ce…4082·#25,751,070·view on Etherscan
cUint(a1) +encUint(minLP)+encAddr(to); const tx={from:account,to:PFACTORY,data,value:native?toHex(a0):"0x0"}; stat.textContent="Creating the band..."; await rpc(C,[tx,L]);await checkWallet();await settle(await rpc(S,[tx])); stat.textContent="Band created."; loadLq();refreshBalance(); }catch(e){err(e);out.textContent="";} finally{--busy;go.disabled=false} } async function lqApprove(token,need){ const al=BigInt(await rpc(C,[{to:token,data:"0x"+SEL_ALLOWANCE+encAddr(account)+encAddr(PFACTORY)},L])); if(al>=need)return; if(al)await settle(await rpc(S,[{from:account,to:token, data:"0x"+SEL_APPROVE+encAddr(PFACTORY)+encUint(0n),value:"0x0"}])); await settle(await rpc(S,[{from:account,to:token, data:"0x"+SEL_APPROVE+encAddr(PFACTORY)+encUint(need),value:"0x0"}])); } lqList.onclick=async ev=>{ if(ev.target.id==="lqCreate"){await lqCreate();return} if(ev.target.id==="lqNewTog"){ const pr=chPair();if(!pr)return; ev.target.outerHTML=lqNewForm(pr[0],pr[1]); lqSyncAmounts(); lqNewPreview(); lqList.querySelector(".lqnew")?.scrollIntoView?.({block:"nearest"}); return} const b=ev.target.closest("[data-act]"); if(!b||busy)return; const p=chPair(); if(!p)return; const [t0,t1]=p,pool=b.dataset.pool; const row=lqRows.find(x=>x.pool.toLowerCase()===pool.toLowerCase()); if(!row)return; const act=b.dataset.act; if(act!=="a"&&!account)return void(stat.textContent="Connect a wallet first."); if(act==="a"){ const box=b.closest(".lqrow").querySelector(".lqadd"); box.classList.toggle("hide"); b.textContent=box.classList.contains("hide")?"Add":"Cancel"; if(!box.classList.contains("hide"))box.querySelector(".lqin").focus(); return; } if(act==="w"){ if(row.shares===0n)return; await doRemove(pool,row.shares,t0,t1); return; } if(act==="ac"){ const box=b.closest(".lqadd"); const ins=box.querySelectorAll(".lqin"); if(isZap(box)){ const z=zapSide(box,t0,t1); if(z.err){stat.textContent=z.err;return} if(!z.amt)return; await doZap(pool,z.tok,z.amt); return; } let a0,a1; try{ a0=parseUnits(ins[0].value.trim(),t0.dec);a1=parseUnits(ins[1].value.trim(),t1.dec); }catch{stat.textContent="Invalid amount";return} if(!a0||!a1)return; await doAdd(pool,a0,a1,t0,t1); } }; /* The docs, behind a footer link rather than a seventh icon: this is a reference, not a mode, and the meta row is already full. */ footDoc.setAttribute("aria-expanded","false"); footDoc.onclick=e=>{ e.preventDefault(); const on=docPanel.classList.contains("hide"); footDoc.setAttribute("aria-expanded",on?"true":"false"); docPanel.classList.toggle("hide",!on); footDoc.textContent=on?"hide":"how it works"; if(on)docPanel.scrollIntoView({behavior:"smooth",block:"nearest"}); }; lq.onclick=()=>lqSet(!lqMode); const PLAUNCH="0x0000002fC8E77585A008Aa45d78A71ad36293aEe"; const SEL_LAUNCH="6a648dc6"; const SEL_LAUNCHART="72cf7bd8"; const SEL_CREATOROF="dea5c2e0"; const LN_MIME={"image/png":0,"image/webp":1,"image/svg+xml":2,"image/gif":3,"image/jpeg":4}; const MIME_NAME=["image/png","image/webp","image/svg+xml","image/gif","image/jpeg","image/avif"]; const LN_MAX_ART=24575; const LN_BUDGET=8192; const LN_MAX_ALLOC=2000; const LN_MIN_POOLED=2000000000000n; const LN_MIN_MCAP=1000000000000n; const lnHex=b=>{let h="";for(const x of b)h+=x.toString(16).padStart(2,"0");return h}; const lnTail=b=>{const h=lnHex(b);return encUint(b.length)+h.padEnd(Math.ceil(h.length/64)*64,"0")}; const encLaunch=(n,y,u,sup,alloc,mcap,own,art,mime)=>{ const T=new TextEncoder(); const dyn=[T.encode(n),T.encode(y),T.encode(u)]; const statics=[encUint(sup),encUint(alloc),encUint(mcap),encAddr(own)]; if(art){dyn.push(art);statics.push(encUint(mime))} const words=statics.length+(art?4:3); let off=words*32,head="",tail=""; const heads=[]; for(const b of dyn){heads.push(encUint(off));off+=lnTail(b).length/2;tail+=lnTail(b)} head=heads.slice(0,3).join("")+statics.slice(0,4).join("") +(art?heads[3]+statics[4]:""); return "0x"+(art?SEL_LAUNCHART:SEL_LAUNCH)+head+tail; }; let lnMode=false,lnArtBytes=null,lnArtMime=0; function lnSet(on){ lnMode=on; if(on&&lqMode)lqSet(false); ln.setAttribute("aria-pressed",on?"true":"false"); ln.classList.toggle("on",on); for(const el of [rate,flip,swap])if(el)el.classList.toggle("hide",on); for(const el of [document.querySelector(".panel"),rcvPanel])if(el)el.classList.toggle("hide",on); document.querySelector(".card").classList.toggle("lnon",on); for(const el of [lnPanel,lnNote,lnGo])el.classList.toggle("hide",!on); rc.value="";rc.classList.remove("bad");rcvEl.textContent=""; rc.title=on?"Who the token belongs to: receives the creator allocation, collects the creator share of trading fees for the life of the token, and may edit its name, symbol 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); 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 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 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]}; async function loadChart(){ chBars=null;chPools=null; const p=chPair(); if(tab!=="swap"||!account||!p){syncChartUI();return} const my=++chSeq,[t0,t1]=p,key=t0.addr+":"+t1.addr; const hit=chCache[key]; if(hit&&Date.now()-hit.at<30000){chBars=hit.bars;chPools=hit.pools;syncChartUI();return} try{ if(PPLENS===ZERO||(await rpc("eth_getCode",[PPLENS,L])).length<5)return syncChartUI(); const [raw]=await mc3([{to:PPLENS,data:"0x"+SEL_MARKETS+encAddr(t0.addr)+encAddr(t1.addr) +encAddr(account)+encUint(0)+encUint(LQ_SCAN)+encUint(0)}]); if(my!==chSeq)return; 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); const pools=found.slice(0,CH_POOLS).map(x=>x.a); if(!pools.length){chCache[key]={at:Date.now(),bars:null,pools: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}; }catch{chBars=null;chPools=null} 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}`; function syncChartUI(){ const on=tab==="swap"&&!!account&&!!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=chPair(); 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 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(chPair()); 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=chPair(),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]; hd.innerHTML=`<b>${num(b.c)}</b> <span style="color:${ch>=0?UP:DN}">${ch>=0?"+":""}${ch.toFixed(2)}%</span>`; 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.h>hi)hi=b.h;if(b.l<lo)lo=b.l;if(b.v>vm)vm=b.v} if(hi===lo){hi*=1.001;lo*=0.999} let sp=hi-lo;hi+=sp*.09;lo-=sp*.09;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):v.toPrecision(3); const UP="#16a34a",DN="#c33"; const p0=chPair(),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">${num(p)}</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 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(slots*per); const np=chPoolsAt(ser.step); chNote.textContent=p0?`${chLabel(p0)} · ${np} pool${np===1?"":"s"} · ${clipped?"last ":""}${took}`:""; } 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(chPair());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=chPair(); 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=!chI