- 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
194 lines
6.1 KiB
JavaScript
Executable File
194 lines
6.1 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
/**
|
|
* Logo URL Validator
|
|
* Validates that all logoURI URLs are accessible and return image content
|
|
*/
|
|
|
|
import { readFileSync, existsSync, statSync } from 'fs';
|
|
import { fileURLToPath } from 'url';
|
|
import { dirname, resolve } from 'path';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = dirname(__filename);
|
|
const PROJECT_ROOT = resolve(__dirname, '../..');
|
|
|
|
const MAX_LOGO_SIZE = 500 * 1024; // 500KB
|
|
const IMAGE_MIME_TYPES = ['image/png', 'image/jpeg', 'image/jpg', 'image/svg+xml', 'image/webp', 'image/gif'];
|
|
|
|
function inferContentTypeFromPath(filePath) {
|
|
if (filePath.endsWith('.svg')) return 'image/svg+xml';
|
|
if (filePath.endsWith('.png')) return 'image/png';
|
|
if (filePath.endsWith('.jpg') || filePath.endsWith('.jpeg')) return 'image/jpeg';
|
|
if (filePath.endsWith('.webp')) return 'image/webp';
|
|
if (filePath.endsWith('.gif')) return 'image/gif';
|
|
return null;
|
|
}
|
|
|
|
function resolveLocalRepoAsset(logoURI) {
|
|
try {
|
|
const url = new URL(logoURI);
|
|
if (url.protocol !== 'https:') return null;
|
|
|
|
if (url.hostname === 'raw.githubusercontent.com') {
|
|
const segments = url.pathname.split('/').filter(Boolean);
|
|
if (segments.length < 4) return null;
|
|
const relativePath = segments.slice(3).join('/');
|
|
const candidate = resolve(PROJECT_ROOT, relativePath);
|
|
return existsSync(candidate) ? candidate : null;
|
|
}
|
|
|
|
if (url.hostname === 'gitea.d-bis.org') {
|
|
const marker = '/raw/branch/';
|
|
const markerIndex = url.pathname.indexOf(marker);
|
|
if (markerIndex === -1) return null;
|
|
const afterMarker = url.pathname.slice(markerIndex + marker.length);
|
|
const pathSegments = afterMarker.split('/').filter(Boolean);
|
|
if (pathSegments.length < 2) return null;
|
|
const relativePath = pathSegments.slice(1).join('/');
|
|
const candidate = resolve(PROJECT_ROOT, relativePath);
|
|
return existsSync(candidate) ? candidate : null;
|
|
}
|
|
} catch {
|
|
return null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
async function validateLogo(logoURI, tokenInfo) {
|
|
const issues = [];
|
|
|
|
// Check protocol
|
|
if (!logoURI.startsWith('https://') && !logoURI.startsWith('ipfs://')) {
|
|
issues.push(`URL should use HTTPS or IPFS (got: ${logoURI.substring(0, 20)}...)`);
|
|
}
|
|
|
|
// For HTTPS URLs, validate accessibility
|
|
if (logoURI.startsWith('https://')) {
|
|
const localAsset = resolveLocalRepoAsset(logoURI);
|
|
if (localAsset) {
|
|
try {
|
|
const size = statSync(localAsset).size;
|
|
const contentType = inferContentTypeFromPath(localAsset);
|
|
if (!contentType || !IMAGE_MIME_TYPES.includes(contentType)) {
|
|
issues.push(`Local repo asset has unsupported extension: ${localAsset}`);
|
|
}
|
|
if (size > MAX_LOGO_SIZE) {
|
|
issues.push(`Local repo asset too large: ${(size / 1024).toFixed(2)}KB (max ${MAX_LOGO_SIZE / 1024}KB)`);
|
|
}
|
|
return issues;
|
|
} catch (error) {
|
|
issues.push(`Failed to stat local repo asset: ${error.message}`);
|
|
return issues;
|
|
}
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(logoURI, { method: 'HEAD' });
|
|
|
|
if (!response.ok) {
|
|
issues.push(`HTTP ${response.status}: ${response.statusText}`);
|
|
} else {
|
|
const contentType = response.headers.get('content-type');
|
|
const contentLength = response.headers.get('content-length');
|
|
|
|
if (contentType && !IMAGE_MIME_TYPES.some(mime => contentType.includes(mime))) {
|
|
issues.push(`Invalid Content-Type: ${contentType} (expected image/*)`);
|
|
}
|
|
|
|
if (contentLength && parseInt(contentLength) > MAX_LOGO_SIZE) {
|
|
issues.push(`Logo too large: ${(parseInt(contentLength) / 1024).toFixed(2)}KB (max ${MAX_LOGO_SIZE / 1024}KB)`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
issues.push(`Failed to fetch: ${error.message}`);
|
|
}
|
|
} else if (logoURI.startsWith('ipfs://')) {
|
|
// IPFS URLs are valid but we can't easily validate them
|
|
// Just check format
|
|
if (!logoURI.match(/^ipfs:\/\/[a-zA-Z0-9]+/)) {
|
|
issues.push('Invalid IPFS URL format');
|
|
}
|
|
}
|
|
|
|
return issues;
|
|
}
|
|
|
|
async function validateLogos(filePath) {
|
|
console.log(`\n🖼️ Validating logos in: ${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);
|
|
}
|
|
|
|
const results = [];
|
|
let totalIssues = 0;
|
|
|
|
// Validate top-level logoURI
|
|
if (tokenList.logoURI) {
|
|
console.log('Validating list logoURI...');
|
|
const issues = await validateLogo(tokenList.logoURI, 'List');
|
|
if (issues.length > 0) {
|
|
results.push({ type: 'list', uri: tokenList.logoURI, issues });
|
|
totalIssues += issues.length;
|
|
}
|
|
}
|
|
|
|
// Validate token logos
|
|
if (tokenList.tokens && Array.isArray(tokenList.tokens)) {
|
|
for (const [index, token] of tokenList.tokens.entries()) {
|
|
if (token.logoURI) {
|
|
const tokenInfo = `${token.symbol || token.name} (Token[${index}])`;
|
|
const issues = await validateLogo(token.logoURI, tokenInfo);
|
|
if (issues.length > 0) {
|
|
results.push({ type: 'token', token: tokenInfo, uri: token.logoURI, issues });
|
|
totalIssues += issues.length;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Report results
|
|
if (totalIssues === 0) {
|
|
console.log('✅ All logos are valid!\n');
|
|
return 0;
|
|
}
|
|
|
|
console.log(`Found ${totalIssues} logo issue(s):\n`);
|
|
results.forEach(result => {
|
|
if (result.type === 'list') {
|
|
console.log(`❌ List logoURI: ${result.uri}`);
|
|
} else {
|
|
console.log(`❌ ${result.token}: ${result.uri}`);
|
|
}
|
|
result.issues.forEach(issue => {
|
|
console.log(` - ${issue}`);
|
|
});
|
|
console.log('');
|
|
});
|
|
|
|
return 1;
|
|
}
|
|
|
|
// Main
|
|
const filePath = process.argv[2] || resolve(__dirname, '../lists/dbis-138.tokenlist.json');
|
|
|
|
if (!filePath) {
|
|
console.error('Usage: node validate-logos.js [path/to/token-list.json]');
|
|
process.exit(1);
|
|
}
|
|
|
|
validateLogos(filePath).then(exitCode => {
|
|
process.exit(exitCode);
|
|
}).catch(error => {
|
|
console.error('Unexpected error:', error);
|
|
process.exit(1);
|
|
});
|