memoscan
← all memos

Memo 0x538ff9a2…8b8aa2 on Ethereum

k', 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 = '#fff'; octx.fillStyle = '#000'; 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 = '#fff'; octx.fillRect(p[0] - r / 2 - 1, p[1] - r / 2 - 1, r + 2, r + 2); octx.fillStyle = '#000'; 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)); } } // label budget: at future scale a full sweep would be soup — the earliest // arrivals (the founders) get named first, zoom + click reach the rest if (st.artists) for (let i = 0; i < G.A && done.size < 900; i++) put(i, aName(i)); if (st.patrons) for (let p = 0; p < G.P && done.size < 1800; p++) put(G.A + p, pName(G.A + p)); } // ---------------- main loop ---------------- // moving: cleared sketch each frame (sampled threads, boosted alpha) // at rest: one clear, then the full ink settles back in over frames const SKETCH_MAX = 30000; let rafId = 0; function tick(now) { if (!G) { rafId = requestAnimationFrame(tick); return; } const w = window.innerWidth, h = window.innerHeight; // idle: the globe turns, slowly, always — the ink gets its 3s to settle first if (now - view.lastInput > 3000 && !drag.on) view.tYaw += 0.0008; // stats fade with the observer's attention; never while the chain is streaming sBox.classList.toggle('hid', now - uiLast > 4000 && !st.progress && !!G && st.drawn >= G.links); const d = Math.abs(view.tYaw - view.yaw) + Math.abs(view.tPitch - view.pitch) + Math.abs(view.tZoom - view.zoom); const moving = d > 0.0006; if (moving) { view.yaw += (view.tYaw - view.yaw) * 0.12; view.pitch += (view.tPitch - view.pitch) * 0.12; view.zoom += (view.tZoom - view.zoom) * 0.18; } setMatrix(w, h); // growth advances regardless of rotation if (st.drawn < G.links) { st.drawn = Math.min(G.links, st.drawn + st.chunk); stats(); } if (moving) { clearAll(); const stride = Math.max(1, Math.ceil(st.drawn / SKETCH_MAX)); if (st.drawn) drawLinks3(0, st.drawn, Math.min(0.7, st.alpha * Math.sqrt(stride)), stride); drawArtists(); view.dirty = true; view.full = 0; } else { if (view.dirty) { // first still frame: restart the ink clearAll(); drawArtists(); view.full = 0; view.dirty = false; } if (view.full < st.drawn) { const fast = Math.max(st.chunk * 12, 20000); const to = Math.min(st.drawn, view.full + fast); drawLinks3(view.full, to, st.alpha, 1); view.full = to; } } drawLabels(); if (FLASH.length) { // fresh threads glow, then fade into the web FLASH = FLASH.filter(f => now - f.t < 5000); for (const f of FLASH) { const a = 0.9 * (1 - (now - f.t) / 5000); const p1 = project(G.dP[f.i]); const x1 = p1[0], y1 = p1[1]; const p2 = project(G.dA[f.i]); octx.strokeStyle = `rgba(0,0,0,${a})`; octx.lineWidth = 1.6; octx.beginPath(); octx.moveTo(x1, y1); octx.lineTo(p2[0], p2[1]); octx.stroke(); } } rafId = requestAnimationFrame(tick); } // ---------------- input ---------------- // grab the globe: drag rotates so the face under the cursor follows the hand, // release keeps a little inertia const drag = { on: false, x: 0, y: 0, x0: 0, y0: 0, vx: 0, vy: 0 }; const clampPitch = v => Math.min(1.4, Math.max(-1.4, v)); function dragBy(dx, dy) { const k = Math.PI / Math.min(window.innerWidth, window.innerHeight); drag.vx = -dx * k; drag.vy = dy * k; view.tYaw += drag.vx; view.tPitch = clampPitch(view.tPitch + drag.vy); view.lastInput = performance.now(); } cv.style.cursor = 'grab'; window.addEventListener('mousedown', e => { drag.on = true; drag.x = drag.x0 = e.clientX;