| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { showToast } from '../shared/js/components/toast-helper.js'; |
| import { showLoading, hideLoading } from '../shared/js/components/loading-helper.js'; |
|
|
| class CryptoAPIHub { |
| constructor() { |
| this.services = null; |
| this.currentFilter = 'all'; |
| this.searchQuery = ''; |
| this.retryCount = 0; |
| this.maxRetries = 3; |
| this.fallbackData = this.getFallbackData(); |
| this.corsProxyEnabled = true; |
| } |
|
|
| |
| |
| |
| async init() { |
| console.log('[CryptoAPIHub] Initializing...'); |
| |
| |
| this.renderLoadingState(); |
| |
| |
| await this.fetchServicesWithHealing(); |
| |
| |
| this.renderServices(); |
| |
| |
| this.setupEventListeners(); |
| |
| |
| this.updateStats(); |
| |
| console.log('[CryptoAPIHub] Initialized successfully'); |
| } |
|
|
| |
| |
| |
| async fetchServicesWithHealing() { |
| try { |
| console.log('[CryptoAPIHub] Fetching services from backend...'); |
| |
| |
| const response = await this.fetchFromBackend(); |
| |
| if (response && response.categories) { |
| this.services = response; |
| this.retryCount = 0; |
| showToast('✅', 'Services loaded successfully', 'success'); |
| return; |
| } |
| } catch (error) { |
| console.warn('[CryptoAPIHub] Backend fetch failed:', error); |
| } |
|
|
| |
| await this.healWithFallback(); |
| } |
|
|
| |
| |
| |
| async fetchFromBackend() { |
| try { |
| |
| const response = await fetch('/api/crypto-hub/services', { |
| method: 'GET', |
| headers: { |
| 'Content-Type': 'application/json', |
| }, |
| }); |
|
|
| if (response.ok) { |
| return await response.json(); |
| } |
|
|
| throw new Error(`HTTP ${response.status}`); |
| } catch (error) { |
| console.error('[CryptoAPIHub] Backend error:', error); |
| throw error; |
| } |
| } |
|
|
| |
| |
| |
| async healWithFallback() { |
| console.log('[CryptoAPIHub] Activating self-healing mechanism...'); |
|
|
| if (this.retryCount < this.maxRetries) { |
| this.retryCount++; |
| showToast('🔄', `Retrying... (${this.retryCount}/${this.maxRetries})`, 'info'); |
| |
| |
| await this.sleep(2000 * this.retryCount); |
| |
| |
| await this.fetchServicesWithHealing(); |
| return; |
| } |
|
|
| |
| console.log('[CryptoAPIHub] Using fallback data...'); |
| this.services = this.fallbackData; |
| showToast('⚠️', 'Using cached data (backend unavailable)', 'warning'); |
| } |
|
|
| |
| |
| |
| getFallbackData() { |
| return { |
| metadata: { |
| version: "1.0.0", |
| total_services: 74, |
| total_endpoints: 150, |
| api_keys_count: 10, |
| last_updated: new Date().toISOString() |
| }, |
| categories: { |
| explorer: { |
| name: "Blockchain Explorers", |
| description: "Track transactions and addresses", |
| services: [ |
| { |
| name: "Etherscan", |
| url: "https://api.etherscan.io/api", |
| key: "SZHYFZK2RR8H9TIMJBVW54V4H81K2Z2KR2", |
| endpoints: [ |
| "?module=account&action=balance&address={address}&apikey={KEY}", |
| "?module=gastracker&action=gasoracle&apikey={KEY}" |
| ] |
| }, |
| { |
| name: "BscScan", |
| url: "https://api.bscscan.com/api", |
| key: "K62RKHGXTDCG53RU4MCG6XABIMJKTN19IT", |
| endpoints: ["?module=account&action=balance&address={address}&apikey={KEY}"] |
| }, |
| { |
| name: "TronScan", |
| url: "https://apilist.tronscanapi.com/api", |
| key: "7ae72726-bffe-4e74-9c33-97b761eeea21", |
| endpoints: ["/account?address={address}"] |
| } |
| ] |
| }, |
| market: { |
| name: "Market Data", |
| description: "Real-time prices and market metrics", |
| services: [ |
| { |
| name: "CoinGecko", |
| url: "https://api.coingecko.com/api/v3", |
| key: "", |
| endpoints: [ |
| "/simple/price?ids=bitcoin,ethereum&vs_currencies=usd", |
| "/coins/markets?vs_currency=usd&per_page=100" |
| ] |
| }, |
| { |
| name: "CoinMarketCap", |
| url: "https://pro-api.coinmarketcap.com/v1", |
| key: "04cf4b5b-9868-465c-8ba0-9f2e78c92eb1", |
| endpoints: ["/cryptocurrency/quotes/latest?symbol=BTC&convert=USD"] |
| }, |
| { |
| name: "Binance", |
| url: "https://api.binance.com/api/v3", |
| key: "", |
| endpoints: ["/ticker/price?symbol=BTCUSDT"] |
| } |
| ] |
| }, |
| news: { |
| name: "News & Media", |
| description: "Crypto news and updates", |
| services: [ |
| { |
| name: "CryptoPanic", |
| url: "https://cryptopanic.com/api/v1", |
| key: "", |
| endpoints: ["/posts/?auth_token={KEY}"] |
| }, |
| { |
| name: "NewsAPI", |
| url: "https://newsapi.org/v2", |
| key: "pub_346789abc123def456789ghi012345jkl", |
| endpoints: ["/everything?q=crypto&apiKey={KEY}"] |
| } |
| ] |
| }, |
| sentiment: { |
| name: "Sentiment Analysis", |
| description: "Market sentiment indicators", |
| services: [ |
| { |
| name: "Fear & Greed", |
| url: "https://api.alternative.me/fng/", |
| key: "", |
| endpoints: ["?limit=1", "?limit=30"] |
| }, |
| { |
| name: "LunarCrush", |
| url: "https://api.lunarcrush.com/v2", |
| key: "", |
| endpoints: ["?data=assets&key={KEY}"] |
| } |
| ] |
| }, |
| analytics: { |
| name: "Analytics & Tools", |
| description: "Advanced analytics and whale tracking", |
| services: [ |
| { |
| name: "Whale Alert", |
| url: "https://api.whale-alert.io/v1", |
| key: "", |
| endpoints: ["/transactions?api_key={KEY}&min_value=1000000"] |
| }, |
| { |
| name: "Glassnode", |
| url: "https://api.glassnode.com/v1", |
| key: "", |
| endpoints: [] |
| }, |
| { |
| name: "Hugging Face", |
| url: "https://api-inference.huggingface.co/models", |
| key: "", |
| endpoints: ["/ElKulako/cryptobert"] |
| } |
| ] |
| } |
| } |
| }; |
| } |
|
|
| |
| |
| |
| renderServices() { |
| const grid = document.getElementById('servicesGrid'); |
| if (!grid) return; |
|
|
| let html = ''; |
| let count = 0; |
|
|
| const categories = this.services?.categories || {}; |
|
|
| Object.entries(categories).forEach(([categoryKey, category]) => { |
| const services = category.services || []; |
|
|
| services.forEach((service, index) => { |
| |
| if (this.currentFilter !== 'all' && categoryKey !== this.currentFilter) { |
| return; |
| } |
|
|
| |
| if (this.searchQuery) { |
| const searchLower = this.searchQuery.toLowerCase(); |
| const matchesSearch = |
| service.name.toLowerCase().includes(searchLower) || |
| service.url.toLowerCase().includes(searchLower) || |
| categoryKey.toLowerCase().includes(searchLower); |
| |
| if (!matchesSearch) return; |
| } |
|
|
| count++; |
| const hasKey = service.key ? `<span class="badge badge-key">🔑 Has Key</span>` : ''; |
| const endpoints = service.endpoints?.length || 0; |
|
|
| html += ` |
| <div class="service-card" data-category="${categoryKey}" data-name="${service.name.toLowerCase()}" style="animation-delay: ${index * 0.05}s"> |
| <div class="service-header"> |
| <div class="service-icon">${this.getIcon(categoryKey)}</div> |
| <div class="service-info"> |
| <div class="service-name">${service.name}</div> |
| <div class="service-url">${service.url}</div> |
| </div> |
| </div> |
| <div class="service-badges"> |
| <span class="badge badge-category">${categoryKey}</span> |
| ${endpoints > 0 ? `<span class="badge badge-endpoints">${endpoints} endpoints</span>` : ''} |
| ${hasKey} |
| </div> |
| ${this.renderEndpoints(service, categoryKey)} |
| </div> |
| `; |
| }); |
| }); |
|
|
| if (html === '') { |
| html = '<div class="empty-state"><div class="empty-icon">🔍</div><div class="empty-text">No services found</div></div>'; |
| } |
|
|
| grid.innerHTML = html; |
| } |
|
|
| |
| |
| |
| renderEndpoints(service, category) { |
| const endpoints = service.endpoints || []; |
|
|
| if (endpoints.length === 0) { |
| return '<div class="no-endpoints">Base endpoint available</div>'; |
| } |
|
|
| let html = '<div class="endpoints-list">'; |
|
|
| endpoints.slice(0, 2).forEach(endpoint => { |
| const fullUrl = service.url + endpoint; |
| const encodedUrl = encodeURIComponent(fullUrl); |
|
|
| html += ` |
| <div class="endpoint-item"> |
| <div class="endpoint-path">${endpoint}</div> |
| <div class="endpoint-actions"> |
| <button class="btn-sm" onclick="window.cryptoAPIHub.copyText('${fullUrl.replace(/'/g, "\\'")}')"> |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect> |
| <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path> |
| </svg> |
| Copy |
| </button> |
| <button class="btn-sm" onclick="window.cryptoAPIHub.testEndpoint('${fullUrl.replace(/'/g, "\\'")}', '${service.key || ''}')"> |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline> |
| </svg> |
| Test |
| </button> |
| </div> |
| </div> |
| `; |
| }); |
| |
| if (endpoints.length > 2) { |
| html += `<div class="more-endpoints">+${endpoints.length - 2} more endpoints</div>`; |
| } |
| |
| html += '</div>'; |
| return html; |
| } |
| |
| /** |
| * Get icon for category |
| */ |
| getIcon(category) { |
| const icons = { |
| explorer: '<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>', |
| market: '<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"><line x1="12" y1="20" x2="12" y2="10"></line><line x1="18" y1="20" x2="18" y2="4"></line><line x1="6" y1="20" x2="6" y2="16"></line></svg>', |
| news: '<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"><path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2Zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2"></path><path d="M18 14h-8"></path><path d="M15 18h-5"></path><path d="M10 6h8v4h-8V6Z"></path></svg>', |
| sentiment: '<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"><path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2Z"></path><path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2Z"></path></svg>', |
| analytics: '<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"><path d="M3 3v18h18"></path><path d="m19 9-5 5-4-4-3 3"></path></svg>' |
| }; |
| return icons[category] || icons.analytics; |
| } |
| |
| /** |
| * Render loading state |
| */ |
| renderLoadingState() { |
| const grid = document.getElementById('servicesGrid'); |
| if (!grid) return; |
| |
| grid.innerHTML = ` |
| <div class="loading-state"> |
| <div class="loading-spinner"></div> |
| <div class="loading-text">Loading services...</div> |
| </div> |
| `; |
| } |
| |
| /** |
| * Update statistics |
| */ |
| updateStats() { |
| const metadata = this.services?.metadata || {}; |
| |
| const statsData = { |
| services: metadata.total_services || 74, |
| endpoints: metadata.total_endpoints || 150, |
| keys: metadata.api_keys_count || 10 |
| }; |
| |
| // Update stat values |
| document.querySelectorAll('.stat-value').forEach((el, index) => { |
| const values = [statsData.services, statsData.endpoints + '+', statsData.keys]; |
| if (el && values[index]) { |
| el.textContent = values[index]; |
| } |
| }); |
| } |
| |
| /** |
| * Setup event listeners |
| */ |
| setupEventListeners() { |
| // Search input |
| const searchInput = document.getElementById('searchInput'); |
| if (searchInput) { |
| searchInput.addEventListener('input', (e) => { |
| this.searchQuery = e.target.value; |
| this.renderServices(); |
| }); |
| } |
| |
| // Filter tabs |
| document.querySelectorAll('.filter-tab').forEach(tab => { |
| tab.addEventListener('click', (e) => { |
| this.setFilter(e.target.dataset.filter); |
| }); |
| }); |
| |
| // Method buttons |
| document.querySelectorAll('.method-btn').forEach(btn => { |
| btn.addEventListener('click', (e) => { |
| const method = e.target.dataset.method; |
| this.setMethod(method); |
| }); |
| }); |
| |
| // Update last update time |
| this.updateLastUpdateTime(); |
| } |
| |
| /** |
| * Set HTTP method |
| */ |
| setMethod(method) { |
| this.currentMethod = method; |
| |
| // Update active button |
| document.querySelectorAll('.method-btn').forEach(btn => { |
| btn.classList.remove('active'); |
| if (btn.dataset.method === method) { |
| btn.classList.add('active'); |
| } |
| }); |
| |
| // Show/hide body field |
| const bodyGroup = document.getElementById('bodyGroup'); |
| if (bodyGroup) { |
| bodyGroup.style.display = (method === 'POST' || method === 'PUT') ? 'block' : 'none'; |
| } |
| } |
| |
| /** |
| * Update last update time |
| */ |
| updateLastUpdateTime() { |
| const el = document.getElementById('lastUpdate'); |
| if (el) { |
| el.textContent = `Last updated: ${new Date().toLocaleTimeString()}`; |
| } |
| } |
| |
| /** |
| * Set filter |
| */ |
| setFilter(filter) { |
| this.currentFilter = filter; |
| |
| // Update active tab |
| document.querySelectorAll('.filter-tab').forEach(t => t.classList.remove('active')); |
| const activeTab = document.querySelector(`[data-filter="${filter}"]`); |
| if (activeTab) activeTab.classList.add('active'); |
| |
| // Re-render |
| this.renderServices(); |
| } |
| |
| /** |
| * Copy text to clipboard |
| */ |
| async copyText(text) { |
| try { |
| await navigator.clipboard.writeText(text); |
| showToast('✅', 'Copied to clipboard!', 'success'); |
| } catch (error) { |
| showToast('❌', 'Failed to copy', 'error'); |
| } |
| } |
| |
| /** |
| * Test endpoint |
| */ |
| async testEndpoint(url, key) { |
| // Replace key placeholders |
| let finalUrl = url; |
| if (key) { |
| finalUrl = url.replace('{KEY}', key).replace('{key}', key); |
| } |
| |
| // Open tester modal with URL |
| this.openTester(finalUrl); |
| } |
| |
| /** |
| * Open API tester modal |
| */ |
| openTester(url = '') { |
| const modal = document.getElementById('testerModal'); |
| const urlInput = document.getElementById('testUrl'); |
| |
| if (modal) { |
| modal.classList.add('active'); |
| if (urlInput && url) { |
| urlInput.value = url; |
| } |
| } |
| } |
| |
| /** |
| * Close API tester modal |
| */ |
| closeTester() { |
| const modal = document.getElementById('testerModal'); |
| if (modal) { |
| modal.classList.remove('active'); |
| } |
| } |
| |
| /** |
| * Send API test request |
| */ |
| async sendTestRequest() { |
| const url = document.getElementById('testUrl')?.value; |
| const headersText = document.getElementById('testHeaders')?.value || '{}'; |
| const bodyText = document.getElementById('testBody')?.value; |
| const responseBox = document.getElementById('responseBox'); |
| const responseJson = document.getElementById('responseJson'); |
| const method = this.currentMethod || 'GET'; |
| |
| if (!url) { |
| showToast('⚠️', 'Please enter a URL', 'warning'); |
| return; |
| } |
| |
| if (responseBox) responseBox.style.display = 'block'; |
| if (responseJson) responseJson.textContent = '⏳ Sending request...'; |
| |
| try { |
| // Use CORS proxy if enabled |
| const requestUrl = this.corsProxyEnabled |
| ? `/api/crypto-hub/test` |
| : url; |
| |
| const requestOptions = this.corsProxyEnabled |
| ? { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| url: url, |
| method: method, |
| headers: JSON.parse(headersText), |
| body: bodyText |
| }) |
| } |
| : { |
| method: method, |
| headers: JSON.parse(headersText), |
| body: (method === 'POST' || method === 'PUT') ? bodyText : undefined |
| }; |
| |
| const response = await fetch(requestUrl, requestOptions); |
| const data = await response.json(); |
| |
| if (responseJson) { |
| responseJson.textContent = JSON.stringify(data, null, 2); |
| } |
| |
| showToast('✅', 'Request successful!', 'success'); |
| } catch (error) { |
| if (responseJson) { |
| responseJson.textContent = `❌ Error: ${error.message}\n\nThis might be due to CORS policy. Try using the CORS proxy.`; |
| } |
| showToast('❌', 'Request failed', 'error'); |
| } |
| } |
| |
| /** |
| * Export services as JSON |
| */ |
| exportJSON() { |
| const data = { |
| metadata: { |
| exported_at: new Date().toISOString(), |
| ...this.services?.metadata |
| }, |
| services: this.services |
| }; |
| |
| const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); |
| const url = URL.createObjectURL(blob); |
| const a = document.createElement('a'); |
| a.href = url; |
| a.download = `crypto-api-hub-${Date.now()}.json`; |
| a.click(); |
| URL.revokeObjectURL(url); |
| |
| showToast('✅', 'JSON exported successfully!', 'success'); |
| } |
| |
| /** |
| * Sleep utility |
| */ |
| sleep(ms) { |
| return new Promise(resolve => setTimeout(resolve, ms)); |
| } |
| } |
| |
| // Initialize when DOM is ready |
| document.addEventListener('DOMContentLoaded', () => { |
| window.cryptoAPIHub = new CryptoAPIHub(); |
| window.cryptoAPIHub.init(); |
| }); |
| |
| // Export for module usage |
| export default CryptoAPIHub; |
| |