0xacfba7ce…4082·#25,751,351·view on Etherscan
class="lqin" data-side="0" inputmode="decimal" autocomplete="off" spellcheck="false" placeholder="0.0 ${t0.sym}" aria-label="${t0.sym} to deposit">`
+`<input class="lqin" data-side="1" inputmode="decimal" autocomplete="off" spellcheck="false" placeholder="0.0 ${t1.sym}" aria-label="${t1.sym} to deposit">`
+`<div class="lqpv"></div>`
+`<button class="lqbtn" data-act="ac" ${p} disabled>Add liquidity</button></div>`
+`</div>`;
}
const LQ_FEES=[[500,"0.05%","pairs that track each other"],
[3000,"0.30%","most pairs"],[10000,"1.00%","thin or volatile"]];
const LQ_RANGES=[["full","Full range"],["100","Wide — 100×"],["10","10×"],
["2","2×"],["1.25","Tight — 25%"],["custom","Custom prices"]];
function lqNewForm(t0,t1){
return `<div class="lqnew" data-t0="${t0.addr}" data-t1="${t1.addr}">`
+`<label class="dly" for="lqRange">Range<select id="lqRange">`
+LQ_RANGES.map(([v,l])=>`<option value="${v}"${v==="full"?" selected":""}>${l}</option>`).join("")
+`</select></label>`
+`<label class="dly hide" id="lqCustom">Prices<span class="r">`
+`<input id="lqLo" inputmode="decimal" autocomplete="off" spellcheck="false" placeholder="Low" aria-label="Lowest price in the band">`
+`<input id="lqHi" inputmode="decimal" autocomplete="off" spellcheck="false" placeholder="High" aria-label="Highest price in the band"></span></label>`
+`<label class="dly hide" id="lqPxRow" for="lqPx">Opening price`
+`<input id="lqPx" inputmode="decimal" autocomplete="off" spellcheck="false" placeholder="${t1.sym} per ${t0.sym}" aria-label="Opening price"></label>`
+`<label class="dly" for="lqFee">Fee<select id="lqFee">`
+LQ_FEES.map(([v,l,h])=>`<option value="${v}"${v===3000?" selected":""}>${l} — ${h}</option>`).join("")
+`</select></label>`
+`<div class="lqout" id="lqRangeOut"></div>`
+`<div class="lqout" id="lqPv"></div>`
+`<div class="lqout" id="lqOwnNote"></div>`
+`<div class="lqout" id="lqGas"></div>`
+`<button class="primary" id="lqCreate" disabled>Create band</button></div>`;
}
const pxToSqrt=(px,d0,d1)=>{
const raw=px*10**(d1-d0);
if(!isFinite(raw)||raw<=0)return 0n;
return BigInt(Math.round(Math.sqrt(raw)*1e9))*10n**9n;
};
const lqMarket=(t0,t1,lo,hi,fee,owner)=>encAddr(t0)+encAddr(t1)+encUint(lo)+encUint(hi)
+encUint(fee)+encAddr(ZERO)+encAddr(owner)+encUint(0n);
const lqSeedPrice=(sl,sh,in0,in1)=>{
const L=Number(sl),H=Number(sh),x=Number(in0),y=Number(in1),W=1e18;
const g=s=>(x*s*H*(s-L))/((H-s)*W*y*W);
let lo=L,hi=H;
for(let i=0;i<90;i++){const m=(lo+hi)/2;if(g(m)<1)lo=m;else hi=m}
const s=(lo+hi)/2;
if(!(s>L&&s<H))return 0n;
return BigInt(Math.round(s));
};
const lqBandFor=(px,mult,a0,a1)=>{
if(a0&&!a1)return[px,px*mult];
if(a1&&!a0)return[px/mult,px];
return[px/mult,px*mult];
};
const LQ_MIN_LIQUIDITY=1000n,LQ_MIN_RESOLUTION=1000000n,LQ_WAD=10n**18n,
LQ_MAX_LIQUIDITY=(1n<<128n)-1n,LQ_MAX_SQRT=10n**36n,LQ_SEED_TOLERANCE=100n;
const lqUp=(a,b,c)=>(a*b+c-1n)/c;
const LQ_NOSEED={ok:false,lp:0n,used0:0n,used1:0n};
function lqSeedQuote(sl,sh,s,a0,a1){
if(!(sl>0n)||!(sh>sl)||sh>LQ_MAX_SQRT||s<sl||s>sh)return LQ_NOSEED;
const f0=()=>a0*s/(sh-s)*sh/LQ_WAD,f1=()=>a1*LQ_WAD/(s-sl);
let total;
if(s===sl)total=f0();
else if(s===sh)total=f1();
else{const x=f0(),y=f1();total=x<y?x:y}
if(total===0n||total>LQ_MAX_LIQUIDITY)return LQ_NOSEED;
let used0=lqUp(total,(sh-s)*LQ_WAD,sh*s),used1=lqUp(total,s-sl,LQ_WAD);
const v0=total*LQ_WAD/sh,v1=total*sl/LQ_WAD;
if(v0<LQ_MIN_RESOLUTION||v1<LQ_MIN_RESOLUTION)return LQ_NOSEED;
let x=v0+used0,y=v1+used1;
const maxX=y*LQ_WAD*LQ_WAD/(sl*sl);
if(x>maxX){used0=maxX>v0?maxX-v0:0n;x=v0+used0}
const maxY=x*sh*sh/(LQ_WAD*LQ_WAD);
if(y>maxY){used1=maxY>v1?maxY-v1:0n;y=v1+used1}
if(used0>a0||used1>a1||total<=LQ_MIN_LIQUIDITY)return LQ_NOSEED;
const wantSq=s*s,gotSq=x===0n?0n:y*LQ_WAD*LQ_WAD/x,slack=wantSq/LQ_SEED_TOLERANCE;
if(gotSq<wantSq-slack||gotSq>wantSq+slack)return LQ_NOSEED;
return {ok:true,lp:total-LQ_MIN_LIQUIDITY,used0,used1};
}
const LQ_MAX128=(1n<<128n)-1n,LQ_FEE_DENOM=1000000n;
const lqInBand=(sl,sh,vX,vY)=>{
if(vX<=0n)return false;
const ratio=vY*LQ_WAD*LQ_WAD/vX,hi=sh+1n;
return ratio>=sl*sl&&ratio<hi*hi;
};
function lqExecutable(sl,sh,fee,augIn,augOut,rIn,rOut,zeroForOne){
if(augOut<=1n)return false;
let need=lqUp(augIn,1n,augOut-1n);
if(need===0n)need=1n;
if(need>LQ_MAX128)return false;
const net=lqUp(need,LQ_FEE_DENOM,LQ_FEE_DENOM-fee);
if(net>LQ_MAX128)return false;
const priced=net*(LQ_FEE_DENOM-fee)/LQ_FEE_DENOM;
if(priced===0n)return false;
if(net>LQ_MAX128-rIn)return false;
const out=priced*augOut/(augIn+priced);
if(out===0n||out>rOut)return false;
return zeroForOne?lqInBand(sl,sh,augIn+net,augOut-out):lqInBand(sl,sh,augOut-out,augIn+net);
}
const lqTradeable=(sl,sh,fee,x,y,r0,r1)=>
(r1!==0n&&lqExecutable(sl,sh,fee,x,y,r0,r1,true))
||(r0!==0n&&lqExecutable(sl,sh,fee,y,x,r1,r0,false));
function lqSeedAdmits(sl,sh,s,fee,q){
if(!q||!q.ok)return false;
const total=q.lp+LQ_MIN_LIQUIDITY;
if(lqUp(total,(sh-s)*LQ_WAD,sh*s)>LQ_MAX128)return false;
if(lqUp(total,s-sl,LQ_WAD)>LQ_MAX128)return false;
const x=total*LQ_WAD/sh+q.used0,y=total*sl/LQ_WAD+q.used1;
if(!lqInBand(sl,sh,x,y))return false;
return lqTradeable(sl,sh,fee,x,y,q.used0,q.used1);
}
const lqBandFits=(sl,sh,sp,fee,in0,in1)=>
lqSeedAdmits(sl,sh,sp,fee,lqSeedQuote(sl,sh,sp,in0,in1));
const LQ_WIDTHS=[1000,100,10,2];
function lqFullWidth(d0,d1,a0,a1,px,fee){
for(const mult of LQ_WIDTHS){
const [lo,hi]=lqBandFor(px,mult,a0,a1);
const sl=pxToSqrt(lo,d0,d1),sh=pxToSqrt(hi,d0,d1);
if(!sl||!sh||sh<=sl)continue;
const sp=a0&&!a1?sl:a1&&!a0?sh:lqSeedPrice(sl,sh,a0,a1);
if(sp&&lqBandFits(sl,sh,sp,fee,a0,a1))return mult;
}
return null;
}
async function loadLq(){
const p=chPair();
const my=++lqSeq;
if(!p){lqList.innerHTML=`<div class="lqempty">Pick two different tokens.</div>`;lqSub.textContent="";lqRows=[];return}lqSyncAmounts();
const [t0,t1]=p;
lqSub.textContent=`${t0.sym} / ${t1.sym}`;
lqList.innerHTML=`<div class="lqempty">Loading…</div>`;
try{
const who=account||ZERO;
const [raw,c]=await mc3([
{to:PPLENS,data:"0x"+SEL_MARKETS+encAddr(t0.addr)+encAddr(t1.addr)
+encAddr(who)+encUint(0)+encUint(LQ_SCAN)+encUint(0)},
{to:PFACTORY,data:"0x"+SEL_PAIR_COUNT+encAddr(t0.addr)+encAddr(t1.addr)},
]);
if(my!==lqSeq)return;
let total=0;try{if(c)total=Number(BigInt(c))}catch{}
const rows=[];
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));
rows.push({pool:"0x"+h.slice(base+(i*PI_WORDS+PI_POOL)*64+24,base+(i*PI_WORDS+PI_POOL+1)*64),
sl:at(PI_SL),sh:at(PI_SH),fee:at(PI_FEE),r0:at(PI_R0),r1:at(PI_R1),px:at(PI_PX),
liq:at(PI_LIQ),hook:"0x"+h.slice(base+(i*PI_WORDS+PI_HOOK)*64+24,base+(i*PI_WORDS+PI_HOOK+1)*64),
shares:0n,mine0:0n,mine1:0n})}}
if(account&&rows.length){
const bs=await mc3Deep(rows.map(r=>({to:r.pool,data:"0x"+SEL_BALANCEOF+encAddr(account)})));
if(my!==lqSeq)return;
rows.forEach((r,i)=>{
if(!bs[i]||r.liq===0n)return;
const s=dec1(bs[i],0);
r.shares=s;r.mine0=s*r.r0/r.liq;r.mine1=s*r.r1/r.liq;
});
}
if(my!==lqSeq)return;
if(!rows.length){
lqList.innerHTML=`<div class="lqempty">No Precision band for this pair yet.</div>`+lqNewForm(t0,t1);lqSyncAmounts();
lqRows=[];
lqNewPreview();
return}
rows.sort((a,b)=>(b.shares>0n)-(a.shares>0n)||(b.liq>a.liq?1:b.liq<a.liq?-1:0));
const shown=rows.slice(0,LQ_SHOW);
if(total>shown.length)lqSub.textContent+=` · showing ${shown.length} of ${total}`
+(total>LQ_SCAN?` (first ${LQ_SCAN} scanned)`:"");
lqRows=shown;
lqList.innerHTML=shown.map(o=>lqRow(o,t0,t1)).join("")
+`<button class="lqbtn lqnewtog" id="lqNewTog">Create a new band</button>`;
lqSyncAmounts();
}catch(e){
if(my!==lqSeq)return;
lqRows=[];
lqList.innerHTML=`<div class="lqempty">Could not read pools.</div>`;lqSyncAmounts();
}
}
const SEL_REMOVE="e39b0eb5",SEL_ADDEXACT="cc0025e4",SEL_PREVIEW_REMOVE="a2eaee07",
SEL_PREVIEW_ADD="e03ec807";
const dec1=(h,i)=>BigInt("0x"+strip0x(h).slice(i*64,(i+1)*64));
async function previewRemove(pool,shares){
const [r]=await mc3([{to:PLQLENS,data:"0x"+SEL_PREVIEW_REMOVE+encAddr(pool)+encUint(shares)}]);
if(!r)return null;
return dec1(r,0)===1n?{a0:dec1(r,1),a1:dec1(r,2)}:null;
}
async function previewAdd(pool,a0,a1){
const [r]=await mc3([{to:PLQLENS,data:"0x"+SEL_PREVIEW_ADD+encAddr(pool)+encUint(a0)+encUint(a1)}]);
if(!r)return null;
return dec1(r,0)===1n?{lp:dec1(r,1),used0:dec1(r,2),used1:dec1(r,3),ref0:dec1(r,4),ref1:dec1(r,5)}:null;
}
async function lqTo(){
const v=rc.value.trim();
if(!v)return account;
const to=await rcvOf(v).catch(()=>"");
if(!to){rc.classList.add("bad");
stat.textContent="Recipient must be an address or a .wei / .gwei / .eth name that resolves.";
return null}
rc.classList.remove("bad");
return to;
}
let lqToSeq=0;
async function lqShowTo(){
const my=++lqToSeq,v=rc.value.trim();
if(!v){rc.classList.remove("bad");rcvEl.textContent="";return}
rcvEl.textContent="resolving...";
const to=await rcvOf(v).catch(()=>"");
if(my!==lqToSeq)return;
rc.classList.toggle("bad",!to);
rcvEl.textContent=to||"";
}
async function previewZap(pool,tokenIn,amountIn){
const [r]=await mc3([{to:PLQLENS,data:"0x"+SEL_PREVIEW_ZAP+encAddr(pool)+encAddr(tokenIn)+encUint(amountIn)}]);
if(!r)return null;
return dec1(r,0)===1n?{portion:dec1(r,1),lp:dec1(r,2)}:null;
}
const lessSlip=v=>{const b=BigInt(Math.round(Math.min(10,Math.max(.01,+slip.value||.5))*100));
return v-(v*b)/10000n};
async function ensureAllowance(token,spender,need){
if(token===ZERO||need===0n)return;
const [r]=await mc3([{to:token,data:"0x"+SEL_ALLOWANCE+encAddr(account)+encAddr(spender)}]);
if(r&&dec1(r,0)>=need)return;
stat.textContent="Approving…";
if(r&&dec1(r,0)>0n)await waitTx(await rpc(S,[{from:account,to:token,
data:"0x"+SEL_APPROVE+encAddr(spender)+encUint(0),value:"0x0"}]));
await waitTx(await rpc(S,[{from:account,to:token,
data:"0x"+SEL_APPROVE+encAddr(spender)+encUint(need),value:"0x0"}]));
}
async function factoryPool(pool){
const [r]=await mc3([{to:PFACTORY,data:"0x"+SEL_ISPOOL+encAddr(pool)}]).catch(()=>[null]);
return !!r&&dec1(r,0)===1n;
}
const SEL_REMOVE_LOSSY="0cc55f06";
const encLossy=(shares,to,take0,take1)=>"0x"+SEL_REMOVE_LOSSY+encUint(shares)
+encUint(0n)+encUint(0n)+encAddr(to)+encBool(take0)+encBool(take1);
async function lossyExit(pool,shares,t0,t1){
const attempt=async(take0,take1)=>{
const req={from:account,to:pool,value:"0x0",data:encLossy(shares,account,take0,take1)};
try{await rpc(C,[req,L]);return req}catch{return null}
};
const both=await attempt(true,true);
if(both)return{req:both,note:`This pool cannot pay out in full — one of its`
+` tokens is short, or takes a fee on transfer.\n\nYou would receive your share`
+` of what it can still pay, on both sides, which is less than the amounts`
+` shown.\n\nContinue?`};
for(const [take0,take1,keep,drop] of [[true,false,t0,t1],[false,true,t1,t0]]){
const req=await attempt(take0,take1);
if(!req)continue;
return{req,note:`The ${drop.sym} side of this pool cannot be transferred.\n\n`
+`You can still leave through ${keep.sym} alone — but doing so GIVES UP your`
+` ${drop.sym} claim permanently, to the holders who remain.\n\nContinue?`};
}
return null;
}
async function doRemove(pool,shares,t0,t1){
const pv=await previewRemove(pool,shares);
if(!pv){stat.textContent="This amount redeems nothing — the pool would refuse it.";return}
if(!await factoryPool(pool)){stat.textContent="Not a Precision pool — refusing to send.";return}
++busy;
try{
await checkWallet();
stat.textContent="Withdrawing…";
let req={from:account,to:pool,value:"0x0",
data:"0x"+SEL_REMOVE+encUint(shares)+encUint(lessSlip(pv.a0))+encUint(lessSlip(pv.a1))+encAddr(account)};
try{await rpc(C,[req,L])}
catch(strict){
const alt=await lossyExit(pool,shares,t0,t1);
if(!alt)throw strict;
let go=false;
try{go=confirm(alt.note)}
catch{go=false}
if(!go){stat.textContent="Withdrawal cancelled.";return}
req=alt.req;
stat.textContent="Withdrawing what the pool can still pay…";
}
await settle(await rpc(S,[req]));
await loadLq();refreshBalance();
}catch(e){err(e)}
finally{--busy}
}
async function doAdd(pool,a0,a1,t0,t1){
const to=await lqTo();
if(!to)return;
const pv=await previewAdd(pool,a0,a1);
if(!pv){stat.textContent="The pool would refuse this deposit.";return}
if(!await factoryPool(pool)){stat.textContent="Not a Precision pool — refusing to send.";return}
++busy;
try{
await checkWallet();
await ensureAllowance(t0.addr,pool,a0);
await ensureAllowance(t1.addr,pool,a1);
stat.textContent="Adding…";
const value=t0.addr===ZERO?a0:0n;
const req={from:account,to:pool,value:toHex(value),
data:"0x"+SEL_ADDEXACT+encUint(0)+encUint(a0)+encUint(a1)+encUint(lessSlip(pv.lp))+encAddr(to)};
await rpc(C,[req,L]);
await settle(await rpc(S,[req]));
await loadLq();refreshBalance();
}catch(e){err(e)}
finally{--busy}
}
async function doZap(pool,tok,amountIn){
const to=await lqTo();
if(!to)return;
const pv=await previewZap(pool,tok.addr,amountIn);
if(!pv||pv.lp===0n){stat.textContent="The pool would refuse this deposit.";return}
if(!await factoryPool(pool)){stat.textContent="Not a Precision pool — refusing to send.";return}
++busy;
try{
await checkWallet();
const minLp=lessSlip(pv.lp);
const zap="0x"+SEL_ZAPIN+encAddr(pool)+encAddr(tok.addr)+encUint(amountIn)
+encUint(pv.portion)+encUint(minLp)+encAddr(to)+encAddr(account);
const leg=encSnwap(tok.addr,amountIn,to,pool,minLp,PROUTE,zap);
let data,value;
if(tok.addr===ZERO){data=leg;value=toHex(amountIn)}
else{
stat.textContent="Approving…";
await ensureAllowance(tok.addr,ZROUTER,amountIn);
data=encCalls([
encSnwap(ZERO,0n,account,ZERO,0n,PROUTE,
"0x"+SEL_PCHECKPOINT+encAddr(tok.addr)+strip0x(keccak(hexToBytes(zap)))+encAddr(account)),
leg]);
value="0x0";
}
stat.textContent="Zapping…";
const req={from:account,to:ZROUTER,value,data};
await rpc(C,[req,L]);
await settle(await rpc(S,[req]));
await loadLq();refreshBalance();
}catch(e){err(e)}
finally{--busy}
}
let lqPvT,lqPvSeq=0;
async function lqPreview(box){
const pool=box.closest(".lqrow")?.dataset.pool||"";
const row=lqRows.find(x=>x.pool.toLowerCase()===pool.toLowerCase());
const p=chPair();
const out=box.querySelector(".lqpv"),go=box.querySelector('[data-act="ac"]');
if(!row||!p){out.textContent="";go.disabled=true;return}
const [t0,t1]=p,ins=box.querySelectorAll(".lqin");
let a0,a1;
try{
a0=ins[0].value.trim()?parseUnits(ins[0].value.trim(),t0.dec):0n;
a1=ins[1].value.trim()?parseUnits(ins[1].value.trim(),t1.dec):0n;
}catch{out.textContent="Invalid amount.";go.disabled=true;return}
if(isZap(box)){
const z=zapSide(box,t0,t1);
if(z.err){out.textContent=z.err;go.disabled=true;return}
if(!z.amt){out.textContent=`Enter an amount of ${t0.sym} or ${t1.sym}.`;go.disabled=true;return}
const mz=++lqPvSeq;
out.textContent="Checking…";go.disabled=true;
const zp=await previewZap(row.pool,z.tok.addr,z.amt).catch(()=>null);
if(mz!==lqPvSeq)return;
if(!zp||zp.lp===0n){out.textContent="The pool would refuse this deposit.";go.disabled=true;return}
out.textContent=`swaps ${trimAmt(zp.portion,z.tok.dec)} of ${trimAmt(z.amt,z.tok.dec)} ${z.tok.sym}`
+` for ${t1.sym}, then deposits both · mints ~${trimAmt(zp.lp,18)} LP`;
go.disabled=false;return;
}
if(!a0||!a1){
out.textContent="Enter both amounts.";go.disabled=true;return}
const my=++lqPvSeq;
out.textContent="Checking…";go.disabled=true;
const pv=await previewAdd(row.pool,a0,a1).catch(()=>null);
if(my!==lqPvSeq)return;
if(!pv){out.textContent="The pool would refuse this deposit.";go.disabled=true;return}
const ref=(pv.ref0>0n?` · refunds ${trimAmt(pv.ref0,t0.dec)} ${t0.sym}`:"")
+(pv.ref1>0n?` · refunds ${trimAmt(pv.ref1,t1.dec)} ${t1.sym}`:"");
out.textContent=`deposits ${trimAmt(pv.used0,t0.dec)} ${t0.sym} + ${trimAmt(pv.used1,t1.dec)} ${t1.sym}${ref}`;
go.disabled=false;
}
const isZap=box=>!!box.querySelector(".lqz")?.checked;
function zapSide(box,t0,t1){
const ins=box.querySelectorAll(".lqin");
const v0=ins[0].value.trim(),v1=ins[1].value.trim();
if(v0&&v1)return{err:`One side only — clear the ${t1.sym} field, or turn one-sided off.`};
if(!v0&&!v1)return{err:`Enter an amount of ${t0.sym} or ${t1.sym}.`};
const tok=v0?t0:t1;
try{return{tok,amt:parseUnits(v0||v1,tok.dec)}}catch{return{err:"Invalid amount."}}
}
function lqMirror(box,src){
if(isZap(box))return;
const pool=box.closest(".lqrow")?.dataset.pool||"";
const row=lqRows.find(x=>x.pool.toLowerCase()===pool.toLowerCase());
const p=chPair();
if(!row||!p||!row.r0||!row.r1)return;
const [t0,t1]=p,ins=box.querySelectorAll(".lqin");
const i=src===ins[0]?0:src===ins[1]?1:-1;
if(i<0)return;
const other=ins[1-i],v=src.value.trim();
if(!v){other.value="";return}
try{
const raw=parseUnits(v,i===0?t0.dec:t1.dec);
other.value=formatUnits(i===0?raw*row.r1/row.r0:raw*row.r0/row.r1,i===0?t1.dec:t0.dec);
}catch{}
}
lqList.addEventListener("input",e=>{
if(!e.target.classList.contains("lqin"))return;
const box=e.target.closest(".lqadd");
if(box){lqMirror(box,e.target);clearTimeout(lqPvT);lqPvT=setTimeout(()=>lqPreview(box),300);return}
if(e.target.closest(".lqnew")){clearTimeout(lqPvT);lqPvT=setTimeout(lqNewPreview,300)}
});
lqList.addEventListener("change",e=>{
if(e.target.id==="lqFee"||e.target.id==="lqRange")return lqNewPreview();
if(!e.target.classList.contains("lqz"))return;
const box=e.target.closest(".lqadd"),ins=box.querySelectorAll(".lqin");
const on=e.target.checked;
if(!on)lqMirror(box,ins[0]);
box.querySelector('[data-act="ac"]').textContent=on?"Zap in":"Add liquidity";
lqPreview(box);
});
let lqNewSeq=0;
let lqPvT2;
const lqPreviewSoon=()=>{clearTimeout(lqPvT2);lqPvT2=setTimeout(lqNewPreview,320)};
async function lqNewPreview(){
const box=lqList.querySelector(".lqnew");
if(!box)return;
const out=box.querySelector("#lqPv"),go=box.querySelector("#lqCreate");
const bandOut=box.querySelector("#lqRangeOut");
const my=++lqNewSeq;
go.disabled=true;
const pair=chPair();
if(!pair){out.textContent="";return}
const [t0,t1]=pair;
const range=box.querySelector("#lqRange").value;
box.querySelector("#lqCustom").classList.toggle("hide",range!=="custom");
const payTok=TOKENS[fromSel.value];
const payIs0=!!payTok&&payTok.addr.toLowerCase()===t0.addr.toLowerCase();
const el0=payIs0?amt:outAmt,el1=payIs0?outAmt:amt;
let a0=0n,a1=0n;
try{
const v0=el0.value.trim(),v1=el1.value.trim();
a0=v0?parseUnits(v0,t0.dec):0n;a1=v1?parseUnits(v1,t1.dec):0n;
}catch{out.textContent="Check the amounts.";bandOut.textContent="";return}
const oneSided=(a0&&!a1)?0:(a1&&!a0)?1:-1;
box.querySelector("#lqPxRow").classList.toggle("hide",oneSided<0);
if(!a0&&!a1){
bandOut.textContent="";
out.textContent=`Enter how much ${t0.sym} and ${t1.sym} to deposit — the ratio sets the opening price.`;
return}
const pxIn=box.querySelector("#lqPx").value.trim();
const px=oneSided<0
?(Number(a1)/10**t1.dec)/(Number(a0)/10**t0.dec)
:(pxIn?+pxIn:NaN);
if(!(px>0)){
bandOut.textContent="";
const only=oneSided===0?t0.sym:t1.sym;
out.textContent=`${/^[AEIOU]/i.test(only)?"An":"A"} ${only}-only band has no ratio to`
+` read a price from — name one.`;
return}
const fee=BigInt(box.querySelector("#lqFee").value);
let lo,hi;
if(range==="custom"){
const n=id=>{const v=box.querySelector("#"+id).value.trim();return v?+v:NaN};
lo=n("lqLo");hi=n("lqHi");
if(!(lo>0)||!(hi>0)){bandOut.textContent="";out.textContent="Name both prices.";return}
if(lo>=hi){out.textContent="The low price must be below the high one.";bandOut.textContent="";return}
if(oneSided<0&&(px<lo||px>hi)){
out.textContent=`These amounts imply ${fmtPx(px)}, which is outside that band.`;return}
}else{
let mult=+range;
if(range==="full"){
const found=lqFullWidth(t0.dec,t1.dec,a0,a1,px,fee);
if(!found){
bandOut.textContent="";
out.textContent="Even the narrowest range needs more than this deposit.";return}
mult=found;
}
[lo,hi]=lqBandFor(px,mult,a0,a1);
}
const sl=pxToSqrt(lo,t0.dec,t1.dec),sh=pxToSqrt(hi,t0.dec,t1.dec);
if(!sl||!sh||sh<=sl){out.textContent="Check the prices.";bandOut.textContent="";return}
const sp=oneSided===0?sl:oneSided===1?sh:lqSeedPrice(sl,sh,a0,a1);
if(!sp){out.textContent="Those amounts cannot open a band that wide.";return}
const openAt=bandPrice(sp,t0.dec,t1.dec);
bandOut.innerHTML=lqBar(lo,hi,openAt)+`<span></span>`;
bandOut.querySelector("span").textContent=
`${fmtPx(lo)} – ${fmtPx(hi)} · opens at ${fmtPx(openAt)} ${t1.sym} per ${t0.sym}`
+(oneSided===0?` · ${t0.sym} only`:oneSided===1?` · ${t1.sym} only`:"");
box.querySelector("#lqOwnNote").textContent=
`Unowned, like any AMM pool: anyone may add, and no creator fee can ever be`
+` charged. Prices are ${t1.sym} per ${t0.sym}.`;
lqGasNote(box);
const market=lqMarket(t0.addr,t1.addr,sl,sh,fee,ZERO);
box.dataset.sl=sl;box.dataset.sh=sh;box.dataset.sp=sp;box.dataset.fee=fee;
box.dataset.a0=a0;box.dataset.a1=a1;box.dataset.lp=0;
const q=lqSeedQuote(sl,sh,sp,a0,a1);
if(!q.ok){out.textContent="This band cannot open at that price with those amounts.";return}
if(!lqSeedAdmits(sl,sh,sp,fee,q)){
out.textContent="A band this size could not be traded once opened —"
+" widen the range, or deposit more.";return}
out.textContent=`Seeds ${trimAmt(q.used0,t0.dec)} ${t0.sym} + ${trimAmt(q.used1,t1.dec)} ${t1.sym}`
+((q.used0<a0||q.used1<a1)?" — the rest is returned":"");
box.dataset.lp=q.lp;
try{
const [addr]=await mc3([{to:PFACTORY,data:"0x"+SEL_POOLFOR+market}]);
if(my!==lqNewSeq)return;
const pool="0x"+strip0x(addr).slice(24);
const [sup]=await mc3([{to:pool,data:"0x"+SEL_TOTALSUPPLY}]).catch(()=>[null]);
if(my!==lqNewSeq)return;
if(sup&&strip0x(sup).length>=64&&dec1(sup,0)>0n){
out.textContent="That exact band already exists — add to it from the list above.";
return}
go.disabled=!account;
}catch{
if(my!==lqNewSeq)return;
out.textContent="Could not reach the factory.";
}
}
const LQ_CREATE_GAS=4700000n;
async function lqGasNote(box){
const el=box.querySelector("#lqGas");
if(!el)return;
el.textContent=`Creating deploys the pool contract — about `
+`${(Number(LQ_CREATE_GAS)/1e6).toFixed(1)}M gas.`;
try{
const wei=BigInt(await rpc("eth_gasPrice",[]))*LQ_CREATE_GAS;
el.textContent=`Creating deploys the pool contract — about `
+`${(Number(LQ_CREATE_GAS)/1e6).toFixed(1)}M gas, roughly `
+`${trimAmt(wei,18)} ETH at the moment. Adding to a band that already `
+`exists costs a fraction of it, so check the fee your wallet offers.`;
}catch{}
}
async function lqCreate(){
const box=lqList.querySelector(".lqnew");
if(!box||busy||!account)return;
const pair=chPair(); if(!pair)return;
const [t0,t1]=pair;
const go=box.querySelector("#lqCreate"),out=box.querySelector("#lqPv");
const sl=BigInt(box.dataset.sl||0),sh=BigInt(box.dataset.sh||0),sp=BigInt(box.dataset.sp||0);
const a0=BigInt(box.dataset.a0||0),a1=BigInt(box.dataset.a1||0),fee=BigInt(box.dataset.fee||0);
if(!sl||!sh||!sp){out.textContent="Preview the band first.";return}
const to=await lqTo();
if(!to)return;
++busy;go.disabled=true;
try{
const bps=BigInt(Math.round(Math.min(10,Math.max(.01,+slip.value||.5))*100));
const lp=BigInt(box.dataset.lp||0);
const minLP=lp>0n?lp*(10000n-bps)/10000n:0n;
const market=lqMarket(t0.addr,t1.addr,sl,sh,fee,ZERO);
const native=t0.addr===ZERO;
if(!native&&a0)await lqApprove(t0.addr,a0);
if(a1)await lqApprove(t1.addr,a1);
const data="0x"+SEL_CREATE_SEED+market+encUint(sp)+encUint(a0)+encUint(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