0x28996f7d…515asent to0x00000000…5b01·#24,349,897·view on Etherscan
4k<!doctype html>
<html>
<head>
<meta name="title" content="PUSH4" />
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
overflow: hidden;
}
#artwork {
width: 100%;
height: calc(100% - 30px);
display: flex;
align-items: flex-start;
justify-content: flex-start;
}
#artwork img,
#artwork iframe {
height: 100%;
width: auto;
max-width: 100%;
image-rendering: pixelated;
}
#info {
height: 30px;
display: flex;
flex-direction: column;
justify-content: flex-end;
gap: 5px;
}
#info-text {
display: flex;
justify-content: space-between;
padding: 0 0 6px;
font-family: monospace;
font-size: 11px;
color: #666;
letter-spacing: 0.3px;
}
#progress-bar {
height: 2px;
background: #222;
}
#progress {
height: 100%;
background: #555;
width: 0%;
}
</style>
</head>
<body>
<div id="artwork"><div id="loading">Loading...</div></div>
<div id="info">
<div id="info-text">
<span><span id="title">--</span> by <span id="creator">--</span></span>
<span id="proxy">--</span>
</div>
<div id="progress-bar"><div id="progress"></div></div>
</div>
<script>
// Injected values (replaced at runtime by the contract)
const RPC_URLS = [{{FILE_URIS}}];
const BLOCK_INTERVAL = {{BLOCK_INTERVAL}};
const CONTRACT_ADDRESS = '{{CORE_ADDRESS}}';
const GAS_LIMIT = '0x20C85580';
let currentRpcIndex = 0;
let lastBlock = 0;
let lastProxyIdx = -1;
let proxyAddress = null;
let proxyCount = 0;
let rendererAddress = null;
function showError(msg) {
const artwork = document.getElementById('artwork');
// Simple error styling, centering it in the container
artwork.innerHTML = '<div style="color: #ff5555; font-family: monospace; padding: 20px; text-align: center;">' + msg + '</div>';
}
async function fetchWithFallback(method, params) {
const errors = [];
for (let i = 0; i < RPC_URLS.length; i++) {
const idx = (currentRpcIndex + i) % RPC_URLS.length;
const rpcUrl = RPC_URLS[idx];
try {
const res = await fetch(rpcUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: method, params: params })
});
if (!res.ok) {
const err = `HTTP ${res.status}: ${res.statusText}`;
errors.push({ rpc: rpcUrl, error: err });
continue;
}
// Use text() first to avoid JSON parse errors on large responses
const text = await res.text();
let json;
try {
json = JSON.parse(text);
} catch (parseErr) {
const err = `JSON parse error: ${parseErr.message} (response length: ${text.length})`;
errors.push({ rpc: rpcUrl, error: err });
continue;
}
if (json.error) {
const err = `RPC error: ${JSON.stringify(json.error)}`;
errors.push({ rpc: rpcUrl, error: err });
continue;
}
currentRpcIndex = idx;
return json.result;
} catch (e) {
const err = e.message || String(e);
errors.push({ rpc: rpcUrl, error: err });
continue;
}
}
console.error('[RPC] All RPCs failed:', errors);
showError('Connection Error: All RPC nodes are unresponsive.');
throw new Error('All RPCs failed: ' + JSON.stringify(errors));
}
async function getBlockNumber() {
const hex = await fetchWithFallback('eth_blockNumber', []);
const blockNum = parseInt(hex, 16);
return blockNum;
}
// Discover the proxy address from PUSH4Core.proxy()
async function discoverProxy() {
// proxy() selector: 0xec556889
const data = '0xec556889';
try {
const res = await fetchWithFallback('eth_call', [{ to: CONTRACT_ADDRESS, data: data }, 'latest']);
// Result is an address (32 bytes, right-padded)
const addr = '0x' + res.slice(26, 66);
if (addr === '0x0000000000000000000000000000000000000000') {
return null;
}
return addr;
} catch (e) {
console.error('[Proxy] Failed to discover proxy:', e);
return null;
}
}
// Get proxy count from orchestrator.proxyCount()
async function getProxyCount(orchestratorAddr) {
if (!orchestratorAddr) {
return 0;
}
// proxyCount() selector: 0x37c954d8
const data = '0x37c954d8';
try {
const res = await fetchWithFallback('eth_call', [{ to: orchestratorAddr, data: data }, 'latest']);
const count = parseInt(res, 16);
return count;
} catch (e) {
console.error('[ProxyCount] Failed to get proxy count:', e);
return 0;
}
}
// Get proxy at index from orchestrator.getProxyAt(uint256)
async function getProxyAt(orchestratorAddr, index) {
if (!orchestratorAddr) return null;
// getProxyAt(uint256) selector: 0xcc935efe
const data = '0xcc935efe' + index.toString(16).padStart(64, '0');
try {
const res = await fetchWithFallback('eth_call', [{ to: orchestratorAddr, data: data }, 'latest']);
const addr = '0x' + res.slice(26, 66);
return addr;
} catch (e) {
console.error('[ProxyAt] Failed to get proxy at index:', e);
return null;
}
}
// Decode a string from ABI-encoded response
function decodeString(hex) {
const data = hex.slice(2);
const offset = parseInt(data.slice(0, 64), 16) * 2;
const len = parseInt(data.slice(offset, offset + 64), 16) * 2;
const strHex = data.slice(offset + 64, offset + 64 + len);
return strHex.match(/.{2}/g).map(x => String.fromCharCode(parseInt(x, 16))).join('');
}
// Get title from proxy.title()
async function getProxyTitle(proxyAddr) {
if (!proxyAddr) return '--';
// title() selector: 0x4a79d50c
try {
const res = await fetchWithFallback('eth_call', [{ to: proxyAddr, data: '0x4a79d50c' }, 'latest']);
const title = decodeString(res);
return title;
} catch (e) {
console.error('[ProxyTitle] Failed to get title:', e);
return '--';
}
}
// Get creator from proxy.creator() - returns (string name, address wallet)
async function getProxyCreator(proxyAddr) {
if (!proxyAddr) return '--';
// creator() selector: 0x02d05d3f
try {
const res = await fetchWithFallback('eth_call', [{ to: proxyAddr, data: '0x02d05d3f' }, 'latest']);
// Creator struct: (string name, address wallet)
// First 32 bytes: offset to tuple, then offset to string, then address (right-padded)
const data = res.slice(2);
// Offset to the struct
const structOffset = parseInt(data.slice(0, 64), 16) * 2;
// At structOffset: offset to name string (relative to struct start)
const nameOffsetRel = parseInt(data.slice(structOffset, structOffset + 64), 16) * 2;
// Name string is at structOffset + nameOffsetRel
const nameStart = structOffset + nameOffsetRel;
const nameLen = parseInt(data.slice(nameStart, nameStart + 64), 16) * 2;
const nameHex = data.slice(nameStart + 64, nameStart + 64 + nameLen);
const name = nameHex.match(/.{2}/g).map(x => String.fromCharCode(parseInt(x, 16))).join('');
return name;
} catch (e) {
console.error('[ProxyCreator] Failed to get creator:', e);
return '--';
}
}
// Discover the renderer address from PUSH4Core.renderer()
async function discoverRenderer() {
// renderer() selector: 0x8ada6b0f
try {
const res = await fetchWithFallback('eth_call', [{ to: CONTRACT_ADDRESS, data: '0x8ada6b0f' }, 'latest']);
return '0x' + res.slice(26, 66);
} catch (e) {
console.error('[Renderer] Failed to discover renderer:', e);
return null;
}
}
async function getSvgDataUri() {
// getSvgDataUri() selector: 0xbfec768f
const res = await fetchWithFallback('eth_call', [{ to: rendererAddress, data: '0xbfec768f', gas: GAS_LIMIT }, 'latest']);
return decodeString(res);
}
function syncInfoWidth() {
const img = document.querySelector('#artwork img');
const info = document.getElementById('info');
if (img) info.style.width = img.clientWidth + 'px';
}
function renderArtwork(svgDataUri) {
const artwork = document.getElementById('artwork');
artwork.innerHTML = '<img src="' + svgDataUri + '" alt="PUSH4">';
artwork.querySelector('img').onload = syncInfoWidth;
}
function updateProgress(elapsed) {
const bar = document.getElementById('progress');
const currentPct = (elapsed / BLOCK_INTERVAL) * 100;
const nextPct = Math.min(((elapsed + 1) / BLOCK_INTERVAL) * 100, 100);
// Jump to current position immediately
bar.style.transition = 'none';
bar.style.width = currentPct + '%';
// Double-rAF ensures the jump is painted before starting the transition
requestAnimationFrame(function() {
requestAnimationFrame(function() {
bar.style.transition = 'width 12s linear';
bar.style.width = nextPct + '%';
});
});
}
async function pollBlock() {
try {
const block = await getBlockNumber();
const proxyIdx = proxyCount > 0 ? Math.floor(block / BLOCK_INTERVAL) % proxyCount : 0;
document.getElementById('proxy').textContent = (proxyIdx + 1) + '/' + proxyCount;
if (block !== lastBlock) {
lastBlock = block;
updateProgress(block % BLOCK_INTERVAL);
if (proxyIdx !== lastProxyIdx) {
lastProxyIdx = proxyIdx;
// Get the current proxy, then fetch title/creator/artwork in parallel
const currentProxy = await getProxyAt(proxyAddress, proxyIdx);
const [title, creator, svgDataUri] = await Promise.all([
getProxyTitle(currentProxy),
getProxyCreator(currentProxy),
getSvgDataUri()
]);
document.getElementById('title').textContent = title;
document.getElementById('creator').textContent = creator;
renderArtwork(svgDataUri);
}
}
} catch (e) {
console.error('[Poll] Poll failed:', e);
}
}
async function init() {
// Discover proxy, renderer and get count
[proxyAddress, rendererAddress] = await Promise.all([
discoverProxy(),
discoverRenderer()
]);
proxyCount = await getProxyCount(proxyAddress);
// Get initial proxy index and fetch title/creator/svg in parallel
const block = await getBlockNumber();
const proxyIdx = proxyCount > 0 ? Math.floor(block / BLOCK_INTERVAL) % proxyCount : 0;
lastProxyIdx = proxyIdx;
const currentProxy = await getProxyAt(proxyAddress, proxyIdx);
const [title, creator, svgDataUri] = await Promise.all([
getProxyTitle(currentProxy),
getProxyCreator(currentProxy),
getSvgDataUri()
]);
document.getElementById('title').textContent = title;
document.getElementById('creator').textContent = creator;
renderArtwork(svgDataUri);
lastBlock = block;
updateProgress(block % BLOCK_INTERVAL);
window.addEventListener('resize', syncInfoWidth);
// Start polling
setInterval(pollBlock, 3000);
}
init().catch(e => console.error('[Init] Fatal error:', e));
</script>
</body>
</html>