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