Files
proxmox/token-lists/scripts/validate-token-list.js
defiQUG cb47cce074 Complete markdown files cleanup and organization
- Organized 252 files across project
- Root directory: 187 → 2 files (98.9% reduction)
- Moved configuration guides to docs/04-configuration/
- Moved troubleshooting guides to docs/09-troubleshooting/
- Moved quick start guides to docs/01-getting-started/
- Moved reports to reports/ directory
- Archived temporary files
- Generated comprehensive reports and documentation
- Created maintenance scripts and guides

All files organized according to established standards.
2026-01-06 01:46:25 -08:00

288 lines
9.1 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* Enhanced Token List Validator
* Validates token lists against the Uniswap Token Lists JSON schema
* Based on: https://github.com/Uniswap/token-lists
* Schema: https://uniswap.org/tokenlist.schema.json
*
* Enhanced with:
* - EIP-55 checksum validation
* - Duplicate detection
* - Logo URL validation
* - Chain ID strict validation
* - Semantic versioning validation
*/
import { readFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';
import { ethers } from 'ethers';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Required chain ID
const REQUIRED_CHAIN_ID = 138;
// Fetch schema from Uniswap
const SCHEMA_URL = 'https://uniswap.org/tokenlist.schema.json';
async function fetchSchema() {
try {
const response = await fetch(SCHEMA_URL);
if (!response.ok) {
throw new Error(`Failed to fetch schema: ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error('Error fetching schema:', error.message);
console.error('Falling back to basic validation...');
return null;
}
}
// Validate EIP-55 checksum
function isChecksummed(address) {
try {
return ethers.isAddress(address) && address === ethers.getAddress(address);
} catch {
return false;
}
}
// Basic validation with enhanced checks
function enhancedValidation(tokenList) {
const errors = [];
const warnings = [];
const seenAddresses = new Set();
const seenSymbols = new Map(); // chainId -> Set of symbols
// Required fields
if (!tokenList.name || typeof tokenList.name !== 'string') {
errors.push('Missing or invalid "name" field');
}
if (!tokenList.version) {
errors.push('Missing "version" field');
} else {
if (typeof tokenList.version.major !== 'number') {
errors.push('version.major must be a number');
}
if (typeof tokenList.version.minor !== 'number') {
errors.push('version.minor must be a number');
}
if (typeof tokenList.version.patch !== 'number') {
errors.push('version.patch must be a number');
}
}
if (!tokenList.tokens || !Array.isArray(tokenList.tokens)) {
errors.push('Missing or invalid "tokens" array');
return { errors, warnings, valid: false };
}
// Validate each token
tokenList.tokens.forEach((token, index) => {
const prefix = `Token[${index}]`;
// Required token fields
if (typeof token.chainId !== 'number') {
errors.push(`${prefix}: Missing or invalid "chainId"`);
} else {
// Strict chain ID validation
if (token.chainId !== REQUIRED_CHAIN_ID) {
errors.push(`${prefix}: chainId must be ${REQUIRED_CHAIN_ID}, got ${token.chainId}`);
}
}
if (!token.address || typeof token.address !== 'string') {
errors.push(`${prefix}: Missing or invalid "address"`);
} else {
// Validate Ethereum address format
if (!/^0x[a-fA-F0-9]{40}$/.test(token.address)) {
errors.push(`${prefix}: Invalid Ethereum address format: ${token.address}`);
} else {
// EIP-55 checksum validation
if (!isChecksummed(token.address)) {
errors.push(`${prefix}: Address not EIP-55 checksummed: ${token.address}`);
}
// Duplicate address detection
const addressLower = token.address.toLowerCase();
if (seenAddresses.has(addressLower)) {
errors.push(`${prefix}: Duplicate address: ${token.address}`);
}
seenAddresses.add(addressLower);
}
}
if (!token.name || typeof token.name !== 'string') {
errors.push(`${prefix}: Missing or invalid "name"`);
}
if (!token.symbol || typeof token.symbol !== 'string') {
errors.push(`${prefix}: Missing or invalid "symbol"`);
} else {
// Symbol uniqueness per chainId
const chainId = token.chainId || 0;
if (!seenSymbols.has(chainId)) {
seenSymbols.set(chainId, new Set());
}
const symbolSet = seenSymbols.get(chainId);
if (symbolSet.has(token.symbol.toUpperCase())) {
warnings.push(`${prefix}: Duplicate symbol "${token.symbol}" on chainId ${chainId}`);
}
symbolSet.add(token.symbol.toUpperCase());
}
if (typeof token.decimals !== 'number' || token.decimals < 0 || token.decimals > 255) {
errors.push(`${prefix}: Invalid "decimals" (must be 0-255), got ${token.decimals}`);
}
// Optional fields (warnings)
if (!token.logoURI) {
warnings.push(`${prefix}: Missing "logoURI" (optional but recommended)`);
} else if (typeof token.logoURI !== 'string') {
warnings.push(`${prefix}: Invalid "logoURI" type`);
} else if (!token.logoURI.startsWith('http://') &&
!token.logoURI.startsWith('https://') &&
!token.logoURI.startsWith('ipfs://')) {
warnings.push(`${prefix}: Invalid "logoURI" format (should be HTTP/HTTPS/IPFS URL): ${token.logoURI}`);
} else if (!token.logoURI.startsWith('https://') && !token.logoURI.startsWith('ipfs://')) {
warnings.push(`${prefix}: logoURI should use HTTPS (not HTTP): ${token.logoURI}`);
}
});
return { errors, warnings, valid: errors.length === 0 };
}
async function validateTokenList(filePath) {
console.log(`\n🔍 Validating token list: ${filePath}\n`);
// Read token list file
let tokenList;
try {
const fileContent = readFileSync(filePath, 'utf-8');
tokenList = JSON.parse(fileContent);
} catch (error) {
console.error('❌ Error reading or parsing token list file:');
console.error(` ${error.message}`);
process.exit(1);
}
// Try to fetch and use Uniswap schema
const schema = await fetchSchema();
let validationResult;
if (schema) {
// Use AJV if available, otherwise fall back to enhanced validation
try {
// Try to use dynamic import for ajv (if installed)
const { default: Ajv } = await import('ajv');
const addFormats = (await import('ajv-formats')).default;
const ajv = new Ajv({ allErrors: true, verbose: true });
addFormats(ajv);
const validate = ajv.compile(schema);
const valid = validate(tokenList);
if (valid) {
// Schema validation passed, now run enhanced checks
validationResult = enhancedValidation(tokenList);
} else {
const schemaErrors = validate.errors?.map(err => {
const path = err.instancePath || err.schemaPath || '';
return `${path}: ${err.message}`;
}) || [];
const enhanced = enhancedValidation(tokenList);
validationResult = {
errors: [...schemaErrors, ...enhanced.errors],
warnings: enhanced.warnings,
valid: false
};
}
} catch (importError) {
// AJV not available, use enhanced validation
console.log('⚠️ AJV not available, using enhanced validation');
validationResult = enhancedValidation(tokenList);
}
} else {
// Schema fetch failed, use enhanced validation
validationResult = enhancedValidation(tokenList);
}
// Display results
if (validationResult.valid) {
console.log('✅ Token list is valid!\n');
// Display token list info
console.log('📋 Token List Info:');
console.log(` Name: ${tokenList.name}`);
if (tokenList.version) {
console.log(` Version: ${tokenList.version.major}.${tokenList.version.minor}.${tokenList.version.patch}`);
}
if (tokenList.timestamp) {
console.log(` Timestamp: ${tokenList.timestamp}`);
}
console.log(` Tokens: ${tokenList.tokens.length}`);
console.log('');
// List tokens
console.log('🪙 Tokens:');
tokenList.tokens.forEach((token, index) => {
console.log(` ${index + 1}. ${token.symbol} (${token.name})`);
console.log(` Address: ${token.address}`);
console.log(` Chain ID: ${token.chainId}`);
console.log(` Decimals: ${token.decimals}`);
if (token.logoURI) {
console.log(` Logo: ${token.logoURI}`);
}
console.log('');
});
if (validationResult.warnings.length > 0) {
console.log('⚠️ Warnings:');
validationResult.warnings.forEach(warning => {
console.log(` - ${warning}`);
});
console.log('');
}
process.exit(0);
} else {
console.error('❌ Token list validation failed!\n');
if (validationResult.errors.length > 0) {
console.error('Errors:');
validationResult.errors.forEach(error => {
console.error(`${error}`);
});
console.log('');
}
if (validationResult.warnings.length > 0) {
console.log('Warnings:');
validationResult.warnings.forEach(warning => {
console.log(` ⚠️ ${warning}`);
});
console.log('');
}
process.exit(1);
}
}
// Main
const filePath = process.argv[2] || resolve(__dirname, '../lists/dbis-138.tokenlist.json');
if (!filePath) {
console.error('Usage: node validate-token-list.js [path/to/token-list.json]');
process.exit(1);
}
validateTokenList(filePath).catch(error => {
console.error('Unexpected error:', error);
process.exit(1);
});