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