Repository Public
1// First source of the token rule, checked by machine: for every deployment of research/candidates.json, fetch the issuer / network2// document named in `source` and look for the address in it. Writes research/evidence.json ("<SYMBOL>@<network>" -> where the3// address was found). gen-tokens.mts refuses a deployment that has no evidence entry for its exact address.4// node packages/core/scripts/check-sources.mts (run from the repository root)5// - a document that is a token list ({ tokens: [{ chainId, address }] }) must list the address under the right chain id;6// any other document must contain the address as text (EVM: case-insensitive; Solana/TRON: exact);7// - a page that needs JavaScript to show its content cannot be checked this way: such a deployment is accepted only with a8// hand-written entry { via: 'manual', note } in evidence.json (who read what, when); the script keeps those and never creates one.9import { createHash } from 'node:crypto';10import { existsSync, readFileSync, writeFileSync } from 'node:fs';1112const CHAIN_ID: Record<string, number> = { ethereum: 1, base: 8453, arbitrum: 42161, polygon: 137, optimism: 10, bsc: 56, avalanche: 43114, hyperevm: 999 };13const UA = { 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0 Safari/537.36', accept: 'text/html,application/json,text/plain,*/*' };14type Evidence = { address: string; url: string; via: 'document' | 'tokenlist' | 'manual'; sha256?: string; at: string; note?: string };15const OUT = 'research/evidence.json';16const cand = JSON.parse(readFileSync('research/candidates.json', 'utf8'));17const prev: Record<string, Evidence> = existsSync(OUT) ? JSON.parse(readFileSync(OUT, 'utf8')) : {};18const docs = new Map<string, Promise<{ text: string; sha: string; list: { chainId: number; address: string }[] | null } | null>>();19const load = (url: string) => { if (!docs.has(url)) docs.set(url, (async () => {20 for (let k = 0; k < 3; k++) {21 try {22 const r = await fetch(url, { headers: UA, redirect: 'follow', signal: AbortSignal.timeout(45000) });23 if (!r.ok) throw new Error(String(r.status));24 const text = await r.text();25 let list: { chainId: number; address: string }[] | null = null;26 try { const j = JSON.parse(text); if (Array.isArray(j?.tokens) && j.tokens.every((t: any) => typeof t.address === 'string' && typeof t.chainId === 'number')) list = j.tokens; } catch { /* not JSON */ }27 return { text, sha: createHash('sha256').update(text).digest('hex'), list };28 } catch { await new Promise((r) => setTimeout(r, 1500 * (k + 1))); }29 }30 return null;31})()); return docs.get(url)!; };3233const out: Record<string, Evidence> = {}, missing: string[] = [], today = new Date().toISOString().slice(0, 10);34const jobs: Promise<void>[] = [];35for (const t of cand.tokens) for (const [net, raw] of Object.entries(t.d as Record<string, any>)) jobs.push((async () => {36 const dep = typeof raw === 'string' ? { a: raw } : raw, tag = `${t.symbol}@${net}`, evm = net in CHAIN_ID;37 const urls = String(dep.source ?? t.source).split(/\s+;\s+/).filter((u) => /^https:\/\//.test(u));38 for (const url of urls) {39 const d = await load(url);40 if (!d) continue;41 if (d.list) {42 if (evm && d.list.some((x) => x.chainId === CHAIN_ID[net] && x.address.toLowerCase() === dep.a.toLowerCase())) { out[tag] = { address: dep.a, url, via: 'tokenlist', sha256: d.sha, at: today }; return; }43 } else if (evm ? d.text.toLowerCase().includes(dep.a.toLowerCase()) : d.text.includes(dep.a)) { out[tag] = { address: dep.a, url, via: 'document', sha256: d.sha, at: today }; return; }44 }45 const p = prev[tag];46 if (p?.via === 'manual' && p.address.toLowerCase() === String(dep.a).toLowerCase()) { out[tag] = p; return; }47 missing.push(`${tag} ${dep.a} — not found in: ${urls.join(' , ') || '(no fetchable source)'}`);48})());49await Promise.all(jobs);50writeFileSync(OUT, JSON.stringify(Object.fromEntries(Object.entries(out).sort(([a], [b]) => a.localeCompare(b))), null, 1) + '\n');51const n = Object.values(out);52console.log(`${n.length} deployments with evidence (${n.filter((e) => e.via === 'document').length} document, ${n.filter((e) => e.via === 'tokenlist').length} token list, ${n.filter((e) => e.via === 'manual').length} manual); ${missing.length} without`);53for (const m of missing.sort()) console.log('MISSING', m);54