171 lines
6.1 KiB
JavaScript
171 lines
6.1 KiB
JavaScript
|
|
#!/usr/bin/env node
|
||
|
|
/**
|
||
|
|
* Full diff: Blockscout /api/v2/tokens vs curated token list (Chain 138).
|
||
|
|
*
|
||
|
|
* Outputs:
|
||
|
|
* - missing_in_blockscout: in tokenlist but not in Blockscout response
|
||
|
|
* - missing_in_tokenlist: in Blockscout but not in tokenlist
|
||
|
|
* - metadata_mismatches: same address, different name/symbol/decimals (or null/0)
|
||
|
|
* - source-of-truth recommendation per field
|
||
|
|
*
|
||
|
|
* Usage:
|
||
|
|
* node diff-blockscout-vs-tokenlist.js
|
||
|
|
* node diff-blockscout-vs-tokenlist.js --url "https://explorer.d-bis.org/api/v2/tokens"
|
||
|
|
* node diff-blockscout-vs-tokenlist.js --file /path/to/blockscout-tokens.json
|
||
|
|
*
|
||
|
|
* Curated list: token-lists/lists/dbis-138.tokenlist.json (Chain 138 tokens).
|
||
|
|
* ETH-USD (oracle) is not an ERC-20 supply token; it is expected to be missing from Blockscout.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { readFileSync } from 'fs';
|
||
|
|
import { dirname, resolve } from 'path';
|
||
|
|
import { fileURLToPath } from 'url';
|
||
|
|
|
||
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||
|
|
const CHAIN_ID = 138;
|
||
|
|
const TOKENLIST_PATH = resolve(__dirname, '../lists/dbis-138.tokenlist.json');
|
||
|
|
|
||
|
|
function normAddr(addr) {
|
||
|
|
return (addr || '').toLowerCase();
|
||
|
|
}
|
||
|
|
|
||
|
|
function parseArgs() {
|
||
|
|
const args = process.argv.slice(2);
|
||
|
|
let url = null;
|
||
|
|
let file = null;
|
||
|
|
for (let i = 0; i < args.length; i++) {
|
||
|
|
if (args[i] === '--url' && args[i + 1]) {
|
||
|
|
url = args[i + 1];
|
||
|
|
i++;
|
||
|
|
} else if (args[i] === '--file' && args[i + 1]) {
|
||
|
|
file = args[i + 1];
|
||
|
|
i++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return { url, file };
|
||
|
|
}
|
||
|
|
|
||
|
|
async function fetchAllBlockscoutTokens(baseUrl) {
|
||
|
|
const items = [];
|
||
|
|
let next = null;
|
||
|
|
const base = baseUrl.replace(/\?.*$/, '');
|
||
|
|
while (true) {
|
||
|
|
const qs = next ? new URLSearchParams({ page_size: 100, ...next }) : new URLSearchParams({ page: 1, page_size: 100 });
|
||
|
|
const res = await fetch(`${base}?${qs}`);
|
||
|
|
if (!res.ok) throw new Error(`Blockscout ${res.status}`);
|
||
|
|
const data = await res.json();
|
||
|
|
const list = data.items ?? data.data ?? (Array.isArray(data) ? data : []);
|
||
|
|
items.push(...list);
|
||
|
|
next = data.next_page_params ?? null;
|
||
|
|
if (!next) break;
|
||
|
|
}
|
||
|
|
return items;
|
||
|
|
}
|
||
|
|
|
||
|
|
function loadTokenList() {
|
||
|
|
const raw = readFileSync(TOKENLIST_PATH, 'utf8');
|
||
|
|
const data = JSON.parse(raw);
|
||
|
|
const tokens = (data.tokens || []).filter((t) => t.chainId === CHAIN_ID);
|
||
|
|
return tokens.map((t) => ({
|
||
|
|
address: normAddr(t.address),
|
||
|
|
name: t.name ?? null,
|
||
|
|
symbol: t.symbol ?? null,
|
||
|
|
decimals: t.decimals != null ? Number(t.decimals) : null,
|
||
|
|
logoURI: t.logoURI ?? null,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
function loadBlockscoutFromFile(path) {
|
||
|
|
const raw = readFileSync(path, 'utf8');
|
||
|
|
const data = JSON.parse(raw);
|
||
|
|
const list = data.items ?? data.data ?? (Array.isArray(data) ? data : []);
|
||
|
|
return list.map((t) => ({
|
||
|
|
address: normAddr(t.address ?? t.hash),
|
||
|
|
name: t.name ?? null,
|
||
|
|
symbol: t.symbol ?? null,
|
||
|
|
decimals: t.decimals != null && t.decimals !== '' ? Number(t.decimals) : null,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
function runDiff(tokenlist, blockscout) {
|
||
|
|
const byAddr = (arr) => Object.fromEntries(arr.map((t) => [t.address, t]));
|
||
|
|
const listMap = byAddr(tokenlist);
|
||
|
|
const scoutMap = byAddr(blockscout);
|
||
|
|
|
||
|
|
const missing_in_blockscout = tokenlist
|
||
|
|
.filter((t) => !scoutMap[t.address])
|
||
|
|
.map((t) => ({ address: t.address, symbol: t.symbol, name: t.name, note: t.symbol === 'ETH-USD' ? 'Oracle; not ERC-20 supply token' : null }));
|
||
|
|
|
||
|
|
const missing_in_tokenlist = blockscout
|
||
|
|
.filter((t) => !listMap[t.address])
|
||
|
|
.map((t) => ({ address: t.address, symbol: t.symbol, name: t.name, decimals: t.decimals }));
|
||
|
|
|
||
|
|
const metadata_mismatches = [];
|
||
|
|
for (const addr of Object.keys(listMap)) {
|
||
|
|
const list = listMap[addr];
|
||
|
|
const scout = scoutMap[addr];
|
||
|
|
if (!scout) continue;
|
||
|
|
const mismatches = [];
|
||
|
|
if (list.name !== scout.name && (scout.name != null || list.name != null)) mismatches.push({ field: 'name', tokenlist: list.name, blockscout: scout.name });
|
||
|
|
if (list.symbol !== scout.symbol && (scout.symbol != null || list.symbol != null)) mismatches.push({ field: 'symbol', tokenlist: list.symbol, blockscout: scout.symbol });
|
||
|
|
if (list.decimals !== scout.decimals && (scout.decimals != null || list.decimals != null)) mismatches.push({ field: 'decimals', tokenlist: list.decimals, blockscout: scout.decimals });
|
||
|
|
if (mismatches.length) metadata_mismatches.push({ address: addr, symbol: list.symbol ?? scout.symbol, mismatches });
|
||
|
|
}
|
||
|
|
|
||
|
|
return { missing_in_blockscout, missing_in_tokenlist, metadata_mismatches };
|
||
|
|
}
|
||
|
|
|
||
|
|
function sourceOfTruthRecommendation(diff) {
|
||
|
|
return {
|
||
|
|
address: 'Token list (dbis-138.tokenlist.json) and CONTRACT_ADDRESSES_REFERENCE; Blockscout is on-chain index.',
|
||
|
|
symbol: 'Token list; use Explorer UI override only when Blockscout returns null (e.g. WETH9).',
|
||
|
|
name: 'Token list; same as symbol.',
|
||
|
|
decimals: 'Token list; use override when Blockscout returns 0 or null.',
|
||
|
|
logo: 'Token list logoURI.',
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function main() {
|
||
|
|
const { url, file } = parseArgs();
|
||
|
|
const baseUrl = url || 'https://explorer.d-bis.org/api/v2/tokens';
|
||
|
|
|
||
|
|
(async () => {
|
||
|
|
let blockscout;
|
||
|
|
if (file) {
|
||
|
|
blockscout = loadBlockscoutFromFile(file);
|
||
|
|
console.error(`Loaded ${blockscout.length} tokens from file: ${file}`);
|
||
|
|
} else {
|
||
|
|
try {
|
||
|
|
blockscout = await fetchAllBlockscoutTokens(baseUrl);
|
||
|
|
console.error(`Fetched ${blockscout.length} tokens from ${baseUrl}`);
|
||
|
|
} catch (e) {
|
||
|
|
console.error('Fetch failed:', e.message);
|
||
|
|
console.error('Use --file path/to/blockscout-tokens.json with a saved snapshot.');
|
||
|
|
process.exit(1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const tokenlist = loadTokenList();
|
||
|
|
console.error(`Loaded ${tokenlist.length} Chain ${CHAIN_ID} tokens from ${TOKENLIST_PATH}`);
|
||
|
|
|
||
|
|
const diff = runDiff(tokenlist, blockscout);
|
||
|
|
const recommendation = sourceOfTruthRecommendation(diff);
|
||
|
|
|
||
|
|
const out = {
|
||
|
|
chainId: CHAIN_ID,
|
||
|
|
tokenlist_path: TOKENLIST_PATH,
|
||
|
|
blockscout_source: file || baseUrl,
|
||
|
|
missing_in_blockscout: diff.missing_in_blockscout,
|
||
|
|
missing_in_tokenlist: diff.missing_in_tokenlist,
|
||
|
|
metadata_mismatches: diff.metadata_mismatches,
|
||
|
|
source_of_truth: recommendation,
|
||
|
|
};
|
||
|
|
console.log(JSON.stringify(out, null, 2));
|
||
|
|
})().catch((e) => {
|
||
|
|
console.error(e);
|
||
|
|
process.exit(1);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
main();
|