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
341 lines (340 loc) ⢠13.3 kB
JavaScript
"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.SetupWizard = void 0;
const prompts_1 = require("@inquirer/prompts");
const chalk_1 = __importDefault(require("chalk"));
const boxen_1 = __importDefault(require("boxen"));
const fs = __importStar(require("fs-extra"));
const path = __importStar(require("path"));
const config_1 = require("../types/config");
const docker_1 = require("../core/docker");
class SetupWizard {
constructor(appDir) {
this.appDir = appDir;
fs.ensureDirSync(appDir);
}
/**
* Setup Docker mode - called from index.ts
* @param skipDockerStart - if true, don't start containers (just return config)
*/
async setupDockerMode(skipDockerStart = false) {
console.log((0, boxen_1.default)(chalk_1.default.white.bold("š³ Docker Mode Configuration\n\n") +
chalk_1.default.gray("Caravan-X will configure:\n") +
chalk_1.default.white(" ⢠Bitcoin Core regtest container\n") +
chalk_1.default.white(" ⢠RPC authentication\n") +
chalk_1.default.white(" ⢠Nginx proxy for easy access\n") +
chalk_1.default.white(" ⢠Watch-only wallet"), {
padding: 1,
margin: 1,
borderStyle: "round",
borderColor: "cyan",
}));
// Bitcoin data directory - default to under appDir
const defaultBitcoinDataDir = path.join(this.appDir, "docker-data", "bitcoin-data");
// Check platform for recommendations
const isMacOS = process.platform === "darwin";
if (isMacOS) {
console.log((0, boxen_1.default)(chalk_1.default.cyan.bold("š macOS Note\n\n") +
chalk_1.default.white("For Docker compatibility, data will be stored under:\n") +
chalk_1.default.cyan(defaultBitcoinDataDir), {
padding: 1,
margin: 1,
borderStyle: "round",
borderColor: "cyan",
}));
}
const useCustomBitcoinDir = await (0, prompts_1.confirm)({
message: `Use default Bitcoin data location? (${defaultBitcoinDataDir})`,
default: true,
});
let bitcoinDataDir;
if (useCustomBitcoinDir) {
bitcoinDataDir = defaultBitcoinDataDir;
}
else {
bitcoinDataDir = await (0, prompts_1.input)({
message: "Enter Bitcoin data directory path:",
default: defaultBitcoinDataDir,
validate: (inputPath) => {
if (!inputPath.trim())
return "Path cannot be empty";
return true;
},
});
}
// Ensure directory exists and is writable
try {
await fs.ensureDir(bitcoinDataDir);
const testFile = path.join(bitcoinDataDir, ".write-test");
await fs.writeFile(testFile, "test");
await fs.remove(testFile);
}
catch (error) {
console.log((0, boxen_1.default)(chalk_1.default.red.bold("ā Permission Error\n\n") +
chalk_1.default.white(`Cannot write to: ${bitcoinDataDir}\n\n`) +
chalk_1.default.yellow("Please choose a different directory or fix permissions."), {
padding: 1,
margin: 1,
borderStyle: "round",
borderColor: "red",
}));
throw error;
}
console.log(chalk_1.default.white("\nš Docker Configuration:\n"));
const containerName = await (0, prompts_1.input)({
message: "Docker container name:",
default: config_1.DEFAULT_DOCKER_CONFIG.containerName,
});
const rpcPort = await (0, prompts_1.number)({
message: "Bitcoin RPC port:",
default: config_1.DEFAULT_DOCKER_CONFIG.ports.rpc,
});
const p2pPort = await (0, prompts_1.number)({
message: "Bitcoin P2P port:",
default: config_1.DEFAULT_DOCKER_CONFIG.ports.p2p,
});
console.log(chalk_1.default.white("\nš RPC Authentication:\n"));
const rpcUser = await (0, prompts_1.input)({
message: "RPC username:",
default: "caravan_user",
validate: (input) => input.length > 0 || "Username cannot be empty",
});
const rpcPassword = await (0, prompts_1.input)({
message: "RPC password:",
default: "caravan_pass",
validate: (input) => input.length > 0 || "Password cannot be empty",
});
console.log(chalk_1.default.white("\nš¼ Wallet Configuration:\n"));
const walletName = await (0, prompts_1.input)({
message: "Watch-only wallet name:",
default: "caravan_watcher",
validate: (input) => {
if (input.length === 0)
return "Wallet name cannot be empty";
if (!/^[a-zA-Z0-9_]+$/.test(input))
return "Only alphanumeric and underscore allowed";
return true;
},
});
const preGenerateBlocks = await (0, prompts_1.confirm)({
message: "Generate 101 initial blocks? (Needed for spending)",
default: true,
});
// Create Docker config
const dockerConfig = {
enabled: true,
image: config_1.DEFAULT_DOCKER_CONFIG.image,
containerName,
ports: {
rpc: rpcPort,
p2p: p2pPort,
nginx: 8080,
},
volumes: {
bitcoinData: bitcoinDataDir,
coordinator: path.join(this.appDir, "coordinator"),
},
network: config_1.DEFAULT_DOCKER_CONFIG.network,
autoStart: true,
};
// Create shared config
const sharedConfig = {
version: "1.0.0",
name: "Caravan-X Docker Setup",
mode: config_1.SetupMode.DOCKER,
bitcoin: {
network: "regtest",
rpcPort: rpcPort,
p2pPort: p2pPort,
rpcUser,
rpcPassword,
},
docker: dockerConfig,
initialState: {
blockHeight: preGenerateBlocks ? 101 : 0,
preGenerateBlocks,
wallets: [],
transactions: [],
},
walletName,
snapshots: {
enabled: true,
autoSnapshot: false,
},
};
// Create enhanced config (nginx port will be updated after Docker starts)
const config = {
mode: config_1.SetupMode.DOCKER,
sharedConfig,
docker: dockerConfig,
bitcoin: {
protocol: "http",
host: "localhost",
port: 8080, // Will be updated to actual nginx port
user: rpcUser,
pass: rpcPassword,
dataDir: bitcoinDataDir,
},
appDir: this.appDir,
caravanDir: path.join(this.appDir, "wallets"),
keysDir: path.join(this.appDir, "keys"),
snapshots: {
enabled: true,
directory: path.join(this.appDir, "snapshots"),
autoSnapshot: false,
},
scenariosDir: path.join(this.appDir, "scenarios"),
};
// Optionally start Docker
if (!skipDockerStart) {
const startNow = await (0, prompts_1.confirm)({
message: "Start Docker containers now?",
default: true,
});
if (startNow) {
const dockerService = new docker_1.DockerService(config.docker, path.join(config.appDir, "docker-data"));
const nginxPort = await dockerService.completeSetup(config.sharedConfig);
config.bitcoin.port = nginxPort;
}
}
// Ensure directories exist
await fs.ensureDir(config.caravanDir);
await fs.ensureDir(config.keysDir);
await fs.ensureDir(config.snapshots.directory);
await fs.ensureDir(config.scenariosDir);
return config;
}
/**
* Setup Manual mode - called from index.ts
*/
async setupManualMode() {
console.log((0, boxen_1.default)(chalk_1.default.white.bold("āļø Manual Mode Configuration\n\n") +
chalk_1.default.gray("You'll need to:\n") +
chalk_1.default.white(" ⢠Have Bitcoin Core installed\n") +
chalk_1.default.white(" ⢠Run bitcoind in regtest mode\n") +
chalk_1.default.white(" ⢠Configure RPC authentication"), {
padding: 1,
margin: 1,
borderStyle: "round",
borderColor: "yellow",
}));
console.log(chalk_1.default.white("\nš” Bitcoin Core RPC Settings:\n"));
const protocol = await (0, prompts_1.select)({
message: "RPC protocol:",
choices: [
{ name: "HTTP", value: "http" },
{ name: "HTTPS", value: "https" },
],
default: "http",
});
const host = await (0, prompts_1.input)({
message: "RPC host:",
default: "127.0.0.1",
});
const port = await (0, prompts_1.number)({
message: "RPC port:",
default: 18443,
});
const user = await (0, prompts_1.input)({
message: "RPC username:",
default: "user",
});
const pass = await (0, prompts_1.input)({
message: "RPC password:",
default: "pass",
});
const dataDir = await (0, prompts_1.input)({
message: "Bitcoin Core data directory:",
default: path.join(process.env.HOME || "~", ".bitcoin"),
});
// Create shared config
const sharedConfig = {
version: "1.0.0",
name: "Caravan-X Manual Setup",
description: "Manual Bitcoin Core configuration",
mode: config_1.SetupMode.MANUAL,
bitcoin: {
network: "regtest",
rpcPort: port,
p2pPort: 18444,
rpcUser: user,
rpcPassword: pass,
},
initialState: {
blockHeight: 0,
preGenerateBlocks: false,
wallets: [],
transactions: [],
},
snapshots: {
enabled: true,
autoSnapshot: false,
},
};
// Create enhanced config
const config = {
mode: config_1.SetupMode.MANUAL,
sharedConfig,
bitcoin: {
protocol: protocol,
host,
port: port,
user,
pass,
dataDir,
},
appDir: this.appDir,
caravanDir: path.join(this.appDir, "wallets"),
keysDir: path.join(this.appDir, "keys"),
snapshots: {
enabled: true,
directory: path.join(this.appDir, "snapshots"),
autoSnapshot: false,
},
scenariosDir: path.join(this.appDir, "scenarios"),
};
// Ensure directories exist
await fs.ensureDir(config.caravanDir);
await fs.ensureDir(config.keysDir);
await fs.ensureDir(config.snapshots.directory);
await fs.ensureDir(config.scenariosDir);
return config;
}
}
exports.SetupWizard = SetupWizard;