Spaces:
Runtime error
Runtime error
| /** | |
| * CRYPTOCURRENCY API RESOURCE MONITOR | |
| * Monitors and manages all API resources from registry | |
| * Tracks online status, validates endpoints, maintains availability metrics | |
| */ | |
| const fs = require('fs'); | |
| const https = require('https'); | |
| const http = require('http'); | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // CONFIGURATION | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const CONFIG = { | |
| REGISTRY_FILE: './all_apis_merged_2025.json', | |
| CHECK_INTERVAL: 5 * 60 * 1000, // 5 minutes | |
| TIMEOUT: 10000, // 10 seconds | |
| MAX_RETRIES: 3, | |
| RETRY_DELAY: 2000, | |
| // Status thresholds | |
| THRESHOLDS: { | |
| ONLINE: { responseTime: 2000, successRate: 0.95 }, | |
| DEGRADED: { responseTime: 5000, successRate: 0.80 }, | |
| SLOW: { responseTime: 10000, successRate: 0.70 }, | |
| UNSTABLE: { responseTime: Infinity, successRate: 0.50 } | |
| } | |
| }; | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // API REGISTRY - Comprehensive resource definitions | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const API_REGISTRY = { | |
| blockchainExplorers: { | |
| etherscan: [ | |
| { name: 'Etherscan-1', url: 'https://api.etherscan.io/api', keyName: 'etherscan', keyIndex: 0, testEndpoint: '?module=stats&action=ethprice&apikey={{KEY}}', tier: 1 }, | |
| { name: 'Etherscan-2', url: 'https://api.etherscan.io/api', keyName: 'etherscan', keyIndex: 1, testEndpoint: '?module=stats&action=ethprice&apikey={{KEY}}', tier: 1 } | |
| ], | |
| bscscan: [ | |
| { name: 'BscScan', url: 'https://api.bscscan.com/api', keyName: 'bscscan', keyIndex: 0, testEndpoint: '?module=stats&action=bnbprice&apikey={{KEY}}', tier: 1 } | |
| ], | |
| tronscan: [ | |
| { name: 'TronScan', url: 'https://apilist.tronscanapi.com/api', keyName: 'tronscan', keyIndex: 0, testEndpoint: '/system/status', tier: 2 } | |
| ] | |
| }, | |
| marketData: { | |
| coingecko: [ | |
| { name: 'CoinGecko', url: 'https://api.coingecko.com/api/v3', testEndpoint: '/ping', requiresKey: false, tier: 1 }, | |
| { name: 'CoinGecko-Price', url: 'https://api.coingecko.com/api/v3', testEndpoint: '/simple/price?ids=bitcoin&vs_currencies=usd', requiresKey: false, tier: 1 } | |
| ], | |
| coinmarketcap: [ | |
| { name: 'CoinMarketCap-1', url: 'https://pro-api.coinmarketcap.com/v1', keyName: 'coinmarketcap', keyIndex: 0, testEndpoint: '/key/info', headerKey: 'X-CMC_PRO_API_KEY', tier: 1 }, | |
| { name: 'CoinMarketCap-2', url: 'https://pro-api.coinmarketcap.com/v1', keyName: 'coinmarketcap', keyIndex: 1, testEndpoint: '/key/info', headerKey: 'X-CMC_PRO_API_KEY', tier: 1 } | |
| ], | |
| cryptocompare: [ | |
| { name: 'CryptoCompare', url: 'https://min-api.cryptocompare.com/data', keyName: 'cryptocompare', keyIndex: 0, testEndpoint: '/price?fsym=BTC&tsyms=USD&api_key={{KEY}}', tier: 2 } | |
| ], | |
| coinpaprika: [ | |
| { name: 'CoinPaprika', url: 'https://api.coinpaprika.com/v1', testEndpoint: '/ping', requiresKey: false, tier: 2 } | |
| ], | |
| coincap: [ | |
| { name: 'CoinCap', url: 'https://api.coincap.io/v2', testEndpoint: '/assets/bitcoin', requiresKey: false, tier: 2 } | |
| ] | |
| }, | |
| newsAndSentiment: { | |
| cryptopanic: [ | |
| { name: 'CryptoPanic', url: 'https://cryptopanic.com/api/v1', testEndpoint: '/posts/?public=true', requiresKey: false, tier: 2 } | |
| ], | |
| newsapi: [ | |
| { name: 'NewsAPI', url: 'https://newsapi.org/v2', keyName: 'newsapi', keyIndex: 0, testEndpoint: '/top-headlines?category=business&apiKey={{KEY}}', tier: 2 } | |
| ], | |
| alternativeme: [ | |
| { name: 'Fear-Greed-Index', url: 'https://api.alternative.me', testEndpoint: '/fng/?limit=1', requiresKey: false, tier: 2 } | |
| ], | |
| reddit: [ | |
| { name: 'Reddit-Crypto', url: 'https://www.reddit.com/r/cryptocurrency', testEndpoint: '/hot.json?limit=1', requiresKey: false, tier: 3 } | |
| ] | |
| }, | |
| rpcNodes: { | |
| ethereum: [ | |
| { name: 'Ankr-ETH', url: 'https://rpc.ankr.com/eth', testEndpoint: '', method: 'POST', rpcTest: true, requiresKey: false, tier: 1 }, | |
| { name: 'PublicNode-ETH', url: 'https://ethereum.publicnode.com', testEndpoint: '', method: 'POST', rpcTest: true, requiresKey: false, tier: 2 }, | |
| { name: 'Cloudflare-ETH', url: 'https://cloudflare-eth.com', testEndpoint: '', method: 'POST', rpcTest: true, requiresKey: false, tier: 2 }, | |
| { name: 'LlamaNodes-ETH', url: 'https://eth.llamarpc.com', testEndpoint: '', method: 'POST', rpcTest: true, requiresKey: false, tier: 3 } | |
| ], | |
| bsc: [ | |
| { name: 'BSC-Official', url: 'https://bsc-dataseed.binance.org', testEndpoint: '', method: 'POST', rpcTest: true, requiresKey: false, tier: 2 }, | |
| { name: 'Ankr-BSC', url: 'https://rpc.ankr.com/bsc', testEndpoint: '', method: 'POST', rpcTest: true, requiresKey: false, tier: 2 }, | |
| { name: 'PublicNode-BSC', url: 'https://bsc-rpc.publicnode.com', testEndpoint: '', method: 'POST', rpcTest: true, requiresKey: false, tier: 3 } | |
| ], | |
| polygon: [ | |
| { name: 'Polygon-Official', url: 'https://polygon-rpc.com', testEndpoint: '', method: 'POST', rpcTest: true, requiresKey: false, tier: 2 }, | |
| { name: 'Ankr-Polygon', url: 'https://rpc.ankr.com/polygon', testEndpoint: '', method: 'POST', rpcTest: true, requiresKey: false, tier: 2 } | |
| ], | |
| tron: [ | |
| { name: 'TronGrid', url: 'https://api.trongrid.io', testEndpoint: '/wallet/getnowblock', method: 'POST', requiresKey: false, tier: 2 }, | |
| { name: 'TronStack', url: 'https://api.tronstack.io', testEndpoint: '/wallet/getnowblock', method: 'POST', requiresKey: false, tier: 3 } | |
| ] | |
| }, | |
| onChainAnalytics: [ | |
| { name: 'TheGraph', url: 'https://api.thegraph.com', testEndpoint: '/index-node/graphql', requiresKey: false, tier: 2 }, | |
| { name: 'Blockchair', url: 'https://api.blockchair.com', testEndpoint: '/stats', requiresKey: false, tier: 3 } | |
| ], | |
| whaleTracking: [ | |
| { name: 'WhaleAlert-Status', url: 'https://api.whale-alert.io/v1', testEndpoint: '/status', requiresKey: false, tier: 1 } | |
| ], | |
| corsProxies: [ | |
| { name: 'AllOrigins', url: 'https://api.allorigins.win', testEndpoint: '/get?url=https://api.coingecko.com/api/v3/ping', requiresKey: false, tier: 3 }, | |
| { name: 'CORS.SH', url: 'https://proxy.cors.sh', testEndpoint: '/https://api.coingecko.com/api/v3/ping', requiresKey: false, tier: 3 }, | |
| { name: 'Corsfix', url: 'https://proxy.corsfix.com', testEndpoint: '/?url=https://api.coingecko.com/api/v3/ping', requiresKey: false, tier: 3 }, | |
| { name: 'ThingProxy', url: 'https://thingproxy.freeboard.io', testEndpoint: '/fetch/https://api.coingecko.com/api/v3/ping', requiresKey: false, tier: 3 } | |
| ] | |
| }; | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // RESOURCE MONITOR CLASS | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class CryptoAPIMonitor { | |
| constructor() { | |
| this.apiKeys = {}; | |
| this.resourceStatus = {}; | |
| this.metrics = { | |
| totalChecks: 0, | |
| successfulChecks: 0, | |
| failedChecks: 0, | |
| totalResponseTime: 0 | |
| }; | |
| this.history = {}; | |
| this.alerts = []; | |
| } | |
| // Load API keys from registry | |
| loadRegistry() { | |
| try { | |
| const data = fs.readFileSync(CONFIG.REGISTRY_FILE, 'utf8'); | |
| const registry = JSON.parse(data); | |
| this.apiKeys = registry.discovered_keys || {}; | |
| console.log('β Registry loaded successfully'); | |
| console.log(` Found ${Object.keys(this.apiKeys).length} API key categories`); | |
| return true; | |
| } catch (error) { | |
| console.error('β Failed to load registry:', error.message); | |
| return false; | |
| } | |
| } | |
| // Get API key for resource | |
| getApiKey(keyName, keyIndex = 0) { | |
| if (!keyName || !this.apiKeys[keyName]) return null; | |
| const keys = this.apiKeys[keyName]; | |
| return Array.isArray(keys) ? keys[keyIndex] : keys; | |
| } | |
| // Mask API key for display | |
| maskKey(key) { | |
| if (!key || key.length < 8) return '****'; | |
| return key.substring(0, 4) + '****' + key.substring(key.length - 4); | |
| } | |
| // HTTP request with timeout | |
| makeRequest(url, options = {}) { | |
| return new Promise((resolve, reject) => { | |
| const startTime = Date.now(); | |
| const protocol = url.startsWith('https') ? https : http; | |
| const req = protocol.request(url, { | |
| method: options.method || 'GET', | |
| headers: options.headers || {}, | |
| timeout: CONFIG.TIMEOUT | |
| }, (res) => { | |
| let data = ''; | |
| res.on('data', chunk => data += chunk); | |
| res.on('end', () => { | |
| const responseTime = Date.now() - startTime; | |
| resolve({ | |
| statusCode: res.statusCode, | |
| data: data, | |
| responseTime: responseTime, | |
| success: res.statusCode >= 200 && res.statusCode < 300 | |
| }); | |
| }); | |
| }); | |
| req.on('error', (error) => { | |
| reject({ | |
| error: error.message, | |
| responseTime: Date.now() - startTime, | |
| success: false | |
| }); | |
| }); | |
| req.on('timeout', () => { | |
| req.destroy(); | |
| reject({ | |
| error: 'Request timeout', | |
| responseTime: CONFIG.TIMEOUT, | |
| success: false | |
| }); | |
| }); | |
| if (options.body) { | |
| req.write(options.body); | |
| } | |
| req.end(); | |
| }); | |
| } | |
| // Check single API endpoint | |
| async checkEndpoint(resource) { | |
| const startTime = Date.now(); | |
| try { | |
| // Build URL | |
| let url = resource.url + (resource.testEndpoint || ''); | |
| // Replace API key placeholder | |
| if (resource.keyName) { | |
| const apiKey = this.getApiKey(resource.keyName, resource.keyIndex || 0); | |
| if (apiKey) { | |
| url = url.replace('{{KEY}}', apiKey); | |
| } | |
| } | |
| // Prepare headers | |
| const headers = { | |
| 'User-Agent': 'CryptoAPIMonitor/1.0' | |
| }; | |
| // Add API key to header if needed | |
| if (resource.headerKey && resource.keyName) { | |
| const apiKey = this.getApiKey(resource.keyName, resource.keyIndex || 0); | |
| if (apiKey) { | |
| headers[resource.headerKey] = apiKey; | |
| } | |
| } | |
| // RPC specific test | |
| let options = { method: resource.method || 'GET', headers }; | |
| if (resource.rpcTest) { | |
| options.method = 'POST'; | |
| options.headers['Content-Type'] = 'application/json'; | |
| options.body = JSON.stringify({ | |
| jsonrpc: '2.0', | |
| method: 'eth_blockNumber', | |
| params: [], | |
| id: 1 | |
| }); | |
| } | |
| // Make request | |
| const result = await this.makeRequest(url, options); | |
| return { | |
| name: resource.name, | |
| url: resource.url, | |
| success: result.success, | |
| statusCode: result.statusCode, | |
| responseTime: result.responseTime, | |
| timestamp: new Date().toISOString(), | |
| tier: resource.tier || 4 | |
| }; | |
| } catch (error) { | |
| return { | |
| name: resource.name, | |
| url: resource.url, | |
| success: false, | |
| error: error.error || error.message, | |
| responseTime: error.responseTime || Date.now() - startTime, | |
| timestamp: new Date().toISOString(), | |
| tier: resource.tier || 4 | |
| }; | |
| } | |
| } | |
| // Classify status based on metrics | |
| classifyStatus(resource) { | |
| if (!this.history[resource.name]) { | |
| return 'UNKNOWN'; | |
| } | |
| const hist = this.history[resource.name]; | |
| const recentChecks = hist.slice(-10); // Last 10 checks | |
| if (recentChecks.length === 0) return 'UNKNOWN'; | |
| const successCount = recentChecks.filter(c => c.success).length; | |
| const successRate = successCount / recentChecks.length; | |
| const avgResponseTime = recentChecks | |
| .filter(c => c.success) | |
| .reduce((sum, c) => sum + c.responseTime, 0) / (successCount || 1); | |
| if (successRate >= CONFIG.THRESHOLDS.ONLINE.successRate && | |
| avgResponseTime < CONFIG.THRESHOLDS.ONLINE.responseTime) { | |
| return 'ONLINE'; | |
| } else if (successRate >= CONFIG.THRESHOLDS.DEGRADED.successRate && | |
| avgResponseTime < CONFIG.THRESHOLDS.DEGRADED.responseTime) { | |
| return 'DEGRADED'; | |
| } else if (successRate >= CONFIG.THRESHOLDS.SLOW.successRate && | |
| avgResponseTime < CONFIG.THRESHOLDS.SLOW.responseTime) { | |
| return 'SLOW'; | |
| } else if (successRate >= CONFIG.THRESHOLDS.UNSTABLE.successRate) { | |
| return 'UNSTABLE'; | |
| } else { | |
| return 'OFFLINE'; | |
| } | |
| } | |
| // Update history for resource | |
| updateHistory(resource, result) { | |
| if (!this.history[resource.name]) { | |
| this.history[resource.name] = []; | |
| } | |
| this.history[resource.name].push(result); | |
| // Keep only last 100 checks | |
| if (this.history[resource.name].length > 100) { | |
| this.history[resource.name] = this.history[resource.name].slice(-100); | |
| } | |
| } | |
| // Check all resources in a category | |
| async checkCategory(categoryName, resources) { | |
| console.log(`\n Checking ${categoryName}...`); | |
| const results = []; | |
| if (Array.isArray(resources)) { | |
| for (const resource of resources) { | |
| const result = await this.checkEndpoint(resource); | |
| this.updateHistory(resource, result); | |
| results.push(result); | |
| // Rate limiting delay | |
| await new Promise(resolve => setTimeout(resolve, 200)); | |
| } | |
| } else { | |
| // Handle nested categories | |
| for (const [subCategory, subResources] of Object.entries(resources)) { | |
| for (const resource of subResources) { | |
| const result = await this.checkEndpoint(resource); | |
| this.updateHistory(resource, result); | |
| results.push(result); | |
| await new Promise(resolve => setTimeout(resolve, 200)); | |
| } | |
| } | |
| } | |
| return results; | |
| } | |
| // Run complete monitoring cycle | |
| async runMonitoringCycle() { | |
| console.log('\nββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ'); | |
| console.log('β CRYPTOCURRENCY API RESOURCE MONITOR - Health Check β'); | |
| console.log('ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ'); | |
| console.log(` Timestamp: ${new Date().toISOString()}`); | |
| const cycleResults = {}; | |
| for (const [category, resources] of Object.entries(API_REGISTRY)) { | |
| const results = await this.checkCategory(category, resources); | |
| cycleResults[category] = results; | |
| } | |
| this.generateReport(cycleResults); | |
| this.checkAlertConditions(cycleResults); | |
| return cycleResults; | |
| } | |
| // Generate status report | |
| generateReport(cycleResults) { | |
| console.log('\nββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ'); | |
| console.log('β RESOURCE STATUS REPORT β'); | |
| console.log('ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ\n'); | |
| let totalResources = 0; | |
| let onlineCount = 0; | |
| let degradedCount = 0; | |
| let offlineCount = 0; | |
| for (const [category, results] of Object.entries(cycleResults)) { | |
| console.log(`\nπ ${category.toUpperCase()}`); | |
| console.log('β'.repeat(60)); | |
| for (const result of results) { | |
| totalResources++; | |
| const status = this.classifyStatus(result); | |
| let statusSymbol = 'β'; | |
| let statusColor = ''; | |
| switch (status) { | |
| case 'ONLINE': | |
| statusSymbol = 'β'; | |
| onlineCount++; | |
| break; | |
| case 'DEGRADED': | |
| case 'SLOW': | |
| statusSymbol = 'β'; | |
| degradedCount++; | |
| break; | |
| case 'OFFLINE': | |
| case 'UNSTABLE': | |
| statusSymbol = 'β'; | |
| offlineCount++; | |
| break; | |
| } | |
| const rt = result.responseTime ? `${result.responseTime}ms` : 'N/A'; | |
| const tierBadge = result.tier === 1 ? '[TIER-1]' : result.tier === 2 ? '[TIER-2]' : ''; | |
| console.log(` ${statusSymbol} ${result.name.padEnd(25)} ${status.padEnd(10)} ${rt.padStart(8)} ${tierBadge}`); | |
| } | |
| } | |
| // Summary | |
| console.log('\nββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ'); | |
| console.log('β SUMMARY β'); | |
| console.log('ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ'); | |
| console.log(` Total Resources: ${totalResources}`); | |
| console.log(` Online: ${onlineCount} (${((onlineCount/totalResources)*100).toFixed(1)}%)`); | |
| console.log(` Degraded: ${degradedCount} (${((degradedCount/totalResources)*100).toFixed(1)}%)`); | |
| console.log(` Offline: ${offlineCount} (${((offlineCount/totalResources)*100).toFixed(1)}%)`); | |
| console.log(` Overall Health: ${((onlineCount/totalResources)*100).toFixed(1)}%`); | |
| } | |
| // Check for alert conditions | |
| checkAlertConditions(cycleResults) { | |
| const newAlerts = []; | |
| // Check TIER-1 APIs | |
| for (const [category, results] of Object.entries(cycleResults)) { | |
| for (const result of results) { | |
| if (result.tier === 1 && !result.success) { | |
| newAlerts.push({ | |
| severity: 'CRITICAL', | |
| message: `TIER-1 API offline: ${result.name}`, | |
| timestamp: new Date().toISOString() | |
| }); | |
| } | |
| if (result.responseTime > 5000) { | |
| newAlerts.push({ | |
| severity: 'WARNING', | |
| message: `Elevated response time: ${result.name} (${result.responseTime}ms)`, | |
| timestamp: new Date().toISOString() | |
| }); | |
| } | |
| } | |
| } | |
| if (newAlerts.length > 0) { | |
| console.log('\nββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ'); | |
| console.log('β β οΈ ALERTS β'); | |
| console.log('ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ'); | |
| for (const alert of newAlerts) { | |
| console.log(` [${alert.severity}] ${alert.message}`); | |
| } | |
| this.alerts.push(...newAlerts); | |
| } | |
| } | |
| // Generate JSON report | |
| exportReport(filename = 'api-monitor-report.json') { | |
| const report = { | |
| timestamp: new Date().toISOString(), | |
| summary: { | |
| totalResources: 0, | |
| onlineResources: 0, | |
| degradedResources: 0, | |
| offlineResources: 0 | |
| }, | |
| categories: {}, | |
| alerts: this.alerts.slice(-50), // Last 50 alerts | |
| history: this.history | |
| }; | |
| // Calculate summary | |
| for (const [category, resources] of Object.entries(API_REGISTRY)) { | |
| report.categories[category] = []; | |
| const flatResources = this.flattenResources(resources); | |
| for (const resource of flatResources) { | |
| const status = this.classifyStatus(resource); | |
| const lastCheck = this.history[resource.name] ? | |
| this.history[resource.name].slice(-1)[0] : null; | |
| report.summary.totalResources++; | |
| if (status === 'ONLINE') report.summary.onlineResources++; | |
| else if (status === 'DEGRADED' || status === 'SLOW') report.summary.degradedResources++; | |
| else if (status === 'OFFLINE' || status === 'UNSTABLE') report.summary.offlineResources++; | |
| report.categories[category].push({ | |
| name: resource.name, | |
| url: resource.url, | |
| status: status, | |
| tier: resource.tier, | |
| lastCheck: lastCheck | |
| }); | |
| } | |
| } | |
| fs.writeFileSync(filename, JSON.stringify(report, null, 2)); | |
| console.log(`\nβ Report exported to ${filename}`); | |
| return report; | |
| } | |
| // Flatten nested resources | |
| flattenResources(resources) { | |
| if (Array.isArray(resources)) { | |
| return resources; | |
| } | |
| const flattened = []; | |
| for (const subResources of Object.values(resources)) { | |
| flattened.push(...subResources); | |
| } | |
| return flattened; | |
| } | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // MAIN EXECUTION | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function main() { | |
| const monitor = new CryptoAPIMonitor(); | |
| // Load registry | |
| if (!monitor.loadRegistry()) { | |
| console.error('Failed to initialize monitor'); | |
| process.exit(1); | |
| } | |
| // Run initial check | |
| console.log('\nπ Starting initial health check...'); | |
| await monitor.runMonitoringCycle(); | |
| // Export report | |
| monitor.exportReport(); | |
| // Continuous monitoring mode | |
| if (process.argv.includes('--continuous')) { | |
| console.log(`\nβΎοΈ Continuous monitoring enabled (interval: ${CONFIG.CHECK_INTERVAL/1000}s)`); | |
| setInterval(async () => { | |
| await monitor.runMonitoringCycle(); | |
| monitor.exportReport(); | |
| }, CONFIG.CHECK_INTERVAL); | |
| } else { | |
| console.log('\nβ Monitoring cycle complete'); | |
| console.log(' Use --continuous flag for continuous monitoring'); | |
| } | |
| } | |
| // Run if executed directly | |
| if (require.main === module) { | |
| main().catch(console.error); | |
| } | |
| module.exports = CryptoAPIMonitor; | |