#!/usr/bin/env node /** * Validates chain configuration for Chainlists submission */ import { readFileSync } from 'fs'; import { fileURLToPath } from 'url'; import { dirname, resolve } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const REQUIRED_FIELDS = [ 'name', 'chain', 'chainId', 'networkId', 'rpc', 'nativeCurrency' ]; function validateChainConfig(filePath) { console.log(`\nšŸ” Validating chain configuration: ${filePath}\n`); let config; try { const fileContent = readFileSync(filePath, 'utf-8'); config = JSON.parse(fileContent); } catch (error) { console.error('āŒ Error reading or parsing chain config file:'); console.error(` ${error.message}`); process.exit(1); } const errors = []; const warnings = []; // Validate required fields REQUIRED_FIELDS.forEach(field => { if (!(field in config)) { errors.push(`Missing required field: ${field}`); } }); // Validate chainId if (config.chainId !== 138) { errors.push(`chainId must be 138, got ${config.chainId}`); } // Validate networkId if (config.networkId && config.networkId !== 138) { warnings.push(`networkId should match chainId (138), got ${config.networkId}`); } // Validate RPC URLs if (!Array.isArray(config.rpc) || config.rpc.length === 0) { errors.push('rpc must be a non-empty array'); } else { config.rpc.forEach((url, index) => { if (typeof url !== 'string' || (!url.startsWith('http://') && !url.startsWith('https://'))) { errors.push(`rpc[${index}] must be a valid HTTP/HTTPS URL`); } if (url.startsWith('http://')) { warnings.push(`rpc[${index}] should use HTTPS, not HTTP: ${url}`); } }); } // Validate nativeCurrency if (config.nativeCurrency) { if (!config.nativeCurrency.symbol) { errors.push('nativeCurrency.symbol is required'); } if (typeof config.nativeCurrency.decimals !== 'number') { errors.push('nativeCurrency.decimals must be a number'); } if (config.nativeCurrency.symbol !== 'ETH') { warnings.push(`Expected nativeCurrency.symbol to be "ETH", got "${config.nativeCurrency.symbol}"`); } if (config.nativeCurrency.decimals !== 18) { warnings.push(`Expected nativeCurrency.decimals to be 18, got ${config.nativeCurrency.decimals}`); } } // Validate explorers (optional but recommended) if (config.explorers && Array.isArray(config.explorers)) { config.explorers.forEach((explorer, index) => { if (!explorer.url) { errors.push(`explorers[${index}].url is required`); } if (explorer.url && !explorer.url.startsWith('https://')) { warnings.push(`explorers[${index}].url should use HTTPS: ${explorer.url}`); } if (!explorer.name) { warnings.push(`explorers[${index}].name is recommended`); } if (!explorer.standard) { warnings.push(`explorers[${index}].standard is recommended (e.g., "EIP3091")`); } }); } else { warnings.push('No explorers configured (recommended for better UX)'); } // Validate shortName if (config.shortName && typeof config.shortName !== 'string') { errors.push('shortName must be a string'); } else if (!config.shortName) { warnings.push('shortName is recommended'); } // Validate icon (optional) if (config.icon && !config.icon.startsWith('https://') && !config.icon.startsWith('ipfs://')) { warnings.push(`icon should use HTTPS or IPFS URL: ${config.icon}`); } // Report results if (errors.length > 0) { console.error('āŒ Validation failed!\n'); console.error('Errors:'); errors.forEach(error => console.error(` āŒ ${error}`)); console.log(''); if (warnings.length > 0) { console.log('āš ļø Warnings:'); warnings.forEach(warning => console.log(` āš ļø ${warning}`)); console.log(''); } process.exit(1); } console.log('āœ… Chain configuration is valid!\n'); if (warnings.length > 0) { console.log('āš ļø Warnings:'); warnings.forEach(warning => console.log(` āš ļø ${warning}`)); console.log(''); } console.log('šŸ“‹ Configuration Summary:'); console.log(` Name: ${config.name}`); console.log(` Chain: ${config.chain}`); console.log(` Short Name: ${config.shortName || '(not set)'}`); console.log(` Chain ID: ${config.chainId}`); console.log(` Network ID: ${config.networkId || '(not set)'}`); console.log(` RPC URLs: ${config.rpc.length}`); config.rpc.forEach((url, i) => console.log(` ${i + 1}. ${url}`)); if (config.explorers && config.explorers.length > 0) { console.log(` Explorers: ${config.explorers.length}`); config.explorers.forEach((exp, i) => { console.log(` ${i + 1}. ${exp.name || '(unnamed)'}: ${exp.url}`); if (exp.standard) { console.log(` Standard: ${exp.standard}`); } }); } if (config.nativeCurrency) { console.log(` Native Currency: ${config.nativeCurrency.symbol} (${config.nativeCurrency.decimals} decimals)`); } if (config.infoURL) { console.log(` Info URL: ${config.infoURL}`); } console.log(''); process.exit(0); } // Main const filePath = process.argv[2] || resolve(__dirname, '../chainlists/chain-138.json'); if (!filePath) { console.error('Usage: node validate-chainlists.js [path/to/chain.json]'); process.exit(1); } validateChainConfig(filePath);