Files
proxmox/token-lists/scripts/diff-blockscout-vs-tokenlist.js
defiQUG e4c9dda0fd
Some checks failed
Deploy to Phoenix / deploy (push) Has been cancelled
chore: update submodule references and documentation
- Marked submodules ai-mcp-pmm-controller, explorer-monorepo, and smom-dbis-138 as dirty to reflect recent changes.
- Updated documentation to clarify operator script usage, including dotenv loading and task execution instructions.
- Enhanced the README and various index files to provide clearer navigation and task completion guidance.

Made-with: Cursor
2026-03-04 02:03:08 -08:00

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();