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

894 lines (878 loc) โ€ข 41.5 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DockerService = void 0; const child_process_1 = require("child_process"); const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const ora_1 = __importDefault(require("ora")); const boxen_1 = __importDefault(require("boxen")); const chalk_1 = __importDefault(require("chalk")); const cli_progress_1 = __importDefault(require("cli-progress")); const exec_1 = require("../utils/exec"); /** * Docker service for managing Bitcoin Core containers * * This service handles: * - Port conflict detection and auto-adjustment * - Bitcoin Core container lifecycle * - Nginx proxy setup for CORS-enabled RPC access * - Watch-only wallet creation * - Connection testing and validation */ class DockerService { constructor(config, dataDir) { this.rpcUser = "user"; this.rpcPassword = "pass"; this.config = config; this.dataDir = dataDir; } // ============================================================================ // DOCKER & SYSTEM CHECKS // ============================================================================ /** * Check if Docker is available and running */ async checkDockerAvailable() { try { await (0, exec_1.execAsync)("docker --version"); return true; } catch (error) { return false; } } /** * Detect system architecture (important for ARM64/M1 Macs) */ async detectArchitecture() { try { const { stdout } = await (0, exec_1.execAsync)("uname -m"); return stdout.trim(); } catch (error) { return "unknown"; } } // ============================================================================ // PORT MANAGEMENT (Auto-detection and conflict resolution) // ============================================================================ /** * Check if a specific port is in use */ async isPortInUse(port) { try { const { stdout } = await (0, exec_1.execAsync)(`lsof -i :${port} 2>/dev/null || netstat -an 2>/dev/null | grep ${port} || true`); return stdout.trim().length > 0; } catch (error) { return false; } } /** * Find an available port starting from a base port * Tries up to 50 ports sequentially */ async findAvailablePort(basePort) { let port = basePort; let attempts = 0; const maxAttempts = 50; while (attempts < maxAttempts) { if (!(await this.isPortInUse(port))) { return port; } port++; attempts++; } throw new Error(`Could not find available port near ${basePort} after ${maxAttempts} attempts`); } /** * Find available ports for both RPC and P2P * This is the helper that finds alternative ports */ async findAvailablePorts() { const rpcPort = await this.findAvailablePort(18443); const p2pPort = await this.findAvailablePort(18444); return { rpc: rpcPort, p2p: p2pPort }; } /** * Check if default ports are available */ async checkPortsAvailable() { const conflicts = []; if (await this.isPortInUse(this.config.ports.rpc)) { conflicts.push(`Port ${this.config.ports.rpc} (RPC) is already in use`); } if (await this.isPortInUse(this.config.ports.p2p)) { conflicts.push(`Port ${this.config.ports.p2p} (P2P) is already in use`); } return { available: conflicts.length === 0, conflicts, }; } /** * Ensure ports are available - auto-adjust if conflicts exist * This is the MAIN port management method called during setup * * If default ports (18443, 18444) are taken, it automatically finds alternatives * and updates the config accordingly. No manual intervention needed! */ async ensurePortsAvailable() { const portStatus = await this.checkPortsAvailable(); if (portStatus.conflicts.length > 0) { console.log(chalk_1.default.yellow("\nโš ๏ธ Default ports are in use:")); portStatus.conflicts.forEach((conflict) => { console.log(chalk_1.default.dim(` โ€ข ${conflict}`)); }); console.log(chalk_1.default.cyan("\n๐Ÿ” Finding available ports...\n")); // Auto-find alternative ports const availablePorts = await this.findAvailablePorts(); console.log(chalk_1.default.green("โœ… Found available ports:")); console.log(chalk_1.default.white(` โ€ข RPC Port: ${availablePorts.rpc}`)); console.log(chalk_1.default.white(` โ€ข P2P Port: ${availablePorts.p2p}\n`)); // Update config with new ports this.config.ports.rpc = availablePorts.rpc; this.config.ports.p2p = availablePorts.p2p; return availablePorts; } // No conflicts, use default ports return { rpc: this.config.ports.rpc, p2p: this.config.ports.p2p, }; } // ============================================================================ // CONTAINER MANAGEMENT // ============================================================================ /** * Get current container status */ async getContainerStatus() { try { const { stdout } = await (0, exec_1.execAsync)(`docker ps -a --filter "name=${this.config.containerName}" --format "{{.ID}}|{{.Status}}|{{.Ports}}"`); if (!stdout.trim()) { return { running: false }; } const [containerId, status, ports] = stdout.trim().split("|"); const running = status.toLowerCase().includes("up"); return { running, containerId, ports: { rpc: this.config.ports.rpc, p2p: this.config.ports.p2p, }, network: this.config.network, }; } catch (error) { return { running: false }; } } /** * Ensure Docker network exists */ async ensureNetwork() { try { await (0, exec_1.execAsync)(`docker network inspect ${this.config.network}`); } catch (error) { await (0, exec_1.execAsync)(`docker network create ${this.config.network}`); } } /** * Generate bitcoin.conf file with proper credentials */ generateBitcoinConf(sharedConfig) { // Use sharedConfig credentials if available, otherwise use instance defaults const rpcUser = sharedConfig?.bitcoin.rpcUser || this.rpcUser || "user"; const rpcPassword = sharedConfig?.bitcoin.rpcPassword || this.rpcPassword || "pass"; const rpcPort = sharedConfig?.bitcoin.rpcPort || 18443; // Store credentials for later use (for bitcoin-cli commands) this.rpcUser = rpcUser; this.rpcPassword = rpcPassword; return `# Bitcoin Core Configuration for Caravan-X Regtest # Global Settings server=1 # NOTE: Do NOT use daemon=1 in Docker - it must run in foreground! # RPC Authentication (global) rpcuser=${rpcUser} rpcpassword=${rpcPassword} # Wallet Settings fallbackfee=0.00001 # Performance dbcache=512 # Logging (verbose for development) debug=1 printtoconsole=1 # Regtest-specific settings [regtest] rpcport=${rpcPort} rpcallowip=0.0.0.0/0 rpcbind=0.0.0.0 listen=1 port=${this.config.ports.p2p}`.trim(); } /** * Start Bitcoin Core container with progress UI * This handles the Bitcoin Core container only - nginx is separate */ async startContainer(sharedConfig) { console.log(chalk_1.default.bold.cyan("\n๐Ÿš€ Starting Bitcoin Core Container\n")); const multibar = new cli_progress_1.default.MultiBar({ clearOnComplete: false, hideCursor: true, format: " {bar} | {status} | {percentage}%", barCompleteChar: "\u2588", barIncompleteChar: "\u2591", }, cli_progress_1.default.Presets.shades_classic); const mainProgress = multibar.create(100, 0, { status: "Initializing..." }); try { // Step 1: Check Docker mainProgress.update(5, { status: "Checking Docker..." }); const dockerAvailable = await this.checkDockerAvailable(); if (!dockerAvailable) { multibar.stop(); throw new Error("Docker is not installed or not running"); } mainProgress.update(10, { status: "Docker OK โœ“" }); // Step 2: Detect Architecture mainProgress.update(15, { status: "Detecting architecture..." }); const arch = await this.detectArchitecture(); mainProgress.update(20, { status: `Architecture: ${arch} โœ“` }); // Step 3: Set credentials from sharedConfig if (sharedConfig?.bitcoin.rpcUser && sharedConfig?.bitcoin.rpcPassword) { this.rpcUser = sharedConfig.bitcoin.rpcUser; this.rpcPassword = sharedConfig.bitcoin.rpcPassword; } // Step 4: Ensure ports are available (auto-adjust if needed) mainProgress.update(25, { status: "Checking ports..." }); const ports = await this.ensurePortsAvailable(); mainProgress.update(30, { status: "Ports available โœ“" }); // Update sharedConfig with actual ports being used if (sharedConfig) { sharedConfig.bitcoin.rpcPort = ports.rpc; sharedConfig.bitcoin.p2pPort = ports.p2p; } // Step 5: Setup Network mainProgress.update(35, { status: "Setting up network..." }); await this.ensureNetwork(); mainProgress.update(40, { status: "Network ready โœ“" }); // Step 6: Prepare Data Directory mainProgress.update(45, { status: "Preparing data directory..." }); const bitcoinDataDir = this.config.volumes.bitcoinData; await fs.ensureDir(bitcoinDataDir); await fs.ensureDir(path.join(bitcoinDataDir, "regtest")); await fs.ensureDir(path.join(bitcoinDataDir, "regtest", "wallets")); // Clean up any existing mining_wallet to prevent conflicts const miningWalletPath = path.join(bitcoinDataDir, "regtest", "wallets", "mining_wallet"); if (await fs.pathExists(miningWalletPath)) { mainProgress.update(47, { status: "Cleaning old mining wallet..." }); await fs.remove(miningWalletPath); } // Set permissions (777 for Docker compatibility) if (process.platform !== "win32") { try { await (0, exec_1.execAsync)(`chmod -R 777 "${bitcoinDataDir}"`); } catch (error) { console.warn(chalk_1.default.yellow("Warning: Could not set directory permissions")); } } mainProgress.update(50, { status: "Directories ready โœ“" }); // Step 7: Check Container Status & Clean Up mainProgress.update(55, { status: "Checking container status..." }); const status = await this.getContainerStatus(); if (status.containerId) { mainProgress.update(57, { status: "Removing old container..." }); try { if (status.running) { await (0, exec_1.execAsync)(`docker stop ${this.config.containerName}`); } await (0, exec_1.execAsync)(`docker rm ${this.config.containerName}`); } catch (error) { // Ignore errors, container might already be gone } } mainProgress.update(60, { status: "Ready to create โœ“" }); // Step 8: Create Container mainProgress.update(65, { status: "Creating container..." }); let dockerCommand = `docker run -d --name ${this.config.containerName}`; // Add platform flag for ARM64 systems if (arch.includes("arm64") || arch.includes("aarch64")) { dockerCommand += ` --platform linux/amd64`; } dockerCommand += ` --network ${this.config.network}`; dockerCommand += ` -p ${this.config.ports.rpc}:${this.config.ports.rpc}`; dockerCommand += ` -p ${this.config.ports.p2p}:${this.config.ports.p2p}`; dockerCommand += ` -v "${bitcoinDataDir}":/home/bitcoin/.bitcoin`; dockerCommand += ` ${this.config.image}`; dockerCommand += ` -regtest=1`; dockerCommand += ` -server=1`; dockerCommand += ` -rest=1`; dockerCommand += ` -txindex=1`; dockerCommand += ` -printtoconsole=1`; dockerCommand += ` -rpcallowip=0.0.0.0/0`; dockerCommand += ` -rpcbind=0.0.0.0`; dockerCommand += ` -rpcport=${this.config.ports.rpc}`; dockerCommand += ` -port=${this.config.ports.p2p}`; dockerCommand += ` -rpcuser=${this.rpcUser}`; dockerCommand += ` -rpcpassword=${this.rpcPassword}`; dockerCommand += ` -fallbackfee=0.00001`; await (0, exec_1.execAsync)(dockerCommand); mainProgress.update(70, { status: "Container created โœ“" }); // Step 9: Wait for Container to Start mainProgress.update(72, { status: "Waiting for container..." }); await new Promise((resolve) => setTimeout(resolve, 3000)); const newStatus = await this.getContainerStatus(); if (!newStatus.running) { multibar.stop(); const logs = await this.getLogs(50); console.error("\n" + chalk_1.default.red("โŒ Container failed to start!")); console.error(chalk_1.default.dim(logs)); throw new Error("Container stopped immediately after starting"); } mainProgress.update(75, { status: "Container running โœ“" }); // Step 10: Wait for RPC to be ready mainProgress.update(78, { status: "Waiting for RPC..." }); await this.waitForRpcReady(multibar, mainProgress); mainProgress.update(90, { status: "RPC ready โœ“" }); // Step 11: Generate Initial Blocks (if requested) if (sharedConfig?.initialState.preGenerateBlocks) { mainProgress.update(92, { status: "Generating blocks..." }); await this.generateInitialBlocks(sharedConfig.initialState.blockHeight, multibar, mainProgress); } mainProgress.update(100, { status: "Complete โœ“" }); multibar.stop(); console.log(chalk_1.default.bold.green("\nโœ… Bitcoin Core container started!\n")); } catch (error) { multibar.stop(); throw error; } } /** * Wait for RPC to be ready with retry logic */ async waitForRpcReady(multibar, mainProgress, maxAttempts = 60) { await new Promise((resolve) => setTimeout(resolve, 2000)); for (let i = 0; i < maxAttempts; i++) { try { const status = await this.getContainerStatus(); if (!status.running) { throw new Error("Container stopped unexpectedly"); } await this.execBitcoinCli("getblockchaininfo"); mainProgress.update(90, { status: "RPC connected โœ“" }); return; } catch (error) { const progress = 78 + (i / maxAttempts) * 12; mainProgress.update(progress, { status: `Waiting for RPC (${i + 1}/${maxAttempts})...`, }); if (i === maxAttempts - 1) { multibar.stop(); console.error(chalk_1.default.red("\nโŒ RPC Connection Failed\n")); console.error(chalk_1.default.white("Credentials being used:")); console.error(chalk_1.default.cyan(` User: ${this.rpcUser}`)); console.error(chalk_1.default.cyan(` Pass: ${this.rpcPassword}`)); throw new Error(`RPC did not become ready after ${maxAttempts} attempts`); } await new Promise((resolve) => setTimeout(resolve, 1000)); } } } /** * Generate initial blocks for testing */ async generateInitialBlocks(targetHeight, multibar, mainProgress) { try { mainProgress.update(93, { status: "Creating mining wallet..." }); await this.execBitcoinCli('createwallet "mining_wallet"'); mainProgress.update(95, { status: "Getting mining address..." }); const address = await this.execBitcoinCli("-rpcwallet=mining_wallet getnewaddress"); mainProgress.update(97, { status: `Generating ${targetHeight} blocks...`, }); await this.execBitcoinCli(`-rpcwallet=mining_wallet generatetoaddress ${targetHeight} ${address.trim()}`); } catch (error) { console.error(chalk_1.default.yellow("\nโš ๏ธ Could not generate initial blocks")); console.error(chalk_1.default.dim(error.message)); } } // ============================================================================ // NGINX PROXY SETUP (CORS-enabled RPC access) // ============================================================================ /** * Generate nginx configuration that proxies to Bitcoin Core */ generateNginxConfig(rpcPort) { return ` events { worker_connections 1024; } http { upstream bitcoin_regtest { server ${this.config.containerName}:${rpcPort}; } server { listen 8080; server_name localhost; # Disable request size limits for large transactions client_max_body_size 100m; location / { # Proxy to Bitcoin Core proxy_pass http://bitcoin_regtest; # Essential headers proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # HTTP version proxy_http_version 1.1; # Don't buffer - important for RPC proxy_buffering off; # Timeouts proxy_connect_timeout 300s; proxy_send_timeout 300s; proxy_read_timeout 300s; # CORS headers for Caravan (allow all methods including POST) add_header 'Access-Control-Allow-Origin' '*' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, Accept, Origin, User-Agent, DNT, Cache-Control, X-Mx-ReqToken, Keep-Alive, X-Requested-With, If-Modified-Since' always; add_header 'Access-Control-Expose-Headers' 'Content-Length, Content-Range' always; add_header 'Access-Control-Allow-Credentials' 'true' always; # Handle preflight OPTIONS requests if ($request_method = 'OPTIONS') { add_header 'Access-Control-Allow-Origin' '*' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, Accept, Origin, User-Agent, DNT, Cache-Control, X-Mx-ReqToken, Keep-Alive, X-Requested-With, If-Modified-Since' always; add_header 'Access-Control-Max-Age' 1728000; add_header 'Content-Type' 'text/plain; charset=utf-8'; add_header 'Content-Length' 0; return 204; } } } }`.trim(); } /** * Setup nginx proxy with automatic port detection * Returns the actual nginx port used (may differ from 8080 if port was taken) */ async setupNginxProxy(sharedConfig, force = false) { const spinner = (0, ora_1.default)("Setting up nginx proxy...").start(); try { // Check if nginx container exists const { stdout: nginxContainers } = await (0, exec_1.execAsync)("docker ps -a --filter name=caravan-x-nginx --format '{{.ID}}'"); // If container exists and we're not forcing recreation if (nginxContainers.trim() && !force) { const { stdout: runningStatus } = await (0, exec_1.execAsync)("docker ps --filter name=caravan-x-nginx --format '{{.Status}}'"); if (runningStatus.trim()) { spinner.succeed("Nginx proxy already running"); // Extract port from existing container const { stdout: portInfo } = await (0, exec_1.execAsync)("docker port caravan-x-nginx 8080 2>/dev/null || echo '8080'"); const nginxPort = parseInt(portInfo.split(":")[1]) || 8080; return nginxPort; } else { // Start existing container spinner.text = "Starting existing nginx container..."; await (0, exec_1.execAsync)("docker start caravan-x-nginx"); spinner.succeed("Nginx proxy started"); return 8080; } } // Remove old container if forcing recreation if (nginxContainers.trim()) { spinner.text = "Removing old nginx container..."; await (0, exec_1.execAsync)("docker rm -f caravan-x-nginx 2>/dev/null || true"); } // Get the Bitcoin Core RPC port (might have been auto-adjusted) const rpcPort = sharedConfig?.bitcoin.rpcPort || this.config.ports.rpc; // Find an available nginx port (starting from 8080) spinner.text = "Finding available nginx port..."; const nginxPort = await this.findAvailablePort(8080); if (nginxPort !== 8080) { console.log(chalk_1.default.yellow(`\n โ„น๏ธ Port 8080 in use, using ${nginxPort} instead\n`)); } // Create nginx config const nginxConfigDir = path.join(this.dataDir, "nginx"); await fs.ensureDir(nginxConfigDir); const nginxConfig = this.generateNginxConfig(rpcPort); await fs.writeFile(path.join(nginxConfigDir, "nginx.conf"), nginxConfig); spinner.text = "Creating nginx container..."; // Start nginx container with dynamic port mapping // Maps host port (nginxPort) to container port 8080 const nginxCommand = `docker run -d \ --name caravan-x-nginx \ --network ${this.config.network} \ -p ${nginxPort}:8080 \ -v "${nginxConfigDir}/nginx.conf":/etc/nginx/nginx.conf:ro \ nginx:alpine`; await (0, exec_1.execAsync)(nginxCommand.replace(/\s+/g, " ")); // Wait for nginx to start await new Promise((resolve) => setTimeout(resolve, 2000)); spinner.succeed(chalk_1.default.green(`Nginx proxy ready โ†’ http://localhost:${nginxPort}`)); return nginxPort; } catch (error) { spinner.fail("Failed to setup nginx proxy"); throw error; } } // ============================================================================ // WALLET MANAGEMENT // ============================================================================ /** * Create watch-only wallet for Caravan */ async createWatchOnlyWallet(walletName) { const spinner = (0, ora_1.default)(`Creating watch-only wallet: ${walletName}...`).start(); try { // Check if wallet already exists const wallets = await this.execBitcoinCli("listwallets"); if (wallets.includes(walletName)) { spinner.info(`Wallet "${walletName}" already exists`); return; } // Create watch-only wallet (descriptor-based, no private keys) await this.execBitcoinCli(`createwallet "${walletName}" true true "" false true true`); spinner.succeed(`Watch-only wallet "${walletName}" created`); } catch (error) { spinner.fail("Failed to create watch-only wallet"); throw error; } } // ============================================================================ // COMPLETE SETUP (Bitcoin + Nginx + Wallet) // ============================================================================ /** * Complete Docker setup - orchestrates all components * * This is the MAIN entry point for Docker mode setup. It: * 1. Checks and auto-adjusts ports if needed * 2. Starts Bitcoin Core container * 3. Sets up nginx proxy for CORS-enabled RPC access * 4. Creates watch-only wallet * 5. Tests the connection * 6. Displays connection info * * @returns The actual nginx port used */ async completeSetup(sharedConfig) { console.log(chalk_1.default.bold.cyan("\n๐Ÿš€ Complete Docker Setup\n")); try { // Step 1: Ensure ports are available (auto-adjust if needed) console.log(chalk_1.default.cyan("๐Ÿ” Checking port availability...\n")); const ports = await this.ensurePortsAvailable(); // Update shared config with actual ports if (sharedConfig) { sharedConfig.bitcoin.rpcPort = ports.rpc; sharedConfig.bitcoin.p2pPort = ports.p2p; } // Step 2: Start Bitcoin Core container await this.startContainer(sharedConfig); // Step 3: Setup nginx proxy console.log(chalk_1.default.cyan("\n๐Ÿ”ง Setting up nginx proxy...\n")); // Clean up any existing nginx first try { await (0, exec_1.execAsync)("docker stop caravan-x-nginx 2>/dev/null || true"); await (0, exec_1.execAsync)("docker rm caravan-x-nginx 2>/dev/null || true"); await new Promise((resolve) => setTimeout(resolve, 1000)); } catch (e) { // Ignore cleanup errors } // Create fresh nginx proxy (returns actual port used) const nginxPort = await this.setupNginxProxy(sharedConfig, true); // Wait for nginx to be fully ready await new Promise((resolve) => setTimeout(resolve, 3000)); // Step 4: Create watch-only wallet const walletName = sharedConfig?.walletName || "caravan_watcher"; console.log(chalk_1.default.cyan(`\n๐Ÿ“ Creating watch-only wallet...\n`)); await this.createWatchOnlyWallet(walletName); // Step 5: Test connection through nginx console.log(chalk_1.default.cyan("\n๐Ÿงช Testing connection...\n")); const connected = await this.testConnection("localhost", nginxPort); if (!connected) { console.log(chalk_1.default.yellow("\nโš ๏ธ Connection test failed, but containers are running")); console.log(chalk_1.default.dim("You can test manually with:")); console.log(chalk_1.default.cyan(` curl -u ${this.rpcUser}:${this.rpcPassword} \\`)); console.log(chalk_1.default.cyan(` -d '{"jsonrpc":"1.0","method":"getblockchaininfo","params":[]}' \\`)); console.log(chalk_1.default.cyan(` http://localhost:${nginxPort}\n`)); } // Step 6: Display connection info await this.displayConnectionInfo(sharedConfig, nginxPort, ports.rpc); // RETURN the actual nginx port so it can be saved to config return nginxPort; } catch (error) { console.error(chalk_1.default.red("\nโŒ Setup failed:"), error.message); throw error; } } // ============================================================================ // CONNECTION TESTING & INFO // ============================================================================ /** * Test RPC connection through nginx proxy */ async testConnection(host = "localhost", port = 8080) { const spinner = (0, ora_1.default)("Testing connection...").start(); try { const axios = require("axios"); const response = await axios.post(`http://${host}:${port}`, { jsonrpc: "1.0", id: "test", method: "getblockchaininfo", params: [], }, { auth: { username: this.rpcUser, password: this.rpcPassword, }, timeout: 5000, }); if (response.data && response.data.result) { spinner.succeed(chalk_1.default.green("Connection successful!")); console.log(chalk_1.default.cyan(" Chain:"), chalk_1.default.white(response.data.result.chain)); console.log(chalk_1.default.cyan(" Blocks:"), chalk_1.default.white(response.data.result.blocks)); return true; } else { spinner.fail("Unexpected response from Bitcoin Core"); return false; } } catch (error) { spinner.fail("Connection failed"); console.error(chalk_1.default.red(" Error:"), error.message); return false; } } /** * Get Bitcoin blockchain info */ async getBitcoinInfo() { const info = JSON.parse(await this.execBitcoinCli("getblockchaininfo")); const walletsStr = await this.execBitcoinCli("listwallets"); const wallets = JSON.parse(walletsStr); return { blocks: info.blocks, chain: info.chain, wallets: wallets, }; } /** * Display comprehensive connection information and instructions */ async displayConnectionInfo(sharedConfig, nginxPort = 8080, bitcoinRpcPort = 18443) { try { const info = await this.getBitcoinInfo(); const walletName = sharedConfig?.walletName || "caravan_watcher"; console.log("\n"); console.log((0, boxen_1.default)(chalk_1.default.bold.green("โœ… Docker Setup Complete!\n\n") + chalk_1.default.white.bold("๐Ÿณ Bitcoin Core is running in Docker\n") + chalk_1.default.white.bold("๐ŸŒ Nginx proxy is active for CORS support\n") + chalk_1.default.white.bold("๐Ÿ“ Watch-only wallet created\n\n") + chalk_1.default.cyan("Ready to connect with Caravan!"), { padding: 1, margin: 1, borderStyle: "double", borderColor: "green", })); console.log(chalk_1.default.bold.cyan("\nโ”".repeat(70))); console.log(chalk_1.default.bold.white("\n๐Ÿ“ก Connection Settings:\n")); console.log(chalk_1.default.cyan(" URL: ") + chalk_1.default.white.bold(`http://localhost:${nginxPort}`)); console.log(chalk_1.default.cyan(" Username: ") + chalk_1.default.white.bold(this.rpcUser)); console.log(chalk_1.default.cyan(" Password: ") + chalk_1.default.white.bold(this.rpcPassword)); console.log(chalk_1.default.cyan(" Wallet Name: ") + chalk_1.default.white.bold(walletName)); console.log(chalk_1.default.bold.white("\n๐Ÿ“Š Blockchain Status:\n")); console.log(chalk_1.default.cyan(" Network: ") + chalk_1.default.white(info.chain)); console.log(chalk_1.default.cyan(" Blocks: ") + chalk_1.default.white(info.blocks)); console.log(chalk_1.default.cyan(" Wallets: ") + chalk_1.default.white(info.wallets.join(", ") || "none")); console.log(chalk_1.default.bold.white("\n๐Ÿ”ง Technical Details:\n")); console.log(chalk_1.default.dim(" Bitcoin RPC: ") + chalk_1.default.dim(`localhost:${bitcoinRpcPort}`)); console.log(chalk_1.default.dim(" Nginx Proxy: ") + chalk_1.default.dim(`localhost:${nginxPort}`)); console.log(chalk_1.default.bold.cyan("โ”".repeat(70))); console.log(chalk_1.default.bold.yellow("\n๐Ÿš€ How to Connect with Caravan:\n")); console.log(chalk_1.default.white.bold("\n Step 1: Start Caravan Coordinator")); console.log(chalk_1.default.dim(" cd caravan")); console.log(chalk_1.default.dim(" npm start")); console.log(chalk_1.default.dim(" (Usually runs on http://localhost:5173)\n")); console.log(chalk_1.default.white.bold(" Step 2: Open Caravan in Browser")); console.log(chalk_1.default.dim(" Navigate to: ") + chalk_1.default.cyan("http://localhost:5173\n")); console.log(chalk_1.default.white.bold(" Step 3: Configure Bitcoin Client")); console.log(chalk_1.default.dim(" 1. Go to ") + chalk_1.default.white("Settings โ†’ Bitcoin Client")); console.log(chalk_1.default.dim(" 2. Select ") + chalk_1.default.white("Private")); console.log(chalk_1.default.dim(" 3. Select ") + chalk_1.default.white("Custom")); console.log(chalk_1.default.dim(" 4. Enter these settings:\n")); console.log((0, boxen_1.default)(chalk_1.default.cyan("URL: ") + chalk_1.default.white.bold(`http://localhost:${nginxPort}\n`) + chalk_1.default.cyan("Username: ") + chalk_1.default.white.bold(`${this.rpcUser}\n`) + chalk_1.default.cyan("Password: ") + chalk_1.default.white.bold(`${this.rpcPassword}\n`) + chalk_1.default.cyan("Wallet Name: ") + chalk_1.default.white.bold(`${walletName}`) + chalk_1.default.dim(" (optional)"), { padding: 1, margin: { top: 0, bottom: 1, left: 4, right: 0 }, borderStyle: "round", borderColor: "cyan", })); console.log(chalk_1.default.white.bold(" Step 4: Test Connection")); console.log(chalk_1.default.dim(" Click ") + chalk_1.default.white("'Test Connection'")); console.log(chalk_1.default.dim(" You should see: ") + chalk_1.default.green("โœ“ Connected to regtest")); console.log(chalk_1.default.bold.cyan("โ”".repeat(70))); console.log(chalk_1.default.bold.white("\n๐Ÿ’ก Quick Tips:\n")); console.log(chalk_1.default.dim(" โ€ข Nginx proxy handles CORS automatically")); console.log(chalk_1.default.dim(" โ€ข All ports are auto-detected to avoid conflicts")); console.log(chalk_1.default.dim(" โ€ข Run ") + chalk_1.default.white("'caravan-x'") + chalk_1.default.dim(" anytime to manage your setup")); console.log(chalk_1.default.dim(" โ€ข Use ") + chalk_1.default.white("'Docker Management'") + chalk_1.default.dim(" menu for container operations")); console.log(chalk_1.default.bold.cyan("โ”".repeat(70) + "\n")); console.log(chalk_1.default.green("๐ŸŽ‰ You're all set! Start building with Caravan.\n")); } catch (error) { console.log(chalk_1.default.yellow("\nโš ๏ธ Could not fetch connection info")); console.log(chalk_1.default.dim("Containers are running, but status check failed")); } } // ============================================================================ // CONTAINER LIFECYCLE OPERATIONS // ============================================================================ async stopContainer() { const spinner = (0, ora_1.default)("Stopping Bitcoin Core...").start(); try { const status = await this.getContainerStatus(); if (!status.running) { spinner.info("Container not running"); return; } await (0, exec_1.execAsync)(`docker stop ${this.config.containerName}`); spinner.succeed("Container stopped"); } catch (error) { spinner.fail("Failed to stop container"); throw error; } } async restartContainer() { const spinner = (0, ora_1.default)("Restarting Bitcoin Core...").start(); try { await (0, exec_1.execAsync)(`docker restart ${this.config.containerName}`); await new Promise((resolve) => setTimeout(resolve, 3000)); spinner.succeed("Container restarted"); } catch (error) { spinner.fail("Failed to restart"); throw error; } } async removeContainer() { const spinner = (0, ora_1.default)("Removing container...").start(); try { const status = await this.getContainerStatus(); if (!status.containerId) { spinner.info("Container does not exist"); return; } if (status.running) { await (0, exec_1.execAsync)(`docker stop ${this.config.containerName}`); } await (0, exec_1.execAsync)(`docker rm ${this.config.containerName}`); spinner.succeed("Container removed"); } catch (error) { spinner.fail("Failed to remove container"); throw error; } } // ============================================================================ // UTILITY METHODS // ============================================================================ /** * Execute bitcoin-cli command inside container */ async execBitcoinCli(command) { try { const cliCommand = `docker exec ${this.config.containerName} bitcoin-cli -regtest -rpcuser="${this.rpcUser}" -rpcpassword="${this.rpcPassword}" ${command}`; const { stdout } = await (0, exec_1.execAsync)(cliCommand); return stdout.trim(); } catch (error) { throw new Error(`Bitcoin CLI failed: ${error.message}`); } } async getLogs(tail = 100) { try { const { stdout } = await (0, exec_1.execAsync)(`docker logs --tail ${tail} ${this.config.containerName}`); return stdout; } catch (error) { throw new Error(`Failed to get logs: ${error.message}`); } } async openShell() { console.log(chalk_1.default.cyan("\n๐Ÿš Opening shell...")); console.log(chalk_1.default.yellow("Type 'exit' to return\n")); const shell = (0, child_process_1.spawn)("docker", ["exec", "-it", this.config.containerName, "/bin/bash"], { stdio: "inherit" }); return new Promise((resolve, reject) => { shell.on("close", (code) => { code === 0 ? resolve() : reject(new Error(`Shell exited with code ${code}`)); }); }); } } exports.DockerService = DockerService;