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

863 lines (862 loc) 45.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.VisualizationCommands = void 0; const prompts_1 = require("@inquirer/prompts"); const VisualizationManager_1 = require("../visualization/VisualizationManager"); const terminal_1 = require("../utils/terminal"); const ora_1 = __importDefault(require("ora")); const open_1 = __importDefault(require("open")); const chalk_1 = __importDefault(require("chalk")); /** * Commands for blockchain visualization */ class VisualizationCommands { constructor(configManager, bitcoinRpcClient, bitcoinService) { this.visualizationManager = null; this.configManager = configManager; this.bitcoinRpcClient = bitcoinRpcClient; this.bitcoinService = bitcoinService; } /** * Initialize visualization manager if not already initialized */ async getVisualizationManager() { if (!this.visualizationManager) { this.visualizationManager = new VisualizationManager_1.VisualizationManager(this.bitcoinRpcClient, this.configManager); } return this.visualizationManager; } /** * Start the blockchain visualization */ async startVisualization() { (0, terminal_1.displayCommandTitle)("Blockchain Visualization"); try { // Check if Bitcoin Core is running const spinner = (0, ora_1.default)("Checking Bitcoin Core connection...").start(); let bitcoinCoreRunning = false; try { const blockchainInfo = (await this.bitcoinRpcClient.callRpc("getblockchaininfo")); bitcoinCoreRunning = blockchainInfo.chain === "regtest"; spinner.succeed("Connected to Bitcoin Core (regtest mode)"); } catch (error) { spinner.fail("Could not connect to Bitcoin Core"); console.log((0, terminal_1.formatError)("Bitcoin Core is not running or RPC connection failed. Visualization requires a working Bitcoin Core connection.")); return; } if (!bitcoinCoreRunning) { console.log((0, terminal_1.formatWarning)("Bitcoin Core is not running in regtest mode. Visualization requires regtest mode.")); return; } // Get visualization manager const vizManager = await this.getVisualizationManager(); // Check if server is already running if (vizManager.isServerRunning()) { console.log((0, terminal_1.formatWarning)("Visualization server is already running.")); const openOptions = [ { name: terminal_1.colors.highlight("Open in browser"), value: "open" }, { name: terminal_1.colors.highlight("Restart server"), value: "restart" }, { name: terminal_1.colors.highlight("Stop server"), value: "stop" }, { name: terminal_1.colors.highlight("Do nothing"), value: "nothing" }, ]; const action = await (0, prompts_1.select)({ message: "What would you like to do?", choices: openOptions, }); switch (action) { case "open": console.log(terminal_1.colors.info("Opening visualization in browser...")); await (0, open_1.default)(vizManager.getServerUrl()); break; case "restart": await vizManager.stop(); await vizManager.start(true); break; case "stop": await vizManager.stop(); break; case "nothing": console.log(terminal_1.colors.info("Visualization server remains running.")); break; } return; } // Ask about port const port = await (0, prompts_1.number)({ message: "Enter port for visualization server:", default: 3000, }); // Start the visualization server const startSpinner = (0, ora_1.default)("Starting visualization server...").start(); try { // Create a new visualization manager with the specified port this.visualizationManager = new VisualizationManager_1.VisualizationManager(this.bitcoinRpcClient, this.configManager, port); await this.visualizationManager.start(false); startSpinner.succeed("Visualization server started"); // Apply some animation to make the message more noticeable console.log("\n"); console.log(chalk_1.default.bgGreenBright.black(" VISUALIZATION READY ")); console.log("\n"); console.log((0, terminal_1.boxText)(`The blockchain visualization is now running on port ${terminal_1.colors.highlight(port.toString())}.\n\n` + `You can access it at: ${terminal_1.colors.highlight(`http://localhost:${port}/`)}\n\n` + `This visualization includes:\n` + `${terminal_1.colors.success("✓")} Interactive block explorer\n` + `${terminal_1.colors.success("✓")} Real-time transaction monitoring\n` + `${terminal_1.colors.success("✓")} Network visualization\n` + `${terminal_1.colors.success("✓")} Mining activity logs\n` + `${terminal_1.colors.success("✓")} Animated blockchain updates\n\n` + `Keep this terminal window open to maintain the visualization server.\n` + `Press Ctrl+C or select "Stop visualization" from the menu to stop the server.`, { title: "Visualization Running", titleColor: terminal_1.colors.success, })); // Ask to open in browser const openBrowser = await (0, prompts_1.confirm)({ message: "Open visualization in browser?", default: true, }); if (openBrowser) { console.log(terminal_1.colors.info("Opening visualization in browser...")); await (0, open_1.default)(`http://localhost:${port}/`); } } catch (error) { startSpinner.fail("Failed to start visualization server"); console.error((0, terminal_1.formatError)(`Error: ${error.message}`)); } } catch (error) { console.error((0, terminal_1.formatError)("Error starting visualization:"), error); } } /** * Stop the blockchain visualization */ async stopVisualization() { (0, terminal_1.displayCommandTitle)("Stop Visualization"); try { const vizManager = await this.getVisualizationManager(); if (!vizManager.isServerRunning()) { console.log((0, terminal_1.formatWarning)("Visualization server is not running.")); return; } const confirmStop = await (0, prompts_1.select)({ message: "Stop the visualization server?", choices: [ { name: terminal_1.colors.highlight("Yes, stop the server"), value: "yes" }, { name: terminal_1.colors.highlight("No, keep it running"), value: "no" }, ], }); if (confirmStop === "yes") { const spinner = (0, ora_1.default)("Stopping visualization server...").start(); await vizManager.stop(); spinner.succeed("Visualization server stopped"); } else { console.log(terminal_1.colors.info("Visualization server remains running.")); } } catch (error) { console.error((0, terminal_1.formatError)("Error stopping visualization:"), error); } } /** * Generate blocks to simulate blockchain activity */ async simulateBlockchain() { (0, terminal_1.displayCommandTitle)("Simulate Blockchain Activity"); try { // First check if visualization is running const vizManager = await this.getVisualizationManager(); const visualizationRunning = vizManager.isServerRunning(); if (!visualizationRunning) { console.log((0, terminal_1.formatWarning)("Visualization server is not running.")); const startViz = await (0, prompts_1.confirm)({ message: "Start visualization server before simulation?", default: true, }); if (startViz) { await this.startVisualization(); } else { console.log((0, terminal_1.formatWarning)("Simulation works best with visualization running.")); } } // Check if Bitcoin Core is running const coreSpinner = (0, ora_1.default)("Checking Bitcoin Core connection...").start(); try { await this.bitcoinRpcClient.callRpc("getblockchaininfo"); coreSpinner.succeed("Connected to Bitcoin Core"); } catch (error) { coreSpinner.fail("Could not connect to Bitcoin Core"); console.log((0, terminal_1.formatError)("Bitcoin Core is not running or RPC connection failed. Simulation requires a working Bitcoin Core connection.")); return; } // Get list of wallets const walletsSpinner = (0, ora_1.default)("Loading wallets...").start(); let wallets; try { wallets = await this.bitcoinService.listWallets(); walletsSpinner.succeed("Wallets loaded"); } catch (error) { walletsSpinner.fail("Error loading wallets"); console.error((0, terminal_1.formatError)(`Error: ${error.message}`)); return; } if (!wallets || wallets.length === 0) { console.log((0, terminal_1.formatWarning)("No wallets found. Please create a wallet first.")); return; } // Show simulation menu const simulationOptions = [ { name: terminal_1.colors.highlight("🚀 Quick simulation (5 blocks, 10 transactions)"), value: "quick", }, { name: terminal_1.colors.highlight("🔄 Continuous simulation (blocks + transactions over time)"), value: "continuous", }, { name: terminal_1.colors.highlight("📊 Custom simulation (specify parameters)"), value: "custom", }, { name: terminal_1.colors.highlight("🧪 Agent-based simulation (miners and users)"), value: "agents", }, ]; const simulationType = await (0, prompts_1.select)({ message: "Select simulation type:", choices: simulationOptions, }); // Source wallet with funds for transactions const sourceWallet = await this.findWalletWithFunds(wallets); if (!sourceWallet) { console.log((0, terminal_1.formatWarning)("No wallet with funds found.")); const fundWallet = await (0, prompts_1.select)({ message: "Would you like to mine some blocks to fund a wallet first?", choices: [ { name: terminal_1.colors.highlight("Yes, mine 5 blocks"), value: "yes" }, { name: terminal_1.colors.highlight("No, cancel simulation"), value: "no" }, ], }); if (fundWallet === "no") { return; } // Mine some blocks to a wallet const miningWallet = wallets[0]; const fundingSpinner = (0, ora_1.default)(`Mining 5 blocks to ${miningWallet}...`).start(); try { const address = await this.bitcoinService.getNewAddress(miningWallet); await this.bitcoinService.generateToAddress(5, address); fundingSpinner.succeed(`Successfully funded wallet: ${miningWallet}`); } catch (error) { fundingSpinner.fail("Error mining blocks"); console.error((0, terminal_1.formatError)(`Error: ${error.message}`)); return; } } switch (simulationType) { case "quick": await this.runQuickSimulation(wallets); break; case "continuous": await this.runContinuousSimulation(wallets); break; case "custom": await this.runCustomSimulation(wallets); break; case "agents": await this.runAgentBasedSimulation(wallets); break; } // If visualization is running, offer to open it if (visualizationRunning || vizManager.isServerRunning()) { const openViz = await (0, prompts_1.confirm)({ message: "Open visualization in browser to see results?", default: true, }); if (openViz) { console.log(terminal_1.colors.info("Opening visualization in browser...")); await (0, open_1.default)(vizManager.getServerUrl()); } } } catch (error) { console.error((0, terminal_1.formatError)("Error simulating blockchain:"), error); } } /** * Find a wallet with funds */ async findWalletWithFunds(wallets) { try { const spinner = (0, ora_1.default)("Looking for a wallet with funds...").start(); for (const wallet of wallets) { try { const info = await this.bitcoinService.getWalletInfo(wallet); if (info.balance > 0) { spinner.succeed(`Found wallet with funds: ${wallet} (${info.balance} BTC)`); return wallet; } } catch (error) { console.error(`Error checking wallet ${wallet}:`, error); } } spinner.warn("No wallet with funds found"); return null; } catch (error) { console.error("Error finding wallet with funds:", error); return null; } } /** * Run a quick simulation (predefined parameters) */ async runQuickSimulation(wallets) { try { console.log((0, terminal_1.boxText)("This simulation will:\n" + "1. Mine 5 blocks\n" + "2. Create 10 random transactions\n" + "3. Mine 1 more block to confirm transactions", { title: "Quick Simulation", titleColor: terminal_1.colors.header })); const confirm = await (0, prompts_1.select)({ message: "Start quick simulation?", choices: [ { name: terminal_1.colors.highlight("Yes, start simulation"), value: "yes" }, { name: terminal_1.colors.highlight("No, cancel"), value: "no" }, ], }); if (confirm === "no") { return; } // Step 1: Mine 5 blocks const miningWallet = wallets[0]; const miningSpinner = (0, ora_1.default)(`Mining 5 blocks to ${miningWallet}...`).start(); try { const address = await this.bitcoinService.getNewAddress(miningWallet); await this.bitcoinService.generateToAddress(5, address); miningSpinner.succeed("Successfully mined 5 blocks"); } catch (error) { miningSpinner.fail("Error mining blocks"); console.error((0, terminal_1.formatError)(`Error: ${error.message}`)); return; } // Step 2: Create transactions const txSpinner = (0, ora_1.default)("Creating 10 random transactions...").start(); try { const sourceWallet = await this.findWalletWithFunds(wallets); if (!sourceWallet) { txSpinner.fail("No wallet with funds found"); return; } const sourceInfo = await this.bitcoinService.getWalletInfo(sourceWallet); const amountPerTx = sourceInfo.balance / 15; // Divide by 15 to ensure enough for fees for (let i = 0; i < 10; i++) { // Generate a new address const destAddress = await this.bitcoinService.getNewAddress(wallets[Math.floor(Math.random() * wallets.length)]); // Add randomness to amount const randomFactor = 0.5 + Math.random(); const amount = Math.min(amountPerTx * randomFactor, sourceInfo.balance * 0.1); // Send transaction await this.bitcoinService.sendToAddress(sourceWallet, destAddress, amount); txSpinner.text = `Created transaction ${i + 1}/10`; } txSpinner.succeed("Created 10 random transactions"); } catch (error) { txSpinner.fail("Error creating transactions"); console.error((0, terminal_1.formatError)(`Error: ${error.message}`)); return; } // Step 3: Mine 1 more block to confirm transactions const confirmSpinner = (0, ora_1.default)("Mining 1 block to confirm transactions...").start(); try { const address = await this.bitcoinService.getNewAddress(wallets[0]); await this.bitcoinService.generateToAddress(1, address); confirmSpinner.succeed("Successfully mined confirmation block"); } catch (error) { confirmSpinner.fail("Error mining confirmation block"); console.error((0, terminal_1.formatError)(`Error: ${error.message}`)); } console.log((0, terminal_1.formatSuccess)("\nQuick simulation completed successfully!")); console.log("You can now view the results in the visualization."); } catch (error) { console.error((0, terminal_1.formatError)("Error running quick simulation:"), error); } } /** * Run a continuous simulation (blocks + transactions over time) */ async runContinuousSimulation(wallets) { try { console.log((0, terminal_1.boxText)("This simulation will create blocks and transactions continuously over time.\n\n" + "You can specify:\n" + "- Duration (in seconds)\n" + "- Block interval (in seconds)\n" + "- Transactions per interval", { title: "Continuous Simulation", titleColor: terminal_1.colors.header })); // Get simulation parameters const duration = await (0, prompts_1.number)({ message: "Enter simulation duration (seconds):", default: 60, }); const blockInterval = await (0, prompts_1.number)({ message: "Enter block interval (seconds):", default: 10, }); const txPerInterval = await (0, prompts_1.number)({ message: "Enter transactions per interval:", default: 3, }); const sourceWallet = await this.findWalletWithFunds(wallets); if (!sourceWallet) { console.log((0, terminal_1.formatWarning)("No wallet with funds found for transactions.")); return; } console.log((0, terminal_1.formatSuccess)("\nStarting continuous simulation...")); console.log(terminal_1.colors.info(`Duration: ${duration} seconds`)); console.log(terminal_1.colors.info(`Block interval: ${blockInterval} seconds`)); console.log(terminal_1.colors.info(`Transactions per interval: ${txPerInterval}`)); let elapsedTime = 0; const updateInterval = Math.min(blockInterval, 5); // Update at least every 5 seconds const startTime = Date.now(); const simulationInterval = setInterval(async () => { const now = Date.now(); elapsedTime = Math.floor((now - startTime) / 1000); if (elapsedTime >= duration) { clearInterval(simulationInterval); console.log((0, terminal_1.formatSuccess)("\nContinuous simulation completed!")); return; } // Update progress const progress = Math.floor((elapsedTime / duration) * 100); console.log(terminal_1.colors.info(`Simulation progress: ${progress}% (${elapsedTime}/${duration} seconds)`)); // Mine block if it's time if (elapsedTime % blockInterval === 0) { try { const address = await this.bitcoinService.getNewAddress(wallets[0]); await this.bitcoinService.generateToAddress(1, address); console.log((0, terminal_1.formatSuccess)("Mined a new block")); } catch (error) { console.error((0, terminal_1.formatError)("Error mining block:"), error); } } // Create transactions if (elapsedTime % updateInterval === 0) { try { const sourceInfo = await this.bitcoinService.getWalletInfo(sourceWallet); if (sourceInfo.balance <= 0) { console.log((0, terminal_1.formatWarning)("Source wallet has no more funds for transactions.")); return; } const amountPerTx = sourceInfo.balance / (txPerInterval * 5); // Ensure enough for future txs for (let i = 0; i < Math.min(txPerInterval, 5); i++) { // Generate a new address const destAddress = await this.bitcoinService.getNewAddress(wallets[Math.floor(Math.random() * wallets.length)]); // Add randomness to amount const randomFactor = 0.5 + Math.random(); const amount = Math.min(amountPerTx * randomFactor, sourceInfo.balance * 0.1); // Send transaction await this.bitcoinService.sendToAddress(sourceWallet, destAddress, amount); } console.log((0, terminal_1.formatSuccess)(`Created ${txPerInterval} transactions`)); } catch (error) { console.error((0, terminal_1.formatError)("Error creating transactions:"), error); } } }, updateInterval * 1000); // Allow user to stop simulation early console.log(terminal_1.colors.warning("\nPress Ctrl+C to stop simulation early")); } catch (error) { console.error((0, terminal_1.formatError)("Error running continuous simulation:"), error); } } /** * Run a custom simulation (user-defined parameters) */ async runCustomSimulation(wallets) { try { console.log((0, terminal_1.boxText)("This simulation allows you to specify custom parameters for blocks and transactions.", { title: "Custom Simulation", titleColor: terminal_1.colors.header })); // Ask for simulation parameters const simulateBlocks = await (0, prompts_1.confirm)({ message: "Would you like to simulate block mining?", default: true, }); let numBlocks = 0; let blockInterval = 0; if (simulateBlocks) { numBlocks = (await (0, prompts_1.number)({ message: "How many blocks would you like to mine?", default: 10, })); blockInterval = (await (0, prompts_1.number)({ message: "Interval between blocks (seconds, 0 for all at once):", default: 0, })); } let simulateTxs = await (0, prompts_1.confirm)({ message: "Would you like to simulate transactions?", default: true, }); let numTransactions = 0; let txInterval = 0; if (simulateTxs) { numTransactions = (await (0, prompts_1.number)({ message: "How many transactions would you like to create?", default: 20, })); txInterval = (await (0, prompts_1.number)({ message: "Interval between transactions (seconds, 0 for all at once):", default: 0, })); } const confirmFinalBlock = await (0, prompts_1.confirm)({ message: "Mine a final block to confirm transactions?", default: true, }); // Start simulation console.log((0, terminal_1.formatSuccess)("\nStarting custom simulation...")); // Source wallet for transactions let sourceWallet = null; if (simulateTxs) { sourceWallet = await this.findWalletWithFunds(wallets); if (!sourceWallet) { console.log((0, terminal_1.formatWarning)("No wallet with funds found for transactions.")); const mineFirst = await (0, prompts_1.confirm)({ message: "Would you like to mine some blocks first to get funds?", default: true, }); if (mineFirst) { sourceWallet = wallets[0]; const address = await this.bitcoinService.getNewAddress(sourceWallet); const spinner = (0, ora_1.default)("Mining initial blocks for funding...").start(); try { await this.bitcoinService.generateToAddress(5, address); spinner.succeed("Mined 5 blocks for funding"); } catch (error) { spinner.fail("Error mining funding blocks"); console.error((0, terminal_1.formatError)(`Error: ${error.message}`)); return; } } else { console.log((0, terminal_1.formatWarning)("Cannot create transactions without funds.")); simulateTxs = false; } } } // Mine blocks if (simulateBlocks) { if (blockInterval === 0) { // Mine all blocks at once const spinner = (0, ora_1.default)(`Mining ${numBlocks} blocks...`).start(); try { const address = await this.bitcoinService.getNewAddress(wallets[0]); await this.bitcoinService.generateToAddress(numBlocks, address); spinner.succeed(`Mined ${numBlocks} blocks`); } catch (error) { spinner.fail("Error mining blocks"); console.error((0, terminal_1.formatError)(`Error: ${error.message}`)); return; } } else { // Mine blocks with interval console.log(terminal_1.colors.info(`Mining ${numBlocks} blocks with ${blockInterval}s interval...`)); for (let i = 0; i < numBlocks; i++) { const spinner = (0, ora_1.default)(`Mining block ${i + 1}/${numBlocks}...`).start(); try { const address = await this.bitcoinService.getNewAddress(wallets[0]); await this.bitcoinService.generateToAddress(1, address); spinner.succeed(`Mined block ${i + 1}/${numBlocks}`); if (i < numBlocks - 1) { await new Promise((resolve) => setTimeout(resolve, blockInterval * 1000)); } } catch (error) { spinner.fail(`Error mining block ${i + 1}`); console.error((0, terminal_1.formatError)(`Error: ${error.message}`)); break; } } } } // Create transactions if (simulateTxs && sourceWallet) { if (txInterval === 0) { // Create all transactions at once const spinner = (0, ora_1.default)(`Creating ${numTransactions} transactions...`).start(); try { const sourceInfo = await this.bitcoinService.getWalletInfo(sourceWallet); const amountPerTx = sourceInfo.balance / (numTransactions * 1.5); // Allow for fees for (let i = 0; i < numTransactions; i++) { const destAddress = await this.bitcoinService.getNewAddress(wallets[Math.floor(Math.random() * wallets.length)]); const randomFactor = 0.5 + Math.random(); const amount = Math.min(amountPerTx * randomFactor, sourceInfo.balance * 0.1); await this.bitcoinService.sendToAddress(sourceWallet, destAddress, amount); spinner.text = `Created transaction ${i + 1}/${numTransactions}`; } spinner.succeed(`Created ${numTransactions} transactions`); } catch (error) { spinner.fail("Error creating transactions"); console.error((0, terminal_1.formatError)(`Error: ${error.message}`)); } } else { // Create transactions with interval console.log(terminal_1.colors.info(`Creating ${numTransactions} transactions with ${txInterval}s interval...`)); for (let i = 0; i < numTransactions; i++) { const spinner = (0, ora_1.default)(`Creating transaction ${i + 1}/${numTransactions}...`).start(); try { const sourceInfo = await this.bitcoinService.getWalletInfo(sourceWallet); if (sourceInfo.balance <= 0) { spinner.warn("Source wallet has no more funds"); break; } const amountPerTx = sourceInfo.balance / (numTransactions - i + 1) / 2; const destAddress = await this.bitcoinService.getNewAddress(wallets[Math.floor(Math.random() * wallets.length)]); const randomFactor = 0.5 + Math.random(); const amount = Math.min(amountPerTx * randomFactor, sourceInfo.balance * 0.1); await this.bitcoinService.sendToAddress(sourceWallet, destAddress, amount); spinner.succeed(`Created transaction ${i + 1}/${numTransactions}`); if (i < numTransactions - 1) { await new Promise((resolve) => setTimeout(resolve, txInterval * 1000)); } } catch (error) { spinner.fail(`Error creating transaction ${i + 1}`); console.error((0, terminal_1.formatError)(`Error: ${error.message}`)); break; } } } } // Mine final block to confirm transactions if (confirmFinalBlock && simulateTxs) { const spinner = (0, ora_1.default)("Mining final block to confirm transactions...").start(); try { const address = await this.bitcoinService.getNewAddress(wallets[0]); await this.bitcoinService.generateToAddress(1, address); spinner.succeed("Mined confirmation block"); } catch (error) { spinner.fail("Error mining confirmation block"); console.error((0, terminal_1.formatError)(`Error: ${error.message}`)); } } console.log((0, terminal_1.formatSuccess)("\nCustom simulation completed successfully!")); } catch (error) { console.error((0, terminal_1.formatError)("Error running custom simulation:"), error); } } /** * Run an agent-based simulation (miners and users) */ async runAgentBasedSimulation(wallets) { try { console.log((0, terminal_1.boxText)("This simulation creates virtual agents to simulate a blockchain network:\n\n" + "- 🧑‍🔧 Miners: Generate blocks at varying speeds\n" + "- 👥 Users: Create transactions with different patterns\n" + "- 🏪 Merchants: Receive payments regularly\n\n" + "Note: This is a simplified simulation for demonstration.", { title: "Agent-Based Simulation", titleColor: terminal_1.colors.header })); // Get simulation parameters const duration = await (0, prompts_1.number)({ message: "Enter simulation duration (seconds):", default: 120, }); const minerCount = await (0, prompts_1.number)({ message: "Enter number of miners (1-5):", default: 3, }); const userCount = await (0, prompts_1.number)({ message: "Enter number of users (1-10):", default: 5, }); // Validate wallet count if (wallets.length < 3) { console.log((0, terminal_1.formatWarning)(`Need at least 3 wallets for simulation, but only have ${wallets.length}.`)); const createWallets = await (0, prompts_1.confirm)({ message: "Create additional wallets?", default: true, }); if (createWallets) { const spinner = (0, ora_1.default)("Creating additional wallets...").start(); try { for (let i = wallets.length; i < 3; i++) { await this.bitcoinService.createWallet(`sim_wallet_${i}`); } // Refresh wallet list wallets = await this.bitcoinService.listWallets(); spinner.succeed(`Now have ${wallets.length} wallets`); } catch (error) { spinner.fail("Error creating wallets"); console.error((0, terminal_1.formatError)(`Error: ${error.message}`)); return; } } else { console.log((0, terminal_1.formatWarning)("Cannot run simulation without enough wallets.")); return; } } // Ensure we have funds const sourceWallet = await this.findWalletWithFunds(wallets); if (!sourceWallet) { console.log((0, terminal_1.formatWarning)("No wallet with funds found.")); const mineFunds = await (0, prompts_1.confirm)({ message: "Mine initial blocks for funding?", default: true, }); if (mineFunds) { const spinner = (0, ora_1.default)("Mining initial blocks for funding...").start(); try { const address = await this.bitcoinService.getNewAddress(wallets[0]); await this.bitcoinService.generateToAddress(10, address); spinner.succeed("Mined 10 blocks for funding"); } catch (error) { spinner.fail("Error mining funding blocks"); console.error((0, terminal_1.formatError)(`Error: ${error.message}`)); return; } } else { console.log((0, terminal_1.formatWarning)("Cannot run simulation without funds.")); return; } } // Show simulation plan console.log((0, terminal_1.formatSuccess)("\nStarting agent-based simulation...")); console.log(terminal_1.colors.info(`Duration: ${duration} seconds`)); console.log(terminal_1.colors.info(`Miners: ${minerCount}`)); console.log(terminal_1.colors.info(`Users: ${userCount}`)); // Assign roles to wallets const minerWallets = wallets.slice(0, Math.min(minerCount, wallets.length)); const userWallets = wallets.slice(0, Math.min(userCount, wallets.length)); const merchantWallet = wallets[wallets.length - 1]; console.log(terminal_1.colors.info(`Miner wallets: ${minerWallets.join(", ")}`)); console.log(terminal_1.colors.info(`User wallets: ${userWallets.join(", ")}`)); console.log(terminal_1.colors.info(`Merchant wallet: ${merchantWallet}`)); // Fund user wallets const fundingSpinner = (0, ora_1.default)("Distributing funds to user wallets...").start(); try { const fundingWallet = await this.findWalletWithFunds(wallets); if (fundingWallet) { const fundingInfo = await this.bitcoinService.getWalletInfo(fundingWallet); const fundPerWallet = fundingInfo.balance / (userWallets.length + 1); for (const userWallet of userWallets) { if (userWallet !== fundingWallet) { const address = await this.bitcoinService.getNewAddress(userWallet); await this.bitcoinService.sendToAddress(fundingWallet, address, fundPerWallet * 0.9); } } fundingSpinner.succeed("Funds distributed to user wallets"); // Mine a block to confirm funding transactions await this.bitcoinService.generateToAddress(1, await this.bitcoinService.getNewAddress(minerWallets[0])); } } catch (error) { fundingSpinner.fail("Error distributing funds"); console.error((0, terminal_1.formatError)(`Error: ${error.message}`)); } let elapsedTime = 0; const startTime = Date.now(); // Setup agent behaviors const miners = minerWallets.map((wallet, index) => ({ wallet, speed: Math.max(10, 30 - index * 5), // Miners have different speeds (10-30s) lastMineTime: 0, })); const users = userWallets.map((wallet, index) => ({ wallet, frequency: Math.max(5, 20 - index * 3), // Users have different transaction frequencies (5-20s) lastTxTime: 0, pattern: index % 3, // Different transaction patterns (0: regular, 1: burst, 2: random) })); // Start simulation loop console.log((0, terminal_1.formatSuccess)("\nSimulation running... (agents are active)")); const simulationInterval = setInterval(async () => { const now = Date.now(); elapsedTime = Math.floor((now - startTime) / 1000); if (elapsedTime >= duration) { clearInterval(simulationInterval); console.log((0, terminal_1.formatSuccess)("\nAgent-based simulation completed!")); return; } // Update progress every 10 seconds if (elapsedTime % 10 === 0) { const progress = Math.floor((elapsedTime / duration) * 100); console.log(terminal_1.colors.info(`Simulation progress: ${progress}% (${elapsedTime}/${duration} seconds)`)); } // Miners behavior for (const miner of miners) { if (elapsedTime - miner.lastMineTime >= miner.speed) { try { const address = await this.bitcoinService.getNewAddress(miner.wallet); await this.bitcoinService.generateToAddress(1, address); miner.lastMineTime = elapsedTime; console.log((0, terminal_1.formatSuccess)(`Miner ${miner.wallet} found a new block!`)); } catch (error) { console.error((0, terminal_1.formatError)(`Error with miner ${miner.wallet}:`), error); } } } // Users behavior for (const user of users) { const shouldCreateTx = user.pattern === 0 ? elapsedTime - user.lastTxTime >= user.frequency // Regular : user.pattern === 1 ? elapsedTime % 30 < 5 && elapsedTime - user.lastTxTime >= 5 // Burst : Math.random() < 0.1; // Random if (shouldCreateTx) { try { const userInfo = await this.bitcoinService.getWalletInfo(user.wallet); if (userInfo.balance > 0.001) { // Randomly choose between merchant and other users const isMerchant = Math.random() < 0.7; // 70% to merchant const destWallet = isMerchant ? merchantWallet : userWallets[Math.floor(Math.random() * userWallets.length)]; const destAddress = await this.bitcoinService.getNewAddress(destWallet); const amount = Math.min(0.001 + Math.random() * 0.01, userInfo.balance * 0.5); await this.bitcoinService.sendToAddress(user.wallet, destAddress, amount); user.lastTxTime = elapsedTime; console.log(terminal_1.colors.info(`User ${user.wallet} sent ${amount.toFixed(4)} BTC to ${isMerchant ? "merchant" : "another user"}`)); } } catch (error) { console.error((0, terminal_1.formatError)(`Error with user ${user.wallet}:`), error); } } } }, 1000); // Check every second // Allow user to stop simulation early console.log(terminal_1.colors.warning("\nPress Ctrl+C to stop simulation early")); } catch (error) { console.error((0, terminal_1.formatError)("Error running agent-based simulation:"), error); } } } exports.VisualizationCommands = VisualizationCommands;