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