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