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
868 lines (867 loc) • 105 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.MultisigCommands = void 0;
const caravan_1 = require("../types/caravan");
const clipboardy_1 = __importDefault(require("clipboardy"));
const prompts_1 = require("@inquirer/prompts");
const crypto_1 = __importDefault(require("crypto"));
const chalk_1 = __importDefault(require("chalk"));
const fs = __importStar(require("fs-extra"));
const path = __importStar(require("path"));
const ora_1 = __importDefault(require("ora"));
const terminal_1 = require("../utils/terminal");
/**
* Commands for managing Caravan multisig wallets
*/
class MultisigCommands {
constructor(caravanService, bitcoinService, bitcoinRpcClient, transactionService) {
this.caravanService = caravanService;
this.bitcoinService = bitcoinService;
this.bitcoinRpcClient = bitcoinRpcClient;
this.transactionService = transactionService;
}
/**
* List all Caravan wallet configurations
*/
async listCaravanWallets() {
(0, terminal_1.displayCommandTitle)("Caravan Wallets");
try {
const spinner = (0, ora_1.default)("Loading Caravan wallet configurations...").start();
const wallets = await this.caravanService.listCaravanWallets();
spinner.succeed("Wallet configurations loaded");
if (wallets.length === 0) {
console.log((0, terminal_1.formatWarning)("No Caravan wallet configurations found."));
return [];
}
// Prepare data for table
const tableRows = wallets.map((wallet, index) => [
(index + 1).toString(),
terminal_1.colors.highlight(wallet.name),
terminal_1.colors.info(wallet.network),
terminal_1.colors.info(wallet.addressType),
terminal_1.colors.info(`${wallet.quorum.requiredSigners} of ${wallet.quorum.totalSigners}`),
]);
// Display table
console.log((0, terminal_1.createTable)(["#", "Wallet Name", "Network", "Address Type", "Quorum"], tableRows));
console.log(`\nTotal wallets: ${terminal_1.colors.highlight(wallets.length.toString())}`);
return wallets;
}
catch (error) {
console.error((0, terminal_1.formatError)("Error listing Caravan wallets:"), error);
return [];
}
}
/**
* Create a new Caravan wallet configuration
*/
async createCaravanWallet() {
(0, terminal_1.displayCommandTitle)("Create New Caravan Multisig Wallet");
try {
// Basic wallet information
const name = await (0, prompts_1.input)({
message: "Enter a name for the wallet:",
validate: (input) => input.trim() !== "" ? true : "Please enter a valid name",
});
const addressType = await (0, prompts_1.select)({
message: "Select address type:",
choices: [
{
name: terminal_1.colors.highlight("P2WSH (Native SegWit)"),
value: caravan_1.AddressType.P2WSH,
},
{
name: terminal_1.colors.highlight("P2SH-P2WSH (Nested SegWit)"),
value: caravan_1.AddressType.P2SH_P2WSH,
},
{ name: terminal_1.colors.highlight("P2SH (Legacy)"), value: caravan_1.AddressType.P2SH },
],
default: caravan_1.AddressType.P2WSH,
});
// We'll focus on regtest for now
const network = caravan_1.Network.REGTEST;
console.log(terminal_1.colors.info(`Using network: ${network}`));
// Quorum information
const requiredSigners = await (0, prompts_1.number)({
message: "Enter the number of required signatures (M):",
validate: (input) => input !== undefined && input > 0
? true
: "Number must be greater than 0",
default: 2,
});
const totalSigners = await (0, prompts_1.number)({
message: "Enter the total number of signers (N):",
validate: (input) => {
if (input === undefined || input <= 0) {
return "Number must be greater than 0";
}
if (input < requiredSigners) {
return `Total signers must be at least ${requiredSigners}`;
}
return true;
},
default: 3,
});
console.log((0, terminal_1.boxText)(`Wallet: ${terminal_1.colors.highlight(name)}\n` +
`Address Type: ${terminal_1.colors.highlight(addressType)}\n` +
`Network: ${terminal_1.colors.highlight(network)}\n` +
`Quorum: ${terminal_1.colors.highlight(`${requiredSigners} of ${totalSigners}`)}`, { title: "Wallet Configuration", titleColor: terminal_1.colors.info }));
// Create watcher wallet for Caravan
const watcherWalletName = `${name.replace(/\s+/g, "_").toLowerCase()}_watcher`;
console.log(terminal_1.colors.info(`\nCreating watch-only wallet "${watcherWalletName}" for Caravan...`));
try {
const spinner = (0, ora_1.default)(`Creating watch-only wallet ${watcherWalletName}...`).start();
await this.bitcoinService.createWallet(watcherWalletName, {
disablePrivateKeys: true,
blank: false,
descriptorWallet: true,
});
spinner.succeed(`Watch-only wallet "${watcherWalletName}" created successfully!`);
}
catch (error) {
console.error((0, terminal_1.formatError)(`Error creating watch wallet: ${error.message}`));
return null;
}
// Ask how to add wallets for signers
const createMethod = await (0, prompts_1.select)({
message: "How would you like to create signer wallets?",
choices: [
{
name: terminal_1.colors.highlight("Create new wallets for each signer"),
value: "new",
},
{ name: terminal_1.colors.highlight("Use existing wallets"), value: "existing" },
],
});
// Array to store extended public keys
const extendedPublicKeys = [];
const signerWallets = [];
// Map of address types to BIP paths and descriptor types
const formatInfo = {
[caravan_1.AddressType.P2WSH]: { path: "84h/1h/0h", descriptorPrefix: "wpkh" },
[caravan_1.AddressType.P2SH_P2WSH]: {
path: "49h/1h/0h",
descriptorPrefix: "sh(wpkh",
},
[caravan_1.AddressType.P2SH]: { path: "44h/1h/0h", descriptorPrefix: "pkh" },
};
// The BIP path to use based on address type
const bipPath = formatInfo[addressType].path;
// Convert BIP path format from 84'/1'/0' to m/84'/1'/0' for display
const displayBipPath = `m/${bipPath}`;
if (createMethod === "new") {
// Create new wallets for each signer
console.log(terminal_1.colors.info(`\nCreating ${totalSigners} wallet(s) for signers...`));
for (let i = 0; i < totalSigners; i++) {
const signerName = `${name.replace(/\s+/g, "_").toLowerCase()}_signer_${i + 1}`;
signerWallets.push(signerName);
console.log(terminal_1.colors.info(`\nCreating wallet for signer ${i + 1}: ${signerName}`));
try {
// Create wallet WITH descriptor support
const createSpinner = (0, ora_1.default)(`Creating wallet ${signerName}...`).start();
await this.bitcoinService.createWallet(signerName, {
disablePrivateKeys: false,
blank: false,
descriptorWallet: true,
});
createSpinner.succeed(`Wallet "${signerName}" created successfully!`);
// Give the wallet some time to initialize
const initSpinner = (0, ora_1.default)("Initializing wallet descriptors...").start();
await new Promise((resolve) => setTimeout(resolve, 1000));
initSpinner.succeed("Wallet initialized");
// Get descriptors from the wallet
const descSpinner = (0, ora_1.default)("Retrieving wallet descriptors...").start();
const descriptors = await this.getWalletDescriptors(signerName);
descSpinner.succeed("Descriptors retrieved");
if (!descriptors) {
throw new Error(`Could not get descriptors for wallet ${signerName}`);
}
// Find the appropriate descriptor based on the address type
const desiredDescType = formatInfo[addressType].descriptorPrefix;
let matchingDesc = null;
console.log(terminal_1.colors.info(`Looking for descriptor: ${terminal_1.colors.code(desiredDescType)} with path: ${terminal_1.colors.code(bipPath)}`));
const matchSpinner = (0, ora_1.default)("Matching descriptor to wallet type...").start();
for (const desc of descriptors) {
// Try different matching strategies
const startsWithPrefix = desc.desc.startsWith(desiredDescType);
const includesPath = desc.desc.includes(bipPath);
const isExternal = !desc.internal;
if (startsWithPrefix && includesPath && isExternal) {
matchingDesc = desc;
break;
}
}
if (!matchingDesc) {
matchSpinner.warn("Could not find exact descriptor match");
// Try to find any descriptor that contains the BIP path
for (const desc of descriptors) {
if (desc.desc.includes(bipPath) && !desc.internal) {
matchingDesc = desc;
break;
}
}
if (!matchingDesc) {
matchSpinner.fail("No suitable descriptor found");
throw new Error(`No suitable descriptor found for wallet ${signerName}`);
}
matchSpinner.succeed("Found compatible descriptor");
}
else {
matchSpinner.succeed("Found exact matching descriptor");
}
// Extract xpub and fingerprint from the descriptor
const extractSpinner = (0, ora_1.default)("Extracting extended public key...").start();
const descStr = matchingDesc.desc;
const xpubMatch = descStr.match(/\[([a-f0-9]+)\/.*?\](.*?)\/[0-9]+\/\*\)/);
if (!xpubMatch) {
extractSpinner.fail("Could not extract xpub");
throw new Error(`Could not extract xpub from descriptor: ${descStr}`);
}
const fingerprint = xpubMatch[1];
const xpub = xpubMatch[2];
extractSpinner.succeed("Extended public key extracted");
console.log((0, terminal_1.boxText)(`Signer: ${terminal_1.colors.highlight(signerName)}\n` +
`Fingerprint: ${terminal_1.colors.code(fingerprint)}\n` +
`XPub: ${terminal_1.colors.code((0, terminal_1.truncate)(xpub, 15))}\n` +
`Path: ${terminal_1.colors.code(displayBipPath)}`, {
title: `Signer ${i + 1} Key Info`,
titleColor: terminal_1.colors.success,
}));
// Add to extended public keys
extendedPublicKeys.push({
name: `Extended Public Key ${i + 1} (${signerName})`,
xpub: xpub,
bip32Path: displayBipPath,
xfp: fingerprint,
method: "text",
});
}
catch (error) {
console.error((0, terminal_1.formatError)(`Error setting up signer ${i + 1}:`), error);
return null;
}
}
}
else {
// Use existing wallets
console.log((0, terminal_1.formatWarning)("\nUsing existing wallets requires manual steps to extract xpubs."));
console.log(terminal_1.colors.info(`For ${addressType} wallets, use BIP path ${terminal_1.colors.code(displayBipPath)}`));
const listSpinner = (0, ora_1.default)("Loading available wallets...").start();
const wallets = await this.bitcoinService.listWallets();
listSpinner.succeed("Wallets loaded");
for (let i = 0; i < totalSigners; i++) {
console.log(terminal_1.colors.header(`\nSigner ${i + 1} of ${totalSigners}`));
// Let user choose existing wallet
const signerWallet = await (0, prompts_1.select)({
message: `Select wallet for signer ${i + 1}:`,
choices: wallets.map((w) => ({
name: terminal_1.colors.highlight(w),
value: w,
})),
});
signerWallets.push(signerWallet);
try {
// Get descriptors from the wallet
const descSpinner = (0, ora_1.default)(`Loading descriptors from ${signerWallet}...`).start();
const descriptors = await this.getWalletDescriptors(signerWallet);
descSpinner.succeed("Descriptors loaded");
if (!descriptors) {
throw new Error(`Could not get descriptors for wallet ${signerWallet}`);
}
// Show descriptors and let user select
const descriptorChoices = descriptors
.filter((d) => !d.internal) // Only show external address descriptors
.map((d, idx) => {
const shortDesc = d.desc.substring(0, 90) + (d.desc.length > 90 ? "..." : "");
return {
name: terminal_1.colors.highlight(`${idx + 1}. ${shortDesc}`),
value: idx,
};
});
console.log(terminal_1.colors.success(`\nAvailable descriptors for ${signerWallet}:`));
const selectedIdx = await (0, prompts_1.select)({
message: "Select the appropriate descriptor:",
choices: descriptorChoices,
});
const selectedDesc = descriptors.filter((d) => !d.internal)[selectedIdx];
// Extract xpub and fingerprint from the descriptor
const extractSpinner = (0, ora_1.default)("Extracting public key information...").start();
const descStr = selectedDesc.desc;
const xpubMatch = descStr.match(/\[([a-f0-9]+)\/.*?\](.*?)\/[0-9]+\/\*\)/);
if (!xpubMatch) {
extractSpinner.fail("Could not extract xpub");
throw new Error(`Could not extract xpub from descriptor: ${descStr}`);
}
const fingerprint = xpubMatch[1];
const xpub = xpubMatch[2];
extractSpinner.succeed("Key information extracted");
console.log((0, terminal_1.boxText)(`Fingerprint: ${terminal_1.colors.code(fingerprint)}\n` +
`XPub: ${terminal_1.colors.code((0, terminal_1.truncate)(xpub, 15))}`, { title: "Extracted Information", titleColor: terminal_1.colors.success }));
// Ask for BIP32 path
const path = await (0, prompts_1.input)({
message: "Enter the BIP32 derivation path:",
default: displayBipPath,
});
// Add to extended public keys
extendedPublicKeys.push({
name: `Extended Public Key ${i + 1} (${signerWallet})`,
xpub: xpub,
bip32Path: path,
xfp: fingerprint,
method: "text",
});
}
catch (error) {
console.error((0, terminal_1.formatError)(`Error processing wallet ${signerWallet}:`), error);
return null;
}
}
}
// Create the Caravan wallet configuration
const caravanConfig = {
name,
addressType,
network: "regtest", // Always regtest for this tool
quorum: {
requiredSigners: requiredSigners,
totalSigners: totalSigners,
},
extendedPublicKeys,
startingAddressIndex: 0,
uuid: crypto_1.default.randomBytes(16).toString("hex"),
client: {
type: "private",
url: this.bitcoinRpcClient?.baseUrl || "http://127.0.0.1:18443",
username: this.bitcoinRpcClient?.auth.username || "user",
walletName: watcherWalletName,
},
};
// Save the configuration
const exportSpinner = (0, ora_1.default)("Saving Caravan wallet configuration...").start();
const exportConfig = this.caravanService.formatCaravanConfigForExport(caravanConfig);
// Save the configuration to a file
const configFileName = `${caravanConfig.name.replace(/\s+/g, "_").toLowerCase()}_config.json`;
const configPath = path.join(this.caravanService.getCaravanDir(), configFileName);
// Remove credentials if present
if (exportConfig.client?.password) {
delete exportConfig.client.password;
}
// Save the formatted config
await fs.writeJson(configPath, exportConfig, { spaces: 2 });
exportSpinner.succeed(`Caravan wallet configuration saved to: ${configPath}`);
// Generate multisig descriptors and import them to watch wallet
console.log(terminal_1.colors.info(`\nImporting multisig descriptors to watch wallet "${watcherWalletName}"...`));
try {
const importSpinner = (0, ora_1.default)("Importing multisig descriptors...").start();
await this.importMultisigToWatchWallet(caravanConfig, watcherWalletName);
importSpinner.succeed("Multisig descriptors imported successfully!");
console.log((0, terminal_1.boxText)("When using Caravan, you need to import addresses to see your funds.\n" +
"Follow these steps in Caravan after importing your wallet configuration:\n\n" +
'1. Go to the "Addresses" tab\n' +
'2. Click the "Import Addresses" button at the bottom\n' +
'3. Toggle the "Rescan" switch if this is your first time importing\n' +
'4. Click "Import Addresses" to complete the process', { title: "IMPORTANT", titleColor: terminal_1.colors.warning }));
}
catch (error) {
console.error((0, terminal_1.formatError)(`Error importing multisig descriptors: ${error}`));
console.log((0, terminal_1.formatWarning)("Watch wallet may not be fully configured for the multisig setup."));
}
// Ask if user wants to create a test transaction
const createTestTx = await (0, prompts_1.confirm)({
message: "Would you like to create a test transaction for this multisig wallet?",
default: true,
});
if (createTestTx) {
await this.createTestTransaction(caravanConfig, watcherWalletName, signerWallets[0]);
}
return caravanConfig;
}
catch (error) {
console.error((0, terminal_1.formatError)("\nError creating Caravan wallet:"), error);
return null;
}
}
/**
* Spend funds from a Caravan multisig wallet
*/
async spendFromCaravanWallet() {
(0, terminal_1.displayCommandTitle)("Spend from Caravan Multisig Wallet");
try {
// Step 1: Select a Caravan wallet
const wallets = await this.caravanService.listCaravanWallets();
if (wallets.length === 0) {
console.log((0, terminal_1.formatWarning)("No Caravan wallets found. Create one first."));
return null;
}
const walletIndex = await (0, prompts_1.select)({
message: "Select Caravan wallet to spend from:",
choices: wallets.map((w, i) => ({
name: terminal_1.colors.highlight(w.name) +
terminal_1.colors.info(` (${w.quorum.requiredSigners} of ${w.quorum.totalSigners}, ${w.addressType})`),
value: i,
})),
});
const selectedWallet = wallets[walletIndex];
// Determine if this is a wallet created by our terminal tool
const isTerminalCreatedWallet = await this.isTerminalCreatedWallet(selectedWallet.name);
console.log((0, terminal_1.boxText)(`Wallet: ${terminal_1.colors.highlight(selectedWallet.name)}\n` +
`Quorum: ${terminal_1.colors.highlight(`${selectedWallet.quorum.requiredSigners} of ${selectedWallet.quorum.totalSigners}`)}\n` +
`Address Type: ${terminal_1.colors.highlight(selectedWallet.addressType)}\n` +
`Creator: ${isTerminalCreatedWallet ? terminal_1.colors.success("Created by this tool") : terminal_1.colors.warning("External wallet")}`, { title: "Selected Wallet", titleColor: terminal_1.colors.info }));
// Find watcher wallet name
let watcherWalletName = `${selectedWallet.name.replace(/\\s+/g, "_").toLowerCase()}_watcher`;
// Check if watcher wallet exists
const bitcoinWallets = await this.bitcoinService.listWallets();
let watchWalletExists = bitcoinWallets.includes(watcherWalletName);
if (!watchWalletExists) {
console.log((0, terminal_1.formatWarning)(`Watch wallet "${watcherWalletName}" not found.`));
if (isTerminalCreatedWallet) {
// Try to create the watch wallet automatically
const createWatch = await (0, prompts_1.confirm)({
message: "Create watch wallet automatically?",
default: true,
});
if (createWatch) {
await this.createWatchWallet(selectedWallet);
watchWalletExists = true;
}
else {
watcherWalletName = await (0, prompts_1.input)({
message: "Enter the name of the watch wallet:",
validate: (input) => input.trim() !== "" ? true : "Please enter a valid wallet name",
});
// Check if the entered wallet exists
watchWalletExists = bitcoinWallets.includes(watcherWalletName);
if (!watchWalletExists) {
console.log((0, terminal_1.formatError)(`Watch wallet "${watcherWalletName}" not found.`));
return null;
}
}
}
else {
watcherWalletName = await (0, prompts_1.input)({
message: "Enter the name of the watch wallet:",
validate: (input) => input.trim() !== "" ? true : "Please enter a valid wallet name",
});
// Check if the entered wallet exists
watchWalletExists = bitcoinWallets.includes(watcherWalletName);
if (!watchWalletExists) {
console.log((0, terminal_1.formatError)(`Watch wallet "${watcherWalletName}" not found.`));
return null;
}
}
}
// Step 2: Guide the user to create a PSBT with Caravan
console.log((0, terminal_1.boxText)("1. Open Caravan in your browser\n" +
'2. Go to "Wallet" tab and select your wallet\n' +
'3. Navigate to the "Spend" tab\n' +
"4. Create a transaction by filling in the recipient address and amount\n" +
'5. Click "Create Transaction" to generate the PSBT\n' +
"6. Copy the PSBT string", { title: "Create Transaction in Caravan", titleColor: terminal_1.colors.info }));
// Get the PSBT from the user
const psbtBase64 = await (0, prompts_1.input)({
message: "Paste the PSBT from Caravan:",
validate: (input) => input.trim() !== "" ? true : "Please enter a valid PSBT",
});
// Step 3: Process the PSBT with the watcher wallet
console.log(terminal_1.colors.info(`\nProcessing PSBT with watch wallet "${watcherWalletName}"...`));
const processSpinner = (0, ora_1.default)("Processing PSBT...").start();
let processedPSBT;
try {
processedPSBT = await this.transactionService.processPSBT(watcherWalletName, psbtBase64.trim());
processSpinner.succeed("PSBT processed successfully with watch wallet");
}
catch (error) {
processSpinner.fail("Error processing PSBT with watch wallet");
console.error((0, terminal_1.formatError)(`Error: ${error}`));
// Try to decode the PSBT to provide more information
try {
const decodedPsbt = await this.transactionService.decodePSBT(psbtBase64.trim());
console.log((0, terminal_1.formatWarning)("The PSBT may be missing UTXO information."));
}
catch (decodeError) {
console.log((0, terminal_1.formatError)("Could not decode the PSBT. It may be invalid."));
}
return null;
}
// Show processed PSBT details
try {
const decodeSpinner = (0, ora_1.default)("Decoding processed PSBT...").start();
const decodedPsbt = await this.transactionService.decodePSBT(processedPSBT);
decodeSpinner.succeed("PSBT decoded");
console.log((0, terminal_1.boxText)(this.formatPSBTSummary(decodedPsbt), {
title: "Transaction Details",
titleColor: terminal_1.colors.info,
}));
}
catch (error) {
console.log((0, terminal_1.formatWarning)("Could not decode the processed PSBT for display."));
}
// Step 4: Collect signatures based on quorum requirements
const { requiredSigners, totalSigners } = selectedWallet.quorum;
const signedPSBTs = [];
// First signed PSBT is the processed one
let currentPSBT = processedPSBT;
signedPSBTs.push(currentPSBT);
console.log((0, terminal_1.boxText)(`This wallet requires ${terminal_1.colors.highlight(requiredSigners.toString())} of ${terminal_1.colors.highlight(totalSigners.toString())} signatures.`, { title: "Signature Requirements", titleColor: terminal_1.colors.info }));
// Track signers we've already used
const usedSigners = new Set();
// Keep collecting signatures until we have enough
for (let i = 0; i < requiredSigners; i++) {
console.log(terminal_1.colors.header(`\nSignature ${i + 1} of ${requiredSigners}`));
// Ask how to provide the signature
const signMethod = await (0, prompts_1.select)({
message: "How would you like to sign?",
choices: [
{ name: terminal_1.colors.highlight("Sign with wallet"), value: "wallet" },
...(i > 0
? [
{
name: terminal_1.colors.highlight("Skip (already have enough signatures)"),
value: "skip",
},
]
: []),
],
});
if (signMethod === "skip") {
console.log((0, terminal_1.formatWarning)("Skipping remaining signatures."));
break;
}
if (signMethod === "wallet") {
// Find potential signer wallets
let signerWallets = [];
if (isTerminalCreatedWallet) {
// For terminal-created wallets, we can guess the signer wallet names
for (let j = 1; j <= totalSigners; j++) {
const signerName = `${selectedWallet.name.replace(/\\s+/g, "_").toLowerCase()}_signer_${j}`;
if (bitcoinWallets.includes(signerName) &&
!usedSigners.has(signerName)) {
signerWallets.push(signerName);
}
}
}
// If we didn't find any or this is an external wallet, let the user select
if (signerWallets.length === 0) {
// Filter out already used signers and the watch wallet
signerWallets = bitcoinWallets.filter((w) => w !== watcherWalletName && !usedSigners.has(w));
}
if (signerWallets.length === 0) {
console.log((0, terminal_1.formatWarning)("No available signer wallets found."));
continue;
}
// Let user select a signer wallet
const signerWallet = await (0, prompts_1.select)({
message: `Select wallet for signature ${i + 1}:`,
choices: signerWallets.map((w) => ({
name: terminal_1.colors.highlight(w),
value: w,
})),
});
// Sign with the selected wallet
const signSpinner = (0, ora_1.default)(`Signing with wallet "${signerWallet}"...`).start();
try {
const newSignedPSBT = await this.transactionService.processPSBT(signerWallet, currentPSBT);
signSpinner.succeed(`Signed with wallet "${signerWallet}"`);
// Update the current PSBT and add to signed list
currentPSBT = newSignedPSBT;
signedPSBTs.push(newSignedPSBT);
// Mark this signer as used
usedSigners.add(signerWallet);
}
catch (error) {
signSpinner.fail(`Failed to sign with wallet "${signerWallet}"`);
console.error((0, terminal_1.formatError)(`Error: ${error}`));
// Let the user try again with a different method
i--; // Decrement to retry this signature
continue;
}
}
else if (signMethod === "privkey") {
// Sign with private key
console.log(terminal_1.colors.info("\nYou'll need the private key for one of the signers."));
const privateKey = await (0, prompts_1.input)({
message: "Enter private key (WIF format):",
validate: (input) => input.trim() !== "" ? true : "Please enter a valid private key",
});
const signSpinner = (0, ora_1.default)("Signing with private key...").start();
try {
const newSignedPSBT = await this.transactionService.signPSBTWithPrivateKey(currentPSBT, privateKey.trim());
signSpinner.succeed("Signed with private key");
// Update the current PSBT and add to signed list
currentPSBT = newSignedPSBT;
signedPSBTs.push(newSignedPSBT);
}
catch (error) {
signSpinner.fail("Failed to sign with private key");
console.error((0, terminal_1.formatError)(`Error: ${error}`));
// Let the user try again with a different method
i--; // Decrement to retry this signature
continue;
}
}
else if (signMethod === "import") {
// Import signature from Caravan
console.log((0, terminal_1.boxText)("1. In Caravan, sign the transaction with your hardware wallet or key\n" +
'2. Once signed, copy the "Signed Transaction Hex" or PSBT', {
title: "Import Signature from Caravan",
titleColor: terminal_1.colors.info,
}));
const importedSignature = await (0, prompts_1.input)({
message: "Paste the signed transaction or PSBT from Caravan:",
validate: (input) => input.trim() !== "" ? true : "Please enter a valid signature",
});
// Try to determine if this is a PSBT or a transaction hex
let importedPSBT = importedSignature.trim();
// If it starts with "70736274" (hex for "psbt"), it might be hex encoded rather than base64
if (importedPSBT.startsWith("70736274")) {
console.log((0, terminal_1.formatWarning)("Detected hex-encoded PSBT, converting to base64..."));
// We'd need to convert hex to base64, but for simplicity we'll assume it's a valid PSBT format
}
try {
// Try to decode the imported data to see if it's valid
const decodeSpinner = (0, ora_1.default)("Validating imported signature...").start();
await this.transactionService.decodePSBT(importedPSBT);
decodeSpinner.succeed("Signature validated");
// Update the current PSBT and add to signed list
currentPSBT = importedPSBT;
signedPSBTs.push(importedPSBT);
}
catch (error) {
console.error((0, terminal_1.formatError)(`Error: ${error}`));
// Let the user try again with a different method
i--; // Decrement to retry this signature
continue;
}
}
// If we've collected enough signatures, give the option to proceed
if (signedPSBTs.length > 1 && i < requiredSigners - 1) {
const proceed = await (0, prompts_1.confirm)({
message: `You have ${signedPSBTs.length - 1} signature(s). Try to finalize now?`,
default: false,
});
if (proceed) {
console.log((0, terminal_1.formatWarning)("Proceeding to finalization with current signatures."));
break;
}
}
}
// Step 5: Finalize the PSBT
console.log(terminal_1.colors.info("\nFinalizing transaction..."));
const finalizeSpinner = (0, ora_1.default)("Finalizing PSBT...").start();
let finalizedPSBT;
try {
finalizedPSBT = await this.transactionService.finalizePSBT(currentPSBT);
finalizeSpinner.succeed("PSBT finalized successfully");
}
catch (error) {
finalizeSpinner.fail("Failed to finalize PSBT");
console.error((0, terminal_1.formatError)(`Error: ${error}`));
console.log((0, terminal_1.boxText)("The transaction could not be finalized. This usually means:\n" +
"1. Not enough signatures were collected\n" +
"2. The signatures are not valid for this transaction\n" +
"3. There was an issue with the PSBT structure", { title: "Finalization Failed", titleColor: terminal_1.colors.error }));
// Ask if the user wants to save the partially signed PSBT
const savePSBT = await (0, prompts_1.confirm)({
message: "Would you like to save the partially signed PSBT?",
default: true,
});
if (savePSBT) {
const filename = await (0, prompts_1.input)({
message: "Enter filename to save PSBT:",
default: `${selectedWallet.name.replace(/\\s+/g, "_").toLowerCase()}_partially_signed.psbt`,
});
const saveSpinner = (0, ora_1.default)(`Saving PSBT to ${filename}...`).start();
await fs.writeFile(filename, currentPSBT);
saveSpinner.succeed(`PSBT saved to ${filename}`);
}
return null;
}
// Check if finalization was complete
if (!finalizedPSBT.complete) {
console.log((0, terminal_1.boxText)("The PSBT could not be fully finalized. More signatures may be required.", { title: "Incomplete Finalization", titleColor: terminal_1.colors.warning }));
// Ask if the user wants to save the partially signed PSBT
const savePSBT = await (0, prompts_1.confirm)({
message: "Would you like to save the partially signed PSBT?",
default: true,
});
if (savePSBT) {
const filename = await (0, prompts_1.input)({
message: "Enter filename to save PSBT:",
default: `${selectedWallet.name.replace(/\\s+/g, "_").toLowerCase()}_partially_signed.psbt`,
});
const saveSpinner = (0, ora_1.default)(`Saving PSBT to ${filename}...`).start();
await fs.writeFile(filename, currentPSBT);
saveSpinner.succeed(`PSBT saved to ${filename}`);
}
return null;
}
// Step 6: Broadcast the transaction
const broadcastTx = await (0, prompts_1.confirm)({
message: "Would you like to broadcast the transaction now?",
default: true,
});
if (broadcastTx) {
const broadcastSpinner = (0, ora_1.default)("Broadcasting transaction...").start();
try {
const txid = await this.transactionService.broadcastTransaction(finalizedPSBT.hex);
broadcastSpinner.succeed("Transaction broadcast successfully");
console.log((0, terminal_1.boxText)(`Transaction ID: ${terminal_1.colors.highlight(txid)}\n` +
`\nThe transaction has been broadcast to the Bitcoin network.`, { title: "Transaction Broadcast", titleColor: terminal_1.colors.success }));
// Offer to mine a block in regtest mode
const mineBlock = await (0, prompts_1.confirm)({
message: "Mine a block to confirm the transaction?",
default: true,
});
if (mineBlock) {
// Find a wallet to mine to (preferably a signer wallet)
let miningWallet;
if (usedSigners.size > 0) {
miningWallet = Array.from(usedSigners)[0];
}
else {
// Fallback: use any wallet that's not the watch wallet
const otherWallets = bitcoinWallets.filter((w) => w !== watcherWalletName);
if (otherWallets.length > 0) {
miningWallet = otherWallets[0];
}
else {
miningWallet = await (0, prompts_1.select)({
message: "Select wallet to mine to:",
choices: bitcoinWallets.map((w) => ({ name: w, value: w })),
});
}
}
const mineSpinner = (0, ora_1.default)(`Mining block to wallet ${miningWallet}...`).start();
try {
const miningAddress = await this.bitcoinService.getNewAddress(miningWallet);
const blockHashes = await this.bitcoinService.generateToAddress(1, miningAddress);
mineSpinner.succeed(`Block mined successfully: ${(0, terminal_1.truncate)(blockHashes[0], 10)}`);
}
catch (error) {
mineSpinner.fail("Failed to mine block");
console.error((0, terminal_1.formatError)(`Error: ${error}`));
}
}
return { txid };
}
catch (error) {
broadcastSpinner.fail("Failed to broadcast transaction");
console.error((0, terminal_1.formatError)(`Error: ${error}`));
console.log((0, terminal_1.boxText)("The transaction could not be broadcast. This could be due to:\n" +
"1. Network connectivity issues\n" +
"2. The transaction is invalid (e.g., spending already spent outputs)\n" +
"3. The transaction violates policy rules (e.g., too low fee)", { title: "Broadcast Failed", titleColor: terminal_1.colors.error }));
// Ask if the user wants to save the transaction hex
const saveHex = await (0, prompts_1.confirm)({
message: "Would you like to save the transaction hex?",
default: true,
});
if (saveHex) {
const filename = await (0, prompts_1.input)({
message: "Enter filename to save transaction hex:",
default: `${selectedWallet.name.replace(/\\s+/g, "_").toLowerCase()}_tx.hex`,
});
const saveSpinner = (0, ora_1.default)(`Saving transaction hex to ${filename}...`).start();
await fs.writeFile(filename, finalizedPSBT.hex);
saveSpinner.succeed(`Transaction hex saved to ${filename}`);
}
return null;
}
}
else {
// The user chose not to broad
// cast
console.log((0, terminal_1.formatWarning)("Transaction not broadcast. You can broadcast it later."));
// Ask if the user wants to save the transaction hex
const saveHex = await (0, prompts_1.confirm)({
message: "Would you like to save the transaction hex?",
default: true,
});
if (saveHex) {
const filename = await (0, prompts_1.input)({
message: "Enter filename to save transaction hex:",
default: `${selectedWallet.name.replace(/\\s+/g, "_").toLowerCase()}_tx.hex`,
});
const saveSpinner = (0, ora_1.default)(`Saving transaction hex to ${filename}...`).start();
await fs.writeFile(filename, finalizedPSBT.hex);
saveSpinner.succeed(`Transaction hex saved to ${filename}`);
}
return { hex: finalizedPSBT.hex };
}
}
catch (error) {
console.error((0, terminal_1.formatError)("Error spending from Caravan wallet:"), error);
return null;
}
}
/**
* Helper method to check if a wallet was created by this terminal tool
*/
async isTerminalCreatedWallet(walletName) {
// Check for watch wallet with expected naming pattern
const watcherWalletName = `${walletName.replace(/\\s+/g, "_").toLowerCase()}_watcher`;
const bitcoinWallets = await this.bitcoinService.listWallets();
if (bitcoinWallets.includes(watcherWalletName)) {
return true;
}
// Check for signer wallets with expected naming pattern
const signerPrefix = `${walletName.replace(/\\s+/g, "_").toLowerCase()}_signer_`;
const matchingSigner = bitcoinWallets.find((w) => w.startsWith(signerPrefix));
if (matchingSigner) {
return true;
}
return false;
}
/**
* Format a decoded PSBT for display
*/
formatPSBTSummary(decodedPsbt) {
let summary = "";
// Display inputs
summary += terminal_1.colors.header("Inputs:") + "\n";
decodedPsbt.inputs.forEach((input, index) => {
summary += `Input #${index + 1}:\n`;
if (input.has_utxo) {
summary += ` TXID: ${(0, terminal_1.truncate)(decodedPsbt.tx.vin[index].txid, 10)}\n`;
summary += ` VOUT: ${decodedPsbt.tx.vin[index].vout}\n`;
summary += ` Amount: ${(0, terminal_1.formatBitcoin)(input.utxo.amount)}\n`;
summary += ` Address: ${input.utxo.scriptPubKey.address || "Unknown"}\n`;
}
else {
summary += ` TXID: ${(0, terminal_1.truncate)(decodedPsbt.tx.vin[index].txid, 10)}\n`;