0xa89f3db4…071asent to0xecb92cc7…1463·#25,767,880·view on Etherscan
<!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:#000;overflow:hidden}
canvas{display:block;touch-action:none}
#s{position:fixed;left:12px;bottom:10px;font:10px/1.4 monospace;color:#fff;
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 · vessel, dimensional — the holders' lattice as a sphere.
Black ground, white filament. Only the wallets that hold the Vessel:
their patronage threads through the networked.art constellation.
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 VESSEL = '0xecb92cc7112b80a2234936315bbb493fb48d1463'; // THE_VESSEL, 10,000 berths
const MC3 = '0xca11bde05977b3631167028862be2a173976ca11'; // Multicall3
const V_KEY = 'networked.vessel.v1'; // vessel berth owners
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);
}
}
}));
}
// ---------------- vessel holders (multicall ownerOf sweep) ----------------
// V = { head, owners: { tokenId: address } } ; HOLDERS = unique owner set
let V = null, HOLDERS = null;
function ownerOfBatch(ids) {
const n = ids.length;
let head = '', tail = '';
for (let k = 0; k < n; k++) {
head += (n * 32 + k * 192).toString(16).padStart(64, '0');
tail += VESSEL.slice(2).padStart(64, '0')
+ '1'.padStart(64, '0') // allowFailure — unclaimed berths revert
+ '60'.padStart(64, '0')
+ '24'.padStart(64, '0')
+ '6352211e' + ids[k].toString(16).padStart(64, '0') + '0'.repeat(56);
}
return '0x82ad56cb' + '20'.padStart(64, '0') + n.toString(16).padStart(64, '0') + head + tail;
}
function decodeOwners(res, ids, into) {
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);
for (let k = 0; k < n; k++) {
const off = base + 1 + N(base + 1 + k) / 32;
if (N(off) === 1 && N(off + 2) >= 32)
into[ids[k]] = '0x' + h.slice((off + 3) * 64 + 24, (off + 3) * 64 + 64);
}
}
async function loadVessel() {
if (!V && Q.get('fresh') !== '1') {
try { V = JSON.parse(localStorage.getItem(V_KEY)); } catch {}
}
if (V) HOLDERS = new Set(Object.values(V.owners));
const head = parseInt(await rpc('eth_blockNumber', []), 16);
if (!V) {
V = { head, owners: {} };
let batchN = 500;
let id = 1;
while (id <= 10000) {
const ids = [];
for (let k = 0; k < batchN && id + k <= 10000; k++) ids.push(id + k);
try {
decodeOwners(await rpc('eth_call', [{ to: MC3, data: ownerOfBatch(ids) }, 'latest']), ids, V.owners);
id += ids.length;
status(`reading the vessel · ${Math.round(id / 100)}%`);
} catch (e) {
batchN = Math.floor(batchN / 2);
if (batchN < 25) throw e;
}
}
V.head = head;
} else if (V.head < head) {
const logs = [];
await streamSweep({ address: VESSEL, topics: [T_721] }, V.head + 1, head, 'reading the vessel', l => logs.push(...l));
for (const l of logs) {
if (l.topics.length < 4) continue; // ERC-721 Transfer has indexed tokenId
const id = parseInt(l.topics[3], 16);
const to = toAddr(l.topics[2]);
if (to === '0x' + '0'.repeat(40)) delete V.owners[id];
else V.owners[id] = to;
}
V.head = head;
}
HOLDERS = new Set(Object.values(V.owners));
try { localStorage.setItem(V_KEY, JSON.stringify(V)); } catch {}
}
// ---------------- 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