UNPKG

caravan-x

Version:

A terminal-based utility for managing Caravan multisig wallets in regtest mode. This tool simplifies development and testing with Caravan by providing an easy-to-use interface

1,601 lines (1,452 loc) 70.9 kB
// Global variables let socket; let blockchainData = { blocks: [], mempool: { txids: [] }, transactions: {}, }; let blockchainChart; let mempoolChart; let transactionNetwork; let mempoolSizeHistory = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; let isFirstLoad = true; // Initialize the application document.addEventListener("DOMContentLoaded", () => { initSocket(); setupEventListeners(); fetchInitialData(); loadThemePreference(); }); // Initialize Socket.io connection function initSocket() { socket = io(); socket.on("connect", () => { updateConnectionStatus(true); console.log("Connected to server"); }); socket.on("disconnect", () => { updateConnectionStatus(false); console.log("Disconnected from server"); }); socket.on("blockchain_update", (data) => { console.log("Received blockchain update", data); updateBlockchainData(data); }); socket.on("new_block", (block) => { console.log("New block mined", block); addNewBlock(block); addMiningLog( `New block mined: ${block.hash.substring(0, 8)}... at height ${block.height}`, ); // Play sound effect playSound("block-mined"); }); socket.on("new_transaction", (tx) => { console.log("New transaction", tx); addNewTransaction(tx); addMiningLog(`New transaction: ${tx.txid.substring(0, 8)}...`); // Play sound effect playSound("transaction"); }); socket.on("mining_started", (data) => { console.log("Mining started", data); showMiningActivity(true); addMiningLog( `Mining started: ${data.blocks} blocks to ${data.address.substring(0, 8)}...`, ); }); socket.on("mining_complete", (data) => { console.log("Mining complete", data); addMiningLog(`Mining complete: ${data.blockHashes.length} blocks mined`); }); } // Sound effects function playSound(type) { // Create an audio element for the sound effect const audio = new Audio(); switch (type) { case "block-mined": audio.src = "sounds/block-mined.mp3"; break; case "transaction": audio.src = "sounds/transaction.mp3"; break; case "error": audio.src = "sounds/error.mp3"; break; case "click": audio.src = "sounds/click.mp3"; break; default: return; // No sound to play } // Load and play the sound audio.load(); audio.volume = 0.5; audio .play() .catch((e) => console.log("Sound play prevented by browser policy", e)); } // Setup event listeners function setupEventListeners() { // View switching buttons document.getElementById("pixelViewBtn").addEventListener("click", () => { playSound("click"); window.location.href = "index-pixel.html"; }); document.getElementById("minecraftViewBtn").addEventListener("click", () => { playSound("click"); window.location.href = "index-minecraft.html"; }); // Refresh button document.getElementById("refreshBtn").addEventListener("click", () => { playSound("click"); fetchInitialData(); }); // Mining buttons const mineBlockBtn = document.getElementById("mineBlockBtn"); if (mineBlockBtn) { mineBlockBtn.addEventListener("click", triggerMineBlock); } // Create transaction button const createTxBtn = document.getElementById("createTxBtn"); if (createTxBtn) { createTxBtn.addEventListener("click", triggerCreateTransaction); } // Mining info button const miningInfoBtn = document.getElementById("miningInfoBtn"); if (miningInfoBtn) { miningInfoBtn.addEventListener("click", () => { playSound("click"); fetch("/api/chain-info") .then((response) => response.json()) .then((info) => { showMiningInfo(info); }) .catch((error) => console.error("Error fetching mining info:", error)); }); } // Close details button const closeDetailsBtn = document.getElementById("closeDetailsBtn"); if (closeDetailsBtn) { closeDetailsBtn.addEventListener("click", hideTransactionDetails); } // Close mining activity button const closeMiningBtn = document.getElementById("closeMiningBtn"); if (closeMiningBtn) { closeMiningBtn.addEventListener("click", () => { document.getElementById("miningActivity").style.display = "none"; }); } // Theme toggle const themeToggle = document.getElementById("themeToggle"); if (themeToggle) { themeToggle.addEventListener("click", toggleTheme); } // Network view type selector const networkViewType = document.getElementById("networkViewType"); if (networkViewType) { networkViewType.addEventListener("change", (e) => { updateNetworkVisualization(e.target.value); }); } // Network control buttons const zoomInBtn = document.getElementById("zoomInBtn"); if (zoomInBtn) { zoomInBtn.addEventListener("click", () => { if (transactionNetwork) { transactionNetwork.zoom(0.2); } }); } const zoomOutBtn = document.getElementById("zoomOutBtn"); if (zoomOutBtn) { zoomOutBtn.addEventListener("click", () => { if (transactionNetwork) { transactionNetwork.zoom(-0.2); } }); } const resetViewBtn = document.getElementById("resetViewBtn"); if (resetViewBtn) { resetViewBtn.addEventListener("click", () => { if (transactionNetwork) { transactionNetwork.fit(); } }); } // Mempool sorting const mempoolSortSelect = document.getElementById("mempoolSortSelect"); if (mempoolSortSelect) { mempoolSortSelect.addEventListener("change", () => { displayMempool(); }); } // Handle window resize for charts window.addEventListener("resize", () => { if (blockchainChart) { blockchainChart.resize(); } if (mempoolChart) { mempoolChart.resize(); } }); } // Fetch initial blockchain data function fetchInitialData() { showLoadingStates(); fetch("/api/blockchain") .then((response) => response.json()) .then((data) => { blockchainData = data; updateDashboard(); initVisualizations(); hideLoadingStates(); if (isFirstLoad) { playIntroAnimation(); isFirstLoad = false; } }) .catch((error) => { console.error("Error fetching blockchain data:", error); showErrorMessage("Failed to load blockchain data. Please try again."); hideLoadingStates(); }); } // Show loading states for all containers function showLoadingStates() { const loadingHTML = `<div class="loading"> <div class="spinner"></div> <span>Loading data...</span> </div>`; // For blocks container const blocksContainer = document.getElementById("blocksContainer"); if (blocksContainer) { blocksContainer.innerHTML = loadingHTML; } // For mempool container const mempoolContainer = document.getElementById("mempoolContainer"); if (mempoolContainer) { mempoolContainer.innerHTML = loadingHTML; } // For network container const networkContainer = document.getElementById("transactionNetwork"); if (networkContainer) { networkContainer.innerHTML = loadingHTML; } } // Hide loading states function hideLoadingStates() { // The content will be replaced by actual data in respective display functions } // Show error message function showErrorMessage(message) { const errorHTML = `<div class="error-message"> <i class="fas fa-exclamation-triangle"></i> <p>${message}</p> </div>`; // Add to different containers as needed const containers = [ document.getElementById("blocksContainer"), document.getElementById("mempoolContainer"), document.getElementById("transactionNetwork"), ]; containers.forEach((container) => { if (container) { container.innerHTML = errorHTML; } }); } // Update connection status indicator function updateConnectionStatus(connected) { const statusElement = document.getElementById("connectionStatus"); if (!statusElement) return; const statusIndicator = statusElement.querySelector(".status-indicator"); const statusText = statusElement.querySelector(".status-text"); if (connected) { statusIndicator.classList.remove("disconnected"); statusIndicator.classList.add("connected"); statusText.textContent = "Connected"; } else { statusIndicator.classList.remove("connected"); statusIndicator.classList.add("disconnected"); statusText.textContent = "Disconnected"; } } // Play intro animation function playIntroAnimation() { // Animate stats cards to fade in one by one const statCards = document.querySelectorAll(".stat-card"); statCards.forEach((card, index) => { card.style.opacity = "0"; setTimeout(() => { gsap.to(card, { opacity: 1, y: 0, duration: 0.5, ease: "power2.out", delay: index * 0.1, }); }, 100); }); // Animate cards to fade in after stats const cards = document.querySelectorAll(".card"); cards.forEach((card, index) => { if (card.closest(".stats-section")) return; // Skip stats cards card.style.opacity = "0"; card.style.transform = "translateY(20px)"; setTimeout(() => { gsap.to(card, { opacity: 1, y: 0, duration: 0.5, ease: "power2.out", delay: index * 0.1, }); }, 500); }); } // Update blockchain data with new information function updateBlockchainData(data) { // Merge new data with existing data if (data.blocks) blockchainData.blocks = data.blocks; if (data.mempool) blockchainData.mempool = data.mempool; if (data.chainInfo) blockchainData.chainInfo = data.chainInfo; if (data.stats) blockchainData.stats = data.stats; // Update the UI updateDashboard(); updateVisualizations(); } // Initialize visualizations function initVisualizations() { initBlockchainVisualization(); initMempoolVisualization(); initTransactionNetwork(); displayBlocks(); displayMempool(); } // Update all visualizations function updateVisualizations() { updateBlockchainVisualization(); updateMempoolVisualization(); updateTransactionNetwork(); displayBlocks(); displayMempool(); } // Initialize blockchain visualization function initBlockchainVisualization() { const container = document.getElementById("blockchainVisualization"); if (!container) return; // Clear previous chart container.innerHTML = ""; const canvas = document.createElement("canvas"); container.appendChild(canvas); const ctx = canvas.getContext("2d"); // Get block data const blockHeights = blockchainData.blocks.map((block) => block.height); const blockSizes = blockchainData.blocks.map((block) => block.size / 1024); // KB const blockColors = blockchainData.blocks.map(() => { return `rgba(0, 116, 217, 0.7)`; // Caravan blue with transparency }); // Create gradient for bars const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height); gradient.addColorStop(0, "rgba(0, 116, 217, 0.8)"); gradient.addColorStop(1, "rgba(0, 116, 217, 0.3)"); // Configure Chart.js Chart.defaults.color = getComputedStyle( document.documentElement, ).getPropertyValue("--text-secondary"); Chart.defaults.borderColor = getComputedStyle( document.documentElement, ).getPropertyValue("--border-color"); blockchainChart = new Chart(ctx, { type: "bar", data: { labels: blockHeights, datasets: [ { label: "Block Size (KB)", data: blockSizes, backgroundColor: gradient, borderColor: "rgba(0, 116, 217, 1)", borderWidth: 1, borderRadius: 4, barPercentage: 0.6, categoryPercentage: 0.8, }, ], }, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 1000, easing: "easeOutQuart", }, plugins: { legend: { display: true, position: "top", labels: { color: getComputedStyle(document.documentElement).getPropertyValue( "--text-color", ), font: { family: "'Inter', sans-serif", size: 12, }, }, }, tooltip: { backgroundColor: getComputedStyle( document.documentElement, ).getPropertyValue("--card-bg"), titleColor: getComputedStyle( document.documentElement, ).getPropertyValue("--caravan-yellow"), bodyColor: getComputedStyle( document.documentElement, ).getPropertyValue("--text-color"), borderColor: getComputedStyle( document.documentElement, ).getPropertyValue("--border-color"), borderWidth: 1, cornerRadius: 6, padding: 10, titleFont: { family: "'Roboto Mono', monospace", size: 13, weight: "bold", }, bodyFont: { family: "'Inter', sans-serif", size: 12, }, callbacks: { title: function (tooltipItems) { return `Block #${tooltipItems[0].label}`; }, label: function (context) { return `Size: ${context.raw.toFixed(2)} KB`; }, afterLabel: function (context) { const blockIndex = context.dataIndex; const block = blockchainData.blocks[blockIndex]; const time = new Date(block.time * 1000).toLocaleString(); return [ `Time: ${time}`, `Transactions: ${block.txCount || 0}`, `Hash: ${block.hash.substring(0, 12)}...`, ]; }, }, }, }, scales: { x: { title: { display: true, text: "Block Height", color: getComputedStyle(document.documentElement).getPropertyValue( "--text-color", ), font: { family: "'Inter', sans-serif", size: 12, }, }, reverse: true, ticks: { color: getComputedStyle(document.documentElement).getPropertyValue( "--text-secondary", ), font: { family: "'Roboto Mono', monospace", size: 11, }, }, grid: { color: getComputedStyle(document.documentElement).getPropertyValue( "--border-color", ), display: false, }, }, y: { title: { display: true, text: "Size (KB)", color: getComputedStyle(document.documentElement).getPropertyValue( "--text-color", ), font: { family: "'Inter', sans-serif", size: 12, }, }, beginAtZero: true, ticks: { color: getComputedStyle(document.documentElement).getPropertyValue( "--text-secondary", ), font: { family: "'Roboto Mono', monospace", size: 11, }, }, grid: { color: getComputedStyle(document.documentElement).getPropertyValue( "--border-color", ), lineWidth: 0.5, }, }, }, }, }); } // Update blockchain visualization function updateBlockchainVisualization() { if (!blockchainChart) return; // Update data blockchainChart.data.labels = blockchainData.blocks.map( (block) => block.height, ); blockchainChart.data.datasets[0].data = blockchainData.blocks.map( (block) => block.size / 1024, ); // Update chart colors based on theme blockchainChart.options.scales.x.ticks.color = getComputedStyle( document.documentElement, ).getPropertyValue("--text-secondary"); blockchainChart.options.scales.y.ticks.color = getComputedStyle( document.documentElement, ).getPropertyValue("--text-secondary"); blockchainChart.options.scales.x.grid.color = getComputedStyle( document.documentElement, ).getPropertyValue("--border-color"); blockchainChart.options.scales.y.grid.color = getComputedStyle( document.documentElement, ).getPropertyValue("--border-color"); blockchainChart.options.scales.x.title.color = getComputedStyle( document.documentElement, ).getPropertyValue("--text-color"); blockchainChart.options.scales.y.title.color = getComputedStyle( document.documentElement, ).getPropertyValue("--text-color"); blockchainChart.options.plugins.legend.labels.color = getComputedStyle( document.documentElement, ).getPropertyValue("--text-color"); // Updating tooltip styles blockchainChart.options.plugins.tooltip.backgroundColor = getComputedStyle( document.documentElement, ).getPropertyValue("--card-bg"); blockchainChart.options.plugins.tooltip.titleColor = getComputedStyle( document.documentElement, ).getPropertyValue("--caravan-yellow"); blockchainChart.options.plugins.tooltip.bodyColor = getComputedStyle( document.documentElement, ).getPropertyValue("--text-color"); blockchainChart.options.plugins.tooltip.borderColor = getComputedStyle( document.documentElement, ).getPropertyValue("--border-color"); blockchainChart.update(); } // Initialize mempool visualization function initMempoolVisualization() { const container = document.getElementById("mempoolVisualization"); if (!container) return; // Clear previous chart container.innerHTML = ""; const canvas = document.createElement("canvas"); container.appendChild(canvas); const ctx = canvas.getContext("2d"); // Update mempool size history const currentSize = blockchainData.mempool?.txCount || 0; mempoolSizeHistory.push(currentSize); mempoolSizeHistory.shift(); // Create gradient for area fill const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height); gradient.addColorStop(0, "rgba(255, 215, 0, 0.7)"); // Caravan yellow with transparency gradient.addColorStop(1, "rgba(255, 215, 0, 0.1)"); // Configure Chart.js Chart.defaults.color = getComputedStyle( document.documentElement, ).getPropertyValue("--text-secondary"); Chart.defaults.borderColor = getComputedStyle( document.documentElement, ).getPropertyValue("--border-color"); mempoolChart = new Chart(ctx, { type: "line", data: { labels: [...Array(mempoolSizeHistory.length).keys()] .map((i) => `${i} min ago`) .reverse(), datasets: [ { label: "Mempool Size (Transactions)", data: mempoolSizeHistory, backgroundColor: gradient, borderColor: "rgba(255, 215, 0, 1)", // Caravan yellow borderWidth: 2, pointBackgroundColor: "rgba(255, 215, 0, 1)", pointBorderColor: getComputedStyle( document.documentElement, ).getPropertyValue("--card-bg"), pointRadius: 4, pointHoverRadius: 6, tension: 0.4, fill: true, }, ], }, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 1000, easing: "easeOutQuart", }, plugins: { legend: { display: true, position: "top", labels: { color: getComputedStyle(document.documentElement).getPropertyValue( "--text-color", ), font: { family: "'Inter', sans-serif", size: 12, }, }, }, tooltip: { backgroundColor: getComputedStyle( document.documentElement, ).getPropertyValue("--card-bg"), titleColor: getComputedStyle( document.documentElement, ).getPropertyValue("--caravan-yellow"), bodyColor: getComputedStyle( document.documentElement, ).getPropertyValue("--text-color"), borderColor: getComputedStyle( document.documentElement, ).getPropertyValue("--border-color"), borderWidth: 1, cornerRadius: 6, padding: 10, titleFont: { family: "'Inter', sans-serif", size: 13, weight: "bold", }, bodyFont: { family: "'Inter', sans-serif", size: 12, }, }, }, scales: { x: { title: { display: true, text: "Time", color: getComputedStyle(document.documentElement).getPropertyValue( "--text-color", ), font: { family: "'Inter', sans-serif", size: 12, }, }, ticks: { color: getComputedStyle(document.documentElement).getPropertyValue( "--text-secondary", ), font: { family: "'Inter', sans-serif", size: 11, }, }, grid: { color: getComputedStyle(document.documentElement).getPropertyValue( "--border-color", ), display: false, }, }, y: { title: { display: true, text: "Transaction Count", color: getComputedStyle(document.documentElement).getPropertyValue( "--text-color", ), font: { family: "'Inter', sans-serif", size: 12, }, }, beginAtZero: true, ticks: { color: getComputedStyle(document.documentElement).getPropertyValue( "--text-secondary", ), font: { family: "'Roboto Mono', monospace", size: 11, }, }, grid: { color: getComputedStyle(document.documentElement).getPropertyValue( "--border-color", ), lineWidth: 0.5, }, }, }, }, }); } // Update mempool visualization function updateMempoolVisualization() { if (!mempoolChart) return; // Update mempool size history const currentSize = blockchainData.mempool?.txCount || 0; mempoolSizeHistory.push(currentSize); mempoolSizeHistory.shift(); // Update chart data mempoolChart.data.datasets[0].data = mempoolSizeHistory; // Update chart colors based on theme mempoolChart.options.scales.x.ticks.color = getComputedStyle( document.documentElement, ).getPropertyValue("--text-secondary"); mempoolChart.options.scales.y.ticks.color = getComputedStyle( document.documentElement, ).getPropertyValue("--text-secondary"); mempoolChart.options.scales.x.grid.color = getComputedStyle( document.documentElement, ).getPropertyValue("--border-color"); mempoolChart.options.scales.y.grid.color = getComputedStyle( document.documentElement, ).getPropertyValue("--border-color"); mempoolChart.options.scales.x.title.color = getComputedStyle( document.documentElement, ).getPropertyValue("--text-color"); mempoolChart.options.scales.y.title.color = getComputedStyle( document.documentElement, ).getPropertyValue("--text-color"); mempoolChart.options.plugins.legend.labels.color = getComputedStyle( document.documentElement, ).getPropertyValue("--text-color"); // Updating tooltip styles mempoolChart.options.plugins.tooltip.backgroundColor = getComputedStyle( document.documentElement, ).getPropertyValue("--card-bg"); mempoolChart.options.plugins.tooltip.titleColor = getComputedStyle( document.documentElement, ).getPropertyValue("--caravan-yellow"); mempoolChart.options.plugins.tooltip.bodyColor = getComputedStyle( document.documentElement, ).getPropertyValue("--text-color"); mempoolChart.options.plugins.tooltip.borderColor = getComputedStyle( document.documentElement, ).getPropertyValue("--border-color"); mempoolChart.update(); } // Initialize transaction network visualization function initTransactionNetwork() { const container = document.getElementById("transactionNetwork"); if (!container) return; // Create nodes and edges from transactions const nodes = []; const edges = []; // Add blocks as nodes blockchainData.blocks.slice(0, 5).forEach((block) => { nodes.push({ id: block.hash, label: `Block ${block.height}`, shape: "box", color: { background: "#0074D9", border: "#0056b3", highlight: { background: "#0074D9", border: "#FFD700", }, }, font: { color: getComputedStyle(document.documentElement).getPropertyValue( "--text-color", ), face: "'Roboto Mono', monospace", size: 12, }, size: 30, }); // Add transactions to the block and connect them if (block.txCount > 0) { // For demonstration, create some transaction nodes for (let i = 0; i < Math.min(block.txCount, 5); i++) { const txid = `tx_${block.height}_${i}`; nodes.push({ id: txid, label: `Tx-${i}`, shape: "dot", color: { background: "#38bdf8", border: "#0284c7", highlight: { background: "#38bdf8", border: "#FFD700", }, }, font: { color: getComputedStyle(document.documentElement).getPropertyValue( "--text-color", ), face: "'Roboto Mono', monospace", size: 11, }, size: 15, }); edges.push({ from: block.hash, to: txid, arrows: "from", color: { color: "#38bdf8", highlight: "#FFD700", }, width: 2, }); } } }); // Add mempool transactions if available if ( blockchainData.mempool && blockchainData.mempool.txids && blockchainData.mempool.txids.length > 0 ) { // Add a mempool node nodes.push({ id: "mempool", label: "Mempool", shape: "hexagon", color: { background: "#FFD700", border: "#e6c100", highlight: { background: "#FFD700", border: "#0074D9", }, }, font: { color: getComputedStyle(document.documentElement).getPropertyValue( "--text-color", ), face: "'Roboto Mono', monospace", size: 12, bold: true, }, size: 35, }); // Add some mempool transactions blockchainData.mempool.txids.slice(0, 10).forEach((txid, i) => { const shortTxid = `mempool_tx_${i}`; nodes.push({ id: shortTxid, label: `Tx-${txid.substring(0, 6)}`, shape: "dot", color: { background: "#FFD700", border: "#e6c100", highlight: { background: "#FFD700", border: "#0074D9", }, }, font: { color: getComputedStyle(document.documentElement).getPropertyValue( "--text-color", ), face: "'Roboto Mono', monospace", size: 10, }, size: 12, }); edges.push({ from: "mempool", to: shortTxid, dashes: true, color: { color: "#FFD700", highlight: "#0074D9", }, width: 1, }); }); } // Create the network visualization const data = { nodes: new vis.DataSet(nodes), edges: new vis.DataSet(edges), }; const options = { physics: { enabled: true, barnesHut: { gravitationalConstant: -2000, centralGravity: 0.3, springLength: 95, springConstant: 0.04, damping: 0.09, }, }, layout: { improvedLayout: true, hierarchical: { enabled: false, }, }, edges: { color: { color: getComputedStyle(document.documentElement).getPropertyValue( "--border-color", ), highlight: "#FFD700", }, smooth: { enabled: true, type: "continuous", }, width: 2, }, nodes: { shape: "dot", size: 16, font: { face: "'Roboto Mono', monospace", size: 12, color: getComputedStyle(document.documentElement).getPropertyValue( "--text-color", ), }, borderWidth: 2, shadow: { enabled: true, color: "rgba(0,0,0,0.2)", size: 5, }, }, interaction: { hover: true, tooltipDelay: 300, multiselect: true, navigationButtons: false, keyboard: { enabled: true, speed: { x: 10, y: 10, zoom: 0.1, }, bindToWindow: false, }, }, }; // Create the network with the data and options transactionNetwork = new vis.Network(container, data, options); // Add click event transactionNetwork.on("click", function (params) { if (params.nodes.length > 0) { const nodeId = params.nodes[0]; if (nodeId.startsWith("tx_") || nodeId.startsWith("mempool_tx_")) { // This would fetch transaction details in a full implementation const txParts = nodeId.split("_"); const txIndex = parseInt(txParts[txParts.length - 1]); // Show some transaction details const detailsElement = document.getElementById("transactionDetails"); if (detailsElement) { detailsElement.innerHTML = ` <div class="details-header"> <h3 style="font-size: 1.25rem; color: var(--caravan-blue); margin-bottom: 0.5rem;">Transaction Details</h3> </div> <div class="details-content"> <div class="details-section"> <h4>Transaction Information</h4> <div class="detail-item"> <span class="detail-label">TxID:</span> <span class="detail-value code">${nodeId}</span> </div> <div class="detail-item"> <span class="detail-label">Status:</span> <span class="detail-value"> ${ nodeId.startsWith("tx_") ? `<span style="color: var(--success-color);"><i class="fas fa-check-circle"></i> Confirmed</span>` : `<span style="color: var(--warning-color);"><i class="fas fa-clock"></i> Pending</span>` } </span> </div> <div class="detail-item"> <span class="detail-label">Simulation Data:</span> <span class="detail-value"> This is a simulated transaction in the network view. </span> </div> </div> </div> `; document.getElementById("closeDetailsBtn").style.display = "block"; } } else if (nodeId === "mempool") { // Show mempool details const detailsElement = document.getElementById("transactionDetails"); if (detailsElement) { detailsElement.innerHTML = ` <div class="details-header"> <h3 style="font-size: 1.25rem; color: var(--caravan-yellow); margin-bottom: 0.5rem;">Mempool Information</h3> </div> <div class="details-content"> <div class="details-section"> <h4>Overview</h4> <div class="detail-item"> <span class="detail-label">Transactions:</span> <span class="detail-value">${blockchainData.mempool?.txCount || 0}</span> </div> <div class="detail-item"> <span class="detail-label">Size:</span> <span class="detail-value">${(blockchainData.mempool?.size / 1024).toFixed(2) || 0} KB</span> </div> <div class="detail-item"> <span class="detail-label">Fees:</span> <span class="detail-value">${blockchainData.mempool?.fees?.toFixed(8) || 0} BTC</span> </div> </div> </div> `; document.getElementById("closeDetailsBtn").style.display = "block"; } } else { // Must be a block node - find the block and show details const block = blockchainData.blocks.find((b) => b.hash === nodeId); if (block) { fetchBlockDetails(block.hash); } } } }); } // Update transaction network function updateTransactionNetwork() { if (!transactionNetwork) return; // Re-initialize with new data initTransactionNetwork(); } // Update network visualization based on selected view type function updateNetworkVisualization(viewType) { if (!transactionNetwork) return; const options = transactionNetwork.getOptions(); switch (viewType) { case "hierarchical": options.layout.hierarchical.enabled = true; options.layout.hierarchical.direction = "UD"; options.layout.hierarchical.sortMethod = "directed"; options.layout.hierarchical.nodeSpacing = 150; options.layout.hierarchical.levelSeparation = 150; break; case "force": options.layout.hierarchical.enabled = false; options.physics.enabled = true; options.physics.barnesHut.gravitationalConstant = -3000; options.physics.barnesHut.centralGravity = 0.5; options.physics.barnesHut.springLength = 120; break; default: options.layout.hierarchical.enabled = false; options.physics.enabled = true; options.physics.barnesHut = { gravitationalConstant: -2000, centralGravity: 0.3, springLength: 95, springConstant: 0.04, damping: 0.09, }; break; } transactionNetwork.setOptions(options); } // Display blocks function displayBlocks() { const blocksContainer = document.getElementById("blocksContainer"); if (!blocksContainer) return; if (!blockchainData.blocks || blockchainData.blocks.length === 0) { blocksContainer.innerHTML = '<p class="empty-message"><i class="fas fa-cube"></i> No blocks found</p>'; return; } let html = ""; blockchainData.blocks.slice(0, 10).forEach((block) => { const time = new Date(block.time * 1000).toLocaleString(); html += ` <div class="block" data-hash="${block.hash}"> <div class="block-header"> <div class="block-height">#${block.height}</div> <div>${time}</div> </div> <div class="block-details"> <div><i class="fas fa-fingerprint"></i> ${block.hash.substring(0, 10)}...</div> <div><i class="fas fa-file-alt"></i> ${(block.size / 1024).toFixed(2)} KB</div> <div><i class="fas fa-exchange-alt"></i> ${block.txCount || 0}</div> </div> </div> `; }); blocksContainer.innerHTML = html; // Add click listeners to blocks document.querySelectorAll(".block").forEach((block) => { block.addEventListener("click", () => { playSound("click"); const hash = block.dataset.hash; fetchBlockDetails(hash); }); }); } // Display mempool transactions function displayMempool() { const mempoolContainer = document.getElementById("mempoolContainer"); if (!mempoolContainer) return; if ( !blockchainData.mempool?.txids || blockchainData.mempool.txids.length === 0 ) { mempoolContainer.innerHTML = '<p class="empty-message"><i class="fas fa-exchange-alt"></i> No transactions in mempool</p>'; return; } // Get sort preference const sortSelect = document.getElementById("mempoolSortSelect"); const sortBy = sortSelect ? sortSelect.value : "time"; // Clone and sort the array let txids = [...blockchainData.mempool.txids]; // In a real app, we would have more transaction data to sort by fee rate, size, etc. // For this demo, we'll just use the txid as a proxy if (sortBy === "feeRate") { txids.sort((a, b) => { // Mock fee rate based on txid const feeA = parseInt(a.substring(0, 8), 16) % 100; const feeB = parseInt(b.substring(0, 8), 16) % 100; return feeB - feeA; // Higher fee first }); } else if (sortBy === "size") { txids.sort((a, b) => { // Mock size based on txid const sizeA = parseInt(a.substring(0, 8), 16) % 1000; const sizeB = parseInt(b.substring(0, 8), 16) % 1000; return sizeB - sizeA; // Larger size first }); } // Default is already by time (order in the array) let html = ""; txids.slice(0, 10).forEach((txid) => { // Mock fee rate based on txid const feeRate = (parseInt(txid.substring(0, 8), 16) % 100) / 10; html += ` <div class="transaction" data-txid="${txid}"> <div class="txid">${txid.substring(0, 16)}...</div> <div style="display: flex; align-items: center; gap: 0.5rem;"> <span style="color: var(--text-secondary); font-size: 0.8rem;">${feeRate.toFixed(1)} sat/vB</span> <button class="action-button secondary" style="padding: 0.25rem 0.5rem; font-size: 0.75rem;"> <i class="fas fa-info-circle"></i> </button> </div> </div> `; }); if (blockchainData.mempool.txids.length > 10) { html += `<p class="more-info">And ${blockchainData.mempool.txids.length - 10} more transactions...</p>`; } mempoolContainer.innerHTML = html; // Add click listeners to transactions document.querySelectorAll(".transaction").forEach((tx) => { tx.addEventListener("click", () => { playSound("click"); const txid = tx.dataset.txid; fetchTransactionDetails(txid); }); }); } // Fetch block details function fetchBlockDetails(hash) { const detailsElement = document.getElementById("transactionDetails"); if (!detailsElement) return; detailsElement.innerHTML = ` <div class="loading"> <div class="spinner"></div> <span>Loading block details...</span> </div> `; document.getElementById("closeDetailsBtn").style.display = "block"; fetch(`/api/block/${hash}`) .then((response) => response.json()) .then((block) => { displayBlockDetails(block); }) .catch((error) => { console.error("Error fetching block details:", error); detailsElement.innerHTML = ` <div class="error-message"> <i class="fas fa-exclamation-triangle" style="color: var(--error-color); font-size: 2rem; margin-bottom: 1rem;"></i> <p>Error loading block details. Please try again.</p> </div> `; playSound("error"); }); } // Fetch transaction details function fetchTransactionDetails(txid) { const detailsElement = document.getElementById("transactionDetails"); if (!detailsElement) return; detailsElement.innerHTML = ` <div class="loading"> <div class="spinner"></div> <span>Loading transaction details...</span> </div> `; document.getElementById("closeDetailsBtn").style.display = "block"; fetch(`/api/tx/${txid}`) .then((response) => response.json()) .then((tx) => { displayTransactionDetails(tx); }) .catch((error) => { console.error("Error fetching transaction details:", error); detailsElement.innerHTML = ` <div class="error-message"> <i class="fas fa-exclamation-triangle" style="color: var(--error-color); font-size: 2rem; margin-bottom: 1rem;"></i> <p>Error loading transaction details. Please try again.</p> </div> `; playSound("error"); }); } // Display block details function displayBlockDetails(block) { const detailsElement = document.getElementById("transactionDetails"); if (!detailsElement) return; const time = new Date(block.time * 1000).toLocaleString(); let html = ` <div class="details-header"> <h3 style="color: var(--caravan-blue);">Block #${block.height}</h3> </div> <div class="details-content"> <div class="details-section"> <h4>Block Information</h4> <div class="detail-item"> <span class="detail-label">Hash:</span> <span class="detail-value code">${block.hash}</span> </div> <div class="detail-item"> <span class="detail-label">Previous Block:</span> <span class="detail-value code">${block.previousBlockHash || "Genesis"}</span> </div> <div class="detail-item"> <span class="detail-label">Time:</span> <span class="detail-value">${time}</span> </div> <div class="detail-item"> <span class="detail-label">Size:</span> <span class="detail-value">${(block.size / 1024).toFixed(2)} KB</span> </div> <div class="detail-item"> <span class="detail-label">Weight:</span> <span class="detail-value">${block.weight}</span> </div> <div class="detail-item"> <span class="detail-label">Transactions:</span> <span class="detail-value">${block.tx?.length || 0}</span> </div> <div class="detail-item"> <span class="detail-label">Difficulty:</span> <span class="detail-value">${block.difficulty}</span> </div> </div> `; if (block.tx && block.tx.length > 0) { html += ` <div class="details-section"> <h4>Transactions</h4> <div style="max-height: 300px; overflow-y: auto;"> `; block.tx.forEach((tx, index) => { const txid = typeof tx === "string" ? tx : tx.txid; html += ` <div class="detail-item" style="padding: 0.5rem 0;"> <span class="detail-label">${index + 1}.</span> <span class="detail-value"> <span class="code" style="font-size: 0.8rem;">${txid}</span> <button class="view-tx-btn action-button secondary" data-txid="${txid}" style="padding: 0.25rem 0.5rem; font-size: 0.75rem; margin-left: 0.5rem;"> <i class="fas fa-external-link-alt"></i> View </button> </span> </div> `; }); html += ` </div> </div> `; } else { html += '<p style="color: var(--text-secondary);">No transactions in this block</p>'; } html += "</div>"; detailsElement.innerHTML = html; // Add event listeners to transaction view buttons document.querySelectorAll(".view-tx-btn").forEach((btn) => { btn.addEventListener("click", (e) => { e.stopPropagation(); playSound("click"); const txid = btn.dataset.txid; fetchTransactionDetails(txid); }); }); } // Display transaction details function displayTransactionDetails(tx) { const detailsElement = document.getElementById("transactionDetails"); if (!detailsElement) return; // Calculate total input and output values let totalInput = 0; let totalOutput = 0; if (tx.vin) { tx.vin.forEach((input) => { if (input.value) totalInput += input.value; }); } if (tx.vout) { tx.vout.forEach((output) => { if (output.value) totalOutput += output.value; }); } const fee = totalInput - totalOutput; let html = ` <div class="details-header"> <h3 style="color: var(--caravan-yellow);">Transaction Details</h3> </div> <div class="details-content"> <div class="details-section"> <h4>Transaction Information</h4> <div class="detail-item"> <span class="detail-label">TxID:</span> <span class="detail-value code">${tx.txid}</span> </div> <div class="detail-item"> <span class="detail-label">Size:</span> <span class="detail-value">${tx.size} bytes</span> </div> <div class="detail-item"> <span class="detail-label">Virtual Size:</span> <span class="detail-value">${tx.vsize} vbytes</span> </div> <div class="detail-item"> <span class="detail-label">Weight:</span> <span class="detail-value">${tx.weight}</span> </div> <div class="detail-item"> <span class="detail-label">Status:</span> <span class="detail-value"> ${ tx.confirmations ? `<span style="color: var(--success-color);"><i class="fas fa-check-circle"></i> Confirmed (${tx.confirmations} confirmations)</span>` : '<span style="color: var(--warning-color);"><i class="fas fa-clock"></i> Unconfirmed</span>' } </span> </div> <div class="detail-item"> <span class="detail-label">Fee:</span> <span class="detail-value">${fee > 0 ? fee.toFixed(8) + " BTC" : "N/A"}</span> </div> </div> `; if (tx.vin && tx.vin.length > 0) { html += ` <div class="details-section"> <h4>Inputs (${tx.vin.length})</h4> <div style="max-height: 200px; overflow-y: auto;"> `; tx.vin.forEach((input, index) => { if (input.coinbase) { html += ` <div class="detail-item" style="padding: 0.5rem 0;"> <span class="detail-label">Coinbase:</span> <span class="detail-value" style="color: var(--success-color);"> <i class="fas fa-coins"></i> New Coins (Block Reward) </span> </div> `; } else { html += ` <div class="detail-item" style="padding: 0.5rem 0;"> <span class="detail-label">${index + 1}.</span> <span class="detail-value"> <div style="display: flex; justify-content: space-between; width: 100%;"> <span class="code" style="font-size: 0.8rem;">${input.txid}:${input.vout}</span> ${input.value ? `<span style="margin-left: 0.5rem;">${input.value} BTC</span>` : ""} </div> </span> </div> `; } }); html += ` </div> </div> `; } if (tx.vout && tx.vout.length > 0) { html += ` <div class="details-section"> <h4>Outputs (${tx.vout.length})</h4> <div style="max-height: 200px; overflow-y: auto;"> `; tx.vout.forEach((output, index) => { const address = output.scriptPubKey && (output.scriptPubKey.address || (output.scriptPubKey.addresses ? output.scriptPubKey.addresses[0] : "No address")); html += ` <div class="detail-item" style="padding: 0.5rem 0;"> <span class="detail-label">${index + 1}.</span> <span class="detail-value"> <div style="display: flex; justify-content: space-between; width: 100%;"> <span>${output.scriptPubKey?.type || "Unknown"}</span> <span style="font-weight: 600;">${output.value} BTC</span> </div> <div style="font-size: 0.8rem; margin-top: 0.25rem;" class="code"> ${address || "Unknown Address"} </div> </span> </div> `; }); html += ` </div> </div> `; } html += "</div>"; detailsElement.innerHTML = html; } // Hide transaction details function hideTransactionDetails() { const detailsElement = document.getElementById("transactionDetails"); if (!detailsElement) return; const closeButton = document.getElementById("closeDetailsBtn"); if (close