memoscan
← all memos

Memo 0x758cef2f…4acc6c on Ethereum

<!doctype html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>networked · dimensional</title> <style> html,body{margin:0;height:100%;background:#fff;overflow:hidden} canvas{display:block;touch-action:none} #s{position:fixed;left:12px;bottom:10px;font:10px/1.4 monospace;color:#000; user-select:none;pointer-events:none;white-space:pre;transition:opacity 1.2s ease} #s.hid{opacity:0} #ab,#pb{pointer-events:auto;cursor:pointer} #s.hid #ab,#s.hid #pb{pointer-events:none} #ab.on,#pb.on{text-decoration:underline} </style> </head> <body> <canvas id="c"></canvas> <canvas id="o" style="position:fixed;inset:0;pointer-events:none"></canvas> <div id="s"><span id="st"></span><span id="ab" style="display:none"> · a artists</span><span id="pb" style="display:none"> · p patrons</span></div> <script> 'use strict'; /* networked · dimensional — the patron lattice as a sphere. White ground, black filament. Every wallet a node; every mint a thread. Artists spiral over a hollow globe in the order they arrived; patrons shell around the artists they minted from; patrons of many artists float between them as bridges. Move the cursor to turn the world: while it moves you see a sketch, when it rests the full ink settles back in. Reads Ethereum mainnet directly: your wallet's RPC if one is installed (read-only, no connection prompt), public RPCs otherwise. CC0 — AI or human: fork this, add a layer of your own. ?t=8 growth duration, seconds ?grow=0 skip the growth animation ?fresh=1 ignore the local cache, re-read the chain from the factory's birth ?rpc=… bring your own mainnet endpoint (also offered when unreachable) */ (() => { const GA = Math.PI * (3 - Math.sqrt(5)); // golden angle const cv = document.getElementById('c'); const ctx = cv.getContext('2d'); const ov = document.getElementById('o'); const octx = ov.getContext('2d'); const sEl = document.getElementById('st'); const sBox = document.getElementById('s'); const abEl = document.getElementById('ab'); const pbEl = document.getElementById('pb'); const Q = new URLSearchParams(location.search); // the stats line withdraws when the observer does let uiLast = performance.now(); for (const ev of ['mousemove', 'mousedown', 'wheel', 'touchstart', 'keydown']) window.addEventListener(ev, () => { uiLast = performance.now(); }, { passive: true }); // ---------------- chain config ---------------- const FACTORY = '0x0c2705cf48e49cc896252dd16dc8c5d31df753b2'; // factory.networked.eth const DEPLOY = 25551944; // factory birth block const RPCS = [ 'https://eth.drpc.org', 'https://rpc.mevblocker.io', 'https://gateway.tenderly.co/public/mainnet', 'https://0xrpc.io/eth' ]; // a viewer-supplied endpoint outlives any hardcoded list: ?rpc=… or the // prompt shown when the chain is unreachable; remembered, tried first const RPC_KEY = 'networked.rpc.v1'; try { const userRpc = Q.get('rpc') || localStorage.getItem(RPC_KEY); if (userRpc && /^https?:\/\//.test(userRpc)) { RPCS.unshift(userRpc); if (Q.get('rpc')) localStorage.setItem(RPC_KEY, userRpc); } } catch {} const T_CREATED = '0x4c9eee098e07d2de26d609f55a57a7fe893079913184f8fccc1bdd6d4e9d539b'; const T_EXT = '0x4954fe29013e393bf2a4b4ca99a121917ad235960cfade963ca908de359fecac'; const T_721 = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'; const T_1155S = '0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62'; const T_1155B = '0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7fb'; const ZERO32 = '0x' + '0'.repeat(64); const CHUNK = 10000; const CACHE_KEY = 'networked.lattice.v1'; // shared with the flat piece const SNAP = null; // ---------------- rpc ---------------- let rpcCursor = 0; async function rpc(method, params) { const provs = []; if (window.ethereum) provs.push(async () => { const cid = await window.ethereum.request({ method: 'eth_chainId' }); if (parseInt(cid, 16) !== 1) throw new Error('wrong chain'); return window.ethereum.request({ method, params }); }); for (let k = 0; k < RPCS.length; k++) provs.push((u => async () => { const r = await fetch(u, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }) }); const j = await r.json(); if (j.error) throw new Error(j.error.message); return j.result; })(RPCS[(rpcCursor + k) % RPCS.length])); rpcCursor++; let err; for (let round = 0; round < 2; round++) for (const p of provs) { try { return await p(); } catch (e) { if (/revert/i.test(String(e && e.message))) throw e; // deterministic — no provider will differ err = e; await new Promise(r => setTimeout(r, 300 * (round + 1))); } } throw err; } const hex = n => '0x' + n.toString(16); const toAddr = topic => '0x' + topic.slice(26); const word = (data, i) => Number(BigInt('0x' + data.slice(2 + i * 64, 66 + i * 64))); const byPos = (a, b) => (parseInt(a.blockNumber, 16) - parseInt(b.blockNumber, 16)) || (parseInt(a.logIndex, 16) - parseInt(b.logIndex, 16)); async function streamSweep(filter, fromB, toB, label, onChunk) { const jobs = []; for (let f = fromB; f <= toB; f += CHUNK) jobs.push([f, Math.min(f + CHUNK - 1, toB)]); const results = new Array(jobs.length); let next = 0, applied = 0; await Promise.all(Array.from({ length: Math.min(3, jobs.length) }, async () => { while (next < jobs.length) { const i = next++; const [f, t] = jobs[i]; results[i] = (await rpc('eth_getLogs', [{ ...filter, fromBlock: hex(f), toBlock: hex(t) }])).sort(byPos); while (applied < jobs.length && results[applied] !== undefined) { st.progress = `${label} ${Math.round((applied + 1) / jobs.length * 100)}%`; onChunk(results[applied++]); if (G) stats(); else status(st.progress); } } })); } // ---------------- data ---------------- let D = null, G = null; function decodeMints(logs) { const out = []; for (const l of logs) { const t0 = l.topics[0]; let from, to, qty = 1; if (t0 === T_721) { from = l.topics[1]; to = l.topics[2]; } else if (t0 === T_1155S) { from = l.topics[2]; to = l.topics[3]; qty = word(l.data, 1); } else if (t0 === T_1155B) { from = l.topics[2]; to = l.topics[3]; const vOff = word(l.data, 1) / 32; const n = word(l.data, vOff); qty = 0; for (let i = 0; i < n; i++) qty += word(l.data, vOff + 1 + i); } else continue; if (from !== ZERO32 || to === ZERO32) continue; out.push([toAddr(to), l.address.toLowerCase(), qty, parseInt(l.blockNumber, 16)]); } return out; } let FLASH = [], booted = false; // fresh threads glow as they land function rebuild() { const prev = G ? G.links : 0; buildFromD(); tiers(); layout3(); if (Q.get('grow') === '0') st.drawn = G.links; if (booted) for (let i = prev; i < G.links; i++) FLASH.push({ i, t: performance.now() }); view.dirty = true; } async function loadChain() { if (!D && Q.get('fresh') !== '1') { try { D = JSON.parse(localStorage.getItem(CACHE_KEY)); } catch {} D = D || SNAP; } if (D && D.C.length && !G) rebuild(); // cache/SNAP renders before any network call const head = parseInt(await rpc('eth_blockNumber', []), 16); const from = D ? D.head + 1 : DEPLOY; if (!D) D = { head, C: [], X: [], M: [] }; if (from > head) return; const c0 = D.C.length, x0 = D.X.length, m0 = D.M.length; try { await streamSweep({ address: FACTORY, topics: [[T_CREATED, T_EXT]] }, from, head, 'reading artists', logs => { for (const l of logs) { if (l.topics[0] === T_CREATED) D.C.push([toAddr(l.topics[1]), toAddr(l.topics[2]), toAddr(l.topics[3])]); else D.X.push([toAddr(l.topics[1]), toAddr(l.topics[2])]); } if (logs.length) rebuild(); }); const editions = D.C.map(c => c[2]).concat(D.X.map(x => x[1])); if (editions.length) await streamSweep({ address: editions, topics: [[T_721, T_1155S, T_1155B]] }, from, head, 'reading patrons', logs => { const m = decodeMints(logs); if (m.length) { D.M.push(...m); rebuild(); } }); } catch (e) { D.C.length = c0; D.X.length = x0; D.M.length = m0; if (D.C.length) rebuild(); throw e; } D.head = head; // past ~3MB the cache stops persisting (quota) — from there, embed a SNAP try { const s = JSON.stringify(D); if (s.length < 3e6) localStorage.setItem(CACHE_KEY, s); } catch {} } async function poll() { try { await loadChain(); st.progress = ''; if (G) { stats(); const rp = document.getElementById('rp'); // the chain came back on its own if (rp) rp.remove(); } } catch {} setTimeout(poll, 60000); } // ---------------- graph ---------------- function buildGraph(pairs, artistIdx) { const A = artistIdx.size; const nodeIdx = new Map(); const plist = [], paddrs = []; const dP = [], dA = [], dE = []; const seen = new Map(); for (const [pa, a, q] of pairs) { let node = artistIdx.has(pa) ? artistIdx.get(pa) : nodeIdx.get(pa); if (node === undefined) { node = A + plist.length; nodeIdx.set(pa, node); plist.push([]); paddrs.push(pa); } const key = node * A + a; const at = seen.get(key); if (at !== undefined) { dE[at] = Math.min(255, dE[at] + q); continue; } seen.set(key, dP.length); dP.push(node); dA.push(a); dE.push(Math.min(255, q)); if (node >= A) plist[node - A].push(a); } const P = plist.length; const off = new Uint32Array(P + 1); for (let p = 0; p < P; p++) off[p + 1] = off[p] + plist[p].length; const csrA = new Uint32Array(off[P]); for (let p = 0; p <