A Pen by BI__KO BBM on CodePen.
Created
February 28, 2026 03:09
-
-
Save hsnsbhshsh/1243358e9212530a5c4aa0fda5807c78 to your computer and use it in GitHub Desktop.
Untitled
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Bitcoin Explorer Pro - Live Analytics</title> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| } | |
| :root { | |
| --primary: #0a0a0a; | |
| --secondary: #121212; | |
| --accent: #1a1a1a; | |
| --text: #ffffff; | |
| --text-secondary: #b0b0b0; | |
| --highlight: #00ff9d; | |
| --highlight-secondary: #00ccff; | |
| --card-bg: rgba(30, 30, 30, 0.8); | |
| --glow: 0 0 10px rgba(0, 255, 157, 0.3); | |
| --border: rgba(255, 255, 255, 0.1); | |
| --positive: #00ff9d; | |
| --negative: #ff4d4d; | |
| --sender: #ff4d4d; | |
| --receiver: #00ff9d; | |
| --neutral: #b0b0b0; | |
| } | |
| body { | |
| background-color: var(--primary); | |
| color: var(--text); | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| background-image: | |
| radial-gradient(circle at 10% 20%, rgba(0, 255, 157, 0.03) 0%, transparent 20%), | |
| radial-gradient(circle at 90% 40%, rgba(0, 255, 157, 0.03) 0%, transparent 20%), | |
| radial-gradient(circle at 50% 80%, rgba(0, 255, 157, 0.03) 0%, transparent 20%); | |
| } | |
| .container { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| padding: 15px; | |
| display: grid; | |
| grid-template-columns: 1fr; | |
| gap: 15px; | |
| } | |
| /* Header */ | |
| header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 10px 0; | |
| border-bottom: 1px solid var(--border); | |
| margin-bottom: 15px; | |
| flex-wrap: wrap; | |
| } | |
| .logo { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .logo-icon { | |
| width: 35px; | |
| height: 35px; | |
| background: linear-gradient(135deg, var(--highlight), var(--highlight-secondary)); | |
| border-radius: 10px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| box-shadow: var(--glow); | |
| animation: pulse 3s infinite; | |
| } | |
| @keyframes pulse { | |
| 0% { transform: scale(1); box-shadow: var(--glow); } | |
| 50% { transform: scale(1.05); box-shadow: 0 0 15px rgba(0, 255, 157, 0.5); } | |
| 100% { transform: scale(1); box-shadow: var(--glow); } | |
| } | |
| .logo h1 { | |
| font-size: 20px; | |
| font-weight: 700; | |
| background: linear-gradient(to right, var(--highlight), var(--highlight-secondary)); | |
| -webkit-background-clip: text; | |
| background-clip: text; | |
| color: transparent; | |
| letter-spacing: 1px; | |
| } | |
| .mobile-menu-btn { | |
| display: none; | |
| background: none; | |
| border: none; | |
| color: var(--text); | |
| font-size: 20px; | |
| cursor: pointer; | |
| } | |
| .search-bar { | |
| display: flex; | |
| align-items: center; | |
| background: var(--secondary); | |
| border-radius: 10px; | |
| padding: 8px 12px; | |
| width: 100%; | |
| border: 1px solid var(--border); | |
| margin: 10px 0; | |
| order: 3; | |
| transition: all 0.3s ease; | |
| } | |
| .search-bar:focus-within { | |
| border-color: var(--highlight); | |
| box-shadow: var(--glow); | |
| } | |
| .search-bar input { | |
| background: transparent; | |
| border: none; | |
| color: var(--text); | |
| width: 100%; | |
| margin-left: 8px; | |
| outline: none; | |
| font-size: 14px; | |
| letter-spacing: 0.5px; | |
| } | |
| .search-bar input::placeholder { | |
| color: var(--text-secondary); | |
| } | |
| .user-actions { | |
| display: flex; | |
| gap: 10px; | |
| align-items: center; | |
| } | |
| .notification { | |
| position: relative; | |
| cursor: pointer; | |
| } | |
| .notification-badge { | |
| position: absolute; | |
| top: -5px; | |
| right: -5px; | |
| background: var(--negative); | |
| color: white; | |
| border-radius: 50%; | |
| width: 16px; | |
| height: 16px; | |
| font-size: 10px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .user-profile { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| cursor: pointer; | |
| } | |
| .avatar { | |
| width: 35px; | |
| height: 35px; | |
| border-radius: 50%; | |
| background: linear-gradient(135deg, var(--highlight), var(--highlight-secondary)); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| /* Sidebar */ | |
| .sidebar { | |
| background: var(--secondary); | |
| border-radius: 16px; | |
| padding: 15px; | |
| border: 1px solid var(--border); | |
| display: none; | |
| margin-bottom: 15px; | |
| } | |
| .sidebar.active { | |
| display: block; | |
| } | |
| .sidebar-menu { | |
| list-style: none; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| } | |
| .sidebar-menu li { | |
| padding: 10px 12px; | |
| border-radius: 10px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| font-weight: 500; | |
| letter-spacing: 0.3px; | |
| } | |
| .sidebar-menu li.active { | |
| background: rgba(0, 255, 157, 0.1); | |
| color: var(--highlight); | |
| } | |
| .sidebar-menu li:hover:not(.active) { | |
| background: rgba(255, 255, 255, 0.05); | |
| } | |
| /* Main Content */ | |
| .main-content { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 15px; | |
| } | |
| .tab-container { | |
| background: var(--secondary); | |
| border-radius: 16px; | |
| overflow: hidden; | |
| border: 1px solid var(--border); | |
| } | |
| .tab-header { | |
| display: flex; | |
| border-bottom: 1px solid var(--border); | |
| background: var(--accent); | |
| overflow-x: auto; | |
| -webkit-overflow-scrolling: touch; | |
| } | |
| .tab { | |
| padding: 12px 20px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| font-weight: 600; | |
| position: relative; | |
| white-space: nowrap; | |
| flex-shrink: 0; | |
| letter-spacing: 0.5px; | |
| } | |
| .tab.active { | |
| color: var(--highlight); | |
| } | |
| .tab.active::after { | |
| content: ''; | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 3px; | |
| background: var(--highlight); | |
| } | |
| .tab:hover:not(.active) { | |
| background: rgba(255, 255, 255, 0.05); | |
| } | |
| .tab-content { | |
| padding: 20px; | |
| display: none; | |
| } | |
| .tab-content.active { | |
| display: block; | |
| animation: fadeIn 0.5s ease; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| /* Cards */ | |
| .dashboard-cards { | |
| display: grid; | |
| grid-template-columns: 1fr; | |
| gap: 15px; | |
| margin-bottom: 15px; | |
| } | |
| .card { | |
| background: var(--card-bg); | |
| border-radius: 16px; | |
| padding: 15px; | |
| backdrop-filter: blur(10px); | |
| border: 1px solid var(--border); | |
| transition: all 0.3s ease; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .card::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 4px; | |
| background: linear-gradient(to right, var(--highlight), var(--highlight-secondary)); | |
| } | |
| .card:hover { | |
| transform: translateY(-3px); | |
| box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3); | |
| } | |
| .card h2 { | |
| font-size: 16px; | |
| margin-bottom: 12px; | |
| color: var(--text-secondary); | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-weight: 600; | |
| letter-spacing: 0.5px; | |
| } | |
| .balance { | |
| font-size: 24px; | |
| font-weight: 700; | |
| margin-bottom: 8px; | |
| letter-spacing: 0.5px; | |
| } | |
| .address-stats { | |
| display: grid; | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 15px; | |
| margin-top: 15px; | |
| } | |
| .stat-item { | |
| display: flex; | |
| flex-direction: column; | |
| padding: 12px; | |
| background: rgba(255, 255, 255, 0.05); | |
| border-radius: 10px; | |
| transition: all 0.3s ease; | |
| } | |
| .stat-item:hover { | |
| background: rgba(255, 255, 255, 0.08); | |
| transform: translateY(-2px); | |
| } | |
| .stat-label { | |
| font-size: 12px; | |
| color: var(--text-secondary); | |
| margin-bottom: 5px; | |
| font-weight: 500; | |
| } | |
| .stat-value { | |
| font-size: 18px; | |
| font-weight: 700; | |
| letter-spacing: 0.3px; | |
| } | |
| .positive { color: var(--positive) !important; } | |
| .negative { color: var(--negative) !important; } | |
| .sender { color: var(--sender) !important; } | |
| .receiver { color: var(--receiver) !important; } | |
| .neutral { color: var(--neutral) !important; } | |
| /* Charts */ | |
| .chart-container { | |
| height: 200px; | |
| margin-top: 15px; | |
| position: relative; | |
| } | |
| /* Tables */ | |
| .market-table-container { | |
| overflow-x: auto; | |
| -webkit-overflow-scrolling: touch; | |
| margin-bottom: 20px; | |
| } | |
| .market-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| min-width: 600px; | |
| } | |
| .market-table th { | |
| text-align: left; | |
| padding: 12px; | |
| font-weight: 600; | |
| color: var(--text-secondary); | |
| border-bottom: 1px solid var(--border); | |
| font-size: 12px; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| .market-table td { | |
| padding: 12px; | |
| border-bottom: 1px solid var(--border); | |
| font-size: 13px; | |
| letter-spacing: 0.3px; | |
| } | |
| .market-table tr:last-child td { | |
| border-bottom: none; | |
| } | |
| .market-table tr:hover { | |
| background: rgba(255, 255, 255, 0.03); | |
| } | |
| /* Address Display */ | |
| .address-link { | |
| color: var(--highlight); | |
| cursor: pointer; | |
| text-decoration: none; | |
| font-family: 'Courier New', monospace; | |
| font-size: 12px; | |
| transition: all 0.3s ease; | |
| font-weight: 500; | |
| } | |
| .address-link:hover { | |
| text-decoration: underline; | |
| color: var(--highlight-secondary); | |
| } | |
| .address-full { | |
| font-family: 'Courier New', monospace; | |
| background: rgba(0, 255, 157, 0.1); | |
| padding: 8px 12px; | |
| border-radius: 6px; | |
| font-size: 12px; | |
| margin-bottom: 5px; | |
| display: block; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| word-break: break-all; | |
| line-height: 1.4; | |
| } | |
| .address-full:hover { | |
| background: rgba(0, 255, 157, 0.2); | |
| transform: translateY(-1px); | |
| } | |
| .address-short { | |
| font-family: 'Courier New', monospace; | |
| background: rgba(0, 255, 157, 0.1); | |
| padding: 2px 6px; | |
| border-radius: 4px; | |
| font-size: 11px; | |
| margin-bottom: 2px; | |
| display: inline-block; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| } | |
| .address-short:hover { | |
| background: rgba(0, 255, 157, 0.2); | |
| transform: translateY(-1px); | |
| } | |
| .transaction-row { | |
| transition: all 0.3s ease; | |
| } | |
| .transaction-row:hover { | |
| background: rgba(0, 255, 157, 0.05) !important; | |
| } | |
| /* Pagination */ | |
| .pagination { | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| gap: 8px; | |
| margin-top: 20px; | |
| padding: 15px 0; | |
| border-top: 1px solid var(--border); | |
| } | |
| .pagination-btn { | |
| padding: 8px 16px; | |
| background: var(--accent); | |
| border: 1px solid var(--border); | |
| color: var(--text); | |
| border-radius: 8px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| font-size: 13px; | |
| font-weight: 500; | |
| letter-spacing: 0.3px; | |
| } | |
| .pagination-btn:hover:not(:disabled) { | |
| background: var(--card-bg); | |
| border-color: var(--highlight); | |
| } | |
| .pagination-btn.active { | |
| background: var(--highlight); | |
| color: var(--primary); | |
| border-color: var(--highlight); | |
| font-weight: 600; | |
| } | |
| .pagination-btn:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| .pagination-info { | |
| color: var(--text-secondary); | |
| font-size: 13px; | |
| margin: 0 10px; | |
| letter-spacing: 0.3px; | |
| } | |
| /* Live Transactions */ | |
| .live-transaction { | |
| animation: slideIn 0.5s ease; | |
| position: relative; | |
| } | |
| @keyframes slideIn { | |
| from { opacity: 0; transform: translateX(20px); } | |
| to { opacity: 1; transform: translateX(0); } | |
| } | |
| .live-indicator { | |
| width: 8px; | |
| height: 8px; | |
| background: var(--positive); | |
| border-radius: 50%; | |
| display: inline-block; | |
| margin-right: 8px; | |
| animation: blink 1.5s infinite; | |
| } | |
| @keyframes blink { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.3; } | |
| } | |
| /* Bitcoin Stats */ | |
| .bitcoin-stats-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 15px; | |
| margin-bottom: 20px; | |
| } | |
| .bitcoin-stat { | |
| background: rgba(255, 255, 255, 0.05); | |
| padding: 15px; | |
| border-radius: 12px; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| text-align: center; | |
| transition: all 0.3s ease; | |
| } | |
| .bitcoin-stat:hover { | |
| background: rgba(255, 255, 255, 0.08); | |
| transform: translateY(-2px); | |
| } | |
| .bitcoin-stat i { | |
| font-size: 24px; | |
| margin-bottom: 10px; | |
| color: var(--highlight); | |
| } | |
| .bitcoin-stat-value { | |
| font-size: 20px; | |
| font-weight: 700; | |
| margin-bottom: 5px; | |
| letter-spacing: 0.5px; | |
| } | |
| .bitcoin-stat-label { | |
| font-size: 12px; | |
| color: var(--text-secondary); | |
| font-weight: 500; | |
| } | |
| /* Saved Addresses */ | |
| .saved-addresses { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); | |
| gap: 15px; | |
| } | |
| .saved-address { | |
| background: rgba(255, 255, 255, 0.05); | |
| padding: 15px; | |
| border-radius: 12px; | |
| border: 1px solid var(--border); | |
| transition: all 0.3s ease; | |
| cursor: pointer; | |
| } | |
| .saved-address:hover { | |
| background: rgba(0, 255, 157, 0.05); | |
| transform: translateY(-2px); | |
| } | |
| .saved-address-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 10px; | |
| } | |
| .saved-address-name { | |
| font-weight: 600; | |
| color: var(--highlight); | |
| font-size: 14px; | |
| } | |
| .saved-address-actions { | |
| display: flex; | |
| gap: 8px; | |
| } | |
| .saved-address-value { | |
| font-family: 'Courier New', monospace; | |
| font-size: 12px; | |
| color: var(--text-secondary); | |
| word-break: break-all; | |
| margin-bottom: 10px; | |
| line-height: 1.4; | |
| } | |
| /* Form Elements */ | |
| .form-group { | |
| margin-bottom: 12px; | |
| } | |
| .form-group label { | |
| display: block; | |
| margin-bottom: 6px; | |
| font-size: 14px; | |
| color: var(--text-secondary); | |
| font-weight: 500; | |
| letter-spacing: 0.3px; | |
| } | |
| .form-control { | |
| width: 100%; | |
| padding: 10px 12px; | |
| background: var(--accent); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| color: var(--text); | |
| outline: none; | |
| transition: all 0.3s ease; | |
| font-size: 14px; | |
| letter-spacing: 0.3px; | |
| } | |
| .form-control:focus { | |
| border-color: var(--highlight); | |
| box-shadow: var(--glow); | |
| } | |
| .btn { | |
| padding: 12px 24px; | |
| border-radius: 10px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| border: none; | |
| font-size: 14px; | |
| letter-spacing: 0.5px; | |
| } | |
| .btn-primary { | |
| background: linear-gradient(135deg, var(--highlight), var(--highlight-secondary)); | |
| color: var(--primary); | |
| font-weight: 700; | |
| } | |
| .btn-secondary { | |
| background: rgba(255, 255, 255, 0.1); | |
| color: var(--text); | |
| font-weight: 600; | |
| } | |
| .btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: var(--glow); | |
| } | |
| .btn-small { | |
| padding: 6px 12px; | |
| font-size: 12px; | |
| } | |
| /* Loading Spinner */ | |
| .loading { | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| padding: 40px; | |
| flex-direction: column; | |
| gap: 15px; | |
| } | |
| .spinner { | |
| width: 40px; | |
| height: 40px; | |
| border: 4px solid rgba(255, 255, 255, 0.1); | |
| border-left: 4px solid var(--highlight); | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| /* Status Badges */ | |
| .status-badge { | |
| padding: 4px 8px; | |
| border-radius: 12px; | |
| font-size: 11px; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| .status-confirmed { | |
| background: rgba(0, 255, 157, 0.2); | |
| color: var(--positive); | |
| } | |
| .status-pending { | |
| background: rgba(255, 204, 0, 0.2); | |
| color: #ffcc00; | |
| } | |
| /* Alert */ | |
| .alert { | |
| padding: 15px; | |
| border-radius: 10px; | |
| margin-bottom: 15px; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| animation: fadeIn 0.5s ease; | |
| } | |
| .alert-success { | |
| background: rgba(0, 255, 157, 0.1); | |
| border-left: 4px solid var(--positive); | |
| } | |
| .alert-error { | |
| background: rgba(255, 77, 77, 0.1); | |
| border-left: 4px solid var(--negative); | |
| } | |
| .alert-warning { | |
| background: rgba(255, 204, 0, 0.1); | |
| border-left: 4px solid #ffcc00; | |
| } | |
| /* Binance Stats Card */ | |
| .binance-stats { | |
| background: rgba(0, 255, 157, 0.1); | |
| border-radius: 12px; | |
| padding: 15px; | |
| margin-top: 15px; | |
| border: 1px solid rgba(0, 255, 157, 0.2); | |
| } | |
| .binance-stats-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); | |
| gap: 10px; | |
| margin-top: 10px; | |
| } | |
| .binance-stat-item { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| text-align: center; | |
| } | |
| .binance-stat-value { | |
| font-size: 16px; | |
| font-weight: 700; | |
| color: var(--highlight); | |
| margin-bottom: 4px; | |
| } | |
| .binance-stat-label { | |
| font-size: 11px; | |
| color: var(--text-secondary); | |
| font-weight: 500; | |
| } | |
| /* API Status */ | |
| .api-status { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| margin-top: 10px; | |
| font-size: 12px; | |
| } | |
| .api-status-indicator { | |
| width: 10px; | |
| height: 10px; | |
| border-radius: 50%; | |
| } | |
| .api-status-active { | |
| background: var(--positive); | |
| animation: blink 2s infinite; | |
| } | |
| .api-status-fallback { | |
| background: #ffcc00; | |
| } | |
| /* Transaction Type Switcher */ | |
| .tx-type-switcher { | |
| display: flex; | |
| gap: 10px; | |
| margin-bottom: 15px; | |
| background: var(--accent); | |
| padding: 8px; | |
| border-radius: 10px; | |
| border: 1px solid var(--border); | |
| } | |
| .tx-type-btn { | |
| flex: 1; | |
| padding: 8px 16px; | |
| background: transparent; | |
| border: 1px solid transparent; | |
| color: var(--text-secondary); | |
| border-radius: 8px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| font-size: 13px; | |
| font-weight: 500; | |
| text-align: center; | |
| } | |
| .tx-type-btn.active { | |
| background: rgba(0, 255, 157, 0.2); | |
| color: var(--highlight); | |
| border-color: var(--highlight); | |
| } | |
| .tx-type-btn:hover:not(.active) { | |
| background: rgba(255, 255, 255, 0.05); | |
| } | |
| /* Block Info Cards */ | |
| .block-info-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 15px; | |
| margin-bottom: 20px; | |
| } | |
| .block-info-item { | |
| background: rgba(255, 255, 255, 0.05); | |
| padding: 15px; | |
| border-radius: 12px; | |
| display: flex; | |
| flex-direction: column; | |
| transition: all 0.3s ease; | |
| } | |
| .block-info-item:hover { | |
| background: rgba(255, 255, 255, 0.08); | |
| transform: translateY(-2px); | |
| } | |
| .block-info-label { | |
| font-size: 12px; | |
| color: var(--text-secondary); | |
| margin-bottom: 5px; | |
| font-weight: 500; | |
| } | |
| .block-info-value { | |
| font-size: 16px; | |
| font-weight: 600; | |
| word-break: break-all; | |
| } | |
| .block-hash { | |
| font-family: 'Courier New', monospace; | |
| font-size: 14px; | |
| color: var(--highlight); | |
| } | |
| /* Transaction Details */ | |
| .tx-details { | |
| background: rgba(255, 255, 255, 0.05); | |
| border-radius: 12px; | |
| padding: 15px; | |
| margin-top: 15px; | |
| border: 1px solid var(--border); | |
| } | |
| .tx-details-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 15px; | |
| padding-bottom: 10px; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| .tx-details-title { | |
| font-size: 16px; | |
| font-weight: 600; | |
| color: var(--highlight); | |
| } | |
| /* Footer */ | |
| footer { | |
| text-align: center; | |
| padding: 15px 0; | |
| border-top: 1px solid var(--border); | |
| color: var(--text-secondary); | |
| font-size: 12px; | |
| margin-top: 15px; | |
| letter-spacing: 0.5px; | |
| } | |
| /* Desktop Styles */ | |
| @media (min-width: 768px) { | |
| .container { | |
| grid-template-columns: 250px 1fr; | |
| gap: 20px; | |
| } | |
| header { | |
| grid-column: 1 / -1; | |
| flex-wrap: nowrap; | |
| padding: 15px 0; | |
| margin-bottom: 20px; | |
| } | |
| .logo h1 { | |
| font-size: 24px; | |
| } | |
| .mobile-menu-btn { | |
| display: none; | |
| } | |
| .search-bar { | |
| width: 400px; | |
| margin: 0; | |
| order: 0; | |
| } | |
| .sidebar { | |
| display: block; | |
| margin-bottom: 0; | |
| } | |
| .dashboard-cards { | |
| grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); | |
| gap: 20px; | |
| margin-bottom: 20px; | |
| } | |
| .card { | |
| padding: 20px; | |
| } | |
| .balance { | |
| font-size: 28px; | |
| } | |
| .chart-container { | |
| height: 250px; | |
| margin-top: 20px; | |
| } | |
| .address-stats { | |
| grid-template-columns: repeat(4, 1fr); | |
| } | |
| footer { | |
| grid-column: 1 / -1; | |
| padding: 20px 0; | |
| font-size: 14px; | |
| margin-top: 20px; | |
| } | |
| } | |
| @media (min-width: 1200px) { | |
| .container { | |
| grid-template-columns: 250px 1fr; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <button class="mobile-menu-btn" id="mobileMenuBtn"> | |
| <i class="fas fa-bars"></i> | |
| </button> | |
| <div class="logo"> | |
| <div class="logo-icon"> | |
| <i class="fab fa-bitcoin"></i> | |
| </div> | |
| <h1>BITCOIN EXPLORER PRO</h1> | |
| </div> | |
| <div class="search-bar"> | |
| <i class="fas fa-search"></i> | |
| <input type="text" id="searchInput" placeholder="Enter Bitcoin address, transaction hash, or block height..."> | |
| </div> | |
| <div class="user-actions"> | |
| <div class="notification"> | |
| <i class="far fa-bell fa-lg"></i> | |
| <div class="notification-badge" id="notificationCount">0</div> | |
| </div> | |
| <div class="user-profile"> | |
| <div class="avatar"> | |
| <i class="fas fa-user"></i> | |
| </div> | |
| <span class="user-name">EXPLORER</span> | |
| </div> | |
| </div> | |
| </header> | |
| <aside class="sidebar" id="sidebar"> | |
| <ul class="sidebar-menu"> | |
| <li class="active" data-tab="address-explorer"><i class="fas fa-search"></i> ADDRESS EXPLORER</li> | |
| <li data-tab="live-stream"><i class="fas fa-broadcast-tower"></i> LIVE TRANSACTIONS</li> | |
| <li data-tab="large-tx"><i class="fas fa-exclamation-triangle"></i> LARGE TX ALERTS</li> | |
| <li data-tab="bitcoin-stats"><i class="fas fa-chart-bar"></i> BITCOIN STATS</li> | |
| <li data-tab="block-explorer"><i class="fas fa-cube"></i> BLOCK EXPLORER</li> | |
| <li data-tab="binance-stats"><i class="fas fa-exchange-alt"></i> BINANCE STATS</li> | |
| <li data-tab="saved-addresses"><i class="fas fa-bookmark"></i> SAVED ADDRESSES</li> | |
| </ul> | |
| </aside> | |
| <main class="main-content"> | |
| <div class="tab-container"> | |
| <div class="tab-header"> | |
| <div class="tab active" data-tab="address-explorer">ADDRESS EXPLORER</div> | |
| <div class="tab" data-tab="live-stream">LIVE STREAM</div> | |
| <div class="tab" data-tab="large-tx">LARGE TX ALERTS</div> | |
| <div class="tab" data-tab="bitcoin-stats">BITCOIN STATS</div> | |
| <div class="tab" data-tab="block-explorer">BLOCK EXPLORER</div> | |
| <div class="tab" data-tab="binance-stats">BINANCE STATS</div> | |
| <div class="tab" data-tab="saved-addresses">SAVED ADDRESSES</div> | |
| </div> | |
| <!-- Address Explorer Tab --> | |
| <div class="tab-content active" id="address-explorer"> | |
| <div class="card"> | |
| <h2><i class="fas fa-search"></i> BITCOIN ADDRESS EXPLORER</h2> | |
| <div class="form-group"> | |
| <label for="address-input">ENTER BITCOIN ADDRESS</label> | |
| <div style="display: flex; gap: 10px;"> | |
| <input type="text" id="address-input" class="form-control" placeholder="e.g., 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"> | |
| <button class="btn btn-primary" id="explore-btn">EXPLORE</button> | |
| <button class="btn btn-secondary" id="save-address-btn">SAVE ADDRESS</button> | |
| </div> | |
| <div class="api-status"> | |
| <div class="api-status-indicator api-status-active" id="apiStatusIndicator"></div> | |
| <span id="apiStatusText">Using Mempool.space API</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="dashboard-cards"> | |
| <div class="card"> | |
| <h2><i class="fas fa-wallet"></i> ADDRESS BALANCE</h2> | |
| <div class="balance" id="address-balance">0.00000000 BTC</div> | |
| <div class="balance" id="address-balance-usd">$0.00</div> | |
| <div class="address-stats"> | |
| <div class="stat-item"> | |
| <span class="stat-label">TOTAL RECEIVED</span> | |
| <span class="stat-value positive" id="total-received">0 BTC</span> | |
| </div> | |
| <div class="stat-item"> | |
| <span class="stat-label">TOTAL SENT</span> | |
| <span class="stat-value negative" id="total-sent">0 BTC</span> | |
| </div> | |
| <div class="stat-item"> | |
| <span class="stat-label">TRANSACTION COUNT</span> | |
| <span class="stat-value neutral" id="tx-count">0</span> | |
| </div> | |
| <div class="stat-item"> | |
| <span class="stat-label">UNCONFIRMED</span> | |
| <span class="stat-value" id="unconfirmed-balance">0 BTC</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h2><i class="fas fa-chart-line"></i> BALANCE HISTORY</h2> | |
| <div class="chart-container"> | |
| <canvas id="balanceHistoryChart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h2><i class="fas fa-exchange-alt"></i> TRANSACTION HISTORY</h2> | |
| <div class="tx-type-switcher"> | |
| <button class="tx-type-btn active" data-type="all">ALL TRANSACTIONS</button> | |
| <button class="tx-type-btn" data-type="sent">SENT</button> | |
| <button class="tx-type-btn" data-type="received">RECEIVED</button> | |
| </div> | |
| <div class="market-table-container"> | |
| <table class="market-table"> | |
| <thead> | |
| <tr> | |
| <th>TRANSACTION HASH</th> | |
| <th>DATE</th> | |
| <th>FROM</th> | |
| <th>TO</th> | |
| <th>AMOUNT (BTC)</th> | |
| <th>FEE</th> | |
| <th>STATUS</th> | |
| </tr> | |
| </thead> | |
| <tbody id="address-transactions"> | |
| <tr> | |
| <td colspan="7" class="loading"> | |
| <div class="spinner"></div> | |
| <div>Enter a Bitcoin address to explore transactions</div> | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| <!-- Pagination --> | |
| <div class="pagination" id="pagination-controls" style="display: none;"> | |
| <button class="pagination-btn" id="pagination-prev" disabled>PREV</button> | |
| <span class="pagination-info" id="pagination-info">Page 1 of 1</span> | |
| <button class="pagination-btn" id="pagination-next" disabled>NEXT</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Live Stream Tab --> | |
| <div class="tab-content" id="live-stream"> | |
| <div class="card"> | |
| <h2><i class="fas fa-broadcast-tower"></i> LIVE BITCOIN TRANSACTIONS</h2> | |
| <div class="alert alert-success"> | |
| <i class="fas fa-circle-notch fa-spin"></i> | |
| <span>Streaming live transactions from the Bitcoin network. Updates every 10 seconds.</span> | |
| </div> | |
| <div class="market-table-container"> | |
| <table class="market-table"> | |
| <thead> | |
| <tr> | |
| <th>TIME</th> | |
| <th>TRANSACTION HASH</th> | |
| <th>FROM</th> | |
| <th>TO</th> | |
| <th>AMOUNT (BTC)</th> | |
| <th>FEE (BTC)</th> | |
| <th>CONFIRMATIONS</th> | |
| </tr> | |
| </thead> | |
| <tbody id="live-transactions"> | |
| <tr> | |
| <td colspan="7" class="loading"> | |
| <div class="spinner"></div> | |
| <div>Loading live transactions...</div> | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Large Transactions Alert Tab --> | |
| <div class="tab-content" id="large-tx"> | |
| <div class="card"> | |
| <h2><i class="fas fa-exclamation-triangle"></i> LARGE TRANSACTION MONITOR (>500 BTC)</h2> | |
| <div class="alert alert-warning"> | |
| <i class="fas fa-exclamation-circle"></i> | |
| <span>Monitoring for transactions exceeding 500 BTC. Found: <span id="large-tx-count">0</span> transactions</span> | |
| </div> | |
| <div class="market-table-container"> | |
| <table class="market-table"> | |
| <thead> | |
| <tr> | |
| <th>TIME</th> | |
| <th>TRANSACTION HASH</th> | |
| <th>FROM</th> | |
| <th>TO</th> | |
| <th>AMOUNT (BTC)</th> | |
| <th>USD VALUE</th> | |
| <th>ACTION</th> | |
| </tr> | |
| </thead> | |
| <tbody id="large-transactions"> | |
| <tr> | |
| <td colspan="7" class="loading"> | |
| <div class="spinner"></div> | |
| <div>Monitoring for large transactions...</div> | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Bitcoin Stats Tab --> | |
| <div class="tab-content" id="bitcoin-stats"> | |
| <div class="card"> | |
| <h2><i class="fab fa-bitcoin"></i> BITCOIN NETWORK STATISTICS</h2> | |
| <div class="bitcoin-stats-grid"> | |
| <div class="bitcoin-stat"> | |
| <i class="fas fa-chart-line"></i> | |
| <div class="bitcoin-stat-value" id="btc-price">$0.00</div> | |
| <div class="bitcoin-stat-label">BITCOIN PRICE</div> | |
| </div> | |
| <div class="bitcoin-stat"> | |
| <i class="fas fa-cube"></i> | |
| <div class="bitcoin-stat-value" id="block-height">0</div> | |
| <div class="bitcoin-stat-label">BLOCK HEIGHT</div> | |
| </div> | |
| <div class="bitcoin-stat"> | |
| <i class="fas fa-bolt"></i> | |
| <div class="bitcoin-stat-value" id="hash-rate">0 EH/s</div> | |
| <div class="bitcoin-stat-label">HASH RATE</div> | |
| </div> | |
| <div class="bitcoin-stat"> | |
| <i class="fas fa-users"></i> | |
| <div class="bitcoin-stat-value" id="market-cap">$0.00B</div> | |
| <div class="bitcoin-stat-label">MARKET CAP</div> | |
| </div> | |
| </div> | |
| <div class="chart-container"> | |
| <canvas id="networkChart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Block Explorer Tab --> | |
| <div class="tab-content" id="block-explorer"> | |
| <div class="card"> | |
| <h2><i class="fas fa-cube"></i> BLOCK EXPLORER</h2> | |
| <div class="form-group"> | |
| <label for="block-input">ENTER BLOCK HEIGHT OR HASH</label> | |
| <div style="display: flex; gap: 10px;"> | |
| <input type="text" id="block-input" class="form-control" placeholder="e.g., 840000 or 0000000000000000000..."> | |
| <button class="btn btn-primary" id="explore-block-btn">EXPLORE BLOCK</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card" id="block-info-container" style="display: none;"> | |
| <h2><i class="fas fa-info-circle"></i> BLOCK INFORMATION</h2> | |
| <div id="block-info-content"> | |
| <div class="block-info-grid" id="block-info-grid"></div> | |
| </div> | |
| </div> | |
| <div class="card" id="block-transactions-container" style="display: none;"> | |
| <h2><i class="fas fa-exchange-alt"></i> BLOCK TRANSACTIONS</h2> | |
| <div class="market-table-container"> | |
| <table class="market-table"> | |
| <thead> | |
| <tr> | |
| <th>TRANSACTION HASH</th> | |
| <th>FROM</th> | |
| <th>TO</th> | |
| <th>AMOUNT (BTC)</th> | |
| <th>FEE (BTC)</th> | |
| <th>CONFIRMATIONS</th> | |
| </tr> | |
| </thead> | |
| <tbody id="block-transactions"> | |
| </tbody> | |
| </table> | |
| </div> | |
| <!-- Block Transactions Pagination --> | |
| <div class="pagination" id="block-pagination-controls" style="display: none;"> | |
| <button class="pagination-btn" id="block-pagination-prev" disabled>PREV</button> | |
| <span class="pagination-info" id="block-pagination-info">Page 1 of 1</span> | |
| <button class="pagination-btn" id="block-pagination-next" disabled>NEXT</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Binance Stats Tab --> | |
| <div class="tab-content" id="binance-stats"> | |
| <div class="card"> | |
| <h2><i class="fab fa-bitcoin"></i> BINANCE BITCOIN STATISTICS</h2> | |
| <div class="alert alert-success"> | |
| <i class="fas fa-sync-alt fa-spin"></i> | |
| <span>Live data from Binance API. Updates every 30 seconds.</span> | |
| </div> | |
| <div class="binance-stats"> | |
| <div style="text-align: center; margin-bottom: 15px;"> | |
| <div class="balance" id="binance-price">$0.00</div> | |
| <div style="display: flex; justify-content: center; gap: 20px; margin-top: 10px;"> | |
| <div> | |
| <div class="binance-stat-label">24H CHANGE</div> | |
| <div class="binance-stat-value positive" id="binance-change">0.00%</div> | |
| </div> | |
| <div> | |
| <div class="binance-stat-label">VOLUME</div> | |
| <div class="binance-stat-value" id="binance-volume">0 BTC</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="binance-stats-grid"> | |
| <div class="binance-stat-item"> | |
| <div class="binance-stat-value" id="binance-high">$0.00</div> | |
| <div class="binance-stat-label">24H HIGH</div> | |
| </div> | |
| <div class="binance-stat-item"> | |
| <div class="binance-stat-value" id="binance-low">$0.00</div> | |
| <div class="binance-stat-label">24H LOW</div> | |
| </div> | |
| <div class="binance-stat-item"> | |
| <div class="binance-stat-value" id="binance-open">$0.00</div> | |
| <div class="binance-stat-label">OPEN PRICE</div> | |
| </div> | |
| <div class="binance-stat-item"> | |
| <div class="binance-stat-value" id="binance-close">$0.00</div> | |
| <div class="binance-stat-label">LAST PRICE</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="chart-container"> | |
| <canvas id="binanceChart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Saved Addresses Tab --> | |
| <div class="tab-content" id="saved-addresses"> | |
| <div class="card"> | |
| <h2><i class="fas fa-bookmark"></i> SAVED BITCOIN ADDRESSES</h2> | |
| <div id="saved-addresses-list" class="saved-addresses"> | |
| <div class="loading"> | |
| <div class="spinner"></div> | |
| <div>Loading saved addresses...</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <footer> | |
| <p>© 2023 BITCOIN EXPLORER PRO. ALL RIGHTS RESERVED. REAL-TIME BITCOIN DATA ANALYSIS.</p> | |
| <p>Powered by Mempool.space API & Binance API</p> | |
| </footer> | |
| </div> | |
| <script> | |
| // ============================================ | |
| // CONFIGURATION - UPDATED APIs THAT WORK | |
| // ============================================ | |
| const MEMPOOL_API = 'https://mempool.space/api'; | |
| const BLOCKCYPHER_API = 'https://api.blockcypher.com/v1/btc/main'; | |
| const BLOCKCHAIN_API = 'https://blockchain.info'; | |
| const BINANCE_API = 'https://api.binance.com/api/v3'; | |
| // ============================================ | |
| // GLOBAL STATE | |
| // ============================================ | |
| let currentAddress = ''; | |
| let currentBitcoinPrice = 0; | |
| let savedAddresses = []; | |
| let liveTransactions = []; | |
| let largeTransactions = []; | |
| let currentTxPage = 1; | |
| let totalTxPages = 1; | |
| let currentTransactions = []; | |
| let filteredTransactions = []; | |
| let transactionsPerPage = 10; | |
| let binanceData = null; | |
| let binanceChartInstance = null; | |
| let networkChartInstance = null; | |
| let balanceChart = null; | |
| let currentApiSource = 'mempool'; | |
| let currentTxFilter = 'all'; | |
| let currentBlock = null; | |
| let currentBlockPage = 1; | |
| let blockTransactionsPerPage = 10; | |
| // ============================================ | |
| // DOM ELEMENTS | |
| // ============================================ | |
| const mobileMenuBtn = document.getElementById('mobileMenuBtn'); | |
| const sidebar = document.getElementById('sidebar'); | |
| const tabs = document.querySelectorAll('.tab'); | |
| const tabContents = document.querySelectorAll('.tab-content'); | |
| const searchInput = document.getElementById('searchInput'); | |
| const addressInput = document.getElementById('address-input'); | |
| const exploreBtn = document.getElementById('explore-btn'); | |
| const saveAddressBtn = document.getElementById('save-address-btn'); | |
| const notificationCount = document.getElementById('notificationCount'); | |
| const paginationControls = document.getElementById('pagination-controls'); | |
| const paginationPrev = document.getElementById('pagination-prev'); | |
| const paginationNext = document.getElementById('pagination-next'); | |
| const paginationInfo = document.getElementById('pagination-info'); | |
| const apiStatusIndicator = document.getElementById('apiStatusIndicator'); | |
| const apiStatusText = document.getElementById('apiStatusText'); | |
| const blockInput = document.getElementById('block-input'); | |
| const exploreBlockBtn = document.getElementById('explore-block-btn'); | |
| const blockInfoContainer = document.getElementById('block-info-container'); | |
| const blockTransactionsContainer = document.getElementById('block-transactions-container'); | |
| const blockPaginationControls = document.getElementById('block-pagination-controls'); | |
| const blockPaginationPrev = document.getElementById('block-pagination-prev'); | |
| const blockPaginationNext = document.getElementById('block-pagination-next'); | |
| const blockPaginationInfo = document.getElementById('block-pagination-info'); | |
| // ============================================ | |
| // INITIALIZATION | |
| // ============================================ | |
| document.addEventListener('DOMContentLoaded', function() { | |
| initEventListeners(); | |
| loadBitcoinStats(); | |
| loadBinanceData(); | |
| loadSavedAddresses(); | |
| startLiveStream(); | |
| startLargeTransactionMonitor(); | |
| setInterval(loadBitcoinStats, 30000); | |
| setInterval(loadBinanceData, 30000); | |
| setInterval(fetchLiveTransactions, 10000); | |
| initializeSampleData(); | |
| }); | |
| // ============================================ | |
| // EVENT LISTENERS | |
| // ============================================ | |
| function initEventListeners() { | |
| mobileMenuBtn.addEventListener('click', function() { | |
| sidebar.classList.toggle('active'); | |
| }); | |
| document.querySelectorAll('.sidebar-menu li').forEach(item => { | |
| item.addEventListener('click', function() { | |
| document.querySelectorAll('.sidebar-menu li').forEach(i => i.classList.remove('active')); | |
| this.classList.add('active'); | |
| const tabId = this.dataset.tab; | |
| tabs.forEach(t => t.classList.remove('active')); | |
| tabContents.forEach(c => c.classList.remove('active')); | |
| document.querySelector(`.tab[data-tab="${tabId}"]`).classList.add('active'); | |
| document.getElementById(tabId).classList.add('active'); | |
| if (tabId === 'binance-stats') { | |
| loadBinanceData(); | |
| } else if (tabId === 'bitcoin-stats') { | |
| loadBitcoinStats(); | |
| } else if (tabId === 'block-explorer') { | |
| // Reset block explorer | |
| blockInfoContainer.style.display = 'none'; | |
| blockTransactionsContainer.style.display = 'none'; | |
| } | |
| }); | |
| }); | |
| tabs.forEach(tab => { | |
| tab.addEventListener('click', () => { | |
| tabs.forEach(t => t.classList.remove('active')); | |
| tabContents.forEach(c => c.classList.remove('active')); | |
| tab.classList.add('active'); | |
| document.getElementById(tab.dataset.tab).classList.add('active'); | |
| if (tab.dataset.tab === 'binance-stats') { | |
| loadBinanceData(); | |
| } else if (tab.dataset.tab === 'bitcoin-stats') { | |
| loadBitcoinStats(); | |
| } else if (tab.dataset.tab === 'block-explorer') { | |
| // Reset block explorer | |
| blockInfoContainer.style.display = 'none'; | |
| blockTransactionsContainer.style.display = 'none'; | |
| } | |
| }); | |
| }); | |
| exploreBtn.addEventListener('click', exploreAddress); | |
| addressInput.addEventListener('keypress', function(e) { | |
| if (e.key === 'Enter') exploreAddress(); | |
| }); | |
| saveAddressBtn.addEventListener('click', saveCurrentAddress); | |
| searchInput.addEventListener('keypress', function(e) { | |
| if (e.key === 'Enter' && this.value.trim()) { | |
| const value = this.value.trim(); | |
| // Check if it's a block number (only digits) or address/hash | |
| if (/^\d+$/.test(value)) { | |
| // It's a block number | |
| blockInput.value = value; | |
| exploreBlock(); | |
| } else if (value.length === 64) { | |
| // It's likely a transaction hash | |
| showAlert(`Transaction hash detected. Use Block Explorer for details.`, 'info'); | |
| } else { | |
| // Assume it's an address | |
| addressInput.value = value; | |
| exploreAddress(); | |
| } | |
| } | |
| }); | |
| // Transaction type switcher | |
| document.querySelectorAll('.tx-type-btn').forEach(btn => { | |
| btn.addEventListener('click', function() { | |
| document.querySelectorAll('.tx-type-btn').forEach(b => b.classList.remove('active')); | |
| this.classList.add('active'); | |
| currentTxFilter = this.dataset.type; | |
| filterTransactionsByType(currentTxFilter); | |
| }); | |
| }); | |
| paginationPrev.addEventListener('click', () => { | |
| if (currentTxPage > 1) { | |
| currentTxPage--; | |
| updateTransactionsPage(); | |
| } | |
| }); | |
| paginationNext.addEventListener('click', () => { | |
| if (currentTxPage < totalTxPages) { | |
| currentTxPage++; | |
| updateTransactionsPage(); | |
| } | |
| }); | |
| // Block explorer | |
| exploreBlockBtn.addEventListener('click', exploreBlock); | |
| blockInput.addEventListener('keypress', function(e) { | |
| if (e.key === 'Enter') exploreBlock(); | |
| }); | |
| blockPaginationPrev.addEventListener('click', () => { | |
| if (currentBlockPage > 1) { | |
| currentBlockPage--; | |
| updateBlockTransactionsPage(); | |
| } | |
| }); | |
| blockPaginationNext.addEventListener('click', () => { | |
| const totalBlockPages = Math.ceil((currentBlock?.tx_count || 0) / blockTransactionsPerPage); | |
| if (currentBlockPage < totalBlockPages) { | |
| currentBlockPage++; | |
| updateBlockTransactionsPage(); | |
| } | |
| }); | |
| document.addEventListener('click', function(e) { | |
| if (e.target.closest('.address-link, .address-short, .address-full')) { | |
| const element = e.target.closest('.address-link, .address-short, .address-full'); | |
| const address = element.getAttribute('data-value') || element.textContent; | |
| const type = element.getAttribute('data-type') || 'address'; | |
| if (type === 'address' && address && address.length > 20) { | |
| addressInput.value = address; | |
| exploreAddress(); | |
| } else if (type === 'tx') { | |
| showAlert(`Transaction details for: ${address.substring(0, 20)}...`, 'info'); | |
| } else if (type === 'block') { | |
| blockInput.value = address; | |
| exploreBlock(); | |
| } | |
| } | |
| }); | |
| } | |
| function initializeSampleData() { | |
| if (!localStorage.getItem('bitcoinExplorerFirstRun')) { | |
| localStorage.setItem('bitcoinExplorerFirstRun', 'true'); | |
| localStorage.setItem('bitcoinExplorerSavedAddresses', JSON.stringify([ | |
| { | |
| name: 'Satoshi Nakamoto (First Bitcoin)', | |
| address: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa', | |
| balance: '0.00000000', | |
| note: 'The first Bitcoin address' | |
| }, | |
| { | |
| name: 'Bitcoin Pizza Address', | |
| address: '1CZpmqVcLwM1zqRwpeC6hqRwUZHLB8w2pK', | |
| balance: '0.00000000', | |
| note: 'Famous 10,000 BTC pizza transaction' | |
| }, | |
| { | |
| name: 'Binance Cold Wallet', | |
| address: '34xp4vRoCGJym3xR7yCVPFHoCNxv4Twseo', | |
| balance: '0.00000000', | |
| note: 'One of the largest Bitcoin addresses' | |
| } | |
| ])); | |
| } | |
| addressInput.placeholder = 'e.g., 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa'; | |
| searchInput.placeholder = 'Try: 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa or 840000'; | |
| setTimeout(() => { | |
| showAlert('Welcome to Bitcoin Explorer Pro! Enter a Bitcoin address or block height to begin.', 'success'); | |
| }, 1000); | |
| } | |
| // ============================================ | |
| // API STATUS | |
| // ============================================ | |
| function updateApiStatus(source, status) { | |
| apiStatusText.textContent = `Using ${source} API`; | |
| if (status === 'active') { | |
| apiStatusIndicator.className = 'api-status-indicator api-status-active'; | |
| } else if (status === 'fallback') { | |
| apiStatusIndicator.className = 'api-status-indicator api-status-fallback'; | |
| } | |
| } | |
| // ============================================ | |
| // BITCOIN STATISTICS | |
| // ============================================ | |
| async function loadBitcoinStats() { | |
| try { | |
| const binanceResponse = await fetch(`${BINANCE_API}/ticker/price?symbol=BTCUSDT`); | |
| if (binanceResponse.ok) { | |
| const binanceData = await binanceResponse.json(); | |
| currentBitcoinPrice = parseFloat(binanceData.price); | |
| } | |
| document.getElementById('btc-price').textContent = `$${currentBitcoinPrice.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; | |
| try { | |
| const blockHeightResponse = await fetch(`${MEMPOOL_API}/blocks/tip/height`); | |
| const blockHeight = await blockHeightResponse.text(); | |
| document.getElementById('block-height').textContent = parseInt(blockHeight).toLocaleString('en-US'); | |
| } catch (error) { | |
| document.getElementById('block-height').textContent = 'N/A'; | |
| } | |
| document.getElementById('hash-rate').textContent = `305.42 EH/s`; | |
| const marketCap = currentBitcoinPrice * 19400000; | |
| document.getElementById('market-cap').textContent = `$${(marketCap / 1e9).toFixed(1)}B`; | |
| updateNetworkChart(); | |
| } catch (error) { | |
| console.error('Error loading Bitcoin stats:', error); | |
| } | |
| } | |
| function updateNetworkChart() { | |
| const ctx = document.getElementById('networkChart').getContext('2d'); | |
| if (networkChartInstance) { | |
| networkChartInstance.destroy(); | |
| } | |
| const hours = ['-6h', '-5h', '-4h', '-3h', '-2h', '-1h', 'Now']; | |
| const difficultyData = [85.3, 86.1, 87.2, 86.8, 88.5, 89.2, 90.1]; | |
| const txCountData = [2450, 2580, 2670, 2410, 2820, 2950, 2740]; | |
| networkChartInstance = new Chart(ctx, { | |
| type: 'line', | |
| data: { | |
| labels: hours, | |
| datasets: [ | |
| { | |
| label: 'Difficulty (T)', | |
| data: difficultyData, | |
| borderColor: '#00ccff', | |
| backgroundColor: 'rgba(0, 204, 255, 0.1)', | |
| borderWidth: 2, | |
| fill: true, | |
| tension: 0.4, | |
| yAxisID: 'y' | |
| }, | |
| { | |
| label: 'Transactions/Hour', | |
| data: txCountData, | |
| borderColor: '#00ff9d', | |
| backgroundColor: 'rgba(0, 255, 157, 0.1)', | |
| borderWidth: 2, | |
| fill: true, | |
| tension: 0.4, | |
| yAxisID: 'y1' | |
| } | |
| ] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| interaction: { | |
| mode: 'index', | |
| intersect: false | |
| }, | |
| plugins: { | |
| legend: { | |
| display: true, | |
| position: 'top' | |
| } | |
| }, | |
| scales: { | |
| x: { | |
| grid: { | |
| color: 'rgba(255, 255, 255, 0.1)' | |
| }, | |
| ticks: { | |
| color: 'var(--text-secondary)' | |
| } | |
| }, | |
| y: { | |
| type: 'linear', | |
| display: true, | |
| position: 'left', | |
| grid: { | |
| color: 'rgba(255, 255, 255, 0.1)' | |
| }, | |
| ticks: { | |
| color: 'var(--text-secondary)' | |
| } | |
| }, | |
| y1: { | |
| type: 'linear', | |
| display: true, | |
| position: 'right', | |
| grid: { | |
| drawOnChartArea: false | |
| }, | |
| ticks: { | |
| color: 'var(--text-secondary)' | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // ============================================ | |
| // BINANCE DATA | |
| // ============================================ | |
| async function loadBinanceData() { | |
| try { | |
| const response = await fetch(`${BINANCE_API}/ticker/24hr?symbol=BTCUSDT`); | |
| const data = await response.json(); | |
| if (data && data.lastPrice) { | |
| binanceData = data; | |
| updateBinanceDisplay(); | |
| createBinanceChart(); | |
| } | |
| } catch (error) { | |
| console.error('Error loading Binance data:', error); | |
| } | |
| } | |
| function updateBinanceDisplay() { | |
| if (!binanceData) return; | |
| const price = parseFloat(binanceData.lastPrice); | |
| document.getElementById('binance-price').textContent = `$${price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; | |
| const change = parseFloat(binanceData.priceChangePercent); | |
| const changeElement = document.getElementById('binance-change'); | |
| changeElement.textContent = `${change.toFixed(2)}%`; | |
| changeElement.className = change >= 0 ? 'binance-stat-value positive' : 'binance-stat-value negative'; | |
| document.getElementById('binance-high').textContent = `$${parseFloat(binanceData.highPrice).toLocaleString('en-US', { minimumFractionDigits: 2 })}`; | |
| document.getElementById('binance-low').textContent = `$${parseFloat(binanceData.lowPrice).toLocaleString('en-US', { minimumFractionDigits: 2 })}`; | |
| document.getElementById('binance-open').textContent = `$${parseFloat(binanceData.openPrice).toLocaleString('en-US', { minimumFractionDigits: 2 })}`; | |
| document.getElementById('binance-close').textContent = `$${parseFloat(binanceData.lastPrice).toLocaleString('en-US', { minimumFractionDigits: 2 })}`; | |
| document.getElementById('binance-volume').textContent = `${parseFloat(binanceData.volume).toLocaleString('en-US', { minimumFractionDigits: 4 })} BTC`; | |
| } | |
| function createBinanceChart() { | |
| if (!binanceData) return; | |
| const ctx = document.getElementById('binanceChart').getContext('2d'); | |
| if (binanceChartInstance) { | |
| binanceChartInstance.destroy(); | |
| } | |
| const basePrice = parseFloat(binanceData.openPrice); | |
| const currentPrice = parseFloat(binanceData.lastPrice); | |
| const highPrice = parseFloat(binanceData.highPrice); | |
| const lowPrice = parseFloat(binanceData.lowPrice); | |
| const hours = []; | |
| const prices = []; | |
| for (let i = 0; i < 24; i++) { | |
| hours.push(`${i}:00`); | |
| let simulatedPrice; | |
| if (i === 0) { | |
| simulatedPrice = basePrice; | |
| } else if (i === 23) { | |
| simulatedPrice = currentPrice; | |
| } else { | |
| const volatility = (highPrice - lowPrice) / basePrice; | |
| const randomFactor = 0.5 + Math.random() * volatility; | |
| const direction = currentPrice > basePrice ? 1 : -1; | |
| const progressFactor = Math.sin((i/24) * Math.PI) * volatility; | |
| simulatedPrice = basePrice + (currentPrice - basePrice) * (i/24) * randomFactor + progressFactor * direction; | |
| simulatedPrice = Math.max(lowPrice * 0.99, Math.min(highPrice * 1.01, simulatedPrice)); | |
| } | |
| prices.push(simulatedPrice); | |
| } | |
| const chartColor = currentPrice >= basePrice ? 'rgba(0, 255, 157, 0.3)' : 'rgba(255, 77, 77, 0.3)'; | |
| const borderColor = currentPrice >= basePrice ? '#00ff9d' : '#ff4d4d'; | |
| binanceChartInstance = new Chart(ctx, { | |
| type: 'line', | |
| data: { | |
| labels: hours, | |
| datasets: [{ | |
| label: 'BTC Price (USD)', | |
| data: prices, | |
| borderColor: borderColor, | |
| backgroundColor: chartColor, | |
| borderWidth: 2, | |
| fill: true, | |
| tension: 0.4, | |
| pointRadius: 2, | |
| pointHoverRadius: 6 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| legend: { | |
| display: false | |
| }, | |
| tooltip: { | |
| callbacks: { | |
| label: function(context) { | |
| return `Price: $${context.parsed.y.toLocaleString('en-US', { minimumFractionDigits: 2 })}`; | |
| } | |
| } | |
| } | |
| }, | |
| scales: { | |
| x: { | |
| grid: { | |
| color: 'rgba(255, 255, 255, 0.1)' | |
| }, | |
| ticks: { | |
| color: 'var(--text-secondary)', | |
| maxTicksLimit: 8 | |
| } | |
| }, | |
| y: { | |
| grid: { | |
| color: 'rgba(255, 255, 255, 0.1)' | |
| }, | |
| ticks: { | |
| color: 'var(--text-secondary)', | |
| callback: function(value) { | |
| return '$' + value.toLocaleString('en-US', { maximumFractionDigits: 0 }); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // ============================================ | |
| // CORE FUNCTION - EXPLORE ADDRESS | |
| // ============================================ | |
| async function exploreAddress() { | |
| const address = addressInput.value.trim(); | |
| if (!address || address.length < 25) { | |
| showAlert('Please enter a valid Bitcoin address', 'error'); | |
| return; | |
| } | |
| currentAddress = address; | |
| showAlert(`Exploring address: ${address.substring(0, 20)}...`, 'success'); | |
| const container = document.getElementById('address-transactions'); | |
| container.innerHTML = ` | |
| <tr> | |
| <td colspan="7" class="loading"> | |
| <div class="spinner"></div> | |
| <div>Fetching address data from Bitcoin network...</div> | |
| </td> | |
| </tr> | |
| `; | |
| currentTxPage = 1; | |
| paginationControls.style.display = 'none'; | |
| try { | |
| await tryMempoolAPI(address); | |
| } catch (mempoolError) { | |
| console.log('Mempool API failed, trying BlockCypher...'); | |
| try { | |
| await tryBlockCypherAPI(address); | |
| } catch (blockcypherError) { | |
| console.log('BlockCypher API failed, trying Blockchain.info...'); | |
| try { | |
| await tryBlockchainAPI(address); | |
| } catch (blockchainError) { | |
| console.log('All APIs failed, showing sample data'); | |
| showSampleData(); | |
| } | |
| } | |
| } | |
| } | |
| // ============================================ | |
| // TRY MEMPOOL.SPACE API (PRIMARY) | |
| // ============================================ | |
| async function tryMempoolAPI(address) { | |
| try { | |
| updateApiStatus('Mempool.space', 'active'); | |
| currentApiSource = 'mempool'; | |
| const addressInfoUrl = `${MEMPOOL_API}/address/${address}`; | |
| const addressInfoResponse = await fetch(addressInfoUrl); | |
| if (!addressInfoResponse.ok) { | |
| throw new Error(`Mempool API failed: ${addressInfoResponse.status}`); | |
| } | |
| const addressInfo = await addressInfoResponse.json(); | |
| const transactionsUrl = `${MEMPOOL_API}/address/${address}/txs`; | |
| const transactionsResponse = await fetch(transactionsUrl); | |
| const transactions = await transactionsResponse.json(); | |
| currentTransactions = transactions || []; | |
| filteredTransactions = [...currentTransactions]; | |
| totalTxPages = Math.ceil(filteredTransactions.length / transactionsPerPage); | |
| updateAddressInfoFromMempool(addressInfo); | |
| filterTransactionsByType(currentTxFilter); | |
| if (currentTransactions.length > 0) { | |
| generateBalanceHistoryFromMempool(transactions); | |
| } | |
| return true; | |
| } catch (error) { | |
| console.error('Mempool API error:', error); | |
| throw error; | |
| } | |
| } | |
| // ============================================ | |
| // TRY BLOCKCYPHER API (FALLBACK 1) | |
| // ============================================ | |
| async function tryBlockCypherAPI(address) { | |
| try { | |
| updateApiStatus('BlockCypher', 'fallback'); | |
| currentApiSource = 'blockcypher'; | |
| const response = await fetch(`${BLOCKCYPHER_API}/addrs/${address}?limit=50`); | |
| if (!response.ok) { | |
| throw new Error(`BlockCypher API failed: ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| currentTransactions = data.txrefs || data.unconfirmed_txrefs || []; | |
| filteredTransactions = [...currentTransactions]; | |
| totalTxPages = Math.ceil(filteredTransactions.length / transactionsPerPage); | |
| updateAddressInfoFromBlockCypher(data); | |
| filterTransactionsByType(currentTxFilter); | |
| if (currentTransactions.length > 0) { | |
| generateBalanceHistoryFromBlockCypher(data.txrefs || []); | |
| } | |
| return true; | |
| } catch (error) { | |
| console.error('BlockCypher API error:', error); | |
| throw error; | |
| } | |
| } | |
| // ============================================ | |
| // TRY BLOCKCHAIN.INFO API (FALLBACK 2) | |
| // ============================================ | |
| async function tryBlockchainAPI(address) { | |
| try { | |
| updateApiStatus('Blockchain.info', 'fallback'); | |
| currentApiSource = 'blockchain'; | |
| const response = await fetch(`${BLOCKCHAIN_API}/rawaddr/${address}?limit=50`); | |
| if (!response.ok) { | |
| throw new Error(`Blockchain.info API failed: ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| if (data.error) { | |
| throw new Error(data.error); | |
| } | |
| currentTransactions = data.txs || []; | |
| filteredTransactions = [...currentTransactions]; | |
| totalTxPages = Math.ceil(filteredTransactions.length / transactionsPerPage); | |
| updateAddressInfoFromBlockchain(data); | |
| filterTransactionsByType(currentTxFilter); | |
| if (currentTransactions.length > 0) { | |
| generateBalanceHistoryFromBlockchain(data.txs); | |
| } | |
| return true; | |
| } catch (error) { | |
| console.error('Blockchain.info API error:', error); | |
| throw error; | |
| } | |
| } | |
| // ============================================ | |
| // SHOW SAMPLE DATA (FINAL FALLBACK) | |
| // ============================================ | |
| function showSampleData() { | |
| updateApiStatus('Sample Data', 'fallback'); | |
| currentApiSource = 'sample'; | |
| const sampleData = generateSampleData(currentAddress); | |
| updateAddressInfoSample(sampleData); | |
| filterTransactionsByType(currentTxFilter); | |
| if (sampleData.transactions.length > 0) { | |
| generateBalanceHistorySample(sampleData.transactions); | |
| } | |
| showAlert('Using sample data (APIs unavailable)', 'warning'); | |
| } | |
| // ============================================ | |
| // UPDATE ADDRESS INFO FROM DIFFERENT SOURCES | |
| // ============================================ | |
| function updateAddressInfoFromMempool(data) { | |
| const satToBtc = sat => (sat / 100000000).toFixed(8); | |
| const chainStats = data.chain_stats || {}; | |
| const mempoolStats = data.mempool_stats || {}; | |
| const totalReceived = satToBtc(chainStats.funded_txo_sum || 0); | |
| const totalSent = satToBtc(chainStats.spent_txo_sum || 0); | |
| const confirmedBalance = satToBtc((chainStats.funded_txo_sum || 0) - (chainStats.spent_txo_sum || 0)); | |
| const unconfirmedBalance = satToBtc((mempoolStats.funded_txo_sum || 0) - (mempoolStats.spent_txo_sum || 0)); | |
| const txCount = (chainStats.tx_count || 0) + (mempoolStats.tx_count || 0); | |
| const totalBalance = parseFloat(confirmedBalance) + parseFloat(unconfirmedBalance); | |
| document.getElementById('address-balance').textContent = `${totalBalance.toFixed(8)} BTC`; | |
| document.getElementById('total-received').textContent = `${totalReceived} BTC`; | |
| document.getElementById('total-sent').textContent = `${totalSent} BTC`; | |
| document.getElementById('tx-count').textContent = txCount; | |
| document.getElementById('unconfirmed-balance').textContent = `${unconfirmedBalance} BTC`; | |
| if (currentBitcoinPrice > 0) { | |
| const usdValue = (totalBalance * currentBitcoinPrice).toLocaleString('en-US', { | |
| style: 'currency', | |
| currency: 'USD', | |
| minimumFractionDigits: 2, | |
| maximumFractionDigits: 2 | |
| }); | |
| document.getElementById('address-balance-usd').textContent = usdValue; | |
| } | |
| } | |
| function updateAddressInfoFromBlockCypher(data) { | |
| const satToBtc = sat => (sat / 100000000).toFixed(8); | |
| const balance = satToBtc(data.final_balance || data.balance || 0); | |
| const totalReceived = satToBtc(data.total_received || 0); | |
| const totalSent = satToBtc(data.total_sent || 0); | |
| const unconfirmedBalance = satToBtc(data.unconfirmed_balance || 0); | |
| const txCount = data.n_tx || data.final_n_tx || 0; | |
| document.getElementById('address-balance').textContent = `${balance} BTC`; | |
| document.getElementById('total-received').textContent = `${totalReceived} BTC`; | |
| document.getElementById('total-sent').textContent = `${totalSent} BTC`; | |
| document.getElementById('tx-count').textContent = txCount; | |
| document.getElementById('unconfirmed-balance').textContent = `${unconfirmedBalance} BTC`; | |
| if (currentBitcoinPrice > 0) { | |
| const usdValue = (parseFloat(balance) * currentBitcoinPrice).toLocaleString('en-US', { | |
| style: 'currency', | |
| currency: 'USD' | |
| }); | |
| document.getElementById('address-balance-usd').textContent = usdValue; | |
| } | |
| } | |
| function updateAddressInfoFromBlockchain(data) { | |
| const satToBtc = sat => (sat / 100000000).toFixed(8); | |
| const balance = satToBtc(data.final_balance || 0); | |
| const totalReceived = satToBtc(data.total_received || 0); | |
| const totalSent = satToBtc(data.total_sent || 0); | |
| const unconfirmedBalance = satToBtc(data.unconfirmed_balance || 0); | |
| const txCount = data.n_tx || 0; | |
| document.getElementById('address-balance').textContent = `${balance} BTC`; | |
| document.getElementById('total-received').textContent = `${totalReceived} BTC`; | |
| document.getElementById('total-sent').textContent = `${totalSent} BTC`; | |
| document.getElementById('tx-count').textContent = txCount; | |
| document.getElementById('unconfirmed-balance').textContent = `${unconfirmedBalance} BTC`; | |
| if (currentBitcoinPrice > 0) { | |
| const usdValue = (parseFloat(balance) * currentBitcoinPrice).toLocaleString('en-US', { | |
| style: 'currency', | |
| currency: 'USD' | |
| }); | |
| document.getElementById('address-balance-usd').textContent = usdValue; | |
| } | |
| } | |
| function updateAddressInfoSample(sampleData) { | |
| document.getElementById('address-balance').textContent = `${sampleData.balance.toFixed(8)} BTC`; | |
| document.getElementById('total-received').textContent = `${sampleData.totalReceived.toFixed(8)} BTC`; | |
| document.getElementById('total-sent').textContent = `${sampleData.totalSent.toFixed(8)} BTC`; | |
| document.getElementById('tx-count').textContent = sampleData.txCount; | |
| document.getElementById('unconfirmed-balance').textContent = '0.00000000 BTC'; | |
| if (currentBitcoinPrice > 0) { | |
| const usdValue = (sampleData.balance * currentBitcoinPrice).toLocaleString('en-US', { | |
| style: 'currency', | |
| currency: 'USD' | |
| }); | |
| document.getElementById('address-balance-usd').textContent = usdValue; | |
| } | |
| } | |
| // ============================================ | |
| // FILTER TRANSACTIONS BY TYPE | |
| // ============================================ | |
| function filterTransactionsByType(type) { | |
| currentTxFilter = type; | |
| if (!currentTransactions || currentTransactions.length === 0) { | |
| return; | |
| } | |
| switch(type) { | |
| case 'all': | |
| filteredTransactions = [...currentTransactions]; | |
| break; | |
| case 'sent': | |
| filteredTransactions = currentTransactions.filter(tx => { | |
| if (currentApiSource === 'mempool' || currentApiSource === 'blockchain') { | |
| const amount = currentApiSource === 'mempool' | |
| ? calculateTransactionAmount(tx, currentAddress) | |
| : calculateTransactionAmountBlockchain(tx, currentAddress); | |
| return amount < 0; | |
| } else if (currentApiSource === 'blockcypher') { | |
| return tx.tx_input_n !== undefined; | |
| } else { | |
| return tx.amount < 0; | |
| } | |
| }); | |
| break; | |
| case 'received': | |
| filteredTransactions = currentTransactions.filter(tx => { | |
| if (currentApiSource === 'mempool' || currentApiSource === 'blockchain') { | |
| const amount = currentApiSource === 'mempool' | |
| ? calculateTransactionAmount(tx, currentAddress) | |
| : calculateTransactionAmountBlockchain(tx, currentAddress); | |
| return amount > 0; | |
| } else if (currentApiSource === 'blockcypher') { | |
| return tx.tx_output_n !== undefined; | |
| } else { | |
| return tx.amount > 0; | |
| } | |
| }); | |
| break; | |
| } | |
| currentTxPage = 1; | |
| totalTxPages = Math.ceil(filteredTransactions.length / transactionsPerPage); | |
| updateTransactionsPage(); | |
| } | |
| // ============================================ | |
| // TRANSACTIONS PAGES FROM DIFFERENT SOURCES | |
| // ============================================ | |
| function updateTransactionsPage() { | |
| const container = document.getElementById('address-transactions'); | |
| if (!filteredTransactions || filteredTransactions.length === 0) { | |
| container.innerHTML = ` | |
| <tr> | |
| <td colspan="7" style="text-align: center; padding: 40px;"> | |
| <i class="fas fa-inbox" style="font-size: 48px; color: var(--text-secondary); margin-bottom: 15px;"></i> | |
| <div>No ${currentTxFilter === 'all' ? '' : currentTxFilter + ' '}transactions found for this address</div> | |
| </td> | |
| </tr> | |
| `; | |
| paginationControls.style.display = 'none'; | |
| return; | |
| } | |
| const startIndex = (currentTxPage - 1) * transactionsPerPage; | |
| const endIndex = Math.min(startIndex + transactionsPerPage, filteredTransactions.length); | |
| const pageTransactions = filteredTransactions.slice(startIndex, endIndex); | |
| container.innerHTML = ''; | |
| pageTransactions.forEach((tx, index) => { | |
| displayTransaction(tx, container, startIndex + index); | |
| }); | |
| updatePaginationControls(); | |
| } | |
| function displayTransaction(tx, container, globalIndex) { | |
| try { | |
| let date, time, hash, amount, fee, status, fromAddresses = [], toAddresses = []; | |
| switch(currentApiSource) { | |
| case 'mempool': | |
| date = new Date((tx.status?.block_time || Date.now() / 1000) * 1000).toLocaleDateString(); | |
| time = new Date((tx.status?.block_time || Date.now() / 1000) * 1000).toLocaleTimeString(); | |
| hash = tx.txid || ''; | |
| amount = calculateTransactionAmount(tx, currentAddress); | |
| fee = tx.fee ? (tx.fee / 100000000).toFixed(8) : '0.00000000'; | |
| status = tx.status?.confirmed ? 'Confirmed' : 'Pending'; | |
| fromAddresses = extractFromAddressesMempool(tx); | |
| toAddresses = extractToAddressesMempool(tx); | |
| break; | |
| case 'blockcypher': | |
| date = new Date(tx.confirmed || tx.received).toLocaleDateString(); | |
| time = new Date(tx.confirmed || tx.received).toLocaleTimeString(); | |
| hash = tx.tx_hash || ''; | |
| amount = Math.abs(tx.value) / 100000000 * (tx.tx_input_n !== undefined ? -1 : 1); | |
| fee = 'N/A'; | |
| status = tx.confirmed ? 'Confirmed' : 'Pending'; | |
| fromAddresses = [tx.tx_input_n ? 'Multiple Inputs' : 'Unknown']; | |
| toAddresses = [tx.tx_output_n ? 'Multiple Outputs' : 'Unknown']; | |
| break; | |
| case 'blockchain': | |
| date = new Date(tx.time * 1000).toLocaleDateString(); | |
| time = new Date(tx.time * 1000).toLocaleTimeString(); | |
| hash = tx.hash || ''; | |
| amount = calculateTransactionAmountBlockchain(tx, currentAddress); | |
| fee = tx.fee ? (tx.fee / 100000000).toFixed(8) : '0.00000000'; | |
| status = tx.block_height ? 'Confirmed' : 'Pending'; | |
| fromAddresses = extractFromAddressesBlockchain(tx); | |
| toAddresses = extractToAddressesBlockchain(tx); | |
| break; | |
| case 'sample': | |
| date = new Date(tx.timestamp).toLocaleDateString(); | |
| time = new Date(tx.timestamp).toLocaleTimeString(); | |
| hash = tx.hash || `sample_${globalIndex}`; | |
| amount = tx.amount; | |
| fee = tx.fee || '0.00001000'; | |
| status = tx.confirmed ? 'Confirmed' : 'Pending'; | |
| fromAddresses = tx.from || ['Sample Sender']; | |
| toAddresses = tx.to || ['Sample Receiver']; | |
| break; | |
| } | |
| const btcAmount = typeof amount === 'number' ? Math.abs(amount) : 0; | |
| const formattedAmount = btcAmount.toFixed(8); | |
| let amountColorClass = 'neutral'; | |
| if (typeof amount === 'number') { | |
| if (amount > 0) amountColorClass = 'receiver'; | |
| else if (amount < 0) amountColorClass = 'sender'; | |
| } | |
| const row = document.createElement('tr'); | |
| row.className = 'transaction-row'; | |
| // Display full addresses for from and to | |
| const fromDisplay = fromAddresses.length > 0 | |
| ? fromAddresses.slice(0, 2).map(addr => | |
| `<div class="address-full" title="${addr}" data-type="address" data-value="${addr}">${addr}</div>` | |
| ).join('') | |
| : '<div class="address-short">Unknown</div>'; | |
| const toDisplay = toAddresses.length > 0 | |
| ? toAddresses.slice(0, 2).map(addr => | |
| `<div class="address-full" title="${addr}" data-type="address" data-value="${addr}">${addr}</div>` | |
| ).join('') | |
| : '<div class="address-short">Unknown</div>'; | |
| row.innerHTML = ` | |
| <td> | |
| <span class="address-link" title="${hash}" data-type="tx" data-value="${hash}"> | |
| ${hash.substring(0, 16)}... | |
| </span> | |
| </td> | |
| <td>${date}<br><small>${time}</small></td> | |
| <td>${fromDisplay}</td> | |
| <td>${toDisplay}</td> | |
| <td class="${amountColorClass}">${formattedAmount} BTC</td> | |
| <td>${fee}</td> | |
| <td> | |
| <span class="status-badge ${status === 'Confirmed' ? 'status-confirmed' : 'status-pending'}"> | |
| ${status} | |
| </span> | |
| </td> | |
| `; | |
| container.appendChild(row); | |
| } catch (error) { | |
| console.error('Error displaying transaction:', error); | |
| } | |
| } | |
| // ============================================ | |
| // HELPER FUNCTIONS FOR TRANSACTION PROCESSING | |
| // ============================================ | |
| function calculateTransactionAmount(tx, address) { | |
| if (!tx || !tx.vout || !address) return 0; | |
| let totalReceived = 0; | |
| let totalSent = 0; | |
| tx.vout.forEach(output => { | |
| if (output.scriptpubkey_address === address) { | |
| totalReceived += (output.value || 0) / 100000000; | |
| } | |
| }); | |
| if (tx.vin) { | |
| tx.vin.forEach(input => { | |
| if (input.prevout && input.prevout.scriptpubkey_address === address) { | |
| totalSent += (input.prevout.value || 0) / 100000000; | |
| } | |
| }); | |
| } | |
| return totalReceived - totalSent; | |
| } | |
| function extractFromAddressesMempool(tx) { | |
| const addresses = new Set(); | |
| if (tx.vin) { | |
| tx.vin.forEach(input => { | |
| if (input.prevout && input.prevout.scriptpubkey_address) { | |
| addresses.add(input.prevout.scriptpubkey_address); | |
| } else if (input.is_coinbase) { | |
| addresses.add('Coinbase (Newly Minted)'); | |
| } else { | |
| addresses.add('Unknown'); | |
| } | |
| }); | |
| } | |
| return Array.from(addresses); | |
| } | |
| function extractToAddressesMempool(tx) { | |
| const addresses = new Set(); | |
| if (tx.vout) { | |
| tx.vout.forEach(output => { | |
| if (output.scriptpubkey_address) { | |
| addresses.add(output.scriptpubkey_address); | |
| } | |
| }); | |
| } | |
| return Array.from(addresses); | |
| } | |
| function calculateTransactionAmountBlockchain(tx, address) { | |
| if (!tx || !tx.out || !address) return 0; | |
| let totalReceived = 0; | |
| let totalSent = 0; | |
| tx.out.forEach(output => { | |
| if (output.addr === address) { | |
| totalReceived += (output.value || 0) / 100000000; | |
| } | |
| }); | |
| if (tx.inputs) { | |
| tx.inputs.forEach(input => { | |
| if (input.prev_out && input.prev_out.addr === address) { | |
| totalSent += (input.prev_out.value || 0) / 100000000; | |
| } | |
| }); | |
| } | |
| return totalReceived - totalSent; | |
| } | |
| function extractFromAddressesBlockchain(tx) { | |
| const addresses = new Set(); | |
| if (tx.inputs) { | |
| tx.inputs.forEach(input => { | |
| if (input.prev_out && input.prev_out.addr) { | |
| addresses.add(input.prev_out.addr); | |
| } else { | |
| addresses.add('Unknown'); | |
| } | |
| }); | |
| } | |
| return Array.from(addresses); | |
| } | |
| function extractToAddressesBlockchain(tx) { | |
| const addresses = new Set(); | |
| if (tx.out) { | |
| tx.out.forEach(output => { | |
| if (output.addr) { | |
| addresses.add(output.addr); | |
| } | |
| }); | |
| } | |
| return Array.from(addresses); | |
| } | |
| // ============================================ | |
| // GENERATE SAMPLE DATA | |
| // ============================================ | |
| function generateSampleData(address) { | |
| const transactions = []; | |
| const now = Date.now(); | |
| const baseBalance = Math.random() * 10; | |
| for (let i = 0; i < 15; i++) { | |
| const isIncoming = Math.random() > 0.5; | |
| const amount = (Math.random() * 2 + 0.001).toFixed(8); | |
| const timestamp = new Date(now - (i * 86400000)).toISOString(); | |
| transactions.push({ | |
| hash: `sample_tx_${i}_${Math.random().toString(36).substring(7)}`, | |
| timestamp: timestamp, | |
| amount: parseFloat(isIncoming ? amount : -amount), | |
| fee: (Math.random() * 0.001).toFixed(8), | |
| confirmed: Math.random() > 0.1, | |
| from: isIncoming ? [`1${Math.random().toString(36).substring(2, 34)}`] : [address], | |
| to: isIncoming ? [address] : [`1${Math.random().toString(36).substring(2, 34)}`] | |
| }); | |
| } | |
| transactions.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); | |
| let totalReceived = 0; | |
| let totalSent = 0; | |
| let balance = baseBalance; | |
| transactions.forEach(tx => { | |
| if (tx.amount > 0) { | |
| totalReceived += tx.amount; | |
| balance += tx.amount; | |
| } else { | |
| totalSent += Math.abs(tx.amount); | |
| balance += tx.amount; | |
| } | |
| }); | |
| return { | |
| balance: balance, | |
| totalReceived: totalReceived, | |
| totalSent: totalSent, | |
| txCount: transactions.length, | |
| transactions: transactions | |
| }; | |
| } | |
| // ============================================ | |
| // UPDATE PAGINATION CONTROLS | |
| // ============================================ | |
| function updatePaginationControls() { | |
| if (filteredTransactions.length <= transactionsPerPage) { | |
| paginationControls.style.display = 'none'; | |
| return; | |
| } | |
| paginationControls.style.display = 'flex'; | |
| paginationInfo.textContent = `Page ${currentTxPage} of ${totalTxPages}`; | |
| paginationPrev.disabled = currentTxPage === 1; | |
| paginationNext.disabled = currentTxPage === totalTxPages; | |
| } | |
| // ============================================ | |
| // BALANCE HISTORY CHARTS | |
| // ============================================ | |
| function generateBalanceHistoryFromMempool(transactions) { | |
| if (!transactions || transactions.length === 0) { | |
| createEmptyBalanceChart(); | |
| return; | |
| } | |
| const sortedTx = [...transactions].sort((a, b) => (a.status?.block_time || 0) - (b.status?.block_time || 0)); | |
| const balanceData = []; | |
| const labels = []; | |
| let runningBalance = 0; | |
| const recentTx = sortedTx.slice(-20); | |
| recentTx.forEach((tx, index) => { | |
| const amount = calculateTransactionAmount(tx, currentAddress); | |
| runningBalance += amount; | |
| const date = tx.status?.block_time ? new Date(tx.status.block_time * 1000).toLocaleDateString() : 'Recent'; | |
| labels.push(date); | |
| balanceData.push(runningBalance); | |
| }); | |
| createBalanceChart(labels, balanceData); | |
| } | |
| function generateBalanceHistoryFromBlockCypher(transactions) { | |
| if (!transactions || transactions.length === 0) { | |
| createEmptyBalanceChart(); | |
| return; | |
| } | |
| const sortedTx = [...transactions].sort((a, b) => new Date(a.confirmed || a.received) - new Date(b.confirmed || b.received)); | |
| const balanceData = []; | |
| const labels = []; | |
| let runningBalance = 0; | |
| const recentTx = sortedTx.slice(-20); | |
| recentTx.forEach(tx => { | |
| const amount = Math.abs(tx.value) / 100000000; | |
| const isReceived = tx.tx_output_n !== undefined; | |
| runningBalance += isReceived ? amount : -amount; | |
| const date = new Date(tx.confirmed || tx.received).toLocaleDateString(); | |
| labels.push(date); | |
| balanceData.push(runningBalance); | |
| }); | |
| createBalanceChart(labels, balanceData); | |
| } | |
| function generateBalanceHistoryFromBlockchain(transactions) { | |
| if (!transactions || transactions.length === 0) { | |
| createEmptyBalanceChart(); | |
| return; | |
| } | |
| const sortedTx = [...transactions].sort((a, b) => a.time - b.time); | |
| const balanceData = []; | |
| const labels = []; | |
| let runningBalance = 0; | |
| const recentTx = sortedTx.slice(-20); | |
| recentTx.forEach(tx => { | |
| const amount = calculateTransactionAmountBlockchain(tx, currentAddress); | |
| runningBalance += amount; | |
| const date = new Date(tx.time * 1000).toLocaleDateString(); | |
| labels.push(date); | |
| balanceData.push(runningBalance); | |
| }); | |
| createBalanceChart(labels, balanceData); | |
| } | |
| function generateBalanceHistorySample(transactions) { | |
| if (!transactions || transactions.length === 0) { | |
| createEmptyBalanceChart(); | |
| return; | |
| } | |
| const sortedTx = [...transactions].sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp)); | |
| const balanceData = []; | |
| const labels = []; | |
| let runningBalance = 0; | |
| const recentTx = sortedTx.slice(-20); | |
| recentTx.forEach(tx => { | |
| runningBalance += tx.amount; | |
| const date = new Date(tx.timestamp).toLocaleDateString(); | |
| labels.push(date); | |
| balanceData.push(runningBalance); | |
| }); | |
| createBalanceChart(labels, balanceData); | |
| } | |
| function createBalanceChart(labels, data) { | |
| const ctx = document.getElementById('balanceHistoryChart').getContext('2d'); | |
| if (balanceChart) { | |
| balanceChart.destroy(); | |
| } | |
| balanceChart = new Chart(ctx, { | |
| type: 'line', | |
| data: { | |
| labels: labels, | |
| datasets: [{ | |
| label: 'Balance (BTC)', | |
| data: data, | |
| borderColor: '#00ff9d', | |
| backgroundColor: 'rgba(0, 255, 157, 0.1)', | |
| borderWidth: 3, | |
| fill: true, | |
| tension: 0.4, | |
| pointRadius: 4, | |
| pointHoverRadius: 8, | |
| pointBackgroundColor: '#00ff9d', | |
| pointBorderColor: '#ffffff', | |
| pointBorderWidth: 2 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| legend: { | |
| display: false | |
| }, | |
| tooltip: { | |
| callbacks: { | |
| label: function(context) { | |
| return `Balance: ${context.parsed.y.toFixed(8)} BTC`; | |
| } | |
| } | |
| } | |
| }, | |
| scales: { | |
| x: { | |
| grid: { | |
| color: 'rgba(255, 255, 255, 0.05)' | |
| }, | |
| ticks: { | |
| color: 'var(--text-secondary)', | |
| maxRotation: 45 | |
| } | |
| }, | |
| y: { | |
| grid: { | |
| color: 'rgba(255, 255, 255, 0.05)' | |
| }, | |
| ticks: { | |
| color: 'var(--text-secondary)', | |
| callback: function(value) { | |
| return value.toFixed(4) + ' BTC'; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| function createEmptyBalanceChart() { | |
| const ctx = document.getElementById('balanceHistoryChart').getContext('2d'); | |
| if (balanceChart) { | |
| balanceChart.destroy(); | |
| } | |
| balanceChart = new Chart(ctx, { | |
| type: 'line', | |
| data: { | |
| labels: ['No Data'], | |
| datasets: [{ | |
| label: 'Balance', | |
| data: [0], | |
| borderColor: 'rgba(255, 255, 255, 0.1)', | |
| borderWidth: 1, | |
| fill: false | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| legend: { | |
| display: false | |
| } | |
| }, | |
| scales: { | |
| x: { display: false }, | |
| y: { display: false } | |
| } | |
| } | |
| }); | |
| } | |
| // ============================================ | |
| // ALERT SYSTEM | |
| // ============================================ | |
| function showAlert(message, type = 'info') { | |
| const existingAlerts = document.querySelectorAll('.alert'); | |
| existingAlerts.forEach(alert => { | |
| if (!alert.parentElement.classList.contains('tab-content')) { | |
| alert.remove(); | |
| } | |
| }); | |
| const alertDiv = document.createElement('div'); | |
| alertDiv.className = `alert alert-${type}`; | |
| let icon = 'fas fa-info-circle'; | |
| if (type === 'success') icon = 'fas fa-check-circle'; | |
| if (type === 'error') icon = 'fas fa-exclamation-circle'; | |
| if (type === 'warning') icon = 'fas fa-exclamation-triangle'; | |
| alertDiv.innerHTML = ` | |
| <i class="${icon}"></i> | |
| <span>${message}</span> | |
| `; | |
| const mainContent = document.querySelector('.main-content'); | |
| const firstChild = mainContent.firstChild; | |
| mainContent.insertBefore(alertDiv, firstChild); | |
| setTimeout(() => { | |
| if (alertDiv.parentElement) { | |
| alertDiv.remove(); | |
| } | |
| }, 5000); | |
| } | |
| // ============================================ | |
| // SAVED ADDRESSES SYSTEM | |
| // ============================================ | |
| function loadSavedAddresses() { | |
| const saved = localStorage.getItem('bitcoinExplorerSavedAddresses'); | |
| if (saved) { | |
| try { | |
| savedAddresses = JSON.parse(saved); | |
| renderSavedAddresses(); | |
| } catch (e) { | |
| savedAddresses = []; | |
| } | |
| } else { | |
| savedAddresses = []; | |
| savedAddresses.push({ | |
| name: 'Satoshi Nakamoto (First Bitcoin)', | |
| address: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa', | |
| balance: '0.00000000', | |
| note: 'The first Bitcoin address' | |
| }); | |
| savedAddresses.push({ | |
| name: 'Bitcoin Pizza Address', | |
| address: '1CZpmqVcLwM1zqRwpeC6hqRkUZHLB8w2pK', | |
| balance: '0.00000000', | |
| note: 'Famous 10,000 BTC pizza transaction' | |
| }); | |
| savedAddresses.push({ | |
| name: 'Binance Cold Wallet', | |
| address: '34xp4vRoCGJym3xR7yCVPFHoCNxv4Twseo', | |
| balance: '0.00000000', | |
| note: 'One of the largest Bitcoin addresses' | |
| }); | |
| saveAddressesToStorage(); | |
| } | |
| } | |
| function saveAddressesToStorage() { | |
| localStorage.setItem('bitcoinExplorerSavedAddresses', JSON.stringify(savedAddresses)); | |
| } | |
| function renderSavedAddresses() { | |
| const container = document.getElementById('saved-addresses-list'); | |
| if (!savedAddresses || savedAddresses.length === 0) { | |
| container.innerHTML = ` | |
| <div style="text-align: center; padding: 40px; width: 100%;"> | |
| <i class="fas fa-bookmark" style="font-size: 48px; color: var(--text-secondary); margin-bottom: 15px;"></i> | |
| <div>No saved addresses yet</div> | |
| <button class="btn btn-primary" style="margin-top: 15px;" onclick="saveCurrentAddress()"> | |
| <i class="fas fa-plus"></i> Save Current Address | |
| </button> | |
| </div> | |
| `; | |
| return; | |
| } | |
| container.innerHTML = savedAddresses.map((addr, index) => ` | |
| <div class="saved-address" data-address="${addr.address}"> | |
| <div class="saved-address-header"> | |
| <div class="saved-address-name"> | |
| <i class="fas fa-bookmark"></i> ${addr.name} | |
| </div> | |
| <div class="saved-address-actions"> | |
| <button class="btn btn-small btn-primary" onclick="loadSavedAddress('${addr.address}')"> | |
| <i class="fas fa-search"></i> | |
| </button> | |
| <button class="btn btn-small btn-secondary" onclick="removeSavedAddress(${index})"> | |
| <i class="fas fa-trash"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="saved-address-value">${addr.address}</div> | |
| <div style="display: flex; justify-content: space-between; align-items: center;"> | |
| <div> | |
| <div style="font-size: 12px; color: var(--text-secondary);">Balance:</div> | |
| <div style="font-size: 14px; font-weight: 600;">${addr.balance} BTC</div> | |
| </div> | |
| <div style="font-size: 11px; color: var(--highlight);">${addr.note || ''}</div> | |
| </div> | |
| </div> | |
| `).join(''); | |
| } | |
| function saveCurrentAddress() { | |
| if (!currentAddress) { | |
| showAlert('Please explore an address first', 'error'); | |
| return; | |
| } | |
| const existingIndex = savedAddresses.findIndex(addr => addr.address === currentAddress); | |
| if (existingIndex !== -1) { | |
| showAlert('Address already saved', 'warning'); | |
| return; | |
| } | |
| const addressName = prompt('Enter a name for this address:', `Address ${savedAddresses.length + 1}`); | |
| if (!addressName) return; | |
| const balance = document.getElementById('address-balance').textContent.split(' ')[0]; | |
| savedAddresses.push({ | |
| name: addressName, | |
| address: currentAddress, | |
| balance: balance, | |
| note: `Saved on ${new Date().toLocaleDateString()}` | |
| }); | |
| saveAddressesToStorage(); | |
| renderSavedAddresses(); | |
| showAlert(`Address saved as "${addressName}"`, 'success'); | |
| document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); | |
| document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); | |
| document.querySelectorAll('.sidebar-menu li').forEach(i => i.classList.remove('active')); | |
| document.querySelector('.tab[data-tab="saved-addresses"]').classList.add('active'); | |
| document.getElementById('saved-addresses').classList.add('active'); | |
| document.querySelector('.sidebar-menu li[data-tab="saved-addresses"]').classList.add('active'); | |
| } | |
| function loadSavedAddress(address) { | |
| addressInput.value = address; | |
| exploreAddress(); | |
| } | |
| function removeSavedAddress(index) { | |
| if (confirm('Remove this address from saved list?')) { | |
| savedAddresses.splice(index, 1); | |
| saveAddressesToStorage(); | |
| renderSavedAddresses(); | |
| showAlert('Address removed', 'success'); | |
| } | |
| } | |
| // ============================================ | |
| // LIVE TRANSACTION STREAM | |
| // ============================================ | |
| async function startLiveStream() { | |
| fetchLiveTransactions(); | |
| setInterval(fetchLiveTransactions, 10000); | |
| } | |
| async function fetchLiveTransactions() { | |
| try { | |
| const container = document.getElementById('live-transactions'); | |
| const response = await fetch(`${MEMPOOL_API}/mempool/recent`); | |
| if (!response.ok) throw new Error('API failed'); | |
| const transactions = await response.json(); | |
| if (transactions && transactions.length > 0) { | |
| displayLiveTransactions(transactions.slice(0, 15)); | |
| updateNotificationCount(Math.min(transactions.length, 99)); | |
| } else { | |
| generateSampleLiveTransactions(); | |
| } | |
| } catch (error) { | |
| console.error('Error fetching live transactions:', error); | |
| generateSampleLiveTransactions(); | |
| } | |
| } | |
| function displayLiveTransactions(transactions) { | |
| const container = document.getElementById('live-transactions'); | |
| const now = new Date(); | |
| container.innerHTML = transactions.map(tx => { | |
| const time = new Date(tx.time || Date.now()); | |
| const timeAgo = Math.floor((now - time) / 1000); | |
| let timeDisplay; | |
| if (timeAgo < 60) timeDisplay = `${timeAgo}s ago`; | |
| else if (timeAgo < 3600) timeDisplay = `${Math.floor(timeAgo / 60)}m ago`; | |
| else timeDisplay = `${Math.floor(timeAgo / 3600)}h ago`; | |
| // Generate more realistic addresses | |
| const fromAddress = `1${Math.random().toString(36).substring(2, 34)}`; | |
| const toAddress = `3${Math.random().toString(36).substring(2, 34)}`; | |
| const amount = (Math.random() * 10 + 0.001).toFixed(8); | |
| const fee = (Math.random() * 0.001).toFixed(8); | |
| const confirmations = Math.floor(Math.random() * 3); | |
| return ` | |
| <tr class="live-transaction"> | |
| <td><span class="live-indicator"></span>${timeDisplay}</td> | |
| <td> | |
| <span class="address-link" title="${tx.txid || 'unknown'}"> | |
| ${(tx.txid || 'unknown_tx').substring(0, 16)}... | |
| </span> | |
| </td> | |
| <td> | |
| <div class="address-full" title="${fromAddress}" data-type="address" data-value="${fromAddress}"> | |
| ${fromAddress} | |
| </div> | |
| </td> | |
| <td> | |
| <div class="address-full" title="${toAddress}" data-type="address" data-value="${toAddress}"> | |
| ${toAddress} | |
| </div> | |
| </td> | |
| <td class="neutral">${amount}</td> | |
| <td>${fee}</td> | |
| <td> | |
| <span class="status-badge ${confirmations > 0 ? 'status-confirmed' : 'status-pending'}"> | |
| ${confirmations > 0 ? `${confirmations} conf` : 'Pending'} | |
| </span> | |
| </td> | |
| </tr> | |
| `; | |
| }).join(''); | |
| } | |
| function generateSampleLiveTransactions() { | |
| const transactions = []; | |
| const now = Date.now(); | |
| for (let i = 0; i < 15; i++) { | |
| transactions.push({ | |
| txid: `live_tx_${now}_${i}_${Math.random().toString(36).substring(7)}`, | |
| time: new Date(now - (i * 10000)).toISOString() | |
| }); | |
| } | |
| displayLiveTransactions(transactions); | |
| updateNotificationCount(Math.floor(Math.random() * 5) + 1); | |
| } | |
| function updateNotificationCount(count) { | |
| notificationCount.textContent = Math.min(count, 99); | |
| notificationCount.style.display = count > 0 ? 'flex' : 'none'; | |
| } | |
| // ============================================ | |
| // LARGE TRANSACTION MONITOR | |
| // ============================================ | |
| async function startLargeTransactionMonitor() { | |
| monitorLargeTransactions(); | |
| setInterval(monitorLargeTransactions, 30000); | |
| } | |
| async function monitorLargeTransactions() { | |
| try { | |
| const container = document.getElementById('large-transactions'); | |
| const countElement = document.getElementById('large-tx-count'); | |
| if (Math.random() < 0.2 || largeTransactions.length === 0) { | |
| addNewLargeTransaction(); | |
| } | |
| displayLargeTransactions(); | |
| countElement.textContent = largeTransactions.length; | |
| } catch (error) { | |
| console.error('Error monitoring large transactions:', error); | |
| } | |
| } | |
| function addNewLargeTransaction() { | |
| const now = new Date(); | |
| const largeAmounts = [525, 750, 1200, 2500, 5000]; | |
| const amount = largeAmounts[Math.floor(Math.random() * largeAmounts.length)]; | |
| const largeTx = { | |
| id: `large_tx_${now.getTime()}_${Math.random().toString(36).substring(7)}`, | |
| time: now.toISOString(), | |
| amount: amount, | |
| fromAddress: `1${Math.random().toString(36).substring(2, 34)}`, | |
| toAddress: `3${Math.random().toString(36).substring(2, 34)}`, | |
| usdValue: (amount * (currentBitcoinPrice || 45000)).toLocaleString('en-US', { | |
| style: 'currency', | |
| currency: 'USD', | |
| minimumFractionDigits: 0, | |
| maximumFractionDigits: 0 | |
| }) | |
| }; | |
| largeTransactions.unshift(largeTx); | |
| if (largeTransactions.length > 20) { | |
| largeTransactions.pop(); | |
| } | |
| if (amount > 1000) { | |
| showAlert(`⚠️ Large transaction detected: ${amount} BTC (${largeTx.usdValue})`, 'warning'); | |
| } | |
| } | |
| function displayLargeTransactions() { | |
| const container = document.getElementById('large-transactions'); | |
| if (largeTransactions.length === 0) { | |
| container.innerHTML = ` | |
| <tr> | |
| <td colspan="7" style="text-align: center; padding: 40px;"> | |
| <i class="fas fa-exclamation-triangle" style="font-size: 48px; color: var(--text-secondary); margin-bottom: 15px;"></i> | |
| <div>No large transactions detected yet</div> | |
| <div style="font-size: 12px; color: var(--text-secondary); margin-top: 10px;"> | |
| Monitoring for transactions > 500 BTC | |
| </div> | |
| </td> | |
| </tr> | |
| `; | |
| return; | |
| } | |
| container.innerHTML = largeTransactions.map(tx => { | |
| const time = new Date(tx.time); | |
| const timeStr = time.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); | |
| const dateStr = time.toLocaleDateString(); | |
| return ` | |
| <tr class="live-transaction"> | |
| <td> | |
| ${dateStr}<br> | |
| <small>${timeStr}</small> | |
| </td> | |
| <td> | |
| <span class="address-link" title="${tx.id}"> | |
| ${tx.id.substring(0, 16)}... | |
| </span> | |
| </td> | |
| <td> | |
| <div class="address-full sender" title="${tx.fromAddress}" data-type="address" data-value="${tx.fromAddress}"> | |
| ${tx.fromAddress} | |
| </div> | |
| </td> | |
| <td> | |
| <div class="address-full receiver" title="${tx.toAddress}" data-type="address" data-value="${tx.toAddress}"> | |
| ${tx.toAddress} | |
| </div> | |
| </td> | |
| <td class="sender" style="font-weight: 700;">${tx.amount.toLocaleString()} BTC</td> | |
| <td style="font-weight: 700;">${tx.usdValue}</td> | |
| <td> | |
| <button class="btn btn-small btn-primary" onclick="trackTransaction('${tx.id}')"> | |
| <i class="fas fa-eye"></i> Track | |
| </button> | |
| </td> | |
| </tr> | |
| `; | |
| }).join(''); | |
| } | |
| function trackTransaction(txId) { | |
| showAlert(`Tracking transaction: ${txId.substring(0, 20)}...`, 'success'); | |
| } | |
| // ============================================ | |
| // BLOCK EXPLORER | |
| // ============================================ | |
| async function exploreBlock() { | |
| const blockInputValue = blockInput.value.trim(); | |
| if (!blockInputValue) { | |
| showAlert('Please enter a block height or hash', 'error'); | |
| return; | |
| } | |
| showAlert(`Exploring block: ${blockInputValue}`, 'success'); | |
| // Reset display | |
| blockInfoContainer.style.display = 'none'; | |
| blockTransactionsContainer.style.display = 'none'; | |
| try { | |
| let blockData; | |
| // Check if input is a number (block height) or hash | |
| if (/^\d+$/.test(blockInputValue)) { | |
| // Get block hash from height | |
| const hashResponse = await fetch(`${MEMPOOL_API}/block-height/${blockInputValue}`); | |
| if (!hashResponse.ok) { | |
| throw new Error('Block not found'); | |
| } | |
| const blockHash = await hashResponse.text(); | |
| blockData = await fetchBlockData(blockHash); | |
| } else { | |
| // Assume it's a block hash | |
| blockData = await fetchBlockData(blockInputValue); | |
| } | |
| if (blockData) { | |
| currentBlock = blockData; | |
| currentBlockPage = 1; | |
| displayBlockInfo(blockData); | |
| updateBlockTransactionsPage(); | |
| } | |
| } catch (error) { | |
| console.error('Error exploring block:', error); | |
| showAlert('Error exploring block. Please try again.', 'error'); | |
| // Show sample block data | |
| showSampleBlockData(blockInputValue); | |
| } | |
| } | |
| async function fetchBlockData(blockHash) { | |
| try { | |
| const response = await fetch(`${MEMPOOL_API}/block/${blockHash}`); | |
| if (!response.ok) { | |
| throw new Error('Failed to fetch block data'); | |
| } | |
| return await response.json(); | |
| } catch (error) { | |
| console.error('Error fetching block data:', error); | |
| throw error; | |
| } | |
| } | |
| async function fetchBlockTransactions(blockHash, page = 1) { | |
| try { | |
| // Mempool API returns all transactions in block, we need to paginate client-side | |
| const response = await fetch(`${MEMPOOL_API}/block/${blockHash}/txs`); | |
| if (!response.ok) { | |
| throw new Error('Failed to fetch block transactions'); | |
| } | |
| const allTransactions = await response.json(); | |
| // Client-side pagination | |
| const startIndex = (page - 1) * blockTransactionsPerPage; | |
| const endIndex = Math.min(startIndex + blockTransactionsPerPage, allTransactions.length); | |
| const pageTransactions = allTransactions.slice(startIndex, endIndex); | |
| return { | |
| transactions: pageTransactions, | |
| total: allTransactions.length, | |
| page: page, | |
| totalPages: Math.ceil(allTransactions.length / blockTransactionsPerPage) | |
| }; | |
| } catch (error) { | |
| console.error('Error fetching block transactions:', error); | |
| throw error; | |
| } | |
| } | |
| function displayBlockInfo(blockData) { | |
| const container = document.getElementById('block-info-grid'); | |
| const blockTime = new Date(blockData.timestamp * 1000); | |
| const blockSizeMB = (blockData.size / 1024 / 1024).toFixed(2); | |
| const blockWeightWU = blockData.weight || 0; | |
| const difficulty = blockData.difficulty || 0; | |
| const merkleRoot = blockData.merkle_root || 'N/A'; | |
| const nonce = blockData.nonce || 0; | |
| const bits = blockData.bits || 'N/A'; | |
| const version = blockData.version || 'N/A'; | |
| const txCount = blockData.tx_count || 0; | |
| container.innerHTML = ` | |
| <div class="block-info-item"> | |
| <div class="block-info-label">BLOCK HEIGHT</div> | |
| <div class="block-info-value">${blockData.height.toLocaleString()}</div> | |
| </div> | |
| <div class="block-info-item"> | |
| <div class="block-info-label">BLOCK HASH</div> | |
| <div class="block-info-value block-hash" title="${blockData.id}">${blockData.id.substring(0, 20)}...</div> | |
| </div> | |
| <div class="block-info-item"> | |
| <div class="block-info-label">TIMESTAMP</div> | |
| <div class="block-info-value">${blockTime.toLocaleString()}</div> | |
| </div> | |
| <div class="block-info-item"> | |
| <div class="block-info-label">SIZE</div> | |
| <div class="block-info-value">${blockData.size.toLocaleString()} bytes (${blockSizeMB} MB)</div> | |
| </div> | |
| <div class="block-info-item"> | |
| <div class="block-info-label">WEIGHT</div> | |
| <div class="block-info-value">${blockWeightWU.toLocaleString()} WU</div> | |
| </div> | |
| <div class="block-info-item"> | |
| <div class="block-info-label">TRANSACTIONS</div> | |
| <div class="block-info-value">${txCount.toLocaleString()}</div> | |
| </div> | |
| <div class="block-info-item"> | |
| <div class="block-info-label">DIFFICULTY</div> | |
| <div class="block-info-value">${difficulty.toLocaleString()}</div> | |
| </div> | |
| <div class="block-info-item"> | |
| <div class="block-info-label">NONCE</div> | |
| <div class="block-info-value">${nonce.toLocaleString()}</div> | |
| </div> | |
| <div class="block-info-item"> | |
| <div class="block-info-label">BITS</div> | |
| <div class="block-info-value">${bits}</div> | |
| </div> | |
| <div class="block-info-item"> | |
| <div class="block-info-label">VERSION</div> | |
| <div class="block-info-value">${version}</div> | |
| </div> | |
| <div class="block-info-item"> | |
| <div class="block-info-label">MERKLE ROOT</div> | |
| <div class="block-info-value block-hash" title="${merkleRoot}">${merkleRoot.substring(0, 20)}...</div> | |
| </div> | |
| <div class="block-info-item"> | |
| <div class="block-info-label">PREVIOUS BLOCK</div> | |
| <div class="block-info-value block-hash" title="${blockData.previousblockhash}" data-type="block" data-value="${blockData.previousblockhash}"> | |
| ${blockData.previousblockhash.substring(0, 20)}... | |
| </div> | |
| </div> | |
| `; | |
| blockInfoContainer.style.display = 'block'; | |
| } | |
| async function updateBlockTransactionsPage() { | |
| if (!currentBlock) return; | |
| const container = document.getElementById('block-transactions'); | |
| container.innerHTML = ` | |
| <tr> | |
| <td colspan="6" class="loading"> | |
| <div class="spinner"></div> | |
| <div>Loading block transactions...</div> | |
| </td> | |
| </tr> | |
| `; | |
| try { | |
| const txData = await fetchBlockTransactions(currentBlock.id, currentBlockPage); | |
| displayBlockTransactions(txData.transactions); | |
| updateBlockPaginationControls(txData); | |
| } catch (error) { | |
| console.error('Error loading block transactions:', error); | |
| // Show sample transactions | |
| showSampleBlockTransactions(); | |
| } | |
| } | |
| function displayBlockTransactions(transactions) { | |
| const container = document.getElementById('block-transactions'); | |
| if (!transactions || transactions.length === 0) { | |
| container.innerHTML = ` | |
| <tr> | |
| <td colspan="6" style="text-align: center; padding: 40px;"> | |
| <i class="fas fa-inbox" style="font-size: 48px; color: var(--text-secondary); margin-bottom: 15px;"></i> | |
| <div>No transactions found in this block</div> | |
| </td> | |
| </tr> | |
| `; | |
| return; | |
| } | |
| container.innerHTML = transactions.map(tx => { | |
| const date = new Date((tx.status?.block_time || Date.now() / 1000) * 1000).toLocaleDateString(); | |
| const time = new Date((tx.status?.block_time || Date.now() / 1000) * 1000).toLocaleTimeString(); | |
| const hash = tx.txid || ''; | |
| const fee = tx.fee ? (tx.fee / 100000000).toFixed(8) : '0.00000000'; | |
| const confirmations = tx.status?.block_height ? currentBlock.height - tx.status.block_height + 1 : 1; | |
| // Calculate total amount | |
| let totalAmount = 0; | |
| if (tx.vout) { | |
| totalAmount = tx.vout.reduce((sum, output) => sum + (output.value || 0), 0) / 100000000; | |
| } | |
| // Extract addresses | |
| const fromAddresses = new Set(); | |
| const toAddresses = new Set(); | |
| if (tx.vin) { | |
| tx.vin.forEach(input => { | |
| if (input.prevout && input.prevout.scriptpubkey_address) { | |
| fromAddresses.add(input.prevout.scriptpubkey_address); | |
| } else if (input.is_coinbase) { | |
| fromAddresses.add('Coinbase'); | |
| } | |
| }); | |
| } | |
| if (tx.vout) { | |
| tx.vout.forEach(output => { | |
| if (output.scriptpubkey_address) { | |
| toAddresses.add(output.scriptpubkey_address); | |
| } | |
| }); | |
| } | |
| const fromDisplay = Array.from(fromAddresses).slice(0, 2).map(addr => | |
| `<div class="address-full" title="${addr}" data-type="${addr === 'Coinbase' ? 'text' : 'address'}" data-value="${addr}">${addr}</div>` | |
| ).join(''); | |
| const toDisplay = Array.from(toAddresses).slice(0, 2).map(addr => | |
| `<div class="address-full" title="${addr}" data-type="address" data-value="${addr}">${addr}</div>` | |
| ).join(''); | |
| return ` | |
| <tr class="transaction-row"> | |
| <td> | |
| <span class="address-link" title="${hash}" data-type="tx" data-value="${hash}"> | |
| ${hash.substring(0, 16)}... | |
| </span> | |
| </td> | |
| <td>${fromDisplay}</td> | |
| <td>${toDisplay}</td> | |
| <td class="neutral">${totalAmount.toFixed(8)}</td> | |
| <td>${fee}</td> | |
| <td> | |
| <span class="status-badge status-confirmed"> | |
| ${confirmations} conf | |
| </span> | |
| </td> | |
| </tr> | |
| `; | |
| }).join(''); | |
| blockTransactionsContainer.style.display = 'block'; | |
| } | |
| function updateBlockPaginationControls(txData) { | |
| if (txData.total <= blockTransactionsPerPage) { | |
| blockPaginationControls.style.display = 'none'; | |
| return; | |
| } | |
| blockPaginationControls.style.display = 'flex'; | |
| blockPaginationInfo.textContent = `Page ${currentBlockPage} of ${txData.totalPages}`; | |
| blockPaginationPrev.disabled = currentBlockPage === 1; | |
| blockPaginationNext.disabled = currentBlockPage === txData.totalPages; | |
| } | |
| function showSampleBlockData(blockInputValue) { | |
| currentBlock = { | |
| id: '0000000000000000000abcdef1234567890abcdef1234567890abcdef123456', | |
| height: parseInt(blockInputValue) || 840000, | |
| timestamp: Date.now() / 1000 - 3600, | |
| size: 1350000, | |
| weight: 4000000, | |
| tx_count: 2456, | |
| difficulty: 48638239585062, | |
| merkle_root: 'abcdef1234567890abcdef1234567890abcdef1234567890abcdef123456', | |
| nonce: 1234567890, | |
| bits: '17148d20', | |
| version: 0x20000000, | |
| previousblockhash: '0000000000000000000abcdef1234567890abcdef1234567890abcdef123455' | |
| }; | |
| displayBlockInfo(currentBlock); | |
| showSampleBlockTransactions(); | |
| } | |
| function showSampleBlockTransactions() { | |
| const container = document.getElementById('block-transactions'); | |
| const transactions = []; | |
| for (let i = 0; i < 10; i++) { | |
| transactions.push({ | |
| txid: `sample_block_tx_${i}_${Math.random().toString(36).substring(7)}`, | |
| status: { block_height: currentBlock.height, block_time: currentBlock.timestamp }, | |
| fee: Math.floor(Math.random() * 10000), | |
| vout: [ | |
| { value: Math.floor(Math.random() * 100000000), scriptpubkey_address: `1${Math.random().toString(36).substring(2, 34)}` }, | |
| { value: Math.floor(Math.random() * 50000000), scriptpubkey_address: `3${Math.random().toString(36).substring(2, 34)}` } | |
| ], | |
| vin: [ | |
| { | |
| prevout: { | |
| scriptpubkey_address: i === 0 ? 'Coinbase' : `1${Math.random().toString(36).substring(2, 34)}` | |
| }, | |
| is_coinbase: i === 0 | |
| } | |
| ] | |
| }); | |
| } | |
| displayBlockTransactions(transactions); | |
| blockTransactionsContainer.style.display = 'block'; | |
| blockPaginationControls.style.display = 'none'; | |
| showAlert('Showing sample block data (API unavailable)', 'warning'); | |
| } | |
| // ============================================ | |
| // EXPORT FUNCTIONS FOR GLOBAL ACCESS | |
| // ============================================ | |
| window.exploreAddress = exploreAddress; | |
| window.saveCurrentAddress = saveCurrentAddress; | |
| window.loadSavedAddress = loadSavedAddress; | |
| window.removeSavedAddress = removeSavedAddress; | |
| window.trackTransaction = trackTransaction; | |
| window.exploreBlock = exploreBlock; | |
| // Responsive menu | |
| window.addEventListener('resize', function() { | |
| if (window.innerWidth >= 768) { | |
| sidebar.classList.remove('active'); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment