memoscan
← all memos

Memo 0xa5c68768…fcbd8c on Ethereum

(i) ? '#fff' : 'rgba(255,255,255,0.12)'; const p = project(i); const r = Math.max(1, (2.4 - 1.6 * (i / G.A)) * p[2] * 0.8); ctx.fillRect(p[0] - r / 2, p[1] - r / 2, r, r); } } // one stroking pass over [from,to): fixed absolute batches so accumulation is // repaint-invariant; pred filters which links belong to this pass function strokePass(from, to, stride, alpha, pred, lw) { const { dP, dA } = G; ctx.strokeStyle = `rgba(255,255,255,${alpha})`; ctx.lineWidth = lw || (G.links > 150000 ? 0.5 : 0.8); const B = (G.links > 200000 ? 64 : 8) * stride; let i = from; while (i < to) { const end = Math.min(to, (Math.floor(i / B) + 1) * B); ctx.beginPath(); for (; i < end; i += stride) { if (pred && !pred(i)) continue; const a = project(dP[i]); const x1 = a[0], y1 = a[1]; const b = project(dA[i]); ctx.moveTo(x1, y1); ctx.lineTo(b[0], b[1]); } ctx.stroke(); } } function drawLinks3(from, to, alpha, stride) { if (SEL) { // the focused node's threads at full strength, the rest of the web ghosted strokePass(from, to, stride, alpha * 0.1, i => !inSel(i)); strokePass(from, to, stride, Math.min(0.75, Math.max(0.5, alpha * 3)), inSel, 1.4); } else { strokePass(from, to, stride, alpha, null); } if (st.dots && stride === 1) { const { dP } = G; for (let j = from; j < to; j++) if (dP[j] >= G.A) { ctx.fillStyle = !SEL || dP[j] === SEL.node || SEL.nbrs.has(dP[j]) ? 'rgba(255,255,255,0.9)' : 'rgba(255,255,255,0.12)'; const p = project(dP[j]); const r = Math.max(0.6, p[2] * 0.9); ctx.fillRect(p[0] - r / 2, p[1] - r / 2, r, r); } } } const fmt = n => n.toLocaleString('en-US'); function status(t) { sEl.textContent = t; } // while a node is focused, the stats line says exactly what you are looking at function selInfo() { let a = 0, p = 0; for (const nb of SEL.nbrs) nb < G.A ? a++ : p++; const nm = SEL.node < G.A ? (G.addrs ? nameOf(G.addrs[SEL.node]) : 'artist ' + SEL.node) : (G.paddrs && G.paddrs[SEL.node - G.A] ? nameOf(G.paddrs[SEL.node - G.A]) : 'patron ' + (SEL.node - G.A)); const parts = []; if (a) parts.push(`collects from ${fmt(a)} artist${a > 1 ? 's' : ''}`); if (p) parts.push(`collected by ${fmt(p)} patron${p > 1 ? 's' : ''}`); return nm + (parts.length ? ' · ' + parts.join(' · ') : ' · no connections yet'); } function stats() { // while growing, count only the wallets the web has reached so far; // complete, count the whole constellation (unlinked artists included) // say what is shown: the woven crew, against the whole crew aboard const w = st.drawn < G.links ? G.wcum[st.drawn] : G.A + G.P; const lead = HOLDERS ? `${fmt(w)} of ${fmt(HOLDERS.size)} holders` : `${fmt(w)} wallets`; status((SEL ? selInfo() : `${lead} · ${fmt(st.drawn)} connections`) + (st.progress ? ` · ${st.progress}` : '')); const show = G && G.addrs ? '' : 'none'; // names need real addresses abEl.style.display = show; pbEl.style.display = show; } function toggleArtists() { st.artists = !st.artists; abEl.classList.toggle('on', st.artists); if (st.artists && G) ensureNames(G.addrs); } function togglePatrons() { st.patrons = !st.patrons; pbEl.classList.toggle('on', st.patrons); if (st.patrons && G) ensureNames(G.paddrs); } abEl.addEventListener('click', toggleArtists); pbEl.addEventListener('click', togglePatrons); // ---------------- names (a: artists, p: patrons) ---------------- // ENS reverse records, batched in one eth_call; short address as fallback. // labels ride a separate overlay canvas so they never pollute the ink. const RR = '0x3671aE578E63FdF66ad4F3E12CC0c0d71Ac7510C'; // ReverseRecords, mainnet const ENS_KEY = 'networked.ens.v1'; const short = a => a.slice(0, 6) + '…' + a.slice(-4); function encodeGetNames(addrs) { let d = '0xcbf8b66c' + '20'.padStart(64, '0') + addrs.length.toString(16).padStart(64, '0'); for (const a of addrs) d += a.slice(2).toLowerCase().padStart(64, '0'); return d; } function decodeStrings(res) { const h = res.slice(2); const N = i => parseInt(h.slice(i * 64, i * 64 + 64), 16); const base = N(0) / 32; const n = N(base); const dec = new TextDecoder(); const out = []; for (let k = 0; k < n; k++) { const so = base + 1 + N(base + 1 + k) / 32; const len = N(so); const bytes = h.slice((so + 1) * 64, (so + 1) * 64 + len * 2); out.push(dec.decode(Uint8Array.from(bytes.match(/../g) || [], x => parseInt(x, 16)))); } return out; } // one wallet with a malicious resolver reverts a whole ReverseRecords batch — // bisect on revert until the poisoned address is isolated and skipped async function fetchNames(batch, cache) { try { const names = decodeStrings(await rpc('eth_call', [{ to: RR, data: encodeGetNames(batch) }, 'latest'])); batch.forEach((a, j) => { cache[a] = names[j] || ''; }); } catch (e) { if (!/revert|execution/i.test(String(e && e.message))) throw e; // transport problem, not poison if (batch.length === 1) { cache[batch[0]] = ''; return; } const mid = batch.length >> 1; await Promise.all([fetchNames(batch.slice(0, mid), cache), fetchNames(batch.slice(mid), cache)]); } } // the name book: one shared address→name cache, streamed in by batches let BOOK = null; function loadBook() { if (BOOK) return; BOOK = {}; try { BOOK = JSON.parse(localStorage.getItem(ENS_KEY)) || {}; } catch {} } const nameOf = a => { loadBook(); return BOOK[a] || short(a); }; const namesPending = new Set(); // the reasonable ceiling: ~10 batched calls. beyond it, later arrivals keep // short addresses — the label budget only names the earliest anyway, and // selections resolve their own neighborhood on demand. const MAX_ENS = 4000; async function ensureNames(list) { if (!list || !list.length) return; loadBook(); let missing = list.filter(a => BOOK[a] === undefined && !namesPending.has(a)); if (missing.length > MAX_ENS) missing = missing.slice(0, MAX_ENS); missing.forEach(a => namesPending.add(a)); for (let k = 0; k < missing.length; k += 400) { try { await fetchNames(missing.slice(k, k + 400), BOOK); } catch { missing.slice(k).forEach(a => namesPending.delete(a)); return; } // retry later try { localStorage.setItem(ENS_KEY, JSON.stringify(BOOK)); } catch {} } } // ---------------- selection (click a node to focus its network) ---------------- let SEL = null; function neighborsOf(node) { const nbrs = new Set(); const { dP, dA, links } = G; for (let i = 0; i < links; i++) { if (dP[i] === node) nbrs.add(dA[i]); else if (dA[i] === node) nbrs.add(dP[i]); } return nbrs; } const addrOf = n => n < G.A ? (G.addrs ? G.addrs[n] : null) : (G.paddrs ? G.paddrs[n - G.A] : null); function reselect(node) { SEL = { node, nbrs: neighborsOf(node) }; if (G.addrs) { // name the neighborhood on demand — always cheap const want = [addrOf(node)]; let n = 0; for (const nb of SEL.nbrs) { if (n++ > 600) break; want.push(addrOf(nb)); } ensureNames(want.filter(Boolean)); } } function select(node) { reselect(node); // rotate the globe so this node comes front and center const x = mx[node], y = my[node], z = mz[node]; const hxz = Math.hypot(x, z) || 1e-6; let ty = Math.atan2(x, -z); ty += Math.round((view.yaw - ty) / (2 * Math.PI)) * 2 * Math.PI; // nearest whole turn view.tYaw = ty; view.tPitch = Math.min(1.5, Math.max(-1.5, Math.atan2(-y, hxz))); view.lastInput = performance.now(); view.dirty = true; stats(); } function clearSel() { if (SEL) { SEL = null; view.dirty = true; stats(); } } function inSel(i) { return G.dP[i] === SEL.node || G.dA[i] === SEL.node; } function hitTest(x, y) { const maxN = G.A + Math.min(G.P, 200000); // huge sims: artists only past this let best = -1, bd = 144; // within 12px for (let i = 0; i < maxN; i++) { const p = project(i); const dx = p[0] - x, dy = p[1] - y; const d = dx * dx + dy * dy; if (d < bd) { bd = d; best = i; } } return best; } let labelBoxes = []; // a visible name IS its node function hitLabel(x, y) { for (const b of labelBoxes) if (x >= b.x - 3 && x <= b.x + b.w + 3 && y >= b.y - 9 && y <= b.y + 9) return b.n; return -1; } function drawLabels() { const w = window.innerWidth, h = window.innerHeight; octx.clearRect(0, 0, w, h); labelBoxes = []; if (!G) return; octx.font = '10px monospace'; octx.textBaseline = 'middle'; octx.lineWidth = 3; octx.strokeStyle = '#000'; octx.fillStyle = '#fff'; const done = new Set(); const put = (node, text) => { if (done.has(node)) return; done.add(node); const p = project(node); if (p[0] < -120 || p[0] > w + 40 || p[1] < -10 || p[1] > h + 10) return; octx.strokeText(text, p[0] + 5, p[1] - 6); octx.fillText(text, p[0] + 5, p[1] - 6); labelBoxes.push({ n: node, x: p[0] + 5, y: p[1] - 6, w: octx.measureText(text).width }); }; const aName = i => G.addrs ? nameOf(G.addrs[i]) : 'artist ' + i; const pName = n => G.paddrs && G.paddrs[n - G.A] ? nameOf(G.paddrs[n - G.A]) : 'patron ' + (n - G.A); const name = n => n < G.A ? aName(n) : pName(n); if (SEL) { // the focused network is always named & marked const mark = (node, r) => { const p = project(node); octx.fillStyle = '#000'; octx.fillRect(p[0] - r / 2 - 1, p[1] - r / 2 - 1, r + 2, r + 2); octx.fillStyle = '#fff'; octx.fillRect(p[0] - r / 2, p[1] - r / 2, r, r); }; let m = 0; for (const nb of SEL.nbrs) { if (m++ > 600) break; mark(nb, 3); } mark(SEL.node, 6); put(SEL.node, name(SEL.node)); let n = 0; for (const nb of SEL.nbrs) { if (n++ > 400) break; put(nb, name(nb)); } } // labe