0x68575b07…67c7·#25,913,255·view on Etherscan
"";
const bar=goal>0n
?`<div class="lqbar fill" style="--at:${Math.max(0,Math.min(100,pct)).toFixed(1)}%" aria-hidden="true"><b></b></div>`
:"";
const rel=cbReady>0n
?`<span class="cbrow"><span>${trimAmt(cbReady,18)} ETH vested and unreleased</span>`
+`<button type="button" id="cbRel" class="fclink">Release</button></span>`
:"";
const top=(head?`<span class="cbrow">${head}</span>${bar}`:"")+rel;
const act=(text,label)=>`<span class="cbact"><span>${text}</span>`
+`<button type="button" id="cbGo" class="lqbtn">${label}</button></span>`;
const msg=t=>`<span class="cbmsg">${t}</span>`;
const paint=(html,tip)=>{
cbEl.innerHTML=top+html;
cbEl.title=tip;
cbEl.classList.remove("hide");
const gi=i=>document.getElementById(i);
const g=gi("cbGo"),r=gi("cbRel");
if(g)g.onclick=cbAct;
if(r)r.onclick=cbRelease;
};
if(buy){
if(!open||left<=0n)
return rqOff
?paint(msg(`Backing for ${safeSym(tok.sym)} has closed, and burn-back is switched off \u2014 it cannot be redeemed`),
"This DAO has set ragequittable to false, so ragequit reverts and the treasury cannot be claimed against. Only the DAO can switch it back on.")
:paint(msg(`Backing for ${safeSym(tok.sym)} has closed — it can still be burned back`),CB_TIP);
if(rqOff)
return paint(msg(`This cause has switched burn-back off \u2014 backing it cannot be redeemed`),
"Backing pays ether for loot whose only value is the right to burn it back. This DAO has turned that right off, so there is no way out of the position.");
if(!cbEth)
return paint(msg(`Back ${safeSym(tok.sym)} with ether — switch the top token to ETH`),CB_TIP);
let pay=0n;try{pay=parseUnits(amt.value.trim()||"0",18)}catch{}
if(pay<=0n)
return paint(msg(`1 ETH backs ${trimAmt(CONE*CONE/price,tok.dec)} ${safeSym(tok.sym)}`),CB_TIP);
let units=pay*CONE/price;
if(units>left)units=left;
if(units<=0n){cbEl.classList.add("hide");return}
cbAmt=units;
cbTok=tok.addr;
cbCost=(units*price+CONE-1n)/CONE;
return paint(act(`Back with ${trimAmt(cbCost,18)} ETH for `
+`${trimAmt(units,tok.dec)} ${safeSym(tok.sym)}`,"Back"),CB_TIP);
}
if(rqOff)
return paint(msg(`Burn-back has been switched off by this cause \u2014 ${safeSym(tok.sym)} cannot be redeemed`),
"This DAO has set ragequittable to false, so ragequit reverts and the treasury "
+"cannot be claimed against. Only the DAO can switch it back on.");
let want=0n;try{want=parseUnits(amt.value.trim()||"0",tok.dec)}catch{}
if(want<=0n)want=fromBalance;
if(want<=0n){
return paint(msg(`1 ${safeSym(tok.sym)} redeems for ${trimAmt(eth*CONE/sup,18)} ETH`),CB_TIP);}
cbAmt=want;
const due=eth*want/sup;
cbDue=due;cbShares=hit.shares;cbTok=tok.addr;
if(due<=0n)
return paint(msg(eth<=0n
?`This cause has drawn down its treasury — burning ${safeSym(tok.sym)} would pay nothing`
:`Burning ${trimAmt(want,tok.dec)} ${safeSym(tok.sym)} would pay less than a wei`),CB_TIP);
return paint(act(`Burn ${trimAmt(want,tok.dec)} ${safeSym(tok.sym)} `
+`for ${trimAmt(due,18)} ETH`,"Burn"),
"This token is a claim on a cause's treasury, not a position in a market. "
+"Burning it pays your share of the ETHER the cause has not drawn down yet, counted "
+"pro rata across every share and unit of loot. Any other token the cause holds is "
+"not paid out by this burn. It cannot be undone.");
}
const CB_TIP="Backing a cause is not a trade and there is no market to leave through. "
+"What you get is a claim: burn it back at any time for your share of the ether the cause "
+"has not drawn down yet, which is the number this line shows once you hold some.";
async function cbRelease(){
if(!cbDao||cbReady<=0n)return;
const b=document.getElementById("cbRel");if(!b||b.disabled)return;
try{
if(!account)return connect();
b.disabled=true;b.textContent="Releasing…";
const req={from:account,to:CTAP,value:"0x0",data:"0x"+SEL_TAPCLAIM+encAddr(cbDao)};
await rpc(C,[req,L]);
await waitTx(await sendTx([req]));sGot();
stat.textContent="Released to the cause.";
cbSync();
}catch(e){err(e);b.disabled=false;b.textContent="Release"}
}
async function cbAct(){
if(!cbDao||cbAmt<=0n)return;
const b=document.getElementById("cbGo");if(!b||b.disabled)return;
const label=b.textContent;
try{
if(!account)return connect();
b.disabled=true;b.textContent=cbBuy?"Backing…":"Burning…";
const req=cbBuy
?{from:account,to:COFF,value:"0x"+cbCost.toString(16),
data:"0x"+SEL_BUY+encAddr(cbDao)+encUint(cbAmt)}
:{from:account,to:cbDao,value:"0x0",
data:"0x"+SEL_RAGEQUIT+encUint(96)+encUint(0)+encUint(cbAmt)+encUint(1)+encAddr(ZERO)};
if(!cbBuy&&cbShares&&cbTok){
let now2=null;
try{
const [b2,r2]=await Promise.all([
rpc("eth_getBalance",[cbDao,L]),
mc3Deep([{to:cbShares,data:"0x"+SEL_TSUP},{to:cbTok,data:"0x"+SEL_TSUP}])]);
const e2=BigInt(b2||"0x0"),s2=BigInt(r2[0]||"0x0")+BigInt(r2[1]||"0x0");
if(s2>0n)now2=e2*cbAmt/s2;
}catch{}
if(now2!==null&&now2<cbDue){
const lost=cbDue-now2;
if(now2<=0n)throw Error("The treasury has been drawn down since this was quoted \u2014 burning now would pay nothing.");
if(lost*100n>cbDue)throw Error(`This burn now pays ${trimAmt(now2,18)} ETH, not ${trimAmt(cbDue,18)} `
+`\u2014 the treasury was drawn down since it was quoted. Check the line again before burning.`);
}
}
await rpc(C,[req,L]);
await waitTx(await sendTx([req]));sGot();
if(cbBuy&&cbTok){
const kt=TOKENS.find(x=>x.addr&&x.addr.toLowerCase()===cbTok.toLowerCase());
if(kt){kt.cause=1;if(kt.cust)keepToken(kt)}
}
stat.textContent=cbBuy?"Backed · burn it back any time for what is left."
:"Burned · the ether is in your wallet.";
refreshBalance();cbSync();
}catch(e){err(e);b.disabled=false;b.textContent=label}
}
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 lnCause=()=>lnKind.value==="cause";
const lnCap=()=>lnCause()?CAU_MAX_ART:LN_MAX_ART;
const lnBud=()=>lnCause()?CAU_MAX_ART:LN_BUDGET;
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;
};
async function lnOwner(){
const rv=rc.value.trim();
if(!rv)return account;
if(/^0x[0-9a-fA-F]{40}$/.test(rv)){
if(/^0x0{40}$/i.test(rv))throw Error("That cannot be the zero address.");
return rv;
}
if(!/\.(g?wei|eth)$/i.test(rv))throw Error("That must be an address, or a .eth / .wei name.");
rcvEl.textContent="resolving\u2026";
let r="";try{r=await nameFwd(rv.toLowerCase())}catch{r=""}
if(!r||/^0x0{40}$/i.test(r)){rcvEl.textContent="";throw Error("That name is not registered.")}
if(!isAddr(r))throw Error("That name resolved to something that is not an address.");
rcvEl.textContent=r;
return r;
}
const CSUM="0x00000000004473e1f31C8266612e7FD5504e6f2a";
const CMSUM="0x0000000000330B8df9E3bc5E553074DA58eE9138";
const CIMPL="643a45b599d81be3f3a68f37eb3de55ff10673c1";
const CRND="0x000000000011C799980827F52d3137b4abD6E654";
const COFF="0x000000A4Ad929C9E108aD2B1D2fBeDe0C2Ae57e1";
const CTAP="0x0000000060cdD33cbE020fAE696E70E7507bF56D";
const CLOOT="0x00000000000000000000000000000000000003ef";
const SEL_DAICO="4e1e3b11",SEL_SETALLOW="da46098c",SEL_CONFIGURE="fca53be5";
const CUNITS=10000000n,CONE=10n**18n,CMAX=(1n<<256n)-1n;
const CAU_MAX_ART=4096;
const CAU_MAX_DAYS=3650;
const cPad=h=>h.padEnd(Math.ceil(h.length/64)*64,"0");
const cTail=h=>{h=strip0x(h);return encUint(h.length/2)+cPad(h)};
const cCall=(to,data)=>encAddr(to)+encUint(0)+encUint(96)+cTail(data);
const cCalls=cs=>{let off=cs.length*32,head="",tail="";
for(const c of cs){head+=encUint(off);off+=c.length/2;tail+=c}
return encUint(cs.length)+head+tail};
const CLIMPL="6f1f2af76a3add953277e9f369242697c87bc6a5";
const cProxy=impl=>strip0x(keccak(hexToBytes(
"602d5f8160095f39f35f5f365f5f37365f73"+impl+"5af43d5f5f3e6029573d5ffd5b3d5ff3")));
const cLoot=dao=>{const d=strip0x(dao).toLowerCase();
return "0x"+keccak(hexToBytes("ff"+d+d+"0".repeat(24)+cProxy(CLIMPL))).slice(26)};
const cPredict=(owner,salt)=>{
const inner=encUint(96)+encUint(160)+strip0x(salt)
+encUint(1)+encAddr(owner)+encUint(1)+encUint(CONE);
const ss=strip0x(keccak(hexToBytes(inner)));
const ih=cProxy(CIMPL);
return "0x"+keccak(hexToBytes("ff"+strip0x(CMSUM)+ss+ih)).slice(26);
};
const encCause=(name,sym,uri,salt,owner,ben,dao,goal,rate,price,deadline,cap)=>{
const T=new TextEncoder(),Z=encUint(0);
const dyn=[lnTail(T.encode(name)),lnTail(T.encode(sym)),lnTail(T.encode(uri)),
encUint(1)+encAddr(owner),encUint(1)+encUint(CONE),encUint(0),
cCalls([
cCall(dao,SEL_SETALLOW+encAddr(COFF)+encAddr(CLOOT)+encUint(CMAX)),
cCall(COFF,SEL_CONFIGURE+encAddr(CLOOT)+encAddr(ZERO)+encUint(price)
+encUint(deadline)+encUint(cap))])];
const cfg=encUint(CONE)+encUint(604800)+encUint(259200)+encUint(CONE)+Z.repeat(17);
const tap=encAddr(CTAP)+Z+encUint(goal)+encAddr(ben)+encUint(rate);
let off=52*32,heads=[],tail="";
for(const d of dyn){heads.push(encUint(off));off+=d.length/2;tail+=d}
return "0x"+SEL_DAICO
+heads[0]+heads[1]+heads[2]
+encUint(1000)+encUint(1)+encAddr(CRND)+strip0x(salt)
+heads[3]+heads[4]+heads[5]
+cfg+Z.repeat(7)+tap+Z.repeat(8)+heads[6]+tail;
};
const cauURI=(name,sym)=>{
let img="";
if(lnArtBytes){let s="";for(const x of lnArtBytes)s+=String.fromCharCode(x);
img="data:"+MIME_NAME[lnArtMime]+";base64,"+btoa(s)}
return "data:application/json,"+encodeURIComponent(
JSON.stringify({name,symbol:sym,image:img,launchType:"cause"}));
};
function cauTerms(){
const g=String(lnGoal.value||"").replace(/[, ]/g,"").trim();
const d=String(lnDays.value||"").replace(/[, ]/g,"").trim();
if(!/^\d+(\.\d+)?$/.test(g)||!/^\d+(\.\d+)?$/.test(d))return null;
let goal;try{goal=parseUnits(g,18)}catch{return null}
const secs=BigInt(Math.round(Number(d)*86400));
if(goal<=0n||secs<3600n||Number(d)>CAU_MAX_DAYS)return null;
const price=goal/CUNITS;
if(price<=0n)return null;
const vest=BigInt(lnVest.value);
if(vest<=secs)return{err:`Backing runs for ${trimNum(Number(secs)/86400)} days but the ether is `
+`fully released in ${trimNum(Number(vest)/86400)} days — the treasury would empty before backing closes. `
+`Shorten the raise, or release over longer.`};
const rate=goal/vest||1n;
return{goal,secs,price,rate,vest,cap:(CUNITS-1n)*CONE};
}
const lnKindQuote=()=>lnKind.value==="cause"?cauQuote():lnQuote();
function cauQuote(){
lnGo.textContent=account?"Launch":"Connect Wallet";
const t=cauTerms();
if(!t){lnNote.textContent="";return}
if(t.err){lnNote.textContent=t.err;lnNote.style.color="var(--w)";return}
const per=(+formatUnits(t.price,18));
const tenthDays=t.rate>0n
?Number((t.goal/10n)/t.rate)/86400
:0;
lnNote.textContent=`1 ETH backs ${trimNum(1/per)} units`
+` · ${trimNum(+formatUnits(t.rate*86400n,18))} ETH a day at full funding, over `
+lnVest.options[lnVest.selectedIndex].textContent
+(tenthDays>0?` · at a tenth of the goal it empties in ${trimNum(Math.round(tenthDays*10)/10)} days`:"")
+` · backers burn back for the rest`
+(lnArtBytes?` · logo ${(lnArtBytes.length/1024).toFixed(1)} KB`
+(lnArtBytes.length>CAU_MAX_ART?` — over the 4 KB a cause allows, pick something simpler`:""):"");
lnNote.style.color=lnArtBytes&&lnArtBytes.length>CAU_MAX_ART?"var(--w)":"";
}
function cauKind(){
const on=lnKind.value==="cause";
for(const el of [lnMktL,lnSupplyL,lnMcapL,lnAllocL])el.classList.toggle("hide",on);
for(const el of [lnGoalL,lnDaysL,lnVestL])el.classList.toggle("hide",!on);
lnTitle.textContent=on?"Launch a cause":"Launch a coin";
lnSub.textContent=on?"backers burn back for whatever you have not drawn"
:"0.4% of each trade goes to the creator";
lnName.placeholder=on?"Feed Ducks":"Zero Cat";
lnSym.placeholder=on?"DUCK":"ZCAT";
rc.placeholder=on?"Beneficiary — defaults to you":"Creator — defaults to you";
rc.title=on?"Who the ether streams to. Leave blank and it is you. Worth setting to a multisig if the cause has one."
:"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.";
stat.textContent="";
on?cauQuote():lnQuote();
}
async function cauGo(){
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.`);
if(lnArtBytes&&lnArtBytes.length>CAU_MAX_ART)
throw Error(`A cause's logo is stored in the DAO itself, so it is capped at 4 KB. `
+`That one is ${(lnArtBytes.length/1024).toFixed(1)} KB — pick something simpler.`);
const t=cauTerms();
if(!t)throw Error("A goal in ether, and how long it runs for — at least an hour, at most ten years.");
if(t.err)throw Error(t.err);
const ben=await lnOwner();
const owner=account;
const salt=wnSecret();
const dao=cPredict(owner,salt);
const deadline=BigInt(Math.floor(Date.now()/1000))+t.secs;
stat.textContent="Launching…";
const req={from:account,to:CSUM,
value:"0x"+t.price.toString(16),
data:encCause(name,sym,cauURI(name,sym),salt,owner,ben,dao,t.goal,t.rate,t.price,deadline,t.cap)};
await rpc(C,[req,L]);
await waitTx(await sendTx([req]));sGot();
lnArtBytes=null;lnArtNote.textContent="none";lnArtNote.classList.remove("set");
lnPrev.classList.remove("set");lnArt.value="";
let loot=cLoot(dao);
try{
const said=cbWord(await rpc(C,[{to:dao,data:"0x"+SEL_LOOTOF},L]));
if(isAddr(said||"")&&said.toLowerCase()!==loot.toLowerCase())loot=said;
}catch{}
try{const ix=await addCustomToken(loot);TOKENS[ix].cause=1;rebuild()}catch{}
stat.innerHTML=`${safeSym(sym)} is raising at `
+`<a href="${chainCfg().explorer}/token/${loot}" target="_blank" rel="noreferrer">${short(loot)}</a>`
+` · ${trimNum(+formatUnits(t.goal,18))} ETH over ${trimNum(Number(t.secs)/86400)} days`
+` · backers can burn back for whatever you have not drawn`;
}
let lnMode=false,lnArtBytes=null,lnArtMime=0;
function lnSet(on){
if(on&&PLAUNCH===ZERO)return;
if(on)cbDisarm();
++seq;quoting=false;
lnMode=on;
if(on&&lqMode)lqSet(false);
if(on&&wnMode)wnSet(false);
if(on&&pvMode)pvSet(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)cauKind();
fcSync();
}
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 alBps=al>0?Math.round(al*100):0;
const pooled=Number(sup)*(1-alBps/10000);
if(!(pooled>0))return;
const sym=lnSym.value.trim().toUpperCase();
const poolFrac=99/(mc+.99);
const firstEth=poolFrac*pooled/Number(sup);
const firstTokens=poolFrac*pooled/100;
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(firstTokens)} ${sym||"tokens"}`
+(al>0&&alBps===0?` \u00b7 that keep is below one basis point \u2014 the smallest is 0.01%`:"")
+(alBps>0?` \u00b7 creator keeps ${trimNum(Number(sup)*alBps/10000)} (${alBps/100}%)`
+` \u00b7 fully diluted ${trimNum(mc*Number(sup)/pooled)} ETH`:"")
+(alBps>=1000?` \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?"var(--w)":"";
}
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(/<script[\s\S]*?<\/script\s*>/gi,"").replace(/<script[\s\S]*$/gi,"")
.replace(/<foreignObject[\s\S]*?<\/foreignObject\s*>/gi,"").replace(/<foreignObject[\s\S]*$/gi,"")
.replace(/\son[a-z-]+\s*=\s*("[^"]*"|'[^']*'|[^\s>]*)/gi,"")
.replace(/\s(?:[a-z]+:)?href\s*=\s*("\s*javascript:[^"]*"|'\s*javascript:[^']*'|javascript:[^\s>]*)/gi,"")
.replace(/>\s+</g,"><").trim());
if(b.length>lnCap())throw Error(`That SVG is ${(b.length/1024).toFixed(1)} KB and the on-chain limit is ${Math.round(lnCap()/1024)} 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<=lnBud())return{bytes,mime:LN_MIME[type]};
}
}
if(smallest&&smallest.bytes.length<=lnCap())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);
}
lnKindQuote();
};
const lnGroup=el=>{
if(!/^[\d, ]*(\.[\d ]*)?$/.test(el.value))return;
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=lnKindQuote;
lnKind.onchange=cauKind;
for(const el of [lnGoal,lnDays])el.oninput=cauQuote;
lnVest.onchange=cauQuote;
ln.onclick=()=>{sBlip();lnSet(!lnMode)};
let wnMode=false,wnQ=null,wnSeq=0,wnRollSeq=0,wnTimer=0,wnRound=0n,wnStampTry=0,wnRollAt=0;
const wnOk=l=>/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(l)&&new TextEncoder().encode(l).length<256;
const wnFree=()=>wnTld.value==="id";
const wnOffer=(t,id)=>{wnEnter.classList.remove("hide");wnEnter.textContent=t;
wnEnter.dataset.id=id||"";wnEnter.classList.toggle("mut",!id&&!!account)};
const wnFull=l=>l+(wnFree()?".id.wei":".wei");
const wnSecret=()=>{const a=new Uint8Array(32);crypto.getRandomValues(a);let h="";for(const x of a)h+=x.toString(16).padStart(2,"0");return"0x"+h};
const wnHash=(l,o,sec)=>keccak(hexToBytes(pad32("60")+encAddr(o)+pad32(sec)+strTail(l)));
const wnKey=()=>WN_KEY+":"+(account||"").toLowerCase();
const wnStamp=async()=>{const p=wnPend();
if(!p||p.at||!account)return;
try{const r=await rpc(C,[{to:WNS,data:"0x"+SEL_WCOMMITS+pad32(wnHash(p.label,account,p.secret))},L]);
const t=Number(dec1(r||"0x0",0));
if(t)wnSave({...p,at:t})}catch{}};
const wnPend=()=>{try{const p=JSON.parse(LS[wnKey()]||"null");return p&&p.label&&p.secret&&Number.isFinite(p.at)?p:null}catch{return null}};
const wnSave=p=>{try{if(p)LS[wnKey()]=JSON.stringify(p);else delete LS[wnKey()]}catch{}};
const wnId=n=>namehash(/\.wei$/.test(n)?n:n+".wei");
const wnCostCall=l=>[{to:WNS,data:"0x"+SEL_WFEE+encUint(new TextEncoder().encode(l).length)},
{to:WNS,data:"0x"+SEL_WPREM+pad32(wnId(l))}];
const wnCost=async l=>{const [f,pr]=await mc3(wnCostCall(l));return dec1(f,0)+dec1(pr,0)};
const wnSync=()=>{
if(!wnMode)return;
const p=wnPend();
if(p&&!p.at){
if(!p.since){wnSave({...p,since:Math.floor(Date.now()/1000)});wnGo.disabled=true;wnGo.textContent="Confirming\u2026";return}
if(Math.floor(Date.now()/1000)-p.since>600){wnGo.disabled=false;wnGo.textContent="Commit again";return}
wnGo.disabled=true;wnGo.textContent="Confirming\u2026";return}
if(p){
const left=WN_MIN-(Math.floor(Date.now()/1000)-p.at);
wnGo.disabled=left>0;
wnGo.textContent=left>0?`Ready in ${left}s`:`Register ${p.label}.wei`;
if(Math.floor(Date.now()/1000)-p.at>=WN_MAX){wnGo.disabled=false;wnGo.textContent="Commit again"}
return}
wnGo.disabled=!wnQ;
wnGo.textContent=wnQ?(wnQ.free?`Claim ${wnQ.label}.id.wei`:"Commit"):"Register"};
const wnTicketOffer=async(l,seq,round)=>{
try{const[t]=await mc3([{to:WROLL,data:"0x"+SEL_RTICKET+encUint(round)+pad32(wnId(l))}]);
if(seq!==wnSeq)return;
const inAlready=!!dec1(t,0);
wnOffer(inAlready?`${l}.wei is already in`:`Enter ${l}.wei in the roll`,inAlready?"":pad32(wnId(l)));
}catch{if(seq===wnSeq)wnOffer(`Could not read the roll for ${l}.wei`)}};
const wnQuote=async()=>{
const my=++wnSeq;
wnQ=null;
wnOffer(account?"Enter a name you own":"Connect to enter");
wnSub.textContent=wnFree()?"free":"from 0.0005 ETH a year";
const l=wnName.value.trim().toLowerCase();
const p=wnPend();
if(p){wnNote.textContent=p.at?`${p.label}.wei is held for you — register it within 24 hours or the commitment lapses.`
:`Waiting for the commitment on ${p.label}.wei to confirm.`;wnSync();return}
if(!