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

491 lines (490 loc) 20.4 kB
#!/usr/bin/env node "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 }); const commander_1 = require("commander"); const index_1 = __importDefault(require("./index")); const prompts_1 = require("@inquirer/prompts"); const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const ora_1 = __importDefault(require("ora")); const terminal_1 = require("./utils/terminal"); const cli_integration_1 = require("./scripting/cli-integration"); // Display the Caravan logo console.log(terminal_1.caravanLogo); // Set up the command line interface const program = new commander_1.Command(); program .name("caravan-regtest") .description("Terminal-based utility for managing Caravan in regtest mode") .version("1.0.0") .addHelpText("before", terminal_1.colors.info("A terminal-based utility for Bitcoin multisig wallet testing")) .option("--json", "Output results in JSON format", false); // List wallets command program .command("list-wallets") .description("List all available wallets") .action(async () => { const spinner = (0, ora_1.default)("Initializing...").start(); const app = new index_1.default(); spinner.succeed("Initialized"); try { await app.walletCommands.listWallets(); } catch (error) { console.error((0, terminal_1.formatError)("Error listing wallets:"), error); } }); // Create wallet command program .command("create-wallet") .description("Create a new wallet") .option("-n, --name <n>", "Name for the new wallet") .option("-w, --watch-only", "Create watch-only wallet (no private keys)") .option("-b, --blank", "Create blank wallet (no keys or addresses)") .action(async (options) => { const spinner = (0, ora_1.default)("Initializing...").start(); const app = new index_1.default(); spinner.succeed("Initialized"); try { // If a name was provided, use it directly if (options.name) { console.log((0, terminal_1.formatSuccess)(`Creating wallet with name: ${options.name}`)); // Here you would implement direct wallet creation with the provided options } else { await app.walletCommands.createWallet(); } } catch (error) { console.error((0, terminal_1.formatError)("Error creating wallet:"), error); } }); // Create private key wallet command // program // .command("create-key-wallet") // .description("Create a wallet with a known private key") // .option("-n, --name <n>", "Name for the new wallet") // .option("-k, --key <key>", "Private key in WIF format (optional)") // .action(async (options) => { // const spinner = ora("Initializing...").start(); // const app = new CaravanRegtestManager(); // spinner.succeed("Initialized"); // try { // await app.walletCommands.createPrivateKeyWallet(); // } catch (error) { // console.error(formatError("Error creating private key wallet:"), error); // } // }); // List Caravan wallets command program .command("list-caravan") .description("List all Caravan wallet configurations") .action(async () => { const spinner = (0, ora_1.default)("Initializing...").start(); const app = new index_1.default(); spinner.succeed("Initialized"); try { await app.multisigCommands.listCaravanWallets(); } catch (error) { console.error((0, terminal_1.formatError)("Error listing Caravan wallets:"), error); } }); // Create Caravan wallet command program .command("create-caravan") .description("Create a new Caravan multisig wallet") .action(async () => { const spinner = (0, ora_1.default)("Initializing...").start(); const app = new index_1.default(); spinner.succeed("Initialized"); try { await app.multisigCommands.createCaravanWallet(); } catch (error) { console.error((0, terminal_1.formatError)("Error creating Caravan wallet:"), error); } }); // Spend from Caravan wallet command program .command("spend-caravan") .description("Spend funds from a Caravan multisig wallet") .action(async () => { const spinner = (0, ora_1.default)("Initializing...").start(); const app = new index_1.default(); spinner.succeed("Initialized"); try { await app.multisigCommands.spendFromCaravanWallet(); } catch (error) { console.error((0, terminal_1.formatError)("Error spending from Caravan wallet:"), error); } }); // Fund wallet command program .command("fund-wallet") .description("Fund a wallet with new coins (using mining)") .option("-w, --wallet <n>", "Wallet name to fund") .option("-b, --blocks <number>", "Number of blocks to mine", "1") .action(async (options) => { const spinner = (0, ora_1.default)("Initializing...").start(); const app = new index_1.default(); spinner.succeed("Initialized"); try { await app.walletCommands.fundWallet(options.wallet); } catch (error) { console.error((0, terminal_1.formatError)("Error funding wallet:"), error); } }); // Create PSBT command program .command("create-psbt") .description("Create a new PSBT from a wallet") .option("-w, --wallet <n>", "Wallet name to create PSBT from") .option("-o, --output <address:amount>", "Output in format 'address:amount' (can be used multiple times)", (val, memo) => { //@ts-ignore memo.push(val); return memo; }, []) .action(async (options) => { const spinner = (0, ora_1.default)("Initializing...").start(); const app = new index_1.default(); spinner.succeed("Initialized"); try { await app.transactionCommands.createPSBT(); } catch (error) { console.error((0, terminal_1.formatError)("Error creating PSBT:"), error); } }); // Sign PSBT command program .command("sign-psbt") .description("Sign a PSBT with a wallet") .option("-f, --file <path>", "Path to PSBT file") .option("-w, --wallet <n>", "Wallet name to sign with") .action(async (options) => { const spinner = (0, ora_1.default)("Initializing...").start(); const app = new index_1.default(); spinner.succeed("Initialized"); try { let psbtBase64; if (options.file) { const readSpinner = (0, ora_1.default)(`Reading PSBT from ${options.file}...`).start(); psbtBase64 = (await fs.readFile(options.file, "utf8")).trim(); readSpinner.succeed("PSBT loaded"); } await app.transactionCommands.signPSBTWithWallet(psbtBase64, options.wallet); } catch (error) { console.error((0, terminal_1.formatError)("Error signing PSBT:"), error); } }); // Sign PSBT with private key command program .command("sign-psbt-key") .description("Sign a PSBT with a private key") .option("-f, --file <path>", "Path to PSBT file") .option("-k, --key <key>", "Private key in WIF format") .action(async (options) => { const spinner = (0, ora_1.default)("Initializing...").start(); const app = new index_1.default(); spinner.succeed("Initialized"); try { await app.transactionCommands.signPSBTWithPrivateKey(); } catch (error) { console.error((0, terminal_1.formatError)("Error signing PSBT with private key:"), error); } }); // Command for changing config path program .command("config-path") .description("Change configuration file location") .requiredOption("-p, --path <path>", "New path for config file") .action(async (options) => { try { const spinner = (0, ora_1.default)("Initializing...").start(); const app = new index_1.default(); spinner.succeed("Initialized"); const changeSpinner = (0, ora_1.default)(`Changing configuration path to ${options.path}...`).start(); await app.configManager.changeConfigLocation(options.path); changeSpinner.succeed(`Configuration path changed to: ${options.path}`); } catch (error) { console.error((0, terminal_1.formatError)("Error changing config path:"), error); } }); // Import Caravan wallet from file program .command("import-caravan") .description("Import a Caravan wallet configuration from a file") .requiredOption("-f, --file <path>", "Path to Caravan wallet JSON file") .action(async (options) => { const spinner = (0, ora_1.default)("Initializing...").start(); const app = new index_1.default(); spinner.succeed("Initialized"); try { const checkSpinner = (0, ora_1.default)(`Checking file ${options.file}...`).start(); if (!fs.existsSync(options.file)) { checkSpinner.fail(`File not found: ${options.file}`); return; } checkSpinner.succeed("File exists"); const readSpinner = (0, ora_1.default)("Reading wallet configuration...").start(); const config = await fs.readJson(options.file); if (!config.name || !config.quorum || !config.extendedPublicKeys) { readSpinner.fail("Invalid Caravan wallet configuration"); return; } readSpinner.succeed("Configuration validated"); const fileName = path.basename(options.file); console.log((0, terminal_1.boxText)(`Importing configuration from ${terminal_1.colors.highlight(fileName)}`, { title: "Import Caravan Wallet", titleColor: terminal_1.colors.info, })); // Import the configuration const importSpinner = (0, ora_1.default)(`Importing wallet "${config.name}"...`).start(); const savedFileName = await app.caravanService.saveCaravanWalletConfig(config); importSpinner.succeed(`Caravan wallet "${config.name}" imported successfully!`); // Ask if user wants to create a watch-only wallet const createWatch = await (0, prompts_1.confirm)({ message: "Create a watch-only wallet for this multisig wallet?", default: true, }); if (createWatch) { await app.multisigCommands.createWatchWallet(config); } } catch (error) { console.error((0, terminal_1.formatError)("Error importing Caravan wallet:"), error); } }); // Mining command program .command("mine") .description("Mine blocks in regtest mode") .option("-n, --blocks <number>", "Number of blocks to mine", "1") .option("-a, --address <address>", "Address to mine to") .option("-w, --wallet <n>", "Wallet to mine to (generates address automatically)") .action(async (options) => { const spinner = (0, ora_1.default)("Initializing...").start(); const app = new index_1.default(); spinner.succeed("Initialized"); try { if (options.wallet) { // Mine to a wallet const mineSpinner = (0, ora_1.default)(`Mining ${options.blocks} blocks to wallet ${options.wallet}...`).start(); const address = await app.bitcoinService.getNewAddress(options.wallet); const blockHashes = await app.bitcoinService.generateToAddress(parseInt(options.blocks), address); mineSpinner.succeed(`Mined ${options.blocks} blocks to wallet ${options.wallet}`); console.log((0, terminal_1.boxText)(`Successfully mined ${blockHashes.length} blocks to address ${address}.\n` + `Latest block hash: ${blockHashes[blockHashes.length - 1]}`, { title: "Mining Complete", titleColor: terminal_1.colors.success })); } else if (options.address) { // Mine to an address const mineSpinner = (0, ora_1.default)(`Mining ${options.blocks} blocks to address ${options.address}...`).start(); const blockHashes = await app.bitcoinService.generateToAddress(parseInt(options.blocks), options.address); mineSpinner.succeed(`Mined ${options.blocks} blocks to address ${options.address}`); console.log((0, terminal_1.boxText)(`Successfully mined ${blockHashes.length} blocks.\n` + `Latest block hash: ${blockHashes[blockHashes.length - 1]}`, { title: "Mining Complete", titleColor: terminal_1.colors.success })); } else { console.log((0, terminal_1.formatWarning)("Please specify either a wallet or an address to mine to.")); } } catch (error) { console.error((0, terminal_1.formatError)("Error mining blocks:"), error); } }); // Analyze PSBT command program .command("analyze-psbt") .description("Analyze and decode a PSBT") .option("-f, --file <path>", "Path to PSBT file") .action(async (options) => { const spinner = (0, ora_1.default)("Initializing...").start(); const app = new index_1.default(); spinner.succeed("Initialized"); try { await app.transactionCommands.analyzePSBT(); } catch (error) { console.error((0, terminal_1.formatError)("Error analyzing PSBT:"), error); } }); // Finalize and broadcast PSBT command program .command("finalize-psbt") .description("Finalize and broadcast a PSBT") .option("-f, --file <path>", "Path to PSBT file") .action(async (options) => { const spinner = (0, ora_1.default)("Initializing...").start(); const app = new index_1.default(); spinner.succeed("Initialized"); try { let psbtBase64; if (options.file) { const readSpinner = (0, ora_1.default)(`Reading PSBT from ${options.file}...`).start(); psbtBase64 = (await fs.readFile(options.file, "utf8")).trim(); readSpinner.succeed("PSBT loaded"); } await app.transactionCommands.finalizeAndBroadcastPSBT(psbtBase64); } catch (error) { console.error((0, terminal_1.formatError)("Error finalizing PSBT:"), error); } }); // Blockchain info command program .command("blockchain-info") .description("Show blockchain information") .action(async () => { const spinner = (0, ora_1.default)("Initializing...").start(); const app = new index_1.default(); spinner.succeed("Initialized"); try { // Otherwise use the RPC client directly const infoSpinner = (0, ora_1.default)("Fetching blockchain information...").start(); const info = await app.bitcoinService.rpc.getBlockchainInfo(); infoSpinner.succeed("Blockchain information fetched"); console.log((0, terminal_1.boxText)(`Chain: ${terminal_1.colors.highlight(info.chain)}\n` + `Blocks: ${terminal_1.colors.highlight(info.blocks.toString())}\n` + `Headers: ${terminal_1.colors.highlight(info.headers.toString())}\n` + `Best Block Hash: ${terminal_1.colors.highlight(info.bestblockhash)}\n` + `Difficulty: ${terminal_1.colors.highlight(info.difficulty.toString())}\n` + `Size on Disk: ${terminal_1.colors.highlight(`${(info.size_on_disk / 1000000).toFixed(2)} MB`)}\n` + `Verification Progress: ${terminal_1.colors.highlight(`${(info.verificationprogress * 100).toFixed(2)}%`)}`, { title: "Blockchain Status", titleColor: terminal_1.colors.header })); } catch (error) { console.error((0, terminal_1.formatError)("Error getting blockchain info:"), error); } }); // Visualization commands program .command("start-visualization") .description("Start the blockchain visualization server") .option("-p, --port <port>", "Port to use for the visualization server", "3000") .option("-n, --no-browser", "Don't open browser automatically") .action(async (options) => { const spinner = (0, ora_1.default)("Initializing...").start(); const app = new index_1.default(); spinner.succeed("Initialized"); try { // Force the port to be a number const port = parseInt(options.port); // Start visualization with browser option await app.visualizationCommands.startVisualization(); } catch (error) { console.error((0, terminal_1.formatError)("Error starting visualization:"), error); } }); program .command("stop-visualization") .description("Stop the blockchain visualization server") .action(async () => { const spinner = (0, ora_1.default)("Initializing...").start(); const app = new index_1.default(); spinner.succeed("Initialized"); try { await app.visualizationCommands.stopVisualization(); } catch (error) { console.error((0, terminal_1.formatError)("Error stopping visualization:"), error); } }); program .command("simulate-blockchain") .description("Simulate blockchain activity for testing") .option("-b, --blocks <number>", "Number of blocks to generate", "1") .option("-t, --transactions <number>", "Number of transactions to create", "3") .option("-w, --wallet <name>", "Wallet to use for mining") .action(async (options) => { const spinner = (0, ora_1.default)("Initializing...").start(); const app = new index_1.default(); spinner.succeed("Initialized"); try { await app.visualizationCommands.simulateBlockchain(); } catch (error) { console.error((0, terminal_1.formatError)("Error simulating blockchain:"), error); } }); // Sign Caravan PSBT command program .command("sign-caravan-psbt") .description("Sign a Caravan PSBT and generate importable signature JSON") .option("-f, --file <path>", "Path to PSBT file") .option("-k, --key <key>", "Private key in WIF format for signing") .option("-o, --output <path>", "Output path for signature JSON file") .action(async (options) => { const spinner = (0, ora_1.default)("Initializing...").start(); const app = new index_1.default(); spinner.succeed("Initialized"); try { await app.multisigCommands.signCaravanPSBT(); } catch (error) { console.error((0, terminal_1.formatError)("Error signing Caravan PSBT:"), error); } }); // Start the interactive app (default command) program .command("start", { isDefault: true }) .description("Start the interactive application") .action(async () => { const spinner = (0, ora_1.default)("Starting Caravan Regtest Manager...").start(); const app = new index_1.default(); spinner.succeed("Caravan Regtest Manager started"); // Add script commands to CLI if launched from CLI if (process.argv.length > 2 && process.argv[1].includes("cli")) { // Create a ScriptEngine instance and add commands to CLI const scriptEngine = app.scriptCommands.getScriptEngine(); // You'll need to add this method (0, cli_integration_1.addScriptCommandsToCLI)(program, scriptEngine); } try { await app.start(); } catch (error) { console.error((0, terminal_1.formatError)("Error starting application:"), error); } }); program.parse(process.argv);