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

574 lines (573 loc) • 26.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.CaravanRegtestManager = void 0; const config_1 = require("./core/config"); const rpc_1 = require("./core/rpc"); const bitcoin_1 = require("./core/bitcoin"); const caravan_1 = require("./core/caravan"); const transaction_1 = require("./core/transaction"); const docker_1 = require("./core/docker"); const snapshot_1 = require("./core/snapshot"); const scenario_1 = require("./core/scenario"); const config_2 = require("./types/config"); const wallet_1 = require("./commands/wallet"); const multisig_1 = require("./commands/multisig"); const transaction_2 = require("./commands/transaction"); const visualizations_1 = require("./commands/visualizations"); const scripts_1 = require("./commands/scripts"); const mainMenu_1 = require("./ui/mainMenu"); const setupWizard_1 = require("./ui/setupWizard"); const profiles_1 = require("./core/profiles"); const prompts_1 = require("@inquirer/prompts"); const chalk_1 = __importDefault(require("chalk")); const boxen_1 = __importDefault(require("boxen")); const figlet_1 = __importDefault(require("figlet")); const path_1 = __importDefault(require("path")); const fs_extra_1 = __importDefault(require("fs-extra")); const ora_1 = __importDefault(require("ora")); const gradient_string_1 = __importDefault(require("gradient-string")); // Define a consistent color scheme const colors = { primary: chalk_1.default.hex("#F7931A"), // Bitcoin orange secondary: chalk_1.default.hex("#1C2C5B"), // Dark blue accent: chalk_1.default.hex("#00ACED"), // Light blue success: chalk_1.default.hex("#28a745"), // Green warning: chalk_1.default.hex("#ffc107"), // Yellow error: chalk_1.default.hex("#dc3545"), // Red info: chalk_1.default.hex("#17a2b8"), // Teal muted: chalk_1.default.hex("#6c757d"), // Gray header: chalk_1.default.bold.hex("#F7931A"), // Bold orange for headers commandName: chalk_1.default.bold.hex("#0095d5"), // Bold dark blue for command names }; /** * Main application class */ class CaravanRegtestManager { constructor() { // Initialize configuration this.configManager = new config_1.ConfigManager(); const config = this.configManager.getConfig(); // Check if enhanced config exists this.setupWizard = new setupWizard_1.SetupWizard(config.appDir); // Initialize RPC client this.bitcoinRpcClient = new rpc_1.BitcoinRpcClient(config.bitcoin); // Initialize services this.bitcoinService = new bitcoin_1.BitcoinService(this.bitcoinRpcClient, true); this.caravanService = new caravan_1.CaravanService(this.bitcoinRpcClient, config.caravanDir, config.keysDir); this.transactionService = new transaction_1.TransactionService(this.bitcoinRpcClient, true); this.snapshotService = new snapshot_1.SnapshotService(this.bitcoinRpcClient, path_1.default.join(config.appDir, "snapshots"), config.bitcoin.dataDir); this.scenarioService = new scenario_1.ScenarioService(this.bitcoinService, this.caravanService, this.transactionService, this.bitcoinRpcClient, path_1.default.join(config.appDir, "scenarios")); // Initialize command modules this.walletCommands = new wallet_1.WalletCommands(this.bitcoinService); this.multisigCommands = new multisig_1.MultisigCommands(this.caravanService, this.bitcoinService, this.bitcoinRpcClient, this.transactionService); this.transactionCommands = new transaction_2.TransactionCommands(this.transactionService, this.caravanService, this.bitcoinService); this.visualizationCommands = new visualizations_1.VisualizationCommands(this.configManager, this.bitcoinRpcClient, this.bitcoinService); this.scriptCommands = new scripts_1.ScriptCommands(this.configManager, this.bitcoinService, this.caravanService, this.transactionService, this.bitcoinRpcClient, this.multisigCommands); } /** * Check if Bitcoin Core is running and accessible */ async checkBitcoinCore() { try { const blockchainInfo = (await this.bitcoinRpcClient.callRpc("getblockchaininfo")); return blockchainInfo && blockchainInfo.chain === "regtest"; } catch (error) { return false; } } /** * Main application startup */ async start() { console.clear(); this.displayWelcomeBanner(); // STEP 1: Ask for base directory const baseDirectory = await this.askForBaseDirectory(); // Initialize profile manager with the base directory this.profileManager = new profiles_1.ProfileManager(baseDirectory); await this.profileManager.initialize(); // STEP 2: Ask for mode (Docker or Manual) const mode = await this.askForMode(); // STEP 3: Check for existing configurations for this mode let enhancedConfig; let justCompletedSetup = false; const existingProfiles = await this.profileManager.getProfilesByMode(mode); if (existingProfiles.length > 0) { // Show existing configurations and ask user what to do const choice = await this.askExistingConfigChoice(existingProfiles, mode); if (choice.action === "use_existing") { // Load the selected profile const profile = await this.profileManager.getProfile(choice.profileId); if (profile) { enhancedConfig = profile.config; await this.profileManager.setActiveProfile(profile.id); console.log(chalk_1.default.green(`\nāœ… Loaded configuration: ${profile.name}\n`)); } else { throw new Error("Failed to load selected profile"); } } else if (choice.action === "new") { // Create new configuration enhancedConfig = await this.runSetupWizardForMode(mode, baseDirectory); justCompletedSetup = true; // Ask if they want to save as a new profile or replace const profileName = await (0, prompts_1.input)({ message: "Name for this configuration:", default: `${mode === config_2.SetupMode.DOCKER ? "Docker" : "Manual"} Config ${existingProfiles.length + 1}`, }); const newProfile = await this.profileManager.createProfile(profileName, mode, enhancedConfig); await this.profileManager.setActiveProfile(newProfile.id); } else { // Delete and recreate for (const profile of existingProfiles) { await this.profileManager.deleteProfile(profile.id); } enhancedConfig = await this.runSetupWizardForMode(mode, baseDirectory); justCompletedSetup = true; const profileName = await (0, prompts_1.input)({ message: "Name for this configuration:", default: `${mode === config_2.SetupMode.DOCKER ? "Docker" : "Manual"} Config`, }); const newProfile = await this.profileManager.createProfile(profileName, mode, enhancedConfig); await this.profileManager.setActiveProfile(newProfile.id); } } else { // No existing profiles, run setup console.log(chalk_1.default.cyan(`\nšŸ“‹ No existing ${mode} configuration found. Let's set one up!\n`)); enhancedConfig = await this.runSetupWizardForMode(mode, baseDirectory); justCompletedSetup = true; const profileName = await (0, prompts_1.input)({ message: "Name for this configuration:", default: `${mode === config_2.SetupMode.DOCKER ? "Docker" : "Manual"} Config`, }); const newProfile = await this.profileManager.createProfile(profileName, mode, enhancedConfig); await this.profileManager.setActiveProfile(newProfile.id); } this.enhancedConfig = enhancedConfig; // Also save to the legacy config.json location for compatibility const legacyConfigPath = path_1.default.join(enhancedConfig.appDir, "config.json"); await fs_extra_1.default.writeJson(legacyConfigPath, enhancedConfig, { spaces: 2 }); // Initialize services with the configuration await this.reinitializeWithConfig(enhancedConfig); // Verify connection if not just completed setup if (!justCompletedSetup) { await this.verifyBitcoinConnection(); } else { console.clear(); console.log(chalk_1.default.green("\nāœ… Setup complete! Starting Caravan-X...\n")); await new Promise((resolve) => setTimeout(resolve, 1000)); } // Start main menu const mainMenu = new mainMenu_1.MainMenu(this); await mainMenu.start(); } /** * Ask user for base directory */ async askForBaseDirectory() { const homeDir = process.env.HOME || "~"; const defaultDir = path_1.default.join(homeDir, ".caravan-x"); console.log((0, boxen_1.default)(chalk_1.default.white.bold("šŸ“ Choose Your Caravan-X Directory\n\n") + chalk_1.default.gray("This is where all your configurations, wallets, and data will be stored.\n\n") + chalk_1.default.cyan("Default: ") + chalk_1.default.white(defaultDir), { padding: 1, margin: 1, borderStyle: "round", borderColor: "cyan", })); const useDefault = await (0, prompts_1.confirm)({ message: `Use default directory (${defaultDir})?`, default: true, }); let chosenDir; if (useDefault) { chosenDir = defaultDir; } else { chosenDir = await (0, prompts_1.input)({ message: "Enter your preferred directory path:", default: defaultDir, validate: (inputPath) => { if (!inputPath.trim()) { return "Directory path cannot be empty"; } // Expand ~ to home directory const expandedPath = inputPath.startsWith("~") ? path_1.default.join(homeDir, inputPath.slice(1)) : inputPath; // Check if it's an absolute path or can be resolved if (!path_1.default.isAbsolute(expandedPath)) { return "Please enter an absolute path"; } return true; }, }); // Expand ~ if present if (chosenDir.startsWith("~")) { chosenDir = path_1.default.join(homeDir, chosenDir.slice(1)); } } // Ensure directory exists await fs_extra_1.default.ensureDir(chosenDir); console.log(chalk_1.default.green(`\nāœ… Using directory: ${chosenDir}\n`)); return chosenDir; } /** * Ask user for mode selection */ async askForMode() { console.log((0, boxen_1.default)(chalk_1.default.white.bold("šŸ”§ Select Operation Mode\n\n") + chalk_1.default.cyan("🐳 Docker Mode") + chalk_1.default.gray(" (Recommended)\n") + chalk_1.default.dim(" • Automatic Bitcoin Core setup in Docker\n") + chalk_1.default.dim(" • No manual configuration needed\n") + chalk_1.default.dim(" • Isolated environment\n\n") + chalk_1.default.yellow("āš™ļø Manual Mode\n") + chalk_1.default.dim(" • Use your own Bitcoin Core installation\n") + chalk_1.default.dim(" • Full control over configuration\n") + chalk_1.default.dim(" • Requires running bitcoind separately"), { padding: 1, margin: 1, borderStyle: "round", borderColor: "cyan", })); const mode = await (0, prompts_1.select)({ message: "Choose your mode:", choices: [ { name: chalk_1.default.cyan("🐳 Docker Mode") + chalk_1.default.gray(" (Recommended)"), value: config_2.SetupMode.DOCKER, }, { name: chalk_1.default.yellow("āš™ļø Manual Mode"), value: config_2.SetupMode.MANUAL, }, ], }); return mode; } /** * Ask user what to do with existing configurations */ async askExistingConfigChoice(profiles, mode) { const modeLabel = mode === config_2.SetupMode.DOCKER ? "Docker" : "Manual"; console.log((0, boxen_1.default)(chalk_1.default.white.bold(`šŸ“‹ Existing ${modeLabel} Configuration(s) Found\n\n`) + profiles .map((p, i) => chalk_1.default.cyan(`${i + 1}. ${p.name}\n`) + chalk_1.default.dim(` Created: ${new Date(p.createdAt).toLocaleDateString()}\n`) + chalk_1.default.dim(` Last used: ${new Date(p.lastUsedAt).toLocaleDateString()}`)) .join("\n\n"), { padding: 1, margin: 1, borderStyle: "round", borderColor: "cyan", })); const action = await (0, prompts_1.select)({ message: "What would you like to do?", choices: [ { name: chalk_1.default.green("šŸ“‚ Use existing configuration") + (profiles.length > 1 ? chalk_1.default.gray(" (choose which one)") : ""), value: "use_existing", }, { name: chalk_1.default.cyan("āž• Create new configuration") + chalk_1.default.gray(" (keep existing)"), value: "new", }, { name: chalk_1.default.red("šŸ—‘ļø Start fresh") + chalk_1.default.gray(" (delete existing and create new)"), value: "delete_and_new", }, ], }); if (action === "use_existing") { if (profiles.length === 1) { return { action: "use_existing", profileId: profiles[0].id }; } // Let user select which profile const selectedId = await (0, prompts_1.select)({ message: "Select configuration to use:", choices: profiles.map((p) => ({ name: `${p.name} (Last used: ${new Date(p.lastUsedAt).toLocaleDateString()})`, value: p.id, })), }); //@ts-ignore return { action: "use_existing", profileId: selectedId }; } //@ts-ignore return { action }; } /** * Run setup wizard for a specific mode */ async runSetupWizardForMode(mode, baseDirectory) { // Update appDir to use the chosen base directory const appDir = baseDirectory; if (mode === config_2.SetupMode.DOCKER) { return await this.setupDockerModeWithDir(mode, appDir); } else { return await this.setupManualModeWithDir(appDir); } } /** * Docker mode setup using specified directory */ async setupDockerModeWithDir(mode, appDir) { const wizard = new setupWizard_1.SetupWizard(appDir); return await wizard.setupDockerMode(); } /** * Manual mode setup using specified directory */ async setupManualModeWithDir(appDir) { const wizard = new setupWizard_1.SetupWizard(appDir); return await wizard.setupManualMode(); } /** * Display welcome banner */ displayWelcomeBanner() { try { // Create a gradient using Bitcoin orange to blue const logoGradient = (0, gradient_string_1.default)(["#F7931A", "#F7931A", "#00ACED"]); // Generate the figlet text const figletText = figlet_1.default.textSync("Caravan - X", { font: "Big", horizontalLayout: "default", verticalLayout: "default", width: 80, whitespaceBreak: true, }); // Apply the gradient to the figlet text const gradientText = logoGradient(figletText); // Output the gradient text console.log(gradientText); console.log(colors.muted("━".repeat(70))); console.log(colors.muted("━".repeat(70)) + "\n"); // Add the subtitle with accent color console.log(colors.accent("========== R E G T E S T M O D E ==========")); console.log(colors.muted("A terminal-based utility for Caravan in regtest mode\n")); } catch (error) { console.log(chalk_1.default.cyan(figlet_1.default.textSync("Caravan-X", { font: "Standard", horizontalLayout: "default", }))); console.log(chalk_1.default.gray("Bitcoin Multisig Development Testing Tool\n")); } } /** * Test Bitcoin Core connection */ async testBitcoinConnection(host, port, user, pass) { try { const testRpc = new rpc_1.BitcoinRpcClient({ protocol: "http", host, port, user, pass, dataDir: "", }); const info = await testRpc.callRpc("getblockchaininfo"); return !!info; } catch (error) { return false; } } /** * Verify Bitcoin connection and display status */ async verifyBitcoinConnection() { console.clear(); this.displayHeader(); const spinner = (0, ora_1.default)("Connecting to Bitcoin Core...").start(); const connected = await this.checkBitcoinCore(); if (!connected) { spinner.fail(chalk_1.default.red("Could not connect to Bitcoin Core")); if (this.enhancedConfig?.mode === config_2.SetupMode.DOCKER && this.dockerService) { console.log((0, boxen_1.default)(chalk_1.default.yellow.bold("āš ļø Docker Container Not Running\n\n") + chalk_1.default.white("Try: ") + chalk_1.default.cyan("Docker Management → Start Container\n") + chalk_1.default.gray("Or restart Caravan-X to reconfigure"), { padding: 1, margin: 1, borderStyle: "round", borderColor: "yellow", })); } else { console.log((0, boxen_1.default)(chalk_1.default.red.bold("āš ļø Bitcoin Core Not Running\n\n") + chalk_1.default.white("Start your node with:\n") + chalk_1.default.cyan(" bitcoind -regtest -daemon"), { padding: 1, margin: 1, borderStyle: "round", borderColor: "red", })); } await (0, prompts_1.input)({ message: "Press Enter to continue anyway..." }); } else { spinner.succeed(chalk_1.default.green("Connected to Bitcoin Core")); } } /** * Display header with current configuration */ displayHeader() { const config = this.enhancedConfig || this.configManager.getConfig(); console.log(chalk_1.default.cyan("━".repeat(70))); console.log(chalk_1.default.cyan.bold(" CARAVAN-X") + chalk_1.default.gray(" │ ") + this.getModeBadge(this.enhancedConfig?.mode || config_2.SetupMode.MANUAL)); console.log(chalk_1.default.cyan("━".repeat(70))); if (this.enhancedConfig?.mode === config_2.SetupMode.DOCKER) { console.log(chalk_1.default.white("\n Bitcoin RPC:")); console.log(chalk_1.default.dim(" URL: ") + chalk_1.default.white("http://localhost:8080")); console.log(chalk_1.default.dim(" User: ") + chalk_1.default.white(config.bitcoin.user)); } else { console.log(chalk_1.default.white("\nBitcoin RPC:")); console.log(chalk_1.default.dim(" Host: ") + chalk_1.default.white(`${config.bitcoin.host}:${config.bitcoin.port}`)); console.log(chalk_1.default.dim(" User: ") + chalk_1.default.white(config.bitcoin.user)); } console.log(chalk_1.default.cyan("━".repeat(70)) + "\n"); } /** * Get mode badge for display */ getModeBadge(mode) { if (mode === config_2.SetupMode.DOCKER) { return chalk_1.default.bgCyan.black.bold(" 🐳 DOCKER "); } else { return chalk_1.default.bgYellow.black.bold(" āš™ļø MANUAL "); } } /** * Load enhanced configuration */ async loadEnhancedConfig() { try { const config = this.configManager.getConfig(); const enhancedConfigPath = path_1.default.join(config.appDir, "config.json"); if (await fs_extra_1.default.pathExists(enhancedConfigPath)) { return await fs_extra_1.default.readJson(enhancedConfigPath); } } catch (error) { // Config doesn't exist yet } return undefined; } /** * Reinitialize services with new configuration */ async reinitializeWithConfig(config) { // Update config manager this.configManager.updateBitcoinConfig(config.bitcoin); // Reinitialize RPC client this.bitcoinRpcClient = new rpc_1.BitcoinRpcClient(config.bitcoin); // Reinitialize services this.bitcoinService = new bitcoin_1.BitcoinService(this.bitcoinRpcClient, true); this.caravanService = new caravan_1.CaravanService(this.bitcoinRpcClient, config.caravanDir, config.keysDir); this.transactionService = new transaction_1.TransactionService(this.bitcoinRpcClient, true); this.snapshotService = new snapshot_1.SnapshotService(this.bitcoinRpcClient, config.snapshots.directory, config.bitcoin.dataDir); this.scenarioService = new scenario_1.ScenarioService(this.bitcoinService, this.caravanService, this.transactionService, this.bitcoinRpcClient, config.scenariosDir); // Initialize Docker service if in Docker mode if (config.mode === config_2.SetupMode.DOCKER && config.docker) { this.dockerService = new docker_1.DockerService(config.docker, path_1.default.join(config.appDir, "docker-data")); } // Update command modules this.walletCommands = new wallet_1.WalletCommands(this.bitcoinService); this.multisigCommands = new multisig_1.MultisigCommands(this.caravanService, this.bitcoinService, this.bitcoinRpcClient, this.transactionService); this.transactionCommands = new transaction_2.TransactionCommands(this.transactionService, this.caravanService, this.bitcoinService); } /** * Set up Bitcoin Core connection configuration interactively */ async setupBitcoinConfig() { console.log(chalk_1.default.cyan("\n=== Bitcoin Core Configuration ===\n")); const currentConfig = this.enhancedConfig || this.configManager.getConfig(); const protocol = await (0, prompts_1.input)({ message: "Enter RPC protocol (http/https):", default: currentConfig.bitcoin.protocol, }); const host = await (0, prompts_1.input)({ message: "Enter RPC host:", default: currentConfig.bitcoin.host, }); const port = await (0, prompts_1.number)({ message: "Enter RPC port:", default: currentConfig.bitcoin.port, }); const user = await (0, prompts_1.input)({ message: "Enter RPC username:", default: currentConfig.bitcoin.user, }); const pass = await (0, prompts_1.input)({ message: "Enter RPC password:", default: currentConfig.bitcoin.pass, }); const dataDir = await (0, prompts_1.input)({ message: "Enter Bitcoin data directory:", default: currentConfig.bitcoin.dataDir, }); // Update the configuration this.configManager.updateBitcoinConfig({ protocol, host, port: port, user, pass, dataDir, }); // If we have enhanced config, update that too if (this.enhancedConfig) { this.enhancedConfig.bitcoin = { protocol, host, port: port, user, pass, dataDir, }; // Save enhanced config const config = this.configManager.getConfig(); const configPath = path_1.default.join(config.appDir, "config.json"); await fs_extra_1.default.writeJson(configPath, this.enhancedConfig, { spaces: 2 }); } // Reinitialize services with new config await this.reinitializeWithConfig(this.enhancedConfig || this.configManager.getConfig()); console.log(chalk_1.default.green("\nāœ“ Configuration updated successfully!")); await (0, prompts_1.input)({ message: "Press Enter to continue..." }); } } exports.CaravanRegtestManager = CaravanRegtestManager; // When run directly if (require.main === module) { const app = new CaravanRegtestManager(); app.start().catch((error) => { console.error(chalk_1.default.red("\nError starting application:"), error); process.exit(1); }); } // Export for use in other files exports.default = CaravanRegtestManager;