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
995 lines • 58 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.ScriptEngine = void 0;
const fs = __importStar(require("fs-extra"));
const path = __importStar(require("path"));
const vm2_1 = require("vm2");
const events_1 = require("events");
const chalk_1 = __importDefault(require("chalk"));
const terminal_1 = require("../utils/terminal");
const scripting_1 = require("../types/scripting");
/**
* Class for managing script execution
*/
class ScriptEngine extends events_1.EventEmitter {
constructor(bitcoinService, caravanService, transactionService, configManager, rpcClient, multisigCommands) {
super();
this.bitcoinService = bitcoinService;
this.caravanService = caravanService;
this.transactionService = transactionService;
this.configManager = configManager;
this.rpcClient = rpcClient;
this.multisigCommands = multisigCommands;
// Set up templates directory
this.templatesDir = path.join(__dirname, "templates");
fs.ensureDirSync(this.templatesDir);
}
/**
* Get the template directory path
*/
getTemplatesDir() {
return this.templatesDir;
}
/**
* Load a script from file
*/
async loadScript(filePath) {
try {
// Check if file exists
if (!fs.existsSync(filePath)) {
throw new Error(`Script file not found: ${filePath}`);
}
// Read the file
const fileContent = await fs.readFile(filePath, "utf8");
// Determine the script type based on file extension
if (filePath.endsWith(".json")) {
// Parse as JSON
return JSON.parse(fileContent);
}
else {
// Return as JavaScript
return fileContent;
}
}
catch (error) {
throw new Error(`Failed to load script: ${error.message}`);
}
}
/**
* Validate a script
*/
validateScript(script) {
const errors = [];
try {
if (typeof script === "string") {
// Validate JavaScript script
try {
// Basic syntax validation
new Function(script);
}
catch (e) {
errors.push(`Syntax error: ${e.message}`);
}
}
else {
// Validate declarative script (JSON format)
if (!script.name) {
errors.push("Script name is required");
}
if (!script.description) {
errors.push("Script description is recommended");
}
if (!script.version) {
errors.push("Script version is recommended");
}
if (!script.actions ||
!Array.isArray(script.actions) ||
script.actions.length === 0) {
errors.push("Script must have at least one action");
}
else {
// Validate each action
script.actions.forEach((action, index) => {
if (!action.type) {
errors.push(`Action #${index + 1} is missing a type`);
}
else if (!Object.values(scripting_1.ActionType).includes(action.type)) {
errors.push(`Action #${index + 1} has invalid type: ${action.type}`);
}
if (!action.params || typeof action.params !== "object") {
errors.push(`Action #${index + 1} is missing parameters`);
}
else {
// Validate parameters based on action type
switch (action.type) {
case scripting_1.ActionType.CREATE_WALLET:
if (!action.params.name) {
errors.push(`Action #${index + 1}: CREATE_WALLET requires a name parameter`);
}
if (action.params.options &&
typeof action.params.options !== "object") {
errors.push(`Action #${index + 1}: CREATE_WALLET options parameter must be an object`);
}
break;
case scripting_1.ActionType.MINE_BLOCKS:
if (typeof action.params.count !== "number" ||
action.params.count <= 0) {
errors.push(`Action #${index + 1}: MINE_BLOCKS requires a positive count parameter`);
}
if (!action.params.toWallet && !action.params.toAddress) {
errors.push(`Action #${index + 1}: MINE_BLOCKS requires either toWallet or toAddress parameter`);
}
break;
case scripting_1.ActionType.CREATE_TRANSACTION:
if (!action.params.fromWallet) {
errors.push(`Action #${index + 1}: CREATE_TRANSACTION requires a fromWallet parameter`);
}
if (!action.params.outputs ||
!Array.isArray(action.params.outputs) ||
action.params.outputs.length === 0) {
errors.push(`Action #${index + 1}: CREATE_TRANSACTION requires outputs parameter as an array`);
}
else {
// Validate each output has an address and amount
action.params.outputs.forEach((output, outputIndex) => {
if (typeof output !== "object" ||
Object.keys(output).length !== 1) {
errors.push(`Action #${index + 1}: Output #${outputIndex + 1} must be an object with a single key-value pair`);
}
else {
const address = Object.keys(output)[0];
const amount = output[address];
if (!address || address.trim().length === 0) {
errors.push(`Action #${index + 1}: Output #${outputIndex + 1} is missing an address`);
}
if (typeof amount !== "number" || amount <= 0) {
errors.push(`Action #${index + 1}: Output #${outputIndex + 1} requires a positive amount`);
}
}
});
}
if (action.params.feeRate !== undefined &&
(typeof action.params.feeRate !== "number" ||
action.params.feeRate < 0)) {
errors.push(`Action #${index + 1}: CREATE_TRANSACTION feeRate must be a non-negative number`);
}
break;
case scripting_1.ActionType.REPLACE_TRANSACTION:
if (!action.params.txid) {
errors.push(`Action #${index + 1}: REPLACE_TRANSACTION requires a txid parameter`);
}
if (action.params.newFeeRate !== undefined &&
(typeof action.params.newFeeRate !== "number" ||
action.params.newFeeRate <= 0)) {
errors.push(`Action #${index + 1}: REPLACE_TRANSACTION newFeeRate must be a positive number`);
}
if (action.params.newOutputs &&
(!Array.isArray(action.params.newOutputs) ||
action.params.newOutputs.length === 0)) {
errors.push(`Action #${index + 1}: REPLACE_TRANSACTION newOutputs must be a non-empty array`);
}
break;
case scripting_1.ActionType.SIGN_TRANSACTION:
if (!action.params.txid) {
errors.push(`Action #${index + 1}: SIGN_TRANSACTION requires a txid parameter`);
}
if (!action.params.wallet && !action.params.privateKey) {
errors.push(`Action #${index + 1}: SIGN_TRANSACTION requires either wallet or privateKey parameter`);
}
break;
case scripting_1.ActionType.BROADCAST_TRANSACTION:
if (!action.params.txid && !action.params.psbt) {
errors.push(`Action #${index + 1}: BROADCAST_TRANSACTION requires either txid or psbt parameter`);
}
break;
case scripting_1.ActionType.CREATE_MULTISIG:
if (!action.params.name) {
errors.push(`Action #${index + 1}: CREATE_MULTISIG requires a name parameter`);
}
if (typeof action.params.requiredSigners !== "number" ||
action.params.requiredSigners <= 0) {
errors.push(`Action #${index + 1}: CREATE_MULTISIG requires a positive requiredSigners parameter`);
}
if (typeof action.params.totalSigners !== "number" ||
action.params.totalSigners <= 0) {
errors.push(`Action #${index + 1}: CREATE_MULTISIG requires a positive totalSigners parameter`);
}
if (action.params.requiredSigners > action.params.totalSigners) {
errors.push(`Action #${index + 1}: CREATE_MULTISIG requiredSigners cannot be greater than totalSigners`);
}
if (!action.params.addressType) {
errors.push(`Action #${index + 1}: CREATE_MULTISIG requires an addressType parameter`);
}
else if (!["P2SH", "P2WSH", "P2SH-P2WSH"].includes(action.params.addressType)) {
errors.push(`Action #${index + 1}: CREATE_MULTISIG addressType must be one of: P2SH, P2WSH, P2SH-P2WSH`);
}
break;
case scripting_1.ActionType.WAIT:
if (typeof action.params.seconds !== "number" ||
action.params.seconds <= 0) {
errors.push(`Action #${index + 1}: WAIT requires a positive seconds parameter`);
}
break;
case scripting_1.ActionType.ASSERT:
if (!action.params.condition) {
errors.push(`Action #${index + 1}: ASSERT requires a condition parameter`);
}
if (typeof action.params.condition !== "string" &&
typeof action.params.condition !== "boolean") {
errors.push(`Action #${index + 1}: ASSERT condition must be a string or boolean`);
}
if (!action.params.message) {
errors.push(`Action #${index + 1}: ASSERT requires a message parameter`);
}
break;
case scripting_1.ActionType.CUSTOM:
if (!action.params.code ||
typeof action.params.code !== "string") {
errors.push(`Action #${index + 1}: CUSTOM requires a code parameter as a string`);
}
try {
// Basic validation of the code
new Function("context", action.params.code);
}
catch (e) {
errors.push(`Action #${index + 1}: CUSTOM code has syntax error: ${e.message}`);
}
break;
default:
errors.push(`Action #${index + 1}: Unknown action type ${action.type}`);
}
// Validate the variableName parameter if present
if (action.params.variableName !== undefined &&
typeof action.params.variableName !== "string") {
errors.push(`Action #${index + 1}: variableName must be a string`);
}
}
});
}
// Validate variable references
if (script.variables && script.actions) {
this.validateVariableReferences(script, errors);
}
}
}
catch (error) {
errors.push(`Validation error: ${error.message}`);
}
return {
valid: errors.length === 0,
errors,
};
}
/**
* Validate that variable references are valid
*/
validateVariableReferences(script, errors) {
const variables = new Set(Object.keys(script.variables || {}));
// Track variables created by actions
let actionIndex = 0;
for (const action of script.actions) {
actionIndex++;
if (action.params && typeof action.params === "object") {
// Add any variables created by this action
if (action.params.variableName &&
typeof action.params.variableName === "string") {
variables.add(action.params.variableName);
}
// Check variable references in parameters
for (const [paramName, paramValue] of Object.entries(action.params)) {
if (typeof paramValue === "string") {
this.checkVariableReferencesInString(paramValue, variables, actionIndex, errors);
}
else if (Array.isArray(paramValue)) {
for (const item of paramValue) {
if (typeof item === "string") {
this.checkVariableReferencesInString(item, variables, actionIndex, errors);
}
else if (typeof item === "object" && item !== null) {
this.checkVariableReferencesInObject(item, variables, actionIndex, errors);
}
}
}
else if (typeof paramValue === "object" && paramValue !== null) {
this.checkVariableReferencesInObject(paramValue, variables, actionIndex, errors);
}
}
}
}
}
/**
* Check for variable references in a string
*/
checkVariableReferencesInString(str, variables, actionIndex, errors) {
const matches = str.match(/\${([^}]+)}/g) || [];
for (const match of matches) {
const varName = match.substring(2, match.length - 1);
if (!variables.has(varName)) {
errors.push(`Action #${actionIndex}: Reference to undefined variable "${varName}"`);
}
}
}
/**
* Check for variable references in an object
*/
checkVariableReferencesInObject(obj, variables, actionIndex, errors) {
for (const [key, value] of Object.entries(obj)) {
if (typeof value === "string") {
this.checkVariableReferencesInString(value, variables, actionIndex, errors);
}
else if (Array.isArray(value)) {
for (const item of value) {
if (typeof item === "string") {
this.checkVariableReferencesInString(item, variables, actionIndex, errors);
}
else if (typeof item === "object" && item !== null) {
this.checkVariableReferencesInObject(item, variables, actionIndex, errors);
}
}
}
else if (typeof value === "object" && value !== null) {
this.checkVariableReferencesInObject(value, variables, actionIndex, errors);
}
}
}
/**
* Generate a summary of what a script will do
*/
generateScriptSummary(script) {
try {
let summary = "";
if (typeof script === "string") {
// JavaScript script
// Extract comments as a basic summary
const comments = script.match(/\/\*\*([\s\S]*?)\*\//g) || [];
const descriptionComment = comments.find((c) => c.includes("@description"));
summary += chalk_1.default.bold.cyan("JavaScript Script Summary\n");
if (descriptionComment) {
const description = descriptionComment
.replace(/\/\*\*|\*\/|\*/g, "")
.trim();
summary += `${description}\n\n`;
}
// Extract name and version
const nameComment = comments.find((c) => c.includes("@name"));
const versionComment = comments.find((c) => c.includes("@version"));
if (nameComment) {
const name = nameComment.match(/@name\s+(.*?)(\r?\n|$)/)?.[1]?.trim();
if (name)
summary += `Name: ${name}\n`;
}
if (versionComment) {
const version = versionComment
.match(/@version\s+(.*?)(\r?\n|$)/)?.[1]
?.trim();
if (version)
summary += `Version: ${version}\n\n`;
}
// Count certain API calls to give a rough idea of what the script does
const createWalletCount = (script.match(/createWallet/g) || []).length;
const mineBlocksCount = (script.match(/generateToAddress|generateBlock/g) || []).length;
const createTxCount = (script.match(/createPSBT|sendToAddress/g) || [])
.length;
const signTxCount = (script.match(/signPSBT|processPSBT|signTransaction/g) || []).length;
const broadcastCount = (script.match(/broadcastTransaction|sendRawTransaction/g) || []).length;
summary += `This script includes approximately:\n`;
summary += `- ${createWalletCount} wallet creation operations\n`;
summary += `- ${mineBlocksCount} block generation operations\n`;
summary += `- ${createTxCount} transaction creation operations\n`;
summary += `- ${signTxCount} transaction signing operations\n`;
summary += `- ${broadcastCount} transaction broadcast operations\n`;
// Look for complex operations
if (script.includes("multisig") || script.includes("quorum")) {
summary += `- Multisig wallet operations\n`;
}
if (script.includes("replaceFee") ||
script.includes("rbf") ||
script.includes("replaceByFee") ||
script.includes("REPLACE_TRANSACTION")) {
summary += `- Replace-by-fee (RBF) operations\n`;
}
if (script.includes("cpfp") ||
script.includes("childPaysForParent") ||
script.includes("Child Pays For Parent")) {
summary += `- Child-pays-for-parent (CPFP) operations\n`;
}
if (script.includes("timelock") ||
script.includes("nLockTime") ||
script.includes("checkSequenceVerify") ||
script.includes("CSV") ||
script.includes("checkLockTimeVerify") ||
script.includes("CLTV")) {
summary += `- Timelock operations\n`;
}
// Warn about potentially destructive operations
if (script.includes("deleteWallet") ||
script.includes("removeWallet")) {
summary += chalk_1.default.yellow("\n⚠️ Warning: This script contains wallet deletion operations\n");
}
if (script.includes("abandonTransaction") ||
script.includes("abandon transaction")) {
summary += chalk_1.default.yellow("\n⚠️ Warning: This script contains transaction abandonment operations\n");
}
if (script.includes("invalidateBlock") ||
script.includes("invalidate block")) {
summary += chalk_1.default.yellow("\n⚠️ Warning: This script contains block invalidation operations\n");
}
}
else {
// Declarative script
summary += chalk_1.default.bold.cyan(`Script: ${script.name}\n`);
summary += `${script.description || "No description provided"}\n\n`;
summary += `Version: ${script.version || "1.0.0"}\n`;
summary += `Contains ${script.actions.length} actions:\n\n`;
// Group by action type for a more compact summary
const actionCounts = script.actions.reduce((acc, action) => {
acc[action.type] = (acc[action.type] || 0) + 1;
return acc;
}, {});
Object.entries(actionCounts).forEach(([type, count]) => {
summary += `- ${count} ${type.replace(/_/g, " ").toLowerCase()} operations\n`;
});
// List each action with its description
summary += "\nDetailed action list:\n";
script.actions.forEach((action, index) => {
const desc = action.description ||
`Execute ${action.type.replace(/_/g, " ").toLowerCase()}`;
summary += `${index + 1}. ${desc}\n`;
// Add details for specific action types
switch (action.type) {
case scripting_1.ActionType.CREATE_WALLET:
summary += ` Create wallet "${action.params.name}"\n`;
if (action.params.options) {
const options = [];
if (action.params.options.disablePrivateKeys)
options.push("without private keys");
if (action.params.options.blank)
options.push("blank");
if (action.params.options.descriptorWallet)
options.push("descriptor wallet");
if (options.length > 0) {
summary += ` Options: ${options.join(", ")}\n`;
}
}
break;
case scripting_1.ActionType.MINE_BLOCKS:
const target = action.params.toWallet
? `to wallet "${action.params.toWallet}"`
: `to address "${action.params.toAddress}"`;
summary += ` Mine ${action.params.count} blocks ${target}\n`;
break;
case scripting_1.ActionType.CREATE_TRANSACTION:
let outputSummary = "";
if (action.params.outputs &&
Array.isArray(action.params.outputs)) {
action.params.outputs.forEach((output) => {
const address = Object.keys(output)[0];
const amount = output[address];
if (outputSummary)
outputSummary += ", ";
outputSummary += `${(0, terminal_1.formatBitcoin)(amount)} to ${(0, terminal_1.truncate)(address, 8)}`;
});
}
summary += ` Send from "${action.params.fromWallet}": ${outputSummary}\n`;
if (action.params.feeRate) {
summary += ` With fee rate: ${action.params.feeRate} sat/vB\n`;
}
if (action.params.rbf) {
summary += ` Enabled for RBF (Replace-By-Fee)\n`;
}
break;
case scripting_1.ActionType.SIGN_TRANSACTION:
if (action.params.wallet) {
summary += ` Sign with wallet "${action.params.wallet}"\n`;
}
else if (action.params.privateKey) {
summary += ` Sign with private key\n`;
}
break;
case scripting_1.ActionType.BROADCAST_TRANSACTION:
if (action.params.txid) {
summary += ` Broadcast transaction with ID: ${(0, terminal_1.truncate)(action.params.txid, 10)}\n`;
}
else if (action.params.psbt) {
summary += ` Broadcast PSBT transaction\n`;
}
break;
case scripting_1.ActionType.REPLACE_TRANSACTION:
summary += ` Replace transaction "${(0, terminal_1.truncate)(action.params.txid, 8)}"\n`;
if (action.params.newFeeRate) {
summary += ` With new fee rate: ${action.params.newFeeRate} sat/vB\n`;
}
if (action.params.newOutputs) {
summary += ` And ${action.params.newOutputs.length} new output(s)\n`;
}
break;
case scripting_1.ActionType.CREATE_MULTISIG:
summary += ` Create ${action.params.requiredSigners}-of-${action.params.totalSigners} multisig wallet\n`;
summary += ` Using address type: ${action.params.addressType}\n`;
break;
case scripting_1.ActionType.WAIT:
summary += ` Wait for ${action.params.seconds} seconds\n`;
break;
case scripting_1.ActionType.ASSERT:
summary += ` Verify: ${typeof action.params.condition === "string"
? action.params.condition
: "condition is met"}\n`;
summary += ` Error if not: "${action.params.message}"\n`;
break;
case scripting_1.ActionType.CUSTOM:
summary += ` Execute custom code\n`;
// Optionally show a snippet of the code (first line or limited chars)
if (action.params.code) {
const codePreview = action.params.code
.split("\n")[0]
.substring(0, 50);
summary += ` Code snippet: ${codePreview}${action.params.code.length > 50 ? "..." : ""}\n`;
}
break;
}
});
}
return summary;
}
catch (error) {
return `Error generating summary: ${error.message}`;
}
}
/**
* Execute a script
*/
async executeScript(script, options = {}) {
const result = {
status: scripting_1.ScriptExecutionStatus.NOT_STARTED,
startTime: new Date(),
steps: [],
outputs: {
wallets: [],
transactions: [],
blocks: [],
},
};
try {
// Create execution context
const context = {
bitcoinService: this.bitcoinService,
caravanService: this.caravanService,
transactionService: this.transactionService,
configManager: this.configManager,
rpcClient: this.rpcClient,
multisigCommands: this.multisigCommands,
variables: { ...(options.params || {}) },
wallets: {},
transactions: {},
blocks: [],
log: (message) => {
if (options.verbose) {
console.log(message);
}
this.emit("log", message);
},
progress: (step, total, message) => {
this.emit("progress", { step, total, message });
},
};
// Start execution
result.status = scripting_1.ScriptExecutionStatus.RUNNING;
this.emit("start", { script, options });
if (options.dryRun) {
// Just generate summary for dry run
const summary = this.generateScriptSummary(script);
console.log(chalk_1.default.cyan("Dry run - script would do the following:"));
console.log(summary);
result.status = scripting_1.ScriptExecutionStatus.COMPLETED;
result.endTime = new Date();
result.duration = result.endTime.getTime() - result.startTime.getTime();
this.emit("complete", result);
return result;
}
if (typeof script === "string") {
// Execute JavaScript script
await this.executeJavaScriptScript(script, context, result, options);
}
else {
// Execute declarative script
await this.executeDeclarativeScript(script, context, result, options);
}
// Update result
result.status = scripting_1.ScriptExecutionStatus.COMPLETED;
result.endTime = new Date();
result.duration = result.endTime.getTime() - result.startTime.getTime();
// Gather outputs
result.outputs.wallets = Object.keys(context.wallets);
result.outputs.transactions = Object.keys(context.transactions);
result.outputs.blocks = context.blocks;
this.emit("complete", result);
return result;
}
catch (error) {
result.status = scripting_1.ScriptExecutionStatus.FAILED;
result.endTime = new Date();
result.duration = result.endTime.getTime() - result.startTime.getTime();
result.error = error;
this.emit("error", { error, result });
throw error;
}
}
/**
* Execute a JavaScript script
*/
async executeJavaScriptScript(script, context, result, options) {
try {
// Create a VM to execute the script in a sandboxed environment
const vm = new vm2_1.VM({
timeout: 30000, // 30 seconds timeout
sandbox: {
// Expose the context to the script
bitcoinService: context.bitcoinService,
caravanService: context.caravanService,
transactionService: context.transactionService,
configManager: context.configManager,
rpcClient: context.rpcClient,
multisigCommands: context.multisigCommands,
variables: context.variables,
wallets: context.wallets,
transactions: context.transactions,
blocks: context.blocks,
console: {
log: context.log,
error: (message) => {
context.log(chalk_1.default.red(`ERROR: ${message}`));
this.emit("error", { message, result });
},
warn: (message) => {
context.log(chalk_1.default.yellow(`WARNING: ${message}`));
this.emit("warning", { message, result });
},
},
progress: context.progress,
// Helper functions
formatBitcoin: terminal_1.formatBitcoin,
truncate: terminal_1.truncate,
// Allow setTimeout and setInterval
setTimeout,
clearTimeout,
setInterval,
clearInterval,
},
});
// If interactive, ask for confirmation before executing the script
if (options.interactive) {
const summary = this.generateScriptSummary(script);
console.log(chalk_1.default.cyan("Script Summary:"));
console.log(summary);
// Get confirmation from user via inquirer
const { confirm } = await Promise.resolve().then(() => __importStar(require("@inquirer/prompts")));
const userConfirmed = await confirm({
message: "Do you want to execute this script?",
default: false, // Default to "No" for safety
});
if (!userConfirmed) {
result.status = scripting_1.ScriptExecutionStatus.ABORTED;
throw new Error("Script execution aborted by user");
}
}
// Execute the script
await vm.run(script);
}
catch (error) {
throw new Error(`Script execution failed: ${error.message}`);
}
}
/**
* Execute a declarative script
*/
async executeDeclarativeScript(script, context, result, options) {
try {
// Initialize variables
if (script.variables) {
context.variables = { ...context.variables, ...script.variables };
}
// If interactive, ask for confirmation before executing the script
if (options.interactive) {
const summary = this.generateScriptSummary(script);
console.log(chalk_1.default.cyan("Script Summary:"));
console.log(summary);
// Simulate confirmation
const confirm = true; // In a real implementation, this would be a user prompt
if (!confirm) {
result.status = scripting_1.ScriptExecutionStatus.ABORTED;
throw new Error("Script execution aborted by user");
}
}
// Execute each action in sequence
for (let i = 0; i < script.actions.length; i++) {
const action = script.actions[i];
const actionResult = {
action: action.type,
status: "success",
result: undefined,
error: undefined,
};
try {
// Progress reporting
context.progress(i + 1, script.actions.length, action.description ||
`Executing ${action.type.replace(/_/g, " ").toLowerCase()}`);
// If interactive mode, ask for confirmation before each action
if (options.interactive) {
console.log(chalk_1.default.cyan(`\nAction ${i + 1}/${script.actions.length}: ${action.type}`));
console.log(chalk_1.default.gray(JSON.stringify(action.params, null, 2)));
// Simulate confirmation
const confirm = true; // In a real implementation, this would be a user prompt
if (!confirm) {
actionResult.status = "skipped";
result.steps.push(actionResult);
continue;
}
}
// Process variable references in params (e.g., ${variableName})
const processedParams = this.processVariableReferences(action.params, context.variables);
// Execute the action
actionResult.result = await this.executeAction(action.type, processedParams, context);
// Log action result if verbose
if (options.verbose) {
context.log((0, terminal_1.formatSuccess)(`Completed action: ${action.type}`));
if (actionResult.result) {
context.log(JSON.stringify(actionResult.result, null, 2));
}
}
}
catch (error) {
actionResult.status = "failed";
actionResult.error = error;
context.log((0, terminal_1.formatError)(`Action failed: ${action.type} - ${error.message}`));
// Add the step to the result
result.steps.push(actionResult);
// Stop execution on error
throw error;
}
// Add the step to the result
result.steps.push(actionResult);
}
}
catch (error) {
throw new Error(`Script execution failed: ${error.message}`);
}
}
/**
* Execute a single action
*/
async executeAction(type, params, context) {
switch (type) {
case scripting_1.ActionType.CREATE_WALLET:
return this.executeCreateWallet(params, context);
case scripting_1.ActionType.MINE_BLOCKS:
return this.executeMineBlocks(params, context);
case scripting_1.ActionType.CREATE_TRANSACTION:
return this.executeCreateTransaction(params, context);
case scripting_1.ActionType.REPLACE_TRANSACTION:
return this.executeReplaceTransaction(params, context);
case scripting_1.ActionType.SIGN_TRANSACTION:
return this.executeSignTransaction(params, context);
case scripting_1.ActionType.BROADCAST_TRANSACTION:
return this.executeBroadcastTransaction(params, context);
case scripting_1.ActionType.CREATE_MULTISIG:
return this.executeCreateMultisig(params, context);
case scripting_1.ActionType.WAIT:
return this.executeWait(params);
case scripting_1.ActionType.ASSERT:
return this.executeAssert(params, context);
case scripting_1.ActionType.CUSTOM:
return this.executeCustom(params, context);
default:
throw new Error(`Unknown action type: ${type}`);
}
}
/**
* Execute CUSTOM action
*/
async executeCustom(params, context) {
try {
const { code } = params;
if (!code || typeof code !== "string") {
throw new Error("CUSTOM action requires a code parameter as a string");
}
// Use Function constructor to create a function that can access the context
const executeFunction = new Function("context", `
return (async () => {
try {
${code}
} catch (error) {
console.error("Error in custom code:", error.message);
throw error;
}
})();
`);
// Execute the function with the context
const result = await executeFunction(context);
// Set variable if name is provided
if (params.variableName && result !== undefined) {
context.variables[params.variableName] = result;
}
return result;
}
catch (error) {
context.log(`Error executing custom code: ${error.message}`);
throw new Error(`Custom code execution failed: ${error.message}`);
}
}
/**
* Process variable references in parameters
*/
processVariableReferences(params, variables) {
if (typeof params === "string") {
// Replace variable references in strings
return params.replace(/\${([^}]+)}/g, (match, varName) => {
return variables[varName] !== undefined ? variables[varName] : match;
});
}
else if (Array.isArray(params)) {
// Process array elements
return params.map((item) => this.processVariableReferences(item, variables));
}
else if (params !== null && typeof params === "object") {
// Process object properties
const result = {};
for (const key in params) {
result[key] = this.processVariableReferences(params[key], variables);
}
return result;
}
// Return primitive values as is
return params;
}
/**
* Execute CREATE_WALLET action
*/
async executeCreateWallet(params, context) {
const { name, options = {} } = params;
// Create the wallet
const result = await context.bitcoinService.createWallet(name, options);
// Store wallet info in context
context.wallets[name] = { name, created: new Date() };
// Set variable if name is provided
if (params.variableName) {
context.variables[params.variableName] = name;
}
return result;
}
/**
* Execute MINE_BLOCKS action
*/
async executeMineBlocks(params, context) {
const { count, toWallet, toAddress } = params;
let address;
if (toAddress) {
// Use provided address
address = toAddress;
}
else if (toWallet) {
// Generate address from wallet
address = await context.bitcoinService.getNewAddress(toWallet);
}
else {
throw new Error("Either toWallet or toAddress parameter is required");
}
// Mine blocks
const blockHashes = await context.bitcoinService.generateToAddress(count, address);
// Add block hashes to context
context.blocks.push(...blockHashes);
// Set variable if name is provided
if (params.variableName) {
context.variables[params.variableName] = blockHashes;
}
return { blockHashes, count, address };
}
/**
* Execute CREATE_TRANSACTION action
*/
async executeCreateTransaction(params, context) {
const { fromWallet, outputs, feeRate } = params;
// Create PSBT
const psbt = await context.transactionService.createPSBT(fromWallet, outputs);
// Generate a txid for reference
const txid = `pending_${Math.random().toString(36).substring(2, 15)}`;
// Store transaction info in context
context.transactions[txid] = {
psbt,
fromWallet,
outputs,
feeRate,
status: "created",
created: new Date(),
};
// Set variable if name is provided
if (params.variableName) {
context.variables[params.variableName] = { txid, psbt };
}
return { txid, psbt };
}
/**
* Execute REPLACE_TRANSACTION action (RBF)
*/
async executeReplaceTransaction(params, context) {
const { txid, newOutputs, newFeeRate } = params;
// Get the original transaction
const tx = context.transactions[txid];
if (!tx) {
throw new Error(`Transaction not found: ${txid}`);
}
// Create a new PSBT with increased fee rate
const newPsbt = await context.transactionService.createPSBT(tx.fromWallet, newOutputs || tx.outputs, { rbf: true, feeRate: newFeeRate });
// Generate a new txid for reference
const newTxid = `pending_${Math.random().toString(36).substring(2, 15)}`;
// Store new transaction info in context
context.transactions[newTxid] = {
psbt: newPsbt,
fromWallet: tx.fromWallet,
outputs: newOutputs || tx.outputs,
feeRate: newFeeRate,
replacesTransaction: txid,
status: "created",
created: new Date(),
};
// Update original transaction status
context.transactions[txid].status = "replaced";
context.transactions[txid].replacedBy = newTxid;
// Set variable if name is provided
if (params.variableName) {
context.variables[params.variableName] = { txid: newTxid, psbt: newPsbt };
}
return { txid: newTxid, psbt: newPsbt, replacedTxid: txid };
}
/**
* Execute SIGN_TRANSACTION action
*/
async executeSignTransaction(params, context) {
const { txid, wallet, privateKey } = params;
// Get the transaction
const tx = context.transactions[txid];
if (!tx) {
throw new Error(`Transaction not found: ${txid}`);
}
let signedPsbt;
if (wallet) {
// Sign with wallet
signedPsbt = await context.transactionService.processPSBT(wallet, tx.psbt);
}
else if (privateKey) {
// Sign with private key