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
JavaScript
// 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