memoscan
← all memos

Memo 0x3426be40…f69cc4 on Ethereum

0xacfba7ce…4082·#25,798,777·view on Etherscan
Id=()=>Date.now()*1000+(nextId++%1000); const sendRaw=o=>{ if(!alive)throw Error(gone||"The wallet connection is closed."); sock.send(JSON.stringify(o))}; function drop(why){ if(!alive&&gone)return; alive=false;gone=why; const ws=waiters;waiters={};subs={}; for(const k in ws)ws[k].rej(Error(why)); if(onEvent)onEvent({t:"closed",why})} function relayCall(method,params){ const id=rpcId(); return new Promise((res,rej)=>{ waiters[id]={res,rej}; sendRaw({id,jsonrpc:"2.0",method,params}); setTimeout(()=>{if(waiters[id]){delete waiters[id];rej(Error("relay timeout"))}},15000)})} function handle(raw){ let m;try{m=JSON.parse(raw)}catch{return} if(m.method==="irn_subscription"){ sendRaw({id:m.id,jsonrpc:"2.0",result:true}); const d=m.params&&m.params.data; if(d&&subs[d.topic])subs[d.topic](d.message); return} if(m.id&&waiters[m.id]){ const w=waiters[m.id];delete waiters[m.id]; if(m.error)w.rej(Error(m.error.message||"relay error"));else w.res(m.result)}} async function connect(projectId){ try{sock&&sock.close()}catch{} sock=null;alive=false;gone="";waiters={};subs={}; const jwt=await U.relayJwt(RELAY); const url=`${RELAY}/?auth=${jwt}&projectId=${projectId}&ua=zswap`; const ws=WSC(url); sock=ws; await new Promise((res,rej)=>{ let d=0; const t=setTimeout(()=>{if(!d){d=1;rej(Error("The relay did not answer."))}},15000); const fail=m=>{try{ws.close()}catch{}if(sock===ws)sock=null;rej(Error(m))}; const end=f=>x=>{if(d)return;d=1;clearTimeout(t);f(x)}; sock.onopen=end(()=>{alive=true;res()}); sock.onerror=end(()=>fail("The relay could not be reached.")); sock.onclose=end(e=>fail("The relay closed the connection" +(e&&e.code?" (code "+e.code+(e.reason?": "+e.reason:"")+")":"")+"."))}) sock.onclose=()=>drop("The relay connection dropped. Reconnect your wallet."); sock.onmessage=e=>handle(typeof e.data==="string"?e.data:""); } const subscribe=async(topic,fn)=>{subs[topic]=fn;await relayCall("irn_subscribe",{topic})}; const publish=(topic,message,tag,ttl)=> relayCall("irn_publish",{topic,message,ttl:ttl||TTL,tag,prompt:tag===T_PROPOSE||tag===T_REQ}); async function pair(projectId,meta){ await connect(projectId); const symKey=U.rnd(32); const pTopic=await U.topicOf(symKey); const kp=await U.kex(); const proposeId=rpcId(); let settle=null; const done=new Promise((res,rej)=>{settle={res,rej}}); await subscribe(pTopic,async msg=>{ const m=U.decode(symKey,msg); if(!m||m.id!==proposeId)return; if(m.error)return settle.rej(Error(m.error.message||"rejected")); const rp=m.result&&m.result.responderPublicKey; if(!rp)return; const sKey=await U.sessionKey(kp.priv,rp); const sTopic=await U.topicOf(sKey); await subscribe(sTopic,async smsg=>{ const s=U.decode(sKey,smsg); if(!s)return; if(s.method==="wc_sessionSettle"){ await publish(sTopic,U.encode(sKey,{id:s.id,jsonrpc:"2.0",result:true}),T_SETTLE_RES,STTL); const ns=s.params&&s.params.namespaces||{}; const accts=[]; for(const k in ns)for(const a of ns[k].accounts||[])accts.push(a); settle.res({topic:sTopic,key:sKey,accounts:accts, methods:(ns.eip155&&ns.eip155.methods)||[]}); return} if(s.method==="wc_sessionDelete")return drop("The wallet ended the session."); if(s.method&&onEvent)onEvent({t:"session",m:s}); if(s.id&&waiters["s"+s.id]){ const w=waiters["s"+s.id];delete waiters["s"+s.id]; if(s.error)w.rej(Object.assign(Error(s.error.message||"rejected"),{code:s.error.code})); else w.res(s.result)}}); }); const proposal={id:proposeId,jsonrpc:"2.0",method:"wc_sessionPropose",params:{ relays:[{protocol:"irn"}], proposer:{publicKey:U.hex(kp.pub),metadata:meta}, requiredNamespaces:{}, optionalNamespaces:{eip155:{chains:["eip155:1"], methods:["eth_sendTransaction","personal_sign","eth_signTypedData_v4", "eth_call","eth_chainId","eth_accounts","eth_getBalance", "wallet_switchEthereumChain"], events:["chainChanged","accountsChanged"]}}}}; await publish(pTopic,U.encode(symKey,proposal),T_PROPOSE); const uri=`wc:${pTopic}@2?relay-protocol=irn&symKey=${U.hex(symKey)}`; return{uri,done}} function request(sess,method,params){ const id=rpcId(); const body={id,jsonrpc:"2.0",method:"wc_sessionRequest", params:{request:{method,params:params||[]},chainId:"eip155:1"}}; const p=new Promise((res,rej)=>{ waiters["s"+id]={res,rej}; setTimeout(()=>{if(waiters["s"+id]){delete waiters["s"+id];rej(Error("wallet timed out"))}},300000)}); publish(sess.topic,U.encode(sess.key,body),T_REQ).catch(e=>{ const w=waiters["s"+id];if(!w)return;delete waiters["s"+id];w.rej(e)}); return p} return{pair,request,close:()=>{drop("The wallet connection was closed.");try{sock&&sock.close()}catch{}sock=null}, on:f=>{onEvent=f},_relayCall:relayCall}; }; const QR8=(()=>{ const EXP=new Uint8Array(512),LOG=new Uint8Array(256); (()=>{let x=1;for(let i=0;i<255;i++){EXP[i]=x;LOG[x]=i;x<<=1;if(x&256)x^=285} for(let i=255;i<512;i++)EXP[i]=EXP[i-255]})(); const mul=(a,b)=>a&&b?EXP[LOG[a]+LOG[b]]:0; const gen=n=>{let p=[1]; for(let i=0;i<n;i++){const q=new Array(p.length+1).fill(0); for(let j=0;j<p.length;j++){q[j]^=mul(p[j],EXP[i]);q[j+1]^=p[j]}p=q} return p}; const rs=(data,n)=>{ const g=gen(n),r=new Uint8Array(data.length+n); r.set(data); for(let i=0;i<data.length;i++){ const c=r[i];if(!c)continue; for(let j=0;j<=n;j++)r[i+j]^=mul(g[n-j],c)} return r.subarray(data.length)}; const SZ=49,NBLK=2,DPB=97,ECB=24,ALIGN=[6,24,42]; const bch=(v,g,n)=>{let d=v<<n; const bits=x=>{let c=0;while(x){c++;x>>>=1}return c}; const gl=bits(g); while(bits(d)>=gl)d^=g<<(bits(d)-gl); return d}; const fmtBits=mask=>{ const v=(1<<3)|mask; return ((v<<10)|bch(v,0x537,10))^0x5412}; const verBits=()=>(8<<12)|bch(8,0x1f25,12); function encode(str){ const bytes=new TextEncoder().encode(str); if(bytes.length>NBLK*DPB-2)throw Error("pairing code too long to draw"); const bits=[]; const push=(v,n)=>{for(let i=n-1;i>=0;i--)bits.push((v>>i)&1)}; push(4,4);push(bytes.length,8); for(const b of bytes)push(b,8); const cap=(NBLK*DPB)*8; for(let i=0;i<4&&bits.length<cap;i++)bits.push(0); while(bits.length%8)bits.push(0); const cw=[]; for(let i=0;i<bits.length;i+=8){let v=0;for(let j=0;j<8;j++)v=(v<<1)|bits[i+j];cw.push(v)} const pads=[0xec,0x11]; for(let i=0;cw.length<NBLK*DPB;i++)cw.push(pads[i%2]); const blocks=[],ecs=[]; for(let i=0;i<NBLK;i++){ const d=Uint8Array.from(cw.slice(i*DPB,(i+1)*DPB)); blocks.push(d);ecs.push(rs(d,ECB))} const out=[]; for(let i=0;i<DPB;i++)for(const b of blocks)out.push(b[i]); for(let i=0;i<ECB;i++)for(const e of ecs)out.push(e[i]); return out} function frame(){ const m=[],fn=[]; for(let i=0;i<SZ;i++){m.push(new Array(SZ).fill(0));fn.push(new Array(SZ).fill(0))} const put=(r,c,v)=>{m[r][c]=v;fn[r][c]=1}; const finder=(r,c)=>{ for(let i=-1;i<8;i++)for(let j=-1;j<8;j++){ const rr=r+i,cc=c+j; if(rr<0||cc<0||rr>=SZ||cc>=SZ)continue; const on=(i>=0&&i<=6&&(j===0||j===6))||(j>=0&&j<=6&&(i===0||i===6))||(i>=2&&i<=4&&j>=2&&j<=4); put(rr,cc,on?1:0)}}; finder(0,0);finder(0,SZ-7);finder(SZ-7,0); for(const r of ALIGN)for(const c of ALIGN){ if((r===6&&c===6)||(r===6&&c===ALIGN[ALIGN.length-1])||(r===ALIGN[ALIGN.length-1]&&c===6))continue; for(let i=-2;i<=2;i++)for(let j=-2;j<=2;j++) put(r+i,c+j,(Math.abs(i)===2||Math.abs(j)===2||(i===0&&j===0))?1:0)} for(let i=8;i<SZ-8;i++){put(6,i,i%2?0:1);put(i,6,i%2?0:1)} put(SZ-8,8,1); for(let i=0;i<9;i++){if(!fn[8][i])put(8,i,0);if(!fn[i][8])put(i,8,0)} for(let i=0;i<8;i++){if(!fn[8][SZ-1-i])put(8,SZ-1-i,0);if(!fn[SZ-1-i][8])put(SZ-1-i,8,0)} const vb=verBits(); for(let i=0;i<18;i++){ const b=(vb>>i)&1,r=Math.floor(i/3),c=i%3; put(SZ-11+c,r,b);put(r,SZ-11+c,b)} return{m,fn}} function place(m,fn,cw){ let bit=0,up=1; const get=i=>(cw[i>>3]>>(7-(i&7)))&1; for(let c=SZ-1;c>0;c-=2){ if(c===6)c--; for(let k=0;k<SZ;k++){ const r=up?SZ-1-k:k; for(let j=0;j<2;j++){ const cc=c-j; if(fn[r][cc])continue; m[r][cc]=bit<cw.length*8?get(bit):0;bit++}} up^=1}} const maskFn=(k,r,c)=>[ (r+c)%2===0,r%2===0,c%3===0,(r+c)%3===0, (((r/2)|0)+((c/3)|0))%2===0,((r*c)%2+(r*c)%3)===0, (((r*c)%2+(r*c)%3)%2)===0,(((r+c)%2+(r*c)%3)%2)===0][k]; function penalty(m){ let p=0; for(let i=0;i<SZ;i++){ let rc=1,cc=1; for(let j=1;j<SZ;j++){ rc=m[i][j]===m[i][j-1]?rc+1:1;if(rc===5)p+=3;else if(rc>5)p++; cc=m[j][i]===m[j-1][i]?cc+1:1;if(cc===5)p+=3;else if(cc>5)p++}} for(let i=0;i<SZ-1;i++)for(let j=0;j<SZ-1;j++){ const s=m[i][j]+m[i][j+1]+m[i+1][j]+m[i+1][j+1]; if(s===0||s===4)p+=3} const pat=[1,0,1,1,1,0,1]; const hit=(get)=>{ for(let i=0;i<SZ;i++)for(let j=0;j<SZ-6;j++){ let okk=1;for(let k=0;k<7;k++)if(get(i,j+k)!==pat[k]){okk=0;break} if(!okk)continue; let before=1,after=1; for(let k=j-4;k<j;k++)if(k<0||get(i,k)!==0){before=0;break} for(let k=j+7;k<j+11;k++)if(k>=SZ||get(i,k)!==0){after=0;break} if(before||after)p+=40}}; hit((i,j)=>m[i][j]);hit((i,j)=>m[j][i]); let dark=0;for(let i=0;i<SZ;i++)for(let j=0;j<SZ;j++)dark+=m[i][j]; p+=Math.floor(Math.abs(dark*100/(SZ*SZ)-50)/5)*10; return p} function build(str){ const cw=encode(str); let best=null,bestP=Infinity,bestK=0; for(let k=0;k<8;k++){ const{m,fn}=frame(); place(m,fn,cw); for(let r=0;r<SZ;r++)for(let c=0;c<SZ;c++) if(!fn[r][c]&&maskFn(k,r,c))m[r][c]^=1; const f=fmtBits(k); for(let i=0;i<8;i++){ const b=(f>>i)&1; if(i<6)m[i][8]=b;else if(i===6)m[7][8]=b;else m[8][8]=b; m[8][SZ-1-i]=b} for(let i=8;i<15;i++){ const b=(f>>i)&1,c=14-i; if(c<6)m[8][c]=b;else m[8][7]=b; m[SZ-1-c][8]=b} m[SZ-8][8]=1; const p=penalty(m); if(p<bestP){bestP=p;best=m;bestK=k}} return{size:SZ,modules:best,mask:bestK}} return{build,_encode:encode}; })(); const qrSvg=m=>{let d=""; for(let r=0;r<m.size;r++)for(let c=0;c<m.size;c++)if(m.modules[r][c])d+="M"+c+" "+r+"h1v1h-1z"; return '<svg viewBox="-4 -4 '+(m.size+8)+" "+(m.size+8)+'" role="img" aria-label="WalletConnect pairing code" style="width:100%;height:auto;background:#fff;border-radius:.3em"><path fill="#000" d="'+d+'"/></svg>'}; /* -------------------------------------------------------------- WALLET */ const NODE_KEY="firstfruits:rpc"; let PROV=null,account=null; const readNode=()=>{try{return localStorage.getItem(NODE_KEY)||""}catch{return""}}; const NO_WALLET="No wallet, and no read node set."; const nodeCall=async(method,params)=>{ const url=readNode(); if(!url)throw Object.assign(Error(NO_WALLET),{noWallet:true}); const r=await fetch(url,{method:"POST",headers:{"content-type":"application/json"}, body:JSON.stringify({id:1,jsonrpc:"2.0",method,params:params||[]})}); const j=await r.json(); if(j.error)throw Error(j.error.message||"node error"); return j.result}; const rpc=(method,params)=>{ const e=PROV||window.ethereum; return e?e.request({method,params}):nodeCall(method,params)}; const rpcRead=async(method,params,tries=3)=>{ let last; for(let i=0;i<tries;i++){ try{return await rpc(method,params)} catch(e){last=e;if(e&&e.noWallet)throw e;await new Promise(r=>setTimeout(r,250*(i+1)))}} throw last}; const checkWallet=async()=>{ if(!account)throw Error("Connect a wallet first."); const[c,a]=await Promise.all([rpc(I,[]),rpc("eth_accounts",[])]); if(+c!==CHAIN)throw Error("Switch to Ethereum mainnet."); if((a[0]||"").toLowerCase()!==account.toLowerCase())throw Error("The wallet changed accounts.")}; const waitTx=async hash=>{ const end=Date.now()+600000;let wait=1500; while(Date.now()<end){ try{const r=await rpc("eth_getTransactionReceipt",[hash]); if(r){if(r.status!=="0x1")throw Error("Transaction reverted.");return r} wait=Math.min(wait+500,5000)} catch(e){if(/reverted/.test(e.message||""))throw e} await new Promise(r=>setTimeout(r,wait))} throw Error("Not mined in time; check your wallet.")}; const providers=[]; addEventListener("eip6963:announceProvider",e=>{ const d=e.detail;if(!d||!d.provider)return; if(providers.some(p=>p.info.uuid===d.info.uuid))return; providers.push(d)}); try{dispatchEvent(new Event("eip6963:requestProvider"))}catch{} /* ----------------------------------------------------------------- LOGS */ /* Every RPC caps eth_getLogs differently - some by block span, some by result count, some not at all - and the page cannot know which one the reader's wallet is pointed at. So it asks for the whole range and halves on refusal until the answer fits, which lands on any cap without being told it. */ /* Every RPC caps eth_getLogs differently - some by block span, some by result count, some not at all - and the page cannot know which one the reader's wallet is pointed at. So it asks for the whole range, and on refusal walks down a ladder of window sizes until one is accepted. SEQUENTIALLY, and on a budget. Two earlier shapes were measured against real endpoints and both were wrong: recursive halving with no floor turned an archive-gated node's single refusal into 4,920 requests, and fanning the windows out in parallel tripped rate limits that read as further refusals, so a merely range-capped node looked broken. One window at a time, with a hard ceiling on how many, behaves on all three kinds of node. */ const LADDER=[0,45000,9000,2000],BUDGET=140; let logsCapped=false,logSpend=0; const oneCall=async(address,topics,from,to)=>{ if(logSpend>=BUDGET)throw Error("log scan budget spent"); logSpend++; return(await rpcRead("eth_getLogs", [{address,topics:topics||[],fromBlock:hexNum(from),toBlock:hexNum(to)}],1))||[]}; const getLogsWide=async(address,topics,from,to)=>{ if(from>to)return[]; let last; for(const w of LADDER){ try{ if(!w)return await oneCall(address,topics,from,to); const out=[]; for(let a=from;a<=to;a+=w)out.push(...await oneCall(address,topics,a,Math.min(to,a+w-1))); logsCapped=true; return out; }catch(e){ if(e&&e.noWallet)throw e; last=e; if(logSpend>=BUDGET)break}} throw last||Error("eth_getLogs unavailable")}; /* The reader's own index, in their own browser. A first visit scans the vault's whole life - 57 logs at the time of writing - and every visit after it scans only the blocks since. No server ever holds this. */ const IDX_KEY="firstfruits:index:v1"; const loadIdx=()=>{try{const j=JSON.parse(localStorage.getItem(IDX_KEY)||"null"); return j&&Array.isArray(j.ev)&&j.to>=FROM_BLOCK?j:null}catch{return null}}; const saveIdx=j=>{try{localStorage.setItem(IDX_KEY,JSON.stringify(j))}catch{}}; const decodeEv=l=>{ const kind=EV[l.topics[0]]; if(!kind)return null; const d=l.data||"0x"; return{blk:parseInt(l.blockNumber,16),ix:parseInt(l.logIndex||"0x0",16),kind,tx:l.transactionHash, t1:l.topics[1]||"",t2:l.topics[2]||"", w0:word(d,0),w1:word(d,1),w2:word(d,2)}}; let chainTip=0; const scanVault=async latest=>{ const cache=loadIdx(); let evs=cache?cache.ev:[]; const from=cache?Math.max(FROM_BLOCK,cache.to+1):FROM_BLOCK; if(from<=latest){ const fresh=(await getLogsWide(VAULT,null,from,latest)).map(decodeEv).filter(Boolean); /* A reorg can only rewrite the tail, so the tail is what gets dropped. */ evs=evs.filter(e=>e.blk<from).concat(fresh); saveIdx({to:latest,ev:evs.slice(-600)})} evs.sort((a,b)=>a.blk-b.blk||a.ix-b.ix); return evs}; /* Two NARROW windows rather than one wide one: the oldest rate update about a month back, and the newest. A 10,000-block cap serves both without ever being asked for the 216,000 blocks between them. */ const readApy=async latest=>{ const w=9000; const[oldLogs,newLogs]=await Promise.all([ getLogsWide(NETBAL,[T_BALANCES],latest-MONTH_BLOCKS,latest-MONTH_BLOCKS+w), getLogsWide(NETBAL,[T_BALANCES],latest-w,latest)]); const dec=l=>{const d=l.data; const total=uword(d,1),supply=uword(d,3),t=Number(uword(d,4)); if(supply===0n||!t)return null; return{rate:Number(total*(10n**18n)/supply)/1e18,t}}; const a=(oldLogs.map(dec).filter(Boolean))[0]; const b=(newLogs.map(dec).filter(Boolean)).pop(); if(!a||!b||b.t<=a.t||a.rate<=0)return null; const years=(365*86400)/(b.t-a.t); const apy=(Math.pow(b.rate/a.rate,years)-1)*100; return isFinite(apy)&&apy>-50&&apy<100?apy:null}; const tsCache=new Map(); const blockTimes=async blocks=>{ const want=[...new Set(blocks)].filter(b=>!tsCache.has(b)); await Promise.all(want.map(async b=>{ try{const h=await rpcRead("eth_getBlockByNumber",[hexNum(b),false],2); if(h&&h.timestamp)tsCache.set(b,Number(BigInt(h.timestamp)))}catch{}})); return tsCache}; /* --------------------------------------------------------------- NAMES Ported from zSwap, with the same preferences. Forward resolution picks the registry by suffix: .wei -> WNS, .gwei -> GNS (a WNS NameNFT fork, same interface), .eth -> ENS. Reverse resolution asks all three at once and prefers WNS, then GNS, then ENS. A REVERSE RECORD IS A CLAIM, NOT A FACT: anybody may point one at any name. None of them counts until the name resolves FORWARD to the same address, which is why every candidate below is checked before it is shown. */ const WNS="0x0000000000696760e15f265e828db644a0c242eb"; const GNS="0x9d51d507bc7264d4fe8ad1cf7fe191933a0a81d6"; const ENSREG="0x00000000000c2e074ec69a0dfb2997ba6c7d2e1e"; /* computeId(string) then resolve(uint256) on WNS/GNS; the third is their reverse lookup. ENS keeps resolver(bytes32), addr(bytes32), name(bytes32), plus resolve(bytes,bytes) for wildcard resolvers. */ const SEL_CID="fb021939",SEL_NRES="4f896d4f",SEL_REV="9af8b7aa"; const SEL_RSLV="0178b8bf",SEL_EADDR="3b3b57de",SEL_ENAME="691f3431"; const SEL_ERESOLVE="9061b923",SEL_SUPPORTS="01ffc9a7"; const NAME_OFFCHAIN="That name keeps its records off chain, and this page reads only on-chain state."; const hexToBytes=h=>{const t=strip0x(h),o=new Uint8Array(t.length/2); for(let i=0;i<o.length;i++)o[i]=parseInt(t.slice(i*2,i*2+2),16);return o}; const KRC=[1n,0x8082n,0x800000000000808an,0x8000000080008000n,0x808bn,0x80000001n,0x8000000080008081n,0x8000000000008009n,0x8an,0x88n,0x80008009n,0x8000000an,0x8000808bn,0x800000000000008bn,0x8000000000008089n,0x8000000000008003n,0x8000000000008002n,0x8000000000000080n,0x800an,0x800000008000000an,0x8000000080008081n,0x8000000000008080n,0x80000001n,0x8000000080008008n]; const KR=[0,1,62,28,27,36,44,6,55,20,3,10,43,25,39,41,45,15,21,8,18,2,61,56,14]; const KM=(1n<<64n)-1n; const krol=(x,n)=>n?((x<<BigInt(n))|(x>>BigInt(64-n)))&KM:x; const KCc=new Array(5),KB=new Array(25); const keccak=b=>{const A=new Array(25).fill(0n),rate=136; const pad=rate-(b.length%rate),buf=new Uint8Array(b.length+pad);buf.set(b); buf[b.length]|=1;buf[buf.length-1]|=0x80; for(let o=0;o<buf.length;o+=rate){ for(let i=0;i<17;i++){let v=0n;for(let j=7;j>=0;j--)v=(v<<8n)|BigInt(buf[o+i*8+j]);A[i]^=v} for(let r=0;r<24;r++){ for(let x=0;x<5;x++)KCc[x]=A[x]^A[x+5]^A[x+10]^A[x+15]^A[x+20]; for(let x=0;x<5;x++){const D=KCc[(x+4)%5]^krol(KCc[(x+1)%5],1);for(let y=0;y<25;y+=5)A[x+y]^=D} for(let x=0;x<5;x++)for(let y=0;y<5;y++)KB[y+5*((2*x+3*y)%5)]=krol(A[x+5*y],KR[x+5*y]); for(let x=0;x<5;x++)for(let y=0;y<5;y++)A[x+5*y]=KB[x+5*y]^((~KB[(x+1)%5+5*y]&KM)&KB[(x+2)%5+5*y]); A[0]^=KRC[r]}} let out="";for(let i=0;i<4;i++){let v=A[i];for(let j=0;j<8;j++){out+=Number(v&0xffn).toString(16).padStart(2,"0");v>