/ ethereum-transaction-visualizer.html
ethereum-transaction-visualizer.html
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>Ethereum Transaction Lifecycle Visualizer</title> 7 <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script> 8 <style> 9 body, html { 10 margin: 0; 11 padding: 0; 12 width: 100%; 13 height: 100%; 14 overflow: hidden; 15 font-family: Arial, sans-serif; 16 } 17 #visualization { 18 width: 100%; 19 height: 100%; 20 } 21 #controls { 22 position: fixed; 23 bottom: 20px; 24 right: 20px; 25 width: 150px; 26 height: 150px; 27 } 28 .control-button { 29 position: absolute; 30 width: 60px; 31 height: 30px; 32 background-color: rgba(255, 255, 255, 0.7); 33 border: 1px solid #999; 34 border-radius: 5px; 35 } 36 #upBtn { top: 0; left: 45px; } 37 #leftBtn { top: 60px; left: 0; } 38 #rightBtn { top: 60px; right: 0; } 39 #downBtn { bottom: 0; left: 45px; } 40 #metadata, #addNodeForm { 41 position: fixed; 42 background-color: rgba(255, 255, 255, 0.9); 43 padding: 10px; 44 border-radius: 5px; 45 max-width: 300px; 46 max-height: 80%; 47 overflow-y: auto; 48 } 49 #metadata { 50 bottom: 20px; 51 left: 20px; 52 } 53 #addNodeForm { 54 top: 20px; 55 left: 20px; 56 display: none; /* Initially hidden */ 57 } 58 #metadata input, #metadata textarea, #addNodeForm input, #addNodeForm select { 59 width: 100%; 60 margin-bottom: 10px; 61 } 62 #metadata button, #addNodeForm button { 63 margin-right: 10px; 64 } 65 #editToggle { 66 position: fixed; 67 top: 20px; 68 right: 20px; 69 } 70 </style> 71 </head> 72 <body> 73 <div id="Title"> 74 <h1>Preconfirmations</h1> 75 </div> 76 <div id="SubTitle"> 77 <h2>The setting</h2> 78 <div id="Description"> 79 <p>Click on the "Up", "Down", "Previous", and "Next" buttons to navigate through the Ethereum transaction lifecycle.</p> 80 </div> 81 <div id="visualization"></div> 82 <div id="controls"> 83 <button id="upBtn" class="control-button">Up</button> 84 <button id="leftBtn" class="control-button">Previous</button> 85 <button id="rightBtn" class="control-button">Next</button> 86 <button id="downBtn" class="control-button">Down</button> 87 </div> 88 <div id="metadata"></div> 89 <div id="addNodeForm"> 90 <h3>Add New Node</h3> 91 <input type="text" id="newNodeName" placeholder="Node Name"> 92 <select id="newNodeDirection"> 93 <option value="next">Next</option> 94 <option value="up">Up</option> 95 <option value="down">Down</option> 96 </select> 97 <button onclick="addNewNode()">Add Node</button> 98 </div> 99 <button id="editToggle" onclick="toggleEditMode()">Edit Mode</button> 100 101 <script> 102 let nodeIds = ["User"]; 103 let connectionMatrix = { 104 "User": {} 105 }; 106 let nodeMetadata = { 107 "User": { 108 "content": "Initiates and signs the transaction", 109 "url": "https://ethereum.org/en/developers/docs/transactions/" 110 } 111 }; 112 113 const directionToGridDelta = { 114 "next": [1, 0], 115 "previous": [-1, 0], 116 "up": [0, -1], 117 "down": [0, 1] 118 }; 119 120 let isEditMode = false; 121 122 function assignGridPositions(startNode) { 123 const gridPositions = {}; 124 const queue = [[startNode, 0, 0]]; 125 const visited = new Set(); 126 127 while (queue.length > 0) { 128 const [node, x, y] = queue.shift(); 129 if (visited.has(node)) continue; 130 visited.add(node); 131 gridPositions[node] = [x, y]; 132 133 for (let neighbor in connectionMatrix[node]) { 134 const connection = connectionMatrix[node][neighbor]; 135 if (connection) { 136 const [dx, dy] = directionToGridDelta[connection[0]]; 137 queue.push([neighbor, x + dx, y + dy]); 138 } 139 } 140 } 141 142 return gridPositions; 143 } 144 145 function createVisualization() { 146 const width = window.innerWidth; 147 const height = window.innerHeight; 148 const nodeRadius = Math.min(width, height) * 0.05; 149 const transactionRadius = nodeRadius * 0.3; 150 const gridSize = Math.min(width, height) * 0.2; 151 152 d3.select("#visualization").selectAll("*").remove(); 153 154 const gridPositions = assignGridPositions(nodeIds[0]); 155 156 let nodes = nodeIds.map(id => ({ 157 id: id, 158 x: (gridPositions[id][0] + 2) * gridSize, 159 y: (gridPositions[id][1] + 2) * gridSize 160 })); 161 162 const links = []; 163 for (let source in connectionMatrix) { 164 for (let target in connectionMatrix[source]) { 165 if (connectionMatrix[source][target]) { 166 links.push({ 167 source: nodes.find(n => n.id === source), 168 target: nodes.find(n => n.id === target), 169 type: connectionMatrix[source][target][0] 170 }); 171 } 172 } 173 } 174 175 const svg = d3.select("#visualization") 176 .append("svg") 177 .attr("width", width) 178 .attr("height", height); 179 180 const link = svg.append("g") 181 .selectAll("line") 182 .data(links) 183 .join("line") 184 .attr("stroke", "#999") 185 .attr("stroke-opacity", 0.6) 186 .attr("x1", d => d.source.x) 187 .attr("y1", d => d.source.y) 188 .attr("x2", d => d.target.x) 189 .attr("y2", d => d.target.y); 190 191 const node = svg.append("g") 192 .selectAll("circle") 193 .data(nodes) 194 .join("circle") 195 .attr("r", nodeRadius) 196 .attr("fill", "lightblue") 197 .attr("cx", d => d.x) 198 .attr("cy", d => d.y); 199 200 const label = svg.append("g") 201 .selectAll("text") 202 .data(nodes) 203 .join("text") 204 .text(d => d.id) 205 .attr("font-size", nodeRadius * 0.3) 206 .attr("text-anchor", "middle") 207 .attr("dominant-baseline", "central") 208 .attr("x", d => d.x) 209 .attr("y", d => d.y); 210 211 const transaction = svg.append("circle") 212 .attr("r", transactionRadius) 213 .attr("fill", "red"); 214 215 return { nodes, transaction }; 216 } 217 218 let { nodes, transaction } = createVisualization(); 219 let currentNode = nodes[0]; 220 transaction.attr("cx", currentNode.x).attr("cy", currentNode.y); 221 222 const directionMapping = { 223 "up": "up", 224 "down": "down", 225 "next": "right", 226 "previous": "left" 227 }; 228 229 function updateMetadataDisplay(nodeId) { 230 const metadata = nodeMetadata[nodeId]; 231 let html = `<h3><a href="${metadata.url}" target="_blank">${nodeId}</a></h3>`; 232 if (isEditMode) { 233 html += ` 234 <form id="metadataForm"> 235 <input type="text" id="nodeNameInput" value="${nodeId}"> 236 <textarea id="content" name="content" placeholder="Content">${metadata.content}</textarea> 237 <input type="url" id="url" name="url" value="${metadata.url}" placeholder="URL"> 238 <button type="button" onclick="saveMetadata('${nodeId}')">Save</button> 239 <button type="button" onclick="resetMetadata('${nodeId}')">Reset</button> 240 <button type="button" onclick="deleteNode('${nodeId}')">Delete</button> 241 </form>`; 242 } else { 243 html += `<p>${metadata.content}</p>`; 244 } 245 document.getElementById("metadata").innerHTML = html; 246 } 247 248 function saveMetadata(oldNodeId) { 249 const newNodeId = document.getElementById("nodeNameInput").value; 250 const content = document.getElementById("content").value; 251 const url = document.getElementById("url").value; 252 253 const newMetadata = { 254 content: content, 255 url: url 256 }; 257 258 if (oldNodeId !== newNodeId) { 259 nodeMetadata[newNodeId] = newMetadata; 260 delete nodeMetadata[oldNodeId]; 261 nodeIds = nodeIds.map(id => id === oldNodeId ? newNodeId : id); 262 263 // Update connectionMatrix 264 connectionMatrix[newNodeId] = connectionMatrix[oldNodeId]; 265 delete connectionMatrix[oldNodeId]; 266 for (let node in connectionMatrix) { 267 if (connectionMatrix[node][oldNodeId]) { 268 connectionMatrix[node][newNodeId] = connectionMatrix[node][oldNodeId]; 269 delete connectionMatrix[node][oldNodeId]; 270 } 271 } 272 273 currentNode = { id: newNodeId, x: currentNode.x, y: currentNode.y }; 274 //Update the nodes object 275 nodes = nodes.map(n => n.id === oldNodeId ? currentNode : n); 276 console.log(nodes); 277 } else { 278 nodeMetadata[newNodeId] = newMetadata; 279 } 280 281 ({ nodes, transaction } = createVisualization()); 282 transaction.attr("cx", currentNode.x).attr("cy", currentNode.y); 283 updateButtons(); 284 updateMetadataDisplay(newNodeId); 285 } 286 287 function resetMetadata(nodeId) { 288 nodeMetadata[nodeId] = { 289 "content": "", 290 "url": "" 291 }; 292 updateMetadataDisplay(nodeId); 293 } 294 295 function moveTransaction(direction) { 296 for (let targetId in connectionMatrix[currentNode.id]) { 297 const connection = connectionMatrix[currentNode.id][targetId]; 298 if (connection && connection[0] === direction) { 299 const targetNode = nodes.find(n => n.id === targetId); 300 if (targetNode) { 301 currentNode = targetNode; 302 transaction.transition() 303 .duration(500) 304 .attr("cx", currentNode.x) 305 .attr("cy", currentNode.y); 306 updateButtons(); 307 updateMetadataDisplay(currentNode.id); 308 break; 309 } 310 } 311 } 312 } 313 314 function updateButtons() { 315 for (let direction in directionMapping) { 316 const buttonId = `#${directionMapping[direction]}Btn`; 317 let isEnabled = false; 318 for (let targetId in connectionMatrix[currentNode.id]) { 319 const connection = connectionMatrix[currentNode.id][targetId]; 320 if (connection && connection[0] === direction) { 321 isEnabled = true; 322 break; 323 } 324 } 325 d3.select(buttonId).property("disabled", !isEnabled); 326 } 327 } 328 329 function toggleEditMode() { 330 isEditMode = !isEditMode; 331 updateMetadataDisplay(currentNode.id); 332 document.getElementById("editToggle").textContent = isEditMode ? "View Mode" : "Edit Mode"; 333 document.getElementById("addNodeForm").style.display = isEditMode ? "block" : "none"; 334 } 335 336 function addNewNode() { 337 const newNodeName = document.getElementById("newNodeName").value; 338 const direction = document.getElementById("newNodeDirection").value; 339 340 if (!newNodeName || nodeIds.includes(newNodeName)) { 341 alert("Please enter a unique node name."); 342 return; 343 } 344 345 nodeIds.push(newNodeName); 346 connectionMatrix[newNodeName] = {}; 347 nodeMetadata[newNodeName] = { 348 "content": "", 349 "url": "" 350 }; 351 352 connectionMatrix[currentNode.id][newNodeName] = [direction, directionToGridDelta[direction]]; 353 const oppositeDirection = direction === "next" ? "previous" : (direction === "up" ? "down" : "up"); 354 connectionMatrix[newNodeName][currentNode.id] = [oppositeDirection, directionToGridDelta[oppositeDirection]]; 355 356 ({ nodes, transaction } = createVisualization()); 357 358 // Move the transaction to the new node 359 currentNode = nodes.find(n => n.id === newNodeName); 360 transaction.transition() 361 .duration(500) 362 .attr("cx", currentNode.x) 363 .attr("cy", currentNode.y); 364 365 updateButtons(); 366 updateMetadataDisplay(currentNode.id); 367 368 // Clear the input field 369 document.getElementById("newNodeName").value = ""; 370 } 371 372 d3.select("#upBtn").on("click", () => moveTransaction("up")); 373 d3.select("#downBtn").on("click", () => moveTransaction("down")); 374 d3.select("#leftBtn").on("click", () => moveTransaction("previous")); 375 d3.select("#rightBtn").on("click", () => moveTransaction("next")); 376 377 updateButtons(); 378 updateMetadataDisplay(currentNode.id); 379 380 window.addEventListener('resize', () => { 381 ({ nodes, transaction } = createVisualization()); 382 currentNode = nodes.find(n => n.id === currentNode.id); 383 transaction.attr("cx", currentNode.x).attr("cy", currentNode.y); 384 updateButtons(); 385 updateMetadataDisplay(currentNode.id); 386 }); 387 388 function deleteNode(nodeId) { 389 if (nodeId === "User") { 390 alert("Cannot delete the initial node."); 391 return; 392 } 393 394 delete nodeMetadata[nodeId]; 395 delete connectionMatrix[nodeId]; 396 nodeIds = nodeIds.filter(id => id !== nodeId); 397 for (let node in connectionMatrix) { 398 delete connectionMatrix[node][nodeId]; 399 } 400 401 ({ nodes, transaction } = createVisualization()); 402 currentNode = nodes[0]; 403 transaction.attr("cx", currentNode.x).attr("cy", currentNode.y); 404 updateButtons(); 405 updateMetadataDisplay(currentNode.id); 406 } 407 </script> 408 </body> 409 </html>