Files
proxmox/token-lists/scripts/validate-token-list.js
defiQUG 6c5fdcfd62 chore: pnpm lockfile, info-defi-oracle-138 app, token-lists, OMNL discovery output
- Refresh pnpm-lock.yaml / workspace after prior merge
- Add Chain 138 info hub SPA (info-defi-oracle-138)
- Token list and validation script tweaks; path_b report; Hyperledger proxmox install notes
- HYBX implementation roadmap and routing graph data model

Note: transaction-composer is a nested git repo — convert to submodule before tracking.
Made-with: Cursor
2026-03-31 22:32:15 -07:00

380 lines
12 KiB
JavaScript
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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
* Uses: @uniswap/token-lists package for schema and types
*
* 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 (optional - if not set, will validate all tokens have same chainId)
// Can be overridden via --chain-id flag
let REQUIRED_CHAIN_ID = null;
/**
* Get schema from @uniswap/token-lists package
* Falls back to fetching from URL if package not available
*/
async function getSchema() {
try {
// Try to import schema from @uniswap/token-lists package
const tokenLists = await import('@uniswap/token-lists');
if (tokenLists.schema) {
console.log('✅ Using schema from @uniswap/token-lists package\n');
return tokenLists.schema;
}
} catch (error) {
console.log(' Using fallback token list schema source\n');
}
// Fallback: fetch schema from Uniswap
try {
const SCHEMA_URL = 'https://uniswap.org/tokenlist.schema.json';
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 infos = [];
const seenAddresses = new Set();
const seenSymbols = new Map(); // chainId -> Map<symbol, token[]>
let detectedChainId = null;
function isAllowedGruVersionDuplicate(existingToken, nextToken) {
if (!existingToken?.extensions || !nextToken?.extensions) return false;
const existingVersion = existingToken.extensions.gruVersion;
const nextVersion = nextToken.extensions.gruVersion;
const existingCurrencyCode = existingToken.extensions.currencyCode;
const nextCurrencyCode = nextToken.extensions.currencyCode;
if (typeof existingVersion !== 'string' || typeof nextVersion !== 'string') {
return false;
}
if (existingVersion === nextVersion) {
return false;
}
if (typeof existingCurrencyCode !== 'string' || typeof nextCurrencyCode !== 'string') {
return false;
}
if (existingCurrencyCode !== nextCurrencyCode) {
return false;
}
return true;
}
// 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 };
}
// Detect chain ID from first token if not specified
if (tokenList.tokens.length > 0 && tokenList.tokens[0].chainId) {
detectedChainId = tokenList.tokens[0].chainId;
}
// 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 {
// Chain ID consistency check
if (detectedChainId === null) {
detectedChainId = token.chainId;
} else if (token.chainId !== detectedChainId) {
errors.push(`${prefix}: chainId mismatch - expected ${detectedChainId}, got ${token.chainId}`);
}
// Strict chain ID validation (if REQUIRED_CHAIN_ID is set)
if (REQUIRED_CHAIN_ID !== null && 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 Map());
}
const symbolMap = seenSymbols.get(chainId);
const symbolKey = token.symbol.toUpperCase();
const existingTokens = symbolMap.get(symbolKey) || [];
if (existingTokens.length > 0) {
const duplicateAllowed = existingTokens.every(existingToken =>
isAllowedGruVersionDuplicate(existingToken, token)
);
if (duplicateAllowed) {
infos.push(
`${prefix}: Allowed staged GRU duplicate symbol "${token.symbol}" on chainId ${chainId} ` +
`(${existingTokens.map(existingToken => existingToken.extensions?.gruVersion).join(', ')} -> ${token.extensions?.gruVersion})`
);
} else {
warnings.push(`${prefix}: Duplicate symbol "${token.symbol}" on chainId ${chainId}`);
}
}
existingTokens.push(token);
symbolMap.set(symbolKey, existingTokens);
}
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, infos, 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);
}
// Get schema from @uniswap/token-lists package or fetch from URL
const schema = await getSchema();
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,
infos: enhanced.infos,
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('');
}
if (validationResult.infos.length > 0) {
console.log(' Notes:');
validationResult.infos.forEach(info => {
console.log(` - ${info}`);
});
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('');
}
if (validationResult.infos.length > 0) {
console.log('Notes:');
validationResult.infos.forEach(info => {
console.log(` ${info}`);
});
console.log('');
}
process.exit(1);
}
}
// Main
const args = process.argv.slice(2);
const filePath = args.find(arg => !arg.startsWith('--')) || resolve(__dirname, '../lists/dbis-138.tokenlist.json');
const chainIdArg = args.find(arg => arg.startsWith('--chain-id='));
if (chainIdArg) {
REQUIRED_CHAIN_ID = parseInt(chainIdArg.split('=')[1], 10);
}
if (!filePath) {
console.error('Usage: node validate-token-list.js [path/to/token-list.json] [--chain-id=138]');
process.exit(1);
}
validateTokenList(filePath).catch(error => {
console.error('Unexpected error:', error);
process.exit(1);
});