UNPKG

caravan-x

Version:

A terminal-based utility for managing Caravan multisig wallets in regtest mode. This tool simplifies development and testing with Caravan by providing an easy-to-use interface

868 lines (867 loc) 105 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.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`;