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
JavaScript
"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;