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

729 lines (720 loc) 31.7 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.ScriptCommands = void 0; const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const prompts_1 = require("@inquirer/prompts"); const ora_1 = __importDefault(require("ora")); const scripting_1 = require("../types/scripting"); const ScriptEngine_1 = require("../scripting/ScriptEngine"); const terminal_1 = require("../utils/terminal"); // Back option constant const BACK_OPTION = "__BACK__"; /** * Commands for managing blockchain scripting scenarios */ class ScriptCommands { constructor(configManager, bitcoinService, caravanService, transactionService, rpcClient, multisigCommands) { this.configManager = configManager; this.bitcoinService = bitcoinService; this.caravanService = caravanService; this.transactionService = transactionService; this.rpcClient = rpcClient; this.multisigCommands = multisigCommands; // Initialize the script engine this.scriptEngine = new ScriptEngine_1.ScriptEngine(bitcoinService, caravanService, transactionService, configManager, rpcClient, multisigCommands); } /** * Get the script engine instance */ getScriptEngine() { return this.scriptEngine; } /** * Add a back option to selection choices */ addBackOption(choices, backLabel = "Back to menu") { return [...choices, { name: terminal_1.colors.muted(backLabel), value: BACK_OPTION }]; } /** * Check if a value is the back option */ isBackOption(value) { return value === BACK_OPTION; } /** * List available script templates */ async listScriptTemplates() { (0, terminal_1.displayCommandTitle)("Available Script Templates"); try { const loadingSpinner = (0, ora_1.default)("Loading script templates...").start(); const templates = await this.scriptEngine.getScriptTemplates(); loadingSpinner.succeed("Templates loaded"); if (templates.length === 0) { console.log((0, terminal_1.formatWarning)("No script templates found.")); return; } // Prepare data for table const tableRows = templates.map((template, index) => [ (index + 1).toString(), terminal_1.colors.highlight(template.name), template.description.substring(0, 60) + (template.description.length > 60 ? "..." : ""), template.path.endsWith(".js") ? terminal_1.colors.success("JavaScript") : terminal_1.colors.info("JSON"), ]); // Display table console.log((0, terminal_1.createTable)(["#", "Template Name", "Description", "Type"], tableRows)); // Ask if user wants to view or run a template const actionChoices = [ { name: terminal_1.colors.highlight("View template details"), value: "view" }, { name: terminal_1.colors.highlight("Run a template script"), value: "run" }, { name: terminal_1.colors.muted("Back to menu"), value: BACK_OPTION }, ]; const action = await (0, prompts_1.select)({ message: "What would you like to do?", choices: actionChoices, }); if (this.isBackOption(action)) { return; } if (action === "view") { await this.viewTemplateDetails(templates); } else if (action === "run") { await this.runScriptTemplate(templates); } } catch (error) { console.error((0, terminal_1.formatError)("Error listing script templates:"), error); } } /** * View details of a specific template */ async viewTemplateDetails(templates) { try { const templateOptions = templates.map((template, index) => ({ name: `${index + 1}. ${terminal_1.colors.highlight(template.name)}`, value: index, })); const selectedTemplateIndex = await (0, prompts_1.select)({ message: "Select a template to view:", choices: this.addBackOption(templateOptions), }); if (this.isBackOption(selectedTemplateIndex)) { return; } const selectedTemplate = templates[selectedTemplateIndex]; const templatePath = selectedTemplate.path; // Load the template content const loadingSpinner = (0, ora_1.default)(`Loading template: ${selectedTemplate.name}...`).start(); const scriptContent = await this.scriptEngine.loadScript(templatePath); loadingSpinner.succeed("Template loaded"); // Generate a summary const summary = this.scriptEngine.generateScriptSummary(scriptContent); // Display template details console.log((0, terminal_1.boxText)(summary, { title: `Template: ${selectedTemplate.name}`, titleColor: terminal_1.colors.header, })); // Display template content const viewContentChoices = [ { name: terminal_1.colors.highlight("View full script content"), value: "content", }, { name: terminal_1.colors.highlight("Run this script"), value: "run" }, { name: terminal_1.colors.muted("Back to template list"), value: BACK_OPTION }, ]; const viewAction = await (0, prompts_1.select)({ message: "What would you like to do next?", choices: viewContentChoices, }); if (this.isBackOption(viewAction)) { await this.listScriptTemplates(); return; } if (viewAction === "content") { // Display the content of the template console.clear(); (0, terminal_1.displayCommandTitle)(`Template: ${selectedTemplate.name}`); if (typeof scriptContent === "string") { // JavaScript content console.log(terminal_1.colors.code(scriptContent)); } else { // JSON content console.log(terminal_1.colors.code(JSON.stringify(scriptContent, null, 2))); } // Wait for user to continue await (0, prompts_1.input)({ message: "Press Enter to continue..." }); await this.viewTemplateDetails(templates); } else if (viewAction === "run") { // Run the template await this.executeScript(scriptContent, selectedTemplate.name); } } catch (error) { console.error((0, terminal_1.formatError)("Error viewing template details:"), error); } } /** * Run a script template */ async runScriptTemplate(templates) { try { const templateOptions = templates.map((template, index) => ({ name: `${index + 1}. ${terminal_1.colors.highlight(template.name)}`, value: index, })); const selectedTemplateIndex = await (0, prompts_1.select)({ message: "Select a template to run:", choices: this.addBackOption(templateOptions), }); if (this.isBackOption(selectedTemplateIndex)) { return; } const selectedTemplate = templates[selectedTemplateIndex]; const templatePath = selectedTemplate.path; // Load the template content const loadingSpinner = (0, ora_1.default)(`Loading template: ${selectedTemplate.name}...`).start(); const scriptContent = await this.scriptEngine.loadScript(templatePath); loadingSpinner.succeed("Template loaded"); // Execute the script await this.executeScript(scriptContent, selectedTemplate.name); } catch (error) { console.error((0, terminal_1.formatError)("Error running script template:"), error); } } /** * Execute a script with validation and confirmation */ async executeScript(script, name) { try { // Validate the script const validationSpinner = (0, ora_1.default)("Validating script...").start(); const validationResult = this.scriptEngine.validateScript(script); if (!validationResult.valid) { validationSpinner.fail("Script validation failed"); console.log((0, terminal_1.formatError)("Validation errors:")); validationResult.errors.forEach((error) => { console.log(` - ${error}`); }); return; } validationSpinner.succeed("Script is valid"); // Generate script summary const summary = this.scriptEngine.generateScriptSummary(script); console.log((0, terminal_1.boxText)(summary, { title: `Script Summary: ${name}`, titleColor: terminal_1.colors.info, })); // Ask for confirmation const confirmRun = await (0, prompts_1.confirm)({ message: "Do you want to execute this script?", default: true, }); if (!confirmRun) { console.log((0, terminal_1.formatWarning)("Script execution cancelled.")); return; } // Ask for execution options console.log(terminal_1.colors.header("\nExecution Options:")); const dryRun = await (0, prompts_1.confirm)({ message: "Run in dry-run mode (no actual changes)?", default: false, }); const verbose = await (0, prompts_1.confirm)({ message: "Enable verbose logging?", default: true, }); const interactive = await (0, prompts_1.confirm)({ message: "Run in interactive mode (confirm each step)?", default: false, }); // Execute the script const executionSpinner = (0, ora_1.default)("Executing script...").start(); // Set up progress event handling this.scriptEngine.on("progress", (progress) => { executionSpinner.text = `Executing step ${progress.step}/${progress.total}: ${progress.message}`; }); this.scriptEngine.on("log", (message) => { // Only show log messages in verbose mode if (verbose) { executionSpinner.stop(); console.log(message); executionSpinner.start(); } }); try { const result = await this.scriptEngine.executeScript(script, { dryRun, verbose, interactive, }); executionSpinner.succeed("Script execution completed"); // Display execution summary console.log((0, terminal_1.boxText)(`Status: ${terminal_1.colors.success(result.status)}\n` + `Duration: ${terminal_1.colors.highlight((result.duration / 1000).toFixed(2))} seconds\n` + `Steps completed: ${terminal_1.colors.highlight(result.steps.filter((s) => s.status === "success").length.toString())} of ${result.steps.length}\n` + `Started: ${new Date(result.startTime).toLocaleString()}\n` + `Ended: ${new Date(result.endTime).toLocaleString()}`, { title: "Execution Results", titleColor: terminal_1.colors.success })); // Display outputs if (result.outputs.wallets && result.outputs.wallets.length > 0) { console.log(terminal_1.colors.header("\nWallets created:")); result.outputs.wallets.forEach((wallet) => { console.log(` - ${terminal_1.colors.highlight(wallet)}`); }); } if (result.outputs.transactions && result.outputs.transactions.length > 0) { console.log(terminal_1.colors.header("\nTransactions created:")); result.outputs.transactions.forEach((tx) => { console.log(` - ${terminal_1.colors.highlight(tx)}`); }); } if (result.outputs.blocks && result.outputs.blocks.length > 0) { console.log(terminal_1.colors.header("\nBlocks mined:")); console.log(` - ${result.outputs.blocks.length} blocks`); } } catch (error) { executionSpinner.fail("Script execution failed"); console.log((0, terminal_1.formatError)(`Error: ${error.message}`)); } } catch (error) { console.error((0, terminal_1.formatError)("Error executing script:"), error); } } /** * Create a new script */ async createNewScript() { (0, terminal_1.displayCommandTitle)("Create New Script"); try { // Ask for script type const scriptType = await (0, prompts_1.select)({ message: "Select script type:", choices: [ { name: terminal_1.colors.highlight("JavaScript (Programmatic)"), value: scripting_1.ScriptType.JAVASCRIPT, }, { name: terminal_1.colors.highlight("JSON (Declarative)"), value: scripting_1.ScriptType.JSON, }, { name: terminal_1.colors.muted("Back to menu"), value: BACK_OPTION }, ], }); if (this.isBackOption(scriptType)) { return; } // Ask for script name const scriptName = await (0, prompts_1.input)({ message: "Enter a name for your script:", validate: (input) => input.trim() !== "" ? true : "Script name is required", }); // Ask for script description const scriptDescription = await (0, prompts_1.input)({ message: "Enter a description for your script:", validate: (input) => input.trim() !== "" ? true : "Script description is required", }); let scriptContent; if (scriptType === scripting_1.ScriptType.JAVASCRIPT) { // Create a JavaScript template scriptContent = this.createJavaScriptTemplate(scriptName, scriptDescription); } else { // Create a JSON template scriptContent = this.createJSONTemplate(scriptName, scriptDescription); } // Show script preview console.log(terminal_1.colors.header("\nScript Preview:")); console.log(terminal_1.colors.code(scriptContent)); // Ask to save or edit const action = await (0, prompts_1.select)({ message: "What would you like to do with this script?", choices: [ { name: terminal_1.colors.highlight("Save script"), value: "save" }, { name: terminal_1.colors.highlight("Edit script before saving"), value: "edit", }, { name: terminal_1.colors.muted("Cancel"), value: "cancel" }, ], }); if (action === "cancel") { console.log((0, terminal_1.formatWarning)("Script creation cancelled.")); return; } if (action === "edit") { console.log((0, terminal_1.formatWarning)("External editing not implemented in this demo.")); // In a real implementation, you would launch an editor here } // Save the script if (action === "save" || action === "edit") { const saveSpinner = (0, ora_1.default)("Saving script...").start(); try { const scriptPath = await this.scriptEngine.saveScript(scriptName, scriptContent, //@ts-expect-error scriptType); saveSpinner.succeed(`Script saved to: ${scriptPath}`); // Ask if user wants to run the script now const runNow = await (0, prompts_1.confirm)({ message: "Would you like to run this script now?", default: false, }); if (runNow) { // Parse the content according to type const parsedContent = scriptType === scripting_1.ScriptType.JSON ? JSON.parse(scriptContent) : scriptContent; await this.executeScript(parsedContent, scriptName); } } catch (error) { saveSpinner.fail("Failed to save script"); console.log((0, terminal_1.formatError)(`Error: ${error.message}`)); } } } catch (error) { console.error((0, terminal_1.formatError)("Error creating script:"), error); } } /** * Create a JavaScript template */ createJavaScriptTemplate(name, description) { return `/** * @name ${name} * @description ${description} * @version 1.0.0 * @author Caravan Regtest Manager */ // Global configuration const config = { // Add your configuration parameters here walletName: 'script_wallet', }; // Main function to run the script async function runScript() { try { console.log('Running script: ${name}'); // Create a wallet await bitcoinService.createWallet(config.walletName, { disablePrivateKeys: false }); // Mine some blocks to fund the wallet const address = await bitcoinService.getNewAddress(config.walletName); const blockHashes = await bitcoinService.generateToAddress(5, address); console.log(\`Mined \${blockHashes.length} blocks to fund wallet\`); // Add your script logic here return { success: true, message: 'Script completed successfully' }; } catch (error) { console.error(\`Error in script: \${error.message}\`); throw error; } } // Run the script runScript() .then(result => { console.log('Script completed successfully!'); console.log(result); }) .catch(error => { console.error('Script failed:', error.message); }); `; } /** * Create a JSON template */ createJSONTemplate(name, description) { const template = { name, description, version: "1.0.0", variables: { walletName: "script_wallet", amount: 1.0, }, actions: [ { type: "CREATE_WALLET", description: "Create a wallet for testing", params: { name: "${walletName}", options: { disablePrivateKeys: false, }, variableName: "wallet", }, }, { type: "MINE_BLOCKS", description: "Mine blocks to fund the wallet", params: { toWallet: "${walletName}", count: 5, variableName: "blocks", }, }, // Add more actions as needed ], }; return JSON.stringify(template, null, 2); } /** * Run a custom script from file */ async runCustomScript() { (0, terminal_1.displayCommandTitle)("Run Custom Script"); try { // Options for loading a script const loadOptions = [ { name: terminal_1.colors.highlight("Load from file"), value: "file" }, { name: terminal_1.colors.highlight("Import from templates"), value: "template" }, { name: terminal_1.colors.muted("Back to menu"), value: BACK_OPTION }, ]; const loadFrom = await (0, prompts_1.select)({ message: "How would you like to load the script?", choices: loadOptions, }); if (this.isBackOption(loadFrom)) { return; } let scriptContent; let scriptName; if (loadFrom === "file") { // Get file path from user const filePath = await (0, prompts_1.input)({ message: "Enter the path to the script file:", validate: (input) => { if (!input.trim()) return "File path is required"; if (!fs.existsSync(input)) return "File not found"; return true; }, }); // Load the script const loadingSpinner = (0, ora_1.default)("Loading script...").start(); scriptContent = await this.scriptEngine.loadScript(filePath); scriptName = path.basename(filePath); loadingSpinner.succeed("Script loaded"); } else { // Load from templates const loadingSpinner = (0, ora_1.default)("Loading templates...").start(); const templates = await this.scriptEngine.getScriptTemplates(); loadingSpinner.succeed("Templates loaded"); if (templates.length === 0) { console.log((0, terminal_1.formatWarning)("No templates found.")); return; } // Let user select a template const templateOptions = templates.map((template, index) => ({ name: `${index + 1}. ${terminal_1.colors.highlight(template.name)}`, value: index, })); const selectedTemplateIndex = await (0, prompts_1.select)({ message: "Select a template:", choices: this.addBackOption(templateOptions), }); if (this.isBackOption(selectedTemplateIndex)) { return; } const selectedTemplate = templates[selectedTemplateIndex]; scriptContent = await this.scriptEngine.loadScript(selectedTemplate.path); scriptName = selectedTemplate.name; } // Execute the script await this.executeScript(scriptContent, scriptName); } catch (error) { console.error((0, terminal_1.formatError)("Error running custom script:"), error); } } /** * Manage saved scripts */ async manageScripts() { (0, terminal_1.displayCommandTitle)("Manage Scripts"); try { // Get the scripts directory const scriptsDir = path.join(this.configManager.getConfig().appDir, "scripts"); await fs.ensureDir(scriptsDir); // List all scripts const listingSpinner = (0, ora_1.default)("Loading saved scripts...").start(); const files = await fs.readdir(scriptsDir); const scriptFiles = files.filter((file) => file.endsWith(".js") || file.endsWith(".json")); listingSpinner.succeed("Scripts loaded"); if (scriptFiles.length === 0) { console.log((0, terminal_1.formatWarning)("No saved scripts found.")); return; } // List the scripts const scripts = []; for (const file of scriptFiles) { const filePath = path.join(scriptsDir, file); try { const content = await fs.readFile(filePath, "utf8"); let name = file; let description = ""; if (file.endsWith(".json")) { // Extract from JSON const json = JSON.parse(content); if (json.name) name = json.name; if (json.description) description = json.description; } else { // Extract from JS comments const nameMatch = content.match(/@name\s+(.+)/); const descMatch = content.match(/@description\s+(.+)/); if (nameMatch) name = nameMatch[1].trim(); if (descMatch) description = descMatch[1].trim(); } scripts.push({ file, path: filePath, name, description, type: file.endsWith(".js") ? scripting_1.ScriptType.JAVASCRIPT : scripting_1.ScriptType.JSON, }); } catch (error) { console.log((0, terminal_1.formatWarning)(`Could not read script ${file}: ${error.message}`)); } } // Display scripts table const tableRows = scripts.map((script, index) => [ (index + 1).toString(), terminal_1.colors.highlight(script.name), script.description.substring(0, 60) + (script.description.length > 60 ? "..." : ""), script.type === scripting_1.ScriptType.JAVASCRIPT ? terminal_1.colors.success("JavaScript") : terminal_1.colors.info("JSON"), ]); console.log((0, terminal_1.createTable)(["#", "Script Name", "Description", "Type"], tableRows)); // Ask what to do with scripts const action = await (0, prompts_1.select)({ message: "What would you like to do?", choices: [ { name: terminal_1.colors.highlight("Run a script"), value: "run" }, { name: terminal_1.colors.highlight("View script details"), value: "view" }, { name: terminal_1.colors.highlight("Delete a script"), value: "delete" }, { name: terminal_1.colors.muted("Back to menu"), value: BACK_OPTION }, ], }); if (this.isBackOption(action)) { return; } // Select a script const scriptOptions = scripts.map((script, index) => ({ name: `${index + 1}. ${terminal_1.colors.highlight(script.name)}`, value: index, })); const selectedIndex = await (0, prompts_1.select)({ message: `Select a script to ${action}:`, choices: this.addBackOption(scriptOptions), }); if (this.isBackOption(selectedIndex)) { return await this.manageScripts(); } const selectedScript = scripts[selectedIndex]; if (action === "run") { // Run the script const loadingSpinner = (0, ora_1.default)(`Loading script: ${selectedScript.name}...`).start(); const scriptContent = await this.scriptEngine.loadScript(selectedScript.path); loadingSpinner.succeed("Script loaded"); await this.executeScript(scriptContent, selectedScript.name); } else if (action === "view") { // View script details console.clear(); (0, terminal_1.displayCommandTitle)(`Script: ${selectedScript.name}`); // Read content const content = await fs.readFile(selectedScript.path, "utf8"); console.log(terminal_1.colors.code(content)); // Wait for user to continue await (0, prompts_1.input)({ message: "Press Enter to continue..." }); await this.manageScripts(); } else if (action === "delete") { // Confirm deletion const confirmDelete = await (0, prompts_1.confirm)({ message: `Are you sure you want to delete the script "${selectedScript.name}"?`, default: false, }); if (confirmDelete) { const deleteSpinner = (0, ora_1.default)(`Deleting script: ${selectedScript.name}...`).start(); await fs.remove(selectedScript.path); deleteSpinner.succeed("Script deleted"); } else { console.log((0, terminal_1.formatWarning)("Deletion cancelled.")); } // Go back to manage scripts await this.manageScripts(); } } catch (error) { console.error((0, terminal_1.formatError)("Error managing scripts:"), error); } } } exports.ScriptCommands = ScriptCommands;