#!/usr/bin/env node import fs from 'fs'; import path from 'path'; import https from 'https'; const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..'); const packsRoot = path.join(repoRoot, 'reports', 'status', 'publication-packs'); const outJson = path.join(repoRoot, 'reports', 'status', 'publication-pack-explorer-status.json'); const outMd = path.join(repoRoot, 'docs', '11-references', 'PUBLICATION_PACK_EXPLORER_STATUS.md'); const chainIdToApi = { '1': 'https://api.etherscan.io/v2/api', '10': 'https://api.etherscan.io/v2/api', '56': 'https://api.etherscan.io/v2/api', '137': 'https://api.etherscan.io/v2/api', '8453': 'https://api.etherscan.io/v2/api', }; const apiKey = process.env.ETHERSCAN_API_KEY || ''; if (!apiKey) { console.error('ETHERSCAN_API_KEY is required for pack explorer status checks.'); process.exit(1); } function ensureDir(filePath) { fs.mkdirSync(path.dirname(filePath), { recursive: true }); } function getJson(url) { return new Promise((resolve, reject) => { https.get(url, { headers: { 'user-agent': 'proxmox-publication-status-checker/1.0', accept: 'application/json', }, }, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { try { resolve(JSON.parse(data)); } catch (err) { reject(new Error(`Invalid JSON from ${url}: ${err.message}`)); } }); }).on('error', reject); }); } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function fetchStatus(chainId, address) { const api = chainIdToApi[chainId]; if (!api) return { status: 'unsupported', detail: 'No configured explorer API' }; const url = `${api}?chainid=${chainId}&module=contract&action=getsourcecode&address=${address}&apikey=${apiKey}`; for (let attempt = 1; attempt <= 4; attempt += 1) { const json = await getJson(url); const result = Array.isArray(json.result) ? json.result[0] : null; if (result) { const source = (result.SourceCode || '').trim(); const name = (result.ContractName || '').trim(); if (source || name) { return { status: 'verified', contractName: name || null, detail: result.CompilerVersion || 'verified', }; } return { status: 'unverified', detail: result.ABI || 'No source metadata' }; } const message = typeof json.message === 'string' ? json.message : ''; const detail = typeof json.result === 'string' && json.result ? `${message}: ${json.result}` : (message || 'No result'); const retryable = /rate limit|timeout|temporarily unavailable|busy/i.test(detail); if (!retryable || attempt === 4) { return { status: 'unknown', detail }; } await sleep(400 * attempt); } return { status: 'unknown', detail: 'Status check exhausted retries' }; } async function main() { const packDirs = fs.readdirSync(packsRoot).sort(); const report = { generatedAt: new Date().toISOString(), packs: [], }; for (const dir of packDirs) { const packPath = path.join(packsRoot, dir, 'pack.json'); if (!fs.existsSync(packPath)) continue; const pack = JSON.parse(fs.readFileSync(packPath, 'utf8')); const entries = []; for (const entry of pack.entries) { try { const status = await fetchStatus(entry.chainId, entry.address); entries.push({ ...entry, explorerStatus: status.status, explorerDetail: status.detail, explorerContractName: status.contractName || null }); } catch (err) { entries.push({ ...entry, explorerStatus: 'error', explorerDetail: err.message, explorerContractName: null }); } } const counts = entries.reduce((acc, entry) => { acc[entry.explorerStatus] = (acc[entry.explorerStatus] || 0) + 1; return acc; }, {}); report.packs.push({ slug: dir, chainId: pack.chainId, chainName: pack.chainName, explorer: pack.explorer, counts, entries, }); } const rows = report.packs.map((pack) => { const verified = pack.counts.verified || 0; const unverified = pack.counts.unverified || 0; const unknown = pack.counts.unknown || 0; const unsupported = pack.counts.unsupported || 0; const error = pack.counts.error || 0; return `| ${pack.chainId} | ${pack.chainName} | ${verified} | ${unverified} | ${unknown} | ${unsupported} | ${error} | ${pack.explorer} |`; }).join('\n'); const md = `# Publication Pack Explorer Status **Generated:** ${report.generatedAt} Live explorer verification status for the grouped publication packs. | Chain ID | Chain | Verified | Unverified | Unknown | Unsupported | Errors | Explorer | | --- | --- | ---: | ---: | ---: | ---: | ---: | --- | ${rows} `; ensureDir(outJson); ensureDir(outMd); fs.writeFileSync(outJson, JSON.stringify(report, null, 2) + '\n'); fs.writeFileSync(outMd, md + '\n'); console.log(`Wrote:\n- ${outJson}\n- ${outMd}`); } main().catch((err) => { console.error(err); process.exit(1); });