diff --git a/lnsimulator/lnsimulator.html b/lnsimulator/lnsimulator.html index c1e6058..79a6ae1 100644 --- a/lnsimulator/lnsimulator.html +++ b/lnsimulator/lnsimulator.html @@ -3,89 +3,374 @@ - Interactive Lightning Network Animation - + Lightning Network Visualization + -
-

Lightning Network Simulator

-

This interactive simulator helps you understand how payments are routed through a Lightning Network. Experiment with different network configurations and see how payments find their way from sender to receiver!

-
- -
-

How to use:

-
    -
  1. Choose the number of nodes and channel probability, then click "Regenerate Network".
  2. -
  3. Select a sender node, receiver node, and payment amount.
  4. -
  5. Click "Send Payment" to see the payment route through the network.
  6. -
+

Lightning Network Visualization

+
+ + +
- -
-
-
- - - - - - - -
- - -
-
-
+
+ + + + + + +
-
-

Transaction History

- - - - - - - - - - - - - - - - -
AttemptFromToAmountStatusFeesPathUpdated Balances
+ + +
+

Transaction History

+

No Transactions

- + + + diff --git a/lnsimulator/script.js b/lnsimulator/script.js index d5f18c1..e69de29 100644 --- a/lnsimulator/script.js +++ b/lnsimulator/script.js @@ -1,409 +0,0 @@ -// This simulator demonstrates the basic concepts of a Lightning Network: -// - Nodes: Participants in the network -// - Channels: Payment channels between nodes -// - Routing: Finding a path to send payments between nodes - -const network = document.querySelector('.network'); -const nodeCountSelect = document.getElementById('nodeCount'); -const channelProbabilitySelect = document.getElementById('channelProbability'); -const regenerateButton = document.getElementById('regenerate'); -const sendPaymentButton = document.getElementById('sendPayment'); -const tooltip = document.getElementById('tooltip'); - -let nodes = []; -let channels = []; -let transactionHistory = []; -let networkRect; // Define networkRect globally - -// Generates a random Lightning Network with the specified number of nodes and channel probability -function generateNetwork() { - network.innerHTML = ''; - nodes = []; - channels = []; - - const numNodes = parseInt(nodeCountSelect.value); - const channelProbability = parseFloat(channelProbabilitySelect.value); - - networkRect = network.getBoundingClientRect(); - - const networkWidth = networkRect.width; - const networkHeight = networkRect.height; - - - - for (let i = 0; i < numNodes; i++) { - const node = document.createElement('div'); - node.className = 'node'; - node.textContent = String.fromCharCode(65 + i % 26) + (i >= 26 ? Math.floor(i / 26) : ''); - node.style.left = `${Math.random() * (networkWidth - 30)}px`; - node.style.top = `${Math.random() * (networkHeight - 30)}px`; - network.appendChild(node); - nodes.push(node); - } - - for (let i = 0; i < numNodes; i++) { - for (let j = i + 1; j < numNodes; j++) { - if (Math.random() < channelProbability) { - const channel = document.createElement('div'); - channel.className = 'channel'; - network.appendChild(channel); - const capacity = generateCapacity(); - const channelClass = getChannelClass(capacity); - channel.classList.add(channelClass); - const balance1 = Math.floor(capacity * Math.random()); - const channelData = { - element: channel, - start: i, - end: j, - capacity: capacity, - balance1: balance1, - balance2: capacity - balance1, - baseFee: 1000, - feeRate: 0.001 - }; - channels.push(channelData); - - channel.addEventListener('mouseenter', (e) => showTooltip(e, channelData)); - channel.addEventListener('mouseleave', hideTooltip); - } - } - } - - updateChannels(); // Pass it to updateChannels - populateNodeDropdowns(); - validateChannelBalances(); -} - -// Simulates the random capacity of a payment channel -function generateCapacity() { - const capacities = [ - 1000000, 5000000, 10000000, 20000000, 30000000, 40000000, 50000000, - 60000000, 70000000, 80000000, 90000000, 100000000, 200000000 - ]; - return capacities[Math.floor(Math.random() * capacities.length)]; -} - -function formatCapacity(sats) { - if (sats >= 100000000) { - return (sats / 100000000).toFixed(2) + " BTC"; - } else { - return (sats / 1000000).toFixed(2) + "m sats"; - } -} - -function updateChannels() { - channels.forEach(channel => { - const start = nodes[channel.start].getBoundingClientRect(); - const end = nodes[channel.end].getBoundingClientRect(); - const dx = end.left - start.left; - const dy = end.top - start.top; - const length = Math.sqrt(dx*dx + dy*dy); - const angle = Math.atan2(dy, dx) * 180 / Math.PI; - - const minWidth = 1; - const maxWidth = 5; - const minCapacity = 1000000; // 1m sats - const maxCapacity = 200000000; // 2 BTC - - const logWidth = Math.log(channel.capacity / minCapacity) / Math.log(maxCapacity / minCapacity); - const width = minWidth + (maxWidth - minWidth) * logWidth; - - channel.element.style.width = `${length}px`; - channel.element.style.height = `${width}px`; - channel.element.style.left = `${start.left - networkRect.left + 15}px`; - channel.element.style.top = `${start.top - networkRect.top + 15}px`; - channel.element.style.transform = `rotate(${angle}deg)`; - - visualizeChannelBalance(channel); - }); -} - -function visualizeChannelBalance(channel) { - const balance1Percent = (channel.balance1 / channel.capacity) * 100; - - const balance1Element = document.createElement('div'); - balance1Element.className = 'channel-balance balance1'; - balance1Element.style.width = `${balance1Percent}%`; - - const balance2Element = document.createElement('div'); - balance2Element.className = 'channel-balance balance2'; - balance2Element.style.width = `${100 - balance1Percent}%`; - - channel.element.innerHTML = ''; - channel.element.appendChild(balance1Element); - channel.element.appendChild(balance2Element); -} - -function showTooltip(event, channel) { - tooltip.innerHTML = ` - Total Capacity: ${formatCapacity(channel.capacity)}
- Node ${nodes[channel.start].textContent} Balance: ${formatCapacity(channel.balance1)}
- Node ${nodes[channel.end].textContent} Balance: ${formatCapacity(channel.balance2)} - `; - tooltip.style.left = `${event.pageX + 10}px`; - tooltip.style.top = `${event.pageY + 10}px`; - tooltip.style.opacity = 1; -} - -function hideTooltip() { - tooltip.style.opacity = 0; -} - -function populateNodeDropdowns() { - const senderSelect = document.getElementById('senderNode'); - const receiverSelect = document.getElementById('receiverNode'); - senderSelect.innerHTML = ''; - receiverSelect.innerHTML = ''; - - nodes.forEach((node, index) => { - const option = document.createElement('option'); - option.value = index; - option.textContent = node.textContent; - senderSelect.appendChild(option.cloneNode(true)); - receiverSelect.appendChild(option); - }); -} - -function getChannelClass(capacity) { - if (capacity < 5000001) return 'Myway'; - if (capacity < 100000001) return 'Highway'; - return 'Freeway'; -} - -// Implements a simple pathfinding algorithm to route payments through the network -function findPathWithCapacity(start, end, amount) { - const visited = new Set(); - const queue = [[start]]; - const fees = new Map([[start, 0]]); - - while (queue.length > 0) { - const path = queue.shift(); - const node = path[path.length - 1]; - - if (node === end) { - return { path, totalFees: fees.get(node) }; - } - - if (!visited.has(node)) { - visited.add(node); - const neighbors = channels - .filter(c => { - if (c.start === node && c.balance1 >= amount + fees.get(node)) return true; - if (c.end === node && c.balance2 >= amount + fees.get(node)) return true; - return false; - }) - .map(c => c.start === node ? c.end : c.start); - - for (const neighbor of neighbors) { - const channel = channels.find(c => - (c.start === node && c.end === neighbor) || - (c.end === node && c.start === neighbor) - ); - const fee = channel.baseFee + Math.floor((amount + fees.get(node)) * channel.feeRate); - const totalFees = fees.get(node) + fee; - - if (!fees.has(neighbor) || totalFees < fees.get(neighbor)) { - fees.set(neighbor, totalFees); - queue.push([...path, neighbor]); - } - } - } - } - - return { path: [], totalFees: 0 }; -} - -function highlightPath(path) { - path.forEach((nodeIndex, i) => { - if (i < path.length - 1) { - const channel = channels.find(c => - (c.start === nodeIndex && c.end === path[i + 1]) || - (c.end === nodeIndex && c.start === path[i + 1]) - ); - if (channel) { - channel.element.classList.add('highlighted'); - } - } - nodes[nodeIndex].classList.add('highlighted'); - }); -} - -function clearHighlights() { - channels.forEach(channel => channel.element.classList.remove('highlighted')); - nodes.forEach(node => node.classList.remove('highlighted')); -} - -// Simulates and visualizes a payment being routed through the network -function animateTransaction() { - const senderIndex = parseInt(document.getElementById('senderNode').value); - const receiverIndex = parseInt(document.getElementById('receiverNode').value); - const paymentAmount = parseInt(document.getElementById('paymentAmount').value); - - if (senderIndex === receiverIndex) { - alert("Sender and receiver must be different nodes."); - return; - } - - const { path, totalFees } = findPathWithCapacity(senderIndex, receiverIndex, paymentAmount); - if (path.length === 0) { - const failedTransaction = { - attempt: transactionHistory.length + 1, - from: nodes[senderIndex].textContent, - to: nodes[receiverIndex].textContent, - amount: paymentAmount, - status: 'Failed', - reason: 'No path found' - }; - addTransactionToHistory(failedTransaction); - alert(`No path found for the payment of ${formatCapacity(paymentAmount)}. Try a smaller amount or choose different nodes.`); - return; - } - - clearHighlights(); - highlightPath(path); - - const transaction = document.createElement('div'); - transaction.className = 'transaction'; - network.appendChild(transaction); - - let step = 0; - function animate() { - if (step >= path.length - 1) { - transaction.remove(); - const updatedBalances = updateChannelBalances(path, paymentAmount, totalFees); - validateChannelBalances(); - - const successfulTransaction = { - attempt: transactionHistory.length + 1, - from: nodes[senderIndex].textContent, - to: nodes[receiverIndex].textContent, - amount: paymentAmount, - path: path.map(nodeIndex => nodes[nodeIndex].textContent), - fees: totalFees, - status: 'Success', - updatedBalances: updatedBalances - }; - addTransactionToHistory(successfulTransaction); - - alert(`Payment of ${formatCapacity(paymentAmount)} successfully sent from Node ${nodes[senderIndex].textContent} to Node ${nodes[receiverIndex].textContent}\nTotal fees paid: ${formatCapacity(totalFees)}`); - return; - } - - const startNode = nodes[path[step]]; - const endNode = nodes[path[step + 1]]; - const start = startNode.getBoundingClientRect(); - const end = endNode.getBoundingClientRect(); - const startX = start.left - networkRect.left + 15; - const startY = start.top - networkRect.top + 15; - const endX = end.left - networkRect.left + 15; - const endY = end.top - networkRect.top + 15; - - transaction.style.display = 'block'; - transaction.style.left = `${startX}px`; - transaction.style.top = `${startY}px`; - - const duration = 1000; - const startTime = Date.now(); - - function moveTransaction() { - const elapsed = Date.now() - startTime; - const progress = Math.min(elapsed / duration, 1); - const x = startX + (endX - startX) * progress; - const y = startY + (endY - startY) * progress; - transaction.style.left = `${x}px`; - transaction.style.top = `${y}px`; - - if (progress < 1) { - requestAnimationFrame(moveTransaction); - } else { - step++; - animate(); - } - } - - moveTransaction(); - } - - animate(); -} - -function updateChannelBalances(path, amount, fees) { - let remainingAmount = amount + fees; - const updatedBalances = []; - for (let i = 0; i < path.length - 1; i++) { - const channel = channels.find(c => - (c.start === path[i] && c.end === path[i + 1]) || - (c.end === path[i] && c.start === path[i + 1]) - ); - - const fee = channel.baseFee + Math.floor(remainingAmount * channel.feeRate); - const amountWithFee = remainingAmount; - remainingAmount -= fee; - - if (channel.start === path[i]) { - channel.balance1 -= amountWithFee; - channel.balance2 += amountWithFee - fee; - } else { - channel.balance2 -= amountWithFee; - channel.balance1 += amountWithFee - fee; - } - - updatedBalances.push({ - channel: `${nodes[channel.start].textContent}-${nodes[channel.end].textContent}`, - balance1: channel.balance1, - balance2: channel.balance2 - }); - } - updateChannels(); - return updatedBalances; -} - -function validateChannelBalances() { - channels.forEach(channel => { - if (channel.balance1 + channel.balance2 !== channel.capacity) { - console.error('Invalid channel balance:', channel); - } - }); -} - -function addTransactionToHistory(transaction) { - transactionHistory.push(transaction); - updateTransactionTable(); -} - -function updateTransactionTable() { - const tableBody = document.getElementById('transactionTableBody'); - tableBody.innerHTML = ''; // Clear existing rows - - transactionHistory.forEach(transaction => { - const row = document.createElement('tr'); - - row.innerHTML = ` - ${transaction.attempt} - ${transaction.from} - ${transaction.to} - ${formatCapacity(transaction.amount)} - ${transaction.status} - ${transaction.status === 'Success' ? formatCapacity(transaction.fees) : 'N/A'} - ${transaction.status === 'Success' ? transaction.path.join(' → ') : 'N/A'} - ${transaction.status === 'Success' ? formatUpdatedBalances(transaction.updatedBalances) : 'N/A'} - `; - - tableBody.appendChild(row); - }); -} - -function formatUpdatedBalances(balances) { - return balances.map(b => `${b.channel}: ${formatCapacity(b.balance1)} | ${formatCapacity(b.balance2)}`).join('
'); -} - -regenerateButton.addEventListener('click', generateNetwork); -sendPaymentButton.addEventListener('click', animateTransaction); - -// Initial network generation -generateNetwork(); - -// Add a window resize event listener to update the network when the window is resized -window.addEventListener('resize', () => { - updateChannels(); -}); diff --git a/lnsimulator/styles.css b/lnsimulator/styles.css index 235f9b3..e69de29 100644 --- a/lnsimulator/styles.css +++ b/lnsimulator/styles.css @@ -1,222 +0,0 @@ -body { - font-family: Arial, sans-serif; - margin: 0; - padding: 20px; - display: flex; - flex-direction: column; /* Stack elements vertically */ - min-height: 100vh; /* Full height of the viewport */ -} - -header { - margin-bottom: 20px; -} - -.instructions { - margin-bottom: 20px; /* Space between instructions and controls */ -} - -.main-container { - display: flex; /* Use flexbox for the main container */ - flex-grow: 1; /* Allow it to take the remaining space */ -} - -.controls { - display: flex; - flex-direction: column; - width: 250px; /* Fixed width for the controls */ - padding: 10px; /* Add some padding */ - box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1); - background-color: #f0f0f0; /* Background color for better contrast */ - height: 100%; -} - -.top-controls { - display: flex; /* Align items in the top control group vertically */ - flex-direction: column; /* Stack items vertically */ - margin-bottom: 15px; /* Space below the top controls */ - background-color: #e0e0e0; /* Different background color for top controls */ - padding: 10px; /* Padding for top controls */ - border-radius: 5px; /* Optional: Rounded corners */ -} - -.sidebar-controls { - display: flex; - flex-direction: column; - background-color: #ffffff; /* Different background color for sidebar controls */ - padding: 10px; /* Padding for sidebar controls */ - border-radius: 5px; /* Optional: Rounded corners */ -} - -.sidebar-controls label, -.sidebar-controls select, -.sidebar-controls button { - margin-bottom: 8px; /* Reduced space between elements in the sidebar */ -} - -label { - margin-right: 5px; /* Reduced margin for a more compact look */ -} - -select, button { - margin-right: 10px; /* Reduced margin for a more compact look */ -} - -.network { - flex-grow: 1; /* Allow the network graph to take the remaining space */ - padding: 10px; /* Add some padding */ - background-color: #f9f9f9; /* Background color for the network area */ - border-left: 1px solid #ddd; /* Border to separate from the sidebar */ - position: relative; - height: 600px; /* Fixed height for the network area */ - overflow: hidden; /* Hide overflow to keep it tidy */ -} - -.node { - position: absolute; - width: 30px; - height: 30px; - background-color: #3498db; - border-radius: 50%; - display: flex; - justify-content: center; - align-items: center; - color: white; - font-weight: bold; - cursor: pointer; - transition: transform 0.2s; -} - -.node:hover { - transform: scale(1.1); -} - -.channel { - position: absolute; - transform-origin: 0 50%; /* Keep this to start from the center */ - transition: opacity 0.2s; - overflow: hidden; -} - -.channel-balance { - height: 100%; - float: left; -} - -.balance1 { - background-color: #3498db; -} - -.balance2 { - background-color: #2ecc71; -} - -.channel:hover { - opacity: 0.8; -} - -.transaction { - position: absolute; - width: 10px; - height: 10px; - background-color: #e74c3c; - border-radius: 50%; -} - -.tooltip { - position: absolute; - background-color: rgba(0, 0, 0, 0.8); - color: white; - padding: 5px; - border-radius: 3px; - font-size: 12px; - pointer-events: none; - opacity: 0; - transition: opacity 0.3s; -} - -.legend { - margin-top: 15px; /* Reduced margin for a more compact look */ -} - -.legend ul { - list-style-type: none; - padding: 0; -} - -.legend li { - margin-bottom: 5px; -} - -.node-icon, .channel-icon { - display: inline-block; - width: 20px; - height: 20px; - margin-right: 10px; - vertical-align: middle; -} - -.node-icon { - background-color: #3498db; - border-radius: 50%; -} - -.channel-icon { - background-color: #2ecc71; -} - -.channel-icon.small { - height: 2px; -} - -.channel-icon.medium { - height: 4px; -} - -.channel-icon.large { - height: 6px; -} - -.instructions { - margin-top: 15px; /* Reduced margin for a more compact look */ -} - -.instructions ol { - padding-left: 20px; -} - -.highlighted { - box-shadow: 0 0 10px #f39c12; -} - -.node.highlighted { - background-color: #f39c12; -} - -.channel.highlighted { - background-color: #f39c12; -} - -.transaction-table-container { - margin-top: 20px; - max-height: 300px; - overflow-y: auto; -} - -#transactionTable { - width: 100%; - border-collapse: collapse; -} - -#transactionTable th, #transactionTable td { - border: 1px solid #ddd; - padding: 8px; - text-align: left; -} - -#transactionTable th { - background-color: #f2f2f2; -} - -#transactionTable tr:nth-child(even) { - background-color: #f9f9f9; -}