0xacfba7ce…4082sent to0x00000000…6f2a·#25,684,017·view on Etherscan
bel>
<select id=sort aria-label="sort listings">
<option value=rank>curation weight</option>
<option value=standard>token standard</option>
<option value=symbol>symbol A–Z</option>
<option value=name>name A–Z</option>
<option value=decimals>decimals</option>
</select>
<span class=ct id=cnt></span>
<span class=exp><button class=lk id=exTL title="Uniswap token list schema">token list json</button> · <button class=lk id=exRaw title="every field the registry holds, plus each listing's rendered card art">raw json</button></span>
</div>
<div class=fc id=fc role=group aria-label="filter by category"></div>
<div class="fc fc2" id=fc2 role=group aria-label="filter by standard" hidden></div>
<p id=msg></p>
<div class=g id=grid></div>
<footer>
<p>Every card is rendered onchain by the registry's own renderer and decoded here — the same bytes a wallet or marketplace reads.</p>
<p id=routes></p>
<details><summary>Integrating</summary>
<p>Every read is an ordinary <code>eth_call</code>; no key, no wallet, no indexer.</p>
<pre id=devdoc></pre>
</details>
<p>Data source <span id=src>—</span> · <button class=lk id=setr aria-expanded=false aria-controls=rw>use your own RPC</button></p>
<div class=rw id=rw hidden>
<input id=rpcu type=url inputmode=url autocomplete=off spellcheck=false placeholder="https://your-node.example/rpc" aria-label="JSON-RPC endpoint for Ethereum mainnet">
<button class=bt id=rpcs>use it</button><button class=bt id=rpcc>reset</button>
</div>
</footer>
</div>
<dialog id=dlg aria-label="listing detail"><div class=dh><b id=dt></b><span><span id=dla><button class=lk id=dsvg>svg</button> · <button class=lk id=djson>json</button> · </span><span id=dda hidden><button class=lk id=ddl>download</button> · <button class=lk id=dcp>copy</button> · </span><button class=bt id=dx>close</button></span></div><div class=db><p class=dd id=ddesc hidden></p><p class=ln id=dlk></p><div class=kv id=dkv></div><p class=nb id=dnote hidden></p><pre id=dj></pre></div></dialog>
<script>
const L='0x0000006013df75a31678b786061c2b54bf531524',
/* Keyless endpoints, each verified against this registry. Popular ones are absent
where they answer eth_chainId but return nothing, or reject batches. */
RPCS=['https://eth.blockrazor.xyz','https://eth-mainnet.public.blastapi.io','https://ethereum-rpc.publicnode.com','https://rpc.mevblocker.io','https://eth.merkle.io','https://0xrpc.io/eth','https://eth.rpc.blxrbdn.com','https://eth.drpc.org','https://mainnet.gateway.tenderly.co','https://eth-pokt.nodies.app','https://eth.api.onfinality.io/public'],
S={ids:'0xdf7ca268',json:'0x74e18e96',uri:'0xc87b56dd',curi:'0xe8a3d485',mc:'0xac9650d8'},
$=i=>document.getElementById(i),
esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])),
z=n=>BigInt(n).toString(16).padStart(64,'0'),
short=u=>String(u).replace(/^https?:\/\//,'').replace(/\/.*$/,'');
let RPC=null,ROWS=[],SEED=null,MARK='';const BAD=new Set();
async function post(u,b){let c=AbortSignal.timeout?AbortSignal.timeout(12000):undefined;
let r=await fetch(u,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(b),signal:c});
if(!r.ok)throw Error('HTTP '+r.status);let j=await r.json();if(j&&j.error)throw Error(j.error.message||'rpc error');return j.result}
/* The registry inherits solady's Multicallable, so ONE eth_call to multicall(bytes[])
carries every read. Beats JSON-RPC batching twice over: it is an ordinary eth_call,
so endpoints that cap or reject batches still work - four below do - and it is one
round trip. No Multicall3, no extra contract to trust. */
const W=x=>BigInt(x).toString(16).padStart(64,'0');
function mc(calls){let n=calls.length,offs=[],body='',cur=n*32;
for(let c of calls){let d=c.slice(2),len=d.length/2,pad=(32-len%32)%32;
offs.push(cur);body+=W(len)+d+'0'.repeat(pad*2);cur+=32+len+pad}
return S.mc+W(32)+W(n)+offs.map(W).join('')+body}
function unmc(hex){let h=(hex||'0x').slice(2);if(h.length<128)return[];
/* Offsets and lengths here come straight from an untrusted RPC, so bounds-check every
read: an out-of-range word must abort, not silently slice out garbage. */
const w=i=>{let s=h.slice(i*64,(i+1)*64);if(s.length<64)throw Error('short return');return Number(BigInt('0x'+s))};
let arr=w(0)/32,n=w(arr),out=[];
if(!Number.isSafeInteger(arr)||!Number.isSafeInteger(n)||n*64>h.length)throw Error('bad return');
for(let i=0;i<n;i++){let p=arr+1+w(arr+1+i)/32,len=w(p);
if(!Number.isSafeInteger(p)||(p+1)*64+len*2>h.length)throw Error('bad return');
out.push('0x'+h.slice((p+1)*64,(p+1)*64+len*2))}
return out}
/* An injected provider can stall forever (locked wallet); bound it and fail over. */
const deadline=(p,ms)=>Promise.race([p,new Promise((_,x)=>setTimeout(()=>x(Error('timeout')),ms))]);
const wallet=(method,params)=>deadline(window.ethereum.request({method,params}),12000);
async function raw(u,calls){let data=mc(calls);
if(u==='wallet')return unmc(await wallet('eth_call',[{to:L,data},'latest']));
return unmc(await post(u,{jsonrpc:'2.0',id:1,method:'eth_call',params:[{to:L,data},'latest']}))}
/* Round-robin the starting point so visitors spread across the pool rather than all
leaning on the first entry, then fail over on error OR empty answer. */
const saved=()=>{try{return localStorage.getItem('tl.rpc')||''}catch(e){return''}};
function order(){let s=saved(),n=RPCS.length,o=Math.floor(Math.random()*n),
ring=Array.from({length:n},(_,i)=>RPCS[(o+i)%n]);
return (s?[s]:[]).concat(window.ethereum?['wallet']:[],ring.filter(u=>u!==s))}
/* Concurrent chunk lanes all reach for an endpoint at once, and before they existed
only one probe could ever be in flight. Without this, a single blip has three lanes
walking the pool independently — three simultaneous probes, each able to mark a
different endpoint BAD, so one hiccup can burn three of them. Share the attempt. */
let PICKING=null;
function pick(){if(RPC&&SEED)return Promise.resolve(RPC);
return PICKING||(PICKING=_pick().finally(()=>{PICKING=null}))}
async function _pick(){
for(let u of order()){if(BAD.has(u))continue;
/* Probe with the calls the page needs and keep the answer, so the probe IS wave one.
An endpoint can answer eth_chainId and still return nothing useful. */
/* CREATE2 means this address can hold something else entirely on another chain. */
try{if(u==='wallet'&&BigInt(await wallet('eth_chainId',[]))!==1n)throw Error('wrong chain');
let r=await raw(u,[S.curi,S.ids]);if(!r[1]||r[1].length<130)throw Error('empty');
SEED=r;RPC=u;$('src').textContent=u==='wallet'?'your wallet':short(u);return u}
catch(e){BAD.add(u)}}
throw Error('No endpoint returned registry data.')}
async function callMany(calls){
/* Every failure marks the endpoint BAD, so pick() throws once the pool is spent. */
for(;;){let u=await pick();
try{return await raw(u,calls)}
catch(e){BAD.add(u);if(RPC===u){RPC=null;SEED=null}}}}
/* One eth_call carries the whole registry only while the return fits an endpoint's
response cap. Chunk, so a large registry costs a few round trips rather than one
oversized call that every provider rejects. */
const CHUNK=48;
const LANES=3;
async function callChunked(calls){
if(calls.length<=CHUNK)return callMany(calls);
/* Slice first, then run at most LANES in flight. Sequential was correct but slow —
a large registry paid one full round trip per chunk. More lanes than this turns a
single visitor into a burst against one keyless endpoint, which is how a pool
entry starts refusing everyone. Results are written back by index, so the order
the lanes finish in cannot reorder the listings. */
let parts=[];
for(let i=0;i<calls.length;i+=CHUNK)parts.push(calls.slice(i,i+CHUNK));
let out=new Array(parts.length),next=0,dead=null;
await Promise.all(Array.from({length:Math.min(LANES,parts.length)},async()=>{
for(let i=next++;i<parts.length&&!dead;i=next++){
try{out[i]=await callMany(parts[i])}catch(e){dead=e;throw e}}}));
return out.flat()}
const cut=h=>(h||'0x').slice(2),
bytesOf=h=>Uint8Array.from((cut(h).match(/../g)||[]),x=>parseInt(x,16)),
utf8=u=>new TextDecoder().decode(u),
b64=s=>utf8(Uint8Array.from(atob(s),c=>c.charCodeAt(0)));
function str(h){h=cut(h);if(h.length<128)return'';let o=Number(BigInt('0x'+h.slice(0,64)))*2,n=Number(BigInt('0x'+h.slice(o,o+64)));return utf8(bytesOf('0x'+h.slice(o+64,o+64+n*2)))}
/* `n` is a length word from an untrusted RPC and it sizes an allocation: without this
bound a bogus word asks for an array of 2^64 entries and takes the tab down before
any of it can be parsed. Require the ids to actually be present in the payload. */
function list(h){h=cut(h);if(h.length<128)return[];
let o=Number(BigInt('0x'+h.slice(0,64)))*2,n=Number(BigInt('0x'+h.slice(o,o+64)));
if(!Number.isSafeInteger(o)||!Number.isSafeInteger(n)||o+64+n*64>h.length)throw Error('bad id list');
return Array.from({length:n},(_,i)=>BigInt('0x'+h.slice(o+64+i*64,o+128+i*64)))}
/* Accepts a bare JSON string or a data: URI in either encoding the renderer may emit. */
function dataJson(u){if(!u.startsWith('data:'))return JSON.parse(u);
let[head,...rest]=u.split(','),d=rest.join(',');
return JSON.parse(/;base64/i.test(head)?b64(d):decodeURIComponent(d))}
/* `sync` is permissionless but only possible for an EVM token on THIS chain with a
non-zero account; everything else is owner-attested by construction, not by
neglect. Naming which of those four cases applies is the honest version of a
single "owner attested" badge - a reservation and a Bitcoin-rooted listing are
unverifiable for very different reasons, and only one of them ever changes. */
function chip(m){
/* A listing whose json() did not parse knows nothing about itself. Saying "not
readable from ethereum" here would assert a specific provenance — the Bitcoin
case — about a listing we simply failed to read. */
if(m.bad)return['at','metadata unreadable'];
if(m.v)return['on','metadata read onchain'];
if(m.p==='Native')return['nc','no contract to read'];
if(m.k!=='eip155')return['nc','not readable from ethereum'];
if(Number(m.c)!==1)return['nc','lives on chain '+esc(m.c)];
return['at','owner attested']}
/* The registry's Standard enum, grouped by the chain each format is rooted in. Held
as a literal taxonomy rather than derived from the listings, so a category can be
offered before its first listing exists — the point of the whole exercise. Labels
match the renderer's `_standard()` exactly; a mismatch here would silently filter
to nothing. Anything the registry gains that is not named below surfaces under
`other` rather than vanishing from the filter. */
const EVMS=['Native','ERC-20','ERC-721','ERC-1155'],BTCS=['Tacit','Rune','Ordinal','BRC-20'];
/* Chains are open-ended, so they are NOT advertised at zero the way the Bitcoin
formats are — that list is a fixed contract enum, this one is not. A chain earns a
chip by having a listing, and gets a human name if we know one. Anything unknown
still groups correctly and reads "chain 999"; nothing has to be added here for a new
chain to work, the name is the only thing this table buys. */
const CHAINS={1:'ethereum',10:'optimism',56:'bnb',100:'gnosis',130:'unichain',137:'polygon',
324:'zksync',480:'world',1868:'soneium',5000:'mantle',8453:'base',34443:'mode',42161:'arbitrum',
42220:'celo',43114:'avalanche',57073:'ink',59144:'linea',81457:'blast',534352:'scroll',7777777:'zora'};
/* Grouping is by CHAIN for EVM listings and by rooted chain otherwise. Grouping ERC-20
under "ethereum" was fine while mainnet was the only chain and a lie the moment a
Base token is listed: a standard is not a chain. */
const groupOf=m=>m.k==='eip155'?'c'+m.c:BTCS.includes(m.p)?'bitcoin':m.k==='solana'?'solana':'other';
const GROUP=g=>g==='bitcoin'?BTCS:/^c\d+$/.test(g)?EVMS:null;
const groupLabel=g=>/^c\d+$/.test(g)?(CHAINS[g.slice(1)]||'chain '+g.slice(1)):g;
/* Art goes in an <img>, never innerHTML: SVG inside an img cannot run script or fire
event handlers, so no renderer - present or future - can inject here. */
/* `t` is rendered from a uint24, so it is always #rrggbb - but it is injected into a
style attribute, so prove it rather than trust it. */
const COLOR=/^#[0-9a-f]{6}$/i;
const hue=m=>COLOR.test(m.t||'')?m.t:'';
/* A listing's `url` is curator-authored and the renderer's `_safe` only strips quotes,
angle brackets and control characters - it does NOT constrain the scheme, so
`javascript:...` survives it intact. Escaping keeps that inside the attribute but
would still run it on click, so allow only schemes that can navigate. Anything else
is shown as text and never becomes a link. */
const safeUrl=u=>/^(https?|ipfs):\/\/[^\s]+$/i.test(String(u||''))?String(u):'';
/* Same reasoning for logos. Inside an <img src> a hostile scheme is inert, but this
value is also copied into the exported token list, where somebody else's app decides
what to do with it — so it is constrained before it leaves here, not just before it
is drawn. */
const safeImg=u=>/^(https:\/\/|ipfs:\/\/|data:image\/)/i.test(String(u||''))?String(u):'';
/* Flags the registry carries that the card never showed. Each is a real claim about
the listing's standing, not decoration: `pending` has no token behind it yet,
`sealed` can never be re-authored, and `o` says the LISTED COLLECTION's own token
ids resolve to data-SVG - a claim about that collection's art, not about this
card's. The contract only lets it mean anything on a collection, so the wording
follows the renderer's ("ERC-721 / ONCHAIN TOKEN SVG") rather than inventing one. */
const COLL=m=>m.p==='ERC-721'||m.p==='ERC-1155';
/* `raw:0` is what the registry stores for anything non-EVM, which is true but tells a
reader nothing; and an unreadable listing has no namespace at all. Name the chain
the taxonomy knows about instead of printing the raw pair. */
const chainLabel=m=>m.k==='eip155'?(CHAINS[m.c]?CHAINS[m.c]+' · eip155:'+m.c:'eip155:'+m.c)
:m.k==='solana'?'solana':GROUP(groupOf(m))?groupLabel(groupOf