// Fetches a name and a website for every token in src/shared/tokenList.js and // writes them, keyed by address, to script/lib/token-enrichment.json, which // script/lib/apply-enrichment.js then writes into tokenList.js. Run by // `make enrich-tokens`; no build runs it and nothing here is shipped. // // Names come from CoinGecko's coin list, fetched whole in one request, matched // on the token's Ethereum contract address. A token CoinGecko does not list // gets its symbol as its name. The list is live data with no fixed version, so // a later run can return different names. // // Websites come from the ethereum-lists/tokens repository, one file per token, // read at a pinned commit. Most tokens have no file there and get an empty URL. // A file that is missing is not an error; any other failed request stops the // run, so a network fault cannot quietly empty a URL. "use strict"; const fs = require("fs"); const path = require("path"); const TOKEN_LIST = path.join(__dirname, "..", "..", "src/shared/tokenList.js"); const OUTPUT = path.join(__dirname, "token-enrichment.json"); const COINGECKO_LIST = "https://api.coingecko.com/api/v3/coins/list?include_platform=true"; // ethereum-lists/tokens at its master of 2026-02-27, when the committed // token-enrichment.json was made. const ETHEREUM_LISTS = "https://raw.githubusercontent.com/ethereum-lists/tokens/c3560d499a380a38e94caa7c4968d59aeffde59a/tokens/eth"; const CONCURRENT = 10; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); async function main() { const src = fs.readFileSync(TOKEN_LIST, "utf8"); const tokens = []; const re = /\{\s*address:\s*"(0x[a-fA-F0-9]{40})",\s*symbol:\s*"([^"]+)",\s*decimals:\s*(\d+)/g; let m; while ((m = re.exec(src)) !== null) { tokens.push({ address: m[1], symbol: m[2], decimals: parseInt(m[3]) }); } console.error(`Parsed ${tokens.length} tokens`); // --- Names from CoinGecko --- console.error("Fetching CoinGecko coins list..."); const listResp = await fetch(COINGECKO_LIST); if (!listResp.ok) throw new Error(`CoinGecko list HTTP ${listResp.status}`); const coins = await listResp.json(); const addrMap = new Map(); for (const coin of coins) { const eth = (coin.platforms?.ethereum || "").toLowerCase().trim(); if (eth && eth.startsWith("0x")) { addrMap.set(eth, { name: coin.name, id: coin.id }); } } console.error(`CoinGecko map: ${addrMap.size} Ethereum tokens`); const enriched = new Map(); let namesMatched = 0; for (const t of tokens) { const info = addrMap.get(t.address.toLowerCase()); if (info) namesMatched++; enriched.set(t.address, { name: info?.name || t.symbol, url: "" }); } console.error(`Matched ${namesMatched}/${tokens.length} names`); // --- Websites from ethereum-lists --- console.error("Fetching URLs from ethereum-lists GitHub..."); let urlsFound = 0; for (let i = 0; i < tokens.length; i += CONCURRENT) { const batch = tokens.slice(i, i + CONCURRENT); await Promise.all( batch.map(async (t) => { const r = await fetch(`${ETHEREUM_LISTS}/${t.address}.json`); if (r.status === 404) return; if (!r.ok) { throw new Error( `ethereum-lists HTTP ${r.status} for ${t.address}`, ); } const data = await r.json(); const website = (data.website || "").trim(); if (website && website !== "https://") { enriched.get(t.address).url = website; urlsFound++; } }), ); if (i % 100 === 0) { console.error( ` GitHub: ${Math.min(i + CONCURRENT, tokens.length)}/${tokens.length}, ${urlsFound} URLs`, ); } await sleep(200); } console.error(`Found ${urlsFound} URLs in ethereum-lists`); // --- Output, in the layout prettier gives it --- const output = {}; for (const [addr, e] of enriched) { output[addr] = { name: e.name, url: e.url }; } fs.writeFileSync(OUTPUT, JSON.stringify(output, null, 4) + "\n"); console.error(`Done! ${tokens.length} names, ${urlsFound} URLs.`); } main().catch((err) => { console.error(`enrich-tokens: ${err.message}`); process.exit(1); });