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

1,030 lines (1,029 loc) 74.9 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.TransactionCommands = void 0; const prompts_1 = require("@inquirer/prompts"); const fs = __importStar(require("fs-extra")); const clipboardy_1 = __importDefault(require("clipboardy")); const ora_1 = __importDefault(require("ora")); const terminal_1 = require("../utils/terminal"); // Back option constant const BACK_OPTION = "__BACK__"; /** * Commands for managing Bitcoin transactions and PSBTs */ class TransactionCommands { constructor(transactionService, caravanService, bitcoinService) { this.transactionService = transactionService; this.caravanService = caravanService; this.bitcoinService = bitcoinService; } /** * Add a back option to selection choices */ addBackOption(choices, backLabel = "Back to menu") { return [...choices, { name: terminal_1.colors.muted(backLabel), value: BACK_OPTION }]; } /** * Check if a value is the back option */ isBackOption(value) { return value === BACK_OPTION; } /** * Custom text input with back option (using 'q' to go back) */ async inputWithBack(options) { const message = options.message + " (or 'q' to go back)"; const result = await (0, prompts_1.input)({ message, default: options.default, validate: (input) => { if (input.toLowerCase() === "q") return true; if (options.validate) return options.validate(input); return true; }, }); if (result.toLowerCase() === "q") { return BACK_OPTION; } return result; } /** * Custom password input with back option (using 'q' to go back) */ async passwordWithBack(options) { const message = options.message + " (or 'q' to go back)"; const result = await (0, prompts_1.password)({ message, validate: (input) => { if (input.toLowerCase() === "q") return true; if (options.validate) return options.validate(input); return true; }, }); if (result.toLowerCase() === "q") { return BACK_OPTION; } return result; } /** * Custom number input with back option (using 'q' to go back) */ async numberWithBack(options) { const message = options.message + " (or 'q' to go back)"; try { const result = await (0, prompts_1.number)({ message, default: options.default, validate: (input) => { if (input === undefined && options.validate) { return options.validate(input); } return true; }, }); return result; } catch (error) { // If the user entered 'q', it will throw an error since 'q' is not a number if (error.toString().includes("q")) { return BACK_OPTION; } throw error; } } /** * Handle spinner errors and return false to indicate operation should be canceled */ handleSpinnerError(spinner, errorMessage, error) { spinner.fail(errorMessage); console.error((0, terminal_1.formatError)(`${errorMessage}: ${error.message}`)); console.log(terminal_1.colors.info("Press Enter to return to menu...")); return false; } /** * Create a PSBT from a watch-only wallet */ async createPSBT() { (0, terminal_1.displayCommandTitle)("Create New PSBT"); try { // First, list all wallets with proper error handling const walletsSpinner = (0, ora_1.default)("Loading wallets...").start(); let wallets; try { wallets = await this.bitcoinService.listWallets(); walletsSpinner.succeed("Wallets loaded"); } catch (error) { return this.handleSpinnerError(walletsSpinner, "Error loading wallets", error); } if (wallets.length === 0) { console.log((0, terminal_1.formatWarning)("No wallets found.")); return false; } // Select the wallet with back option const selectedWallet = await (0, prompts_1.select)({ message: "Select a wallet to create the PSBT from:", choices: this.addBackOption(wallets.map((w) => ({ name: terminal_1.colors.highlight(w), value: w, }))), }); // Check if user wants to go back if (this.isBackOption(selectedWallet)) { return false; } // Get wallet info with proper error handling const infoSpinner = (0, ora_1.default)(`Loading wallet information for ${selectedWallet}...`).start(); let walletInfo; try { walletInfo = await this.bitcoinService.getWalletInfo(selectedWallet); infoSpinner.succeed("Wallet information loaded"); } catch (error) { return this.handleSpinnerError(infoSpinner, "Error loading wallet information", error); } console.log((0, terminal_1.keyValue)("Selected wallet", selectedWallet)); console.log((0, terminal_1.keyValue)("Available balance", (0, terminal_1.formatBitcoin)(walletInfo.balance))); if (walletInfo.balance <= 0) { console.log((0, terminal_1.formatWarning)("Wallet has no funds. Please fund it first.")); return false; } // Configure outputs const outputs = []; let totalAmount = 0; // Ask for number of outputs with back option const numOutputs = await this.numberWithBack({ message: "How many outputs do you want to create?", validate: (input) => input > 0 ? true : "Please enter a positive number", default: 1, }); // Check if user wants to go back if (this.isBackOption(numOutputs)) { return false; } console.log((0, terminal_1.divider)()); console.log(terminal_1.colors.header("Output Configuration")); // Collect output details for (let i = 0; i < numOutputs; i++) { console.log(terminal_1.colors.info(`\nOutput #${i + 1}:`)); // Get address with back option const address = await this.inputWithBack({ message: `Enter destination address for output #${i + 1}:`, validate: (input) => input.trim() !== "" ? true : "Please enter a valid address", }); // Check if user wants to go back if (this.isBackOption(address)) { return false; } // Get amount with back option const amount = await this.numberWithBack({ message: `Enter amount in BTC for output #${i + 1}:`, validate: (input) => { if (isNaN(input) || input <= 0) { return "Please enter a valid positive amount"; } if (totalAmount + input > walletInfo.balance) { return `Total amount (${(0, terminal_1.formatBitcoin)(totalAmount + input)}) exceeds balance (${(0, terminal_1.formatBitcoin)(walletInfo.balance)})`; } return true; }, }); // Check if user wants to go back if (this.isBackOption(amount)) { return false; } outputs.push({ [address]: amount }); totalAmount += amount; console.log((0, terminal_1.keyValue)(`Output #${i + 1}`, `${(0, terminal_1.formatBitcoin)(amount)} to ${address}`)); } // Show summary console.log((0, terminal_1.divider)()); console.log(terminal_1.colors.header("Transaction Summary")); console.log((0, terminal_1.keyValue)("Total amount", (0, terminal_1.formatBitcoin)(totalAmount))); console.log((0, terminal_1.keyValue)("From wallet", selectedWallet)); console.log((0, terminal_1.keyValue)("Number of outputs", numOutputs.toString())); // Confirm with back option const confirmOptions = [ { name: terminal_1.colors.success("Yes, create PSBT"), value: "yes" }, { name: terminal_1.colors.error("No, cancel"), value: "no" }, ]; const confirmChoice = await (0, prompts_1.select)({ message: "Proceed with creating the PSBT?", choices: this.addBackOption(confirmOptions), }); // Check if user wants to go back if (this.isBackOption(confirmChoice) || confirmChoice === "no") { console.log((0, terminal_1.formatWarning)("PSBT creation cancelled.")); return false; } // Create the PSBT with proper error handling const createSpinner = (0, ora_1.default)("Creating PSBT...").start(); let psbt; try { psbt = await this.transactionService.createPSBT(selectedWallet, outputs); createSpinner.succeed("PSBT created successfully"); } catch (error) { return this.handleSpinnerError(createSpinner, "Error creating PSBT", error); } if (!psbt) { console.log((0, terminal_1.formatError)("Failed to create PSBT.")); return false; } // Handle the PSBT with back option const actionOptions = [ { name: terminal_1.colors.highlight("Save to file"), value: "file" }, { name: terminal_1.colors.highlight("Copy to clipboard"), value: "clipboard" }, { name: terminal_1.colors.highlight("Display"), value: "display" }, { name: terminal_1.colors.highlight("Process with this wallet"), value: "sign" }, ]; const action = await (0, prompts_1.select)({ message: "What would you like to do with the PSBT?", choices: this.addBackOption(actionOptions), }); // Check if user wants to go back if (this.isBackOption(action)) { return false; } switch (action) { case "file": { // Get filename with back option const filename = await this.inputWithBack({ message: "Enter file name:", default: "unsigned-psbt.txt", }); // Check if user wants to go back if (this.isBackOption(filename)) { return false; } // Save with proper error handling const saveSpinner = (0, ora_1.default)(`Saving PSBT to ${filename}...`).start(); try { await fs.writeFile(filename, psbt); saveSpinner.succeed("PSBT saved"); } catch (error) { return this.handleSpinnerError(saveSpinner, `Error saving to ${filename}`, error); } console.log((0, terminal_1.boxText)(`The PSBT has been saved to ${terminal_1.colors.highlight(filename)}`, { title: "PSBT Saved", titleColor: terminal_1.colors.success })); break; } case "clipboard": // Copy with proper error handling const clipboardSpinner = (0, ora_1.default)("Copying PSBT to clipboard...").start(); try { await clipboardy_1.default.write(psbt); clipboardSpinner.succeed("PSBT copied to clipboard"); } catch (error) { return this.handleSpinnerError(clipboardSpinner, "Error copying to clipboard", error); } break; case "display": console.log((0, terminal_1.boxText)(terminal_1.colors.code(psbt), { title: "PSBT (Base64)", titleColor: terminal_1.colors.info, })); break; case "sign": // Process the PSBT with the same wallet return this.signPSBTWithWallet(psbt, selectedWallet); } return psbt; } catch (error) { console.error((0, terminal_1.formatError)("Error creating PSBT:"), error); return false; } } /** * Sign a PSBT with a wallet */ async signPSBTWithWallet(psbtBase64, walletName) { (0, terminal_1.displayCommandTitle)("Sign PSBT with Wallet"); try { // If no PSBT provided, get it from the user if (!psbtBase64) { // Add back option to source selection const sourceOptions = [ { name: terminal_1.colors.highlight("Load from file"), value: "file" }, { name: terminal_1.colors.highlight("Paste Base64 string"), value: "paste" }, { name: terminal_1.colors.highlight("Read from clipboard"), value: "clipboard", }, ]; const source = await (0, prompts_1.select)({ message: "How would you like to provide the PSBT?", choices: this.addBackOption(sourceOptions), }); // Check if user wants to go back if (this.isBackOption(source)) { return false; } switch (source) { case "file": { // Get filename with back option const filename = await this.inputWithBack({ message: "Enter path to PSBT file:", validate: (input) => fs.existsSync(input) ? true : "File does not exist", }); // Check if user wants to go back if (this.isBackOption(filename)) { return false; } // Read with proper error handling const readSpinner = (0, ora_1.default)(`Reading PSBT from ${filename}...`).start(); try { psbtBase64 = (await fs.readFile(filename, "utf8")).trim(); readSpinner.succeed("PSBT loaded from file"); } catch (error) { return this.handleSpinnerError(readSpinner, `Error reading from ${filename}`, error); } break; } case "paste": { // Get PSBT string with back option const pastedPsbt = await this.inputWithBack({ message: "Paste the base64-encoded PSBT:", validate: (input) => input.trim() !== "" ? true : "Please enter a valid PSBT", }); // Check if user wants to go back if (this.isBackOption(pastedPsbt)) { return false; } psbtBase64 = pastedPsbt.trim(); break; } case "clipboard": // Read from clipboard with proper error handling try { const clipboardSpinner = (0, ora_1.default)("Reading from clipboard...").start(); psbtBase64 = await clipboardy_1.default.read(); clipboardSpinner.succeed("PSBT read from clipboard"); } catch (error) { console.error((0, terminal_1.formatError)("Error reading from clipboard:"), error); return false; } break; } } // Try to decode the PSBT for inspection with proper error handling try { const decodeSpinner = (0, ora_1.default)("Decoding PSBT...").start(); const decodedPsbt = await this.transactionService.decodePSBT(psbtBase64); decodeSpinner.succeed("PSBT decoded successfully"); console.log((0, terminal_1.boxText)(this.formatPSBTSummary(decodedPsbt), { title: "PSBT Summary", titleColor: terminal_1.colors.info, })); } catch (error) { console.log((0, terminal_1.formatWarning)("Could not decode PSBT for inspection.")); } // If no wallet provided, ask user to select one with back option if (!walletName) { const walletsSpinner = (0, ora_1.default)("Loading wallets...").start(); let wallets; try { wallets = await this.bitcoinService.listWallets(); walletsSpinner.succeed("Wallets loaded"); } catch (error) { return this.handleSpinnerError(walletsSpinner, "Error loading wallets", error); } if (wallets.length === 0) { console.log((0, terminal_1.formatWarning)("No wallets found.")); return false; } // Add back option to wallet selection walletName = await (0, prompts_1.select)({ message: "Select a wallet to sign with:", choices: this.addBackOption(wallets.map((w) => ({ name: terminal_1.colors.highlight(w), value: w, }))), }); // Check if user wants to go back if (this.isBackOption(walletName)) { return false; } } // Sign the PSBT with the selected wallet console.log(terminal_1.colors.info(`\nSigning PSBT with wallet "${walletName}"...`)); // Sign with proper error handling const signSpinner = (0, ora_1.default)("Signing PSBT...").start(); let signedPsbt; try { signedPsbt = await this.transactionService.processPSBT(walletName, psbtBase64); signSpinner.succeed("PSBT signed successfully"); } catch (error) { return this.handleSpinnerError(signSpinner, "Error signing PSBT", error); } // Handle the signed PSBT with back option const actionOptions = [ { name: terminal_1.colors.highlight("Save to file"), value: "file" }, { name: terminal_1.colors.highlight("Copy to clipboard"), value: "clipboard" }, { name: terminal_1.colors.highlight("Display"), value: "display" }, { name: terminal_1.colors.highlight("Try to finalize and broadcast"), value: "finalize", }, ]; const action = await (0, prompts_1.select)({ message: "What would you like to do with the signed PSBT?", choices: this.addBackOption(actionOptions), }); // Check if user wants to go back if (this.isBackOption(action)) { return false; } switch (action) { case "file": { // Get filename with back option const filename = await this.inputWithBack({ message: "Enter file name:", default: "signed-psbt.txt", }); // Check if user wants to go back if (this.isBackOption(filename)) { return false; } // Save with proper error handling const saveSpinner = (0, ora_1.default)(`Saving signed PSBT to ${filename}...`).start(); try { await fs.writeFile(filename, signedPsbt); saveSpinner.succeed("Signed PSBT saved"); } catch (error) { return this.handleSpinnerError(saveSpinner, `Error saving to ${filename}`, error); } console.log((0, terminal_1.boxText)(`The signed PSBT has been saved to ${terminal_1.colors.highlight(filename)}`, { title: "Signed PSBT Saved", titleColor: terminal_1.colors.success })); break; } case "clipboard": // Copy with proper error handling const clipboardSpinner = (0, ora_1.default)("Copying signed PSBT to clipboard...").start(); try { await clipboardy_1.default.write(signedPsbt); clipboardSpinner.succeed("Signed PSBT copied to clipboard"); } catch (error) { return this.handleSpinnerError(clipboardSpinner, "Error copying to clipboard", error); } break; case "display": console.log((0, terminal_1.boxText)(terminal_1.colors.code(signedPsbt), { title: "Signed PSBT (Base64)", titleColor: terminal_1.colors.info, })); break; case "finalize": return this.finalizeAndBroadcastPSBT(signedPsbt); } return signedPsbt; } catch (error) { console.error((0, terminal_1.formatError)("Error signing PSBT:"), error); 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`; summary += ` VOUT: ${decodedPsbt.tx.vin[index].vout}\n`; } summary += "\n"; }); // Display outputs summary += terminal_1.colors.header("Outputs:") + "\n"; decodedPsbt.tx.vout.forEach((output, index) => { summary += `Output #${index + 1}:\n`; summary += ` Amount: ${(0, terminal_1.formatBitcoin)(output.value)}\n`; summary += ` Address: ${output.scriptPubKey.address || "Unknown"}\n\n`; }); // Display fee if (decodedPsbt.fee) { summary += terminal_1.colors.header("Fee:") + "\n"; summary += ` ${(0, terminal_1.formatBitcoin)(decodedPsbt.fee)}\n`; // Calculate fee rate if (decodedPsbt.tx.vsize) { const feeRate = decodedPsbt.fee / (decodedPsbt.tx.vsize / 1000); summary += ` Rate: ${feeRate.toFixed(2)} BTC/kB\n`; } } return summary; } /** * Sign a PSBT with a private key */ async signPSBTWithPrivateKey() { (0, terminal_1.displayCommandTitle)("Sign PSBT with Private Key"); try { // Get the PSBT from the user with back option const sourceOptions = [ { name: terminal_1.colors.highlight("Load from file"), value: "file" }, { name: terminal_1.colors.highlight("Paste Base64 string"), value: "paste" }, { name: terminal_1.colors.highlight("Read from clipboard"), value: "clipboard" }, ]; const source = await (0, prompts_1.select)({ message: "How would you like to provide the PSBT?", choices: this.addBackOption(sourceOptions), }); // Check if user wants to go back if (this.isBackOption(source)) { return false; } let psbtBase64; switch (source) { case "file": { // Get filename with back option const filename = await this.inputWithBack({ message: "Enter path to PSBT file:", validate: (input) => fs.existsSync(input) ? true : "File does not exist", }); // Check if user wants to go back if (this.isBackOption(filename)) { return false; } // Read with proper error handling const readSpinner = (0, ora_1.default)(`Reading PSBT from ${filename}...`).start(); try { psbtBase64 = (await fs.readFile(filename, "utf8")).trim(); readSpinner.succeed("PSBT loaded from file"); } catch (error) { return this.handleSpinnerError(readSpinner, `Error reading from ${filename}`, error); } break; } case "paste": { // Get PSBT string with back option const pastedPsbt = await this.inputWithBack({ message: "Paste the base64-encoded PSBT:", validate: (input) => input.trim() !== "" ? true : "Please enter a valid PSBT", }); // Check if user wants to go back if (this.isBackOption(pastedPsbt)) { return false; } psbtBase64 = pastedPsbt.trim(); break; } case "clipboard": // Read from clipboard with proper error handling try { const clipboardSpinner = (0, ora_1.default)("Reading from clipboard...").start(); psbtBase64 = await clipboardy_1.default.read(); clipboardSpinner.succeed("PSBT read from clipboard"); } catch (error) { return this.handleSpinnerError(clipboardSpinner, "Error reading from clipboard", error); } break; } // Try to detect if this is a Caravan PSBT with proper error handling const detectSpinner = (0, ora_1.default)("Analyzing PSBT...").start(); let caravanWallets; let caravanConfig; try { caravanWallets = await this.caravanService.listCaravanWallets(); caravanConfig = await this.transactionService.detectCaravanWalletForPSBT(psbtBase64, caravanWallets); detectSpinner.succeed("PSBT analysis complete"); } catch (error) { return this.handleSpinnerError(detectSpinner, "Error analyzing PSBT", error); } let privateKey; if (caravanConfig) { console.log((0, terminal_1.boxText)(`This PSBT belongs to Caravan wallet: ${terminal_1.colors.highlight(caravanConfig.name)}`, { title: "Caravan PSBT Detected", titleColor: terminal_1.colors.success })); // Load private key data with proper error handling const keyDataSpinner = (0, ora_1.default)("Loading private key data...").start(); let keyData; try { keyData = await this.caravanService.loadCaravanPrivateKeyData(caravanConfig); keyDataSpinner.succeed("Key data loaded"); } catch (error) { keyDataSpinner.warn("Could not load key data"); console.log((0, terminal_1.formatWarning)("Will proceed with manual key entry")); keyData = null; } if (keyData && keyData.keyData.some((k) => k.privateKey)) { console.log((0, terminal_1.formatSuccess)("Found configured private keys for this wallet.")); // Let user select which key to use with back option const availableKeys = keyData.keyData.filter((k) => k.privateKey); const keyOptions = availableKeys.map((key, index) => { const xpub = key.xpub; const keyName = caravanConfig.extendedPublicKeys.find((k) => k.xpub === xpub) ?.name || `Key #${index + 1}`; return { name: terminal_1.colors.highlight(`${keyName} (${(0, terminal_1.truncate)(xpub, 8)})`), value: index, }; }); if (keyOptions.length > 0) { const keyIndex = await (0, prompts_1.select)({ message: "Select a key to sign with:", choices: this.addBackOption(keyOptions), }); // Check if user wants to go back if (this.isBackOption(keyIndex)) { return false; } privateKey = availableKeys[keyIndex].privateKey; } else { console.log((0, terminal_1.formatWarning)("No private keys available.")); // Ask for manual entry with back option privateKey = await this.passwordWithBack({ message: "Enter the private key (WIF format):", validate: (input) => input.trim() !== "" ? true : "Please enter a valid private key", }); // Check if user wants to go back if (this.isBackOption(privateKey)) { return false; } privateKey = privateKey.trim(); } } else { console.log((0, terminal_1.formatWarning)("No private key data found for this wallet.")); // Ask for manual entry with back option privateKey = await this.passwordWithBack({ message: "Enter the private key (WIF format):", validate: (input) => input.trim() !== "" ? true : "Please enter a valid private key", }); // Check if user wants to go back if (this.isBackOption(privateKey)) { return false; } privateKey = privateKey.trim(); } // For Caravan, we need to extract signatures with proper error handling console.log(terminal_1.colors.info("\nSigning PSBT for Caravan...")); const signSpinner = (0, ora_1.default)("Extracting signatures for Caravan...").start(); let signedResult; try { signedResult = await this.transactionService.extractSignaturesForCaravan(psbtBase64, privateKey); signSpinner.succeed("PSBT signed successfully for Caravan"); } catch (error) { return this.handleSpinnerError(signSpinner, "Error signing PSBT", error); } // Handle the signed result with back option const actionOptions = [ { name: terminal_1.colors.highlight("Save to file (JSON format)"), value: "file", }, { name: terminal_1.colors.highlight("Copy to clipboard (JSON format)"), value: "clipboard", }, { name: terminal_1.colors.highlight("Display"), value: "display" }, { name: terminal_1.colors.highlight("Just continue with the base64 PSBT"), value: "psbt", }, ]; const action = await (0, prompts_1.select)({ message: "What would you like to do with the Caravan signature data?", choices: this.addBackOption(actionOptions), }); // Check if user wants to go back if (this.isBackOption(action)) { return false; } // Format the output as JSON const caravanJson = JSON.stringify({ hex: signedResult.base64, signatures: signedResult.signatures, signingPubKey: signedResult.signingPubKey, }, null, 2); switch (action) { case "file": { // Get filename with back option const filename = await this.inputWithBack({ message: "Enter file name:", default: "caravan-signatures.json", }); // Check if user wants to go back if (this.isBackOption(filename)) { return false; } // Save with proper error handling const saveSpinner = (0, ora_1.default)(`Saving signatures to ${filename}...`).start(); try { await fs.writeFile(filename, caravanJson); saveSpinner.succeed("Caravan signature data saved"); } catch (error) { return this.handleSpinnerError(saveSpinner, `Error saving to ${filename}`, error); } console.log((0, terminal_1.boxText)(`The signature data has been saved to ${terminal_1.colors.highlight(filename)}`, { title: "Signatures Saved", titleColor: terminal_1.colors.success })); break; } case "clipboard": // Copy with proper error handling const clipboardSpinner = (0, ora_1.default)("Copying signatures to clipboard...").start(); try { await clipboardy_1.default.write(caravanJson); clipboardSpinner.succeed("Signature data copied to clipboard"); } catch (error) { return this.handleSpinnerError(clipboardSpinner, "Error copying to clipboard", error); } break; case "display": console.log((0, terminal_1.boxText)(terminal_1.colors.code(caravanJson), { title: "Caravan Signature Data (JSON)", titleColor: terminal_1.colors.info, })); break; case "psbt": return this.handleSignedPSBT(signedResult.base64); } return signedResult; } else { // Standard PSBT signing console.log(terminal_1.colors.info("\nStandard PSBT signing (not Caravan-specific).")); // Ask for private key with back option privateKey = await this.passwordWithBack({ message: "Enter the private key (WIF format):", validate: (input) => input.trim() !== "" ? true : "Please enter a valid private key", }); // Check if user wants to go back if (this.isBackOption(privateKey)) { return false; } privateKey = privateKey.trim(); // Sign the PSBT with proper error handling console.log(terminal_1.colors.info("\nSigning PSBT with private key...")); const signSpinner = (0, ora_1.default)("Signing PSBT...").start(); let signedPsbt; try { signedPsbt = await this.transactionService.signPSBTWithPrivateKey(psbtBase64, privateKey); signSpinner.succeed("PSBT signed successfully"); } catch (error) { return this.handleSpinnerError(signSpinner, "Error signing PSBT", error); } return this.handleSignedPSBT(signedPsbt); } } catch (error) { console.error((0, terminal_1.formatError)("Error signing PSBT with private key:"), error); return false; } } /** * Helper method to handle a signed PSBT */ async handleSignedPSBT(signedPsbt) { // Handle the signed PSBT with back option const actionOptions = [ { name: terminal_1.colors.highlight("Save to file"), value: "file" }, { name: terminal_1.colors.highlight("Copy to clipboard"), value: "clipboard" }, { name: terminal_1.colors.highlight("Display"), value: "display" }, { name: terminal_1.colors.highlight("Try to finalize and broadcast"), value: "finalize", }, ]; const action = await (0, prompts_1.select)({ message: "What would you like to do with the signed PSBT?", choices: this.addBackOption(actionOptions), }); // Check if user wants to go back if (this.isBackOption(action)) { return false; } switch (action) { case "file": { // Get filename with back option const filename = await this.inputWithBack({ message: "Enter file name:", default: "signed-psbt.txt", }); // Check if user wants to go back if (this.isBackOption(filename)) { return false; } // Save with proper error handling const saveSpinner = (0, ora_1.default)(`Saving signed PSBT to ${filename}...`).start(); try { await fs.writeFile(filename, signedPsbt); saveSpinner.succeed("Signed PSBT saved"); } catch (error) { return this.handleSpinnerError(saveSpinner, `Error saving to ${filename}`, error); } console.log((0, terminal_1.boxText)(`The signed PSBT has been saved to ${terminal_1.colors.highlight(filename)}`, { title: "Signed PSBT Saved", titleColor: terminal_1.colors.success })); break; } case "clipboard": // Copy with proper error handling const clipboardSpinner = (0, ora_1.default)("Copying signed PSBT to clipboard...").start(); try { await clipboardy_1.default.write(signedPsbt); clipboardSpinner.succeed("Signed PSBT copied to clipboard"); } catch (error) { return this.handleSpinnerError(clipboardSpinner, "Error copying to clipboard", error); } break; case "display": console.log((0, terminal_1.boxText)(terminal_1.colors.code(signedPsbt), { title: "Signed PSBT (Base64)", titleColor: terminal_1.colors.info, })); break; case "finalize": return this.finalizeAndBroadcastPSBT(signedPsbt); } return signedPsbt; } /** * Finalize and broadcast a PSBT */ async finalizeAndBroadcastPSBT(psbtBase64) { (0, terminal_1.displayCommandTitle)("Finalize and Broadcast PSBT"); try { // If no PSBT provided, get it from the user with back option if (!psbtBase64) { const sourceOptions = [ { name: terminal_1.colors.highlight("Load from file"), value: "file" }, { name: terminal_1.colors.highlight("Paste Base64 string"), value: "paste" }, { name: terminal_1.colors.highlight("Read from clipboard"), value: "clipboard", }, ]; const source = await (0, prompts_1.select)({ message: "How would you like to provide the PSBT?", choices: this.addBackOption(sourceOptions), }); // Check if user wants to go back if (this.isBackOption(source)) { return false; } switch (source) { case "file": { // Get filename with back option const filename = await this.inputWithBack({ message: "Enter path to PSBT file:", validate: (input) => fs.existsSync(input) ? true : "File does not exist", }); // Check if user wants to go back if (this.isBackOption(filename)) { return false; } // Read with proper error handling const readSpinner = (0, ora_1.default)(`Reading PSBT from ${filename}...`).start(); try { psbtBase64 = (await fs.readFile(filename, "utf8")).trim(); readSpinner.succeed("PSBT loaded from file"); } catch (error) { return this.handleSpinnerError(readSpinner, `Error reading from ${filename}`, error); } break; } case "paste": { // Get PSBT string with back option const pastedPsbt = await this.inputWithBack({ message: "Paste the base64-encoded PSBT:", validate: (input) => input.trim() !== "" ? true : "Please enter a valid PSBT", }); // Check if user wants to go back if (this.isBackOption(pastedPsbt)) { return false; } psbtBase64 = pastedPsbt.trim(); break; } case "clipboard": // Read from clipboard with proper error handling try { const clipboardSpinner = (0, ora_1.default)("Reading from clipboard...").start(); psbtBase64 = await clipboardy_1.default.read(); clipboardSpinner.succeed("PSBT read from clipboard"); } catch (error) { return this.handleSpinnerError(clipboardSpinner, "Error reading from clipboard", error); } break; } } // Finalize the PSBT with proper error handling console.log(terminal_1.colors.info("\nFinalizing PSBT...")); const finalizeSpinner = (0, ora_1.default)("Finalizing PSBT...").start(); let finalizedPsbt; try { finalizedPsbt = await this.transactionService.finalizePSBT(psbtBase64); finalizeSpinner.succeed("PSBT finalization complete"); } catch (error) { return this.handleSpinnerError(finalizeSpinner, "Error finalizing PSBT", error); } if (!finalizedPsbt.complete) { console.log((0, terminal_1.boxText)((0, terminal_1.formatWarning)("PSBT is not complete yet. Additional signatures may be required."), { title: "Incomplete PSBT", titleColor: terminal_1.colors.warning })); // Show PSBT details with proper error handling try { const decodeSpinner = (0, ora_1.default)("Analyzing incomplete PSBT...").start(); const decodedPsbt = await this.transactionService.decodePSBT(psbtBase64); decodeSpinner.succeed("PSBT analysis complete"); console.log((0, terminal_1.boxText)(`Inputs: ${terminal_1.colors.highlight(decodedPsbt.tx.vin.length.toString())}\n` + `Outputs: ${terminal_1.colors.highlight(decodedPsbt.tx.vout.length.toString())}\n` + (decodedPsbt.fee ? `Fee: ${terminal_1.colors.highlight((0, terminal_1.formatBitcoin)(decodedPsbt.fee))}` : ""), { title: "PSBT Details", titleColor: terminal_1.colors.info })); } catch (error) { console.log((0, terminal_1.formatWarning)("Could not decode PSBT for analysis.")); } // Ask about signing again with back option const signOptions = [ { name: terminal_1.colors.highlight("Yes, sign with another key"), value: "yes", }, { name: terminal_1.colors.highlight("No, don't sign again"), value: "no" }, ]; const signAgain = await (0, prompts_1.select)({ message: "Would you like to sign with another key?", choices: this.addBackOption(signOptions), }); // Check if user wants to go back if (this.isBackOption(signAgain)) { return false; } if (signAgain === "yes") { // Ask how to sign with back option const methodOptions = [ { name: terminal_1.colors.highlight("Sign with wallet"), value: "wallet" }, { name: terminal_1.colors.highlight("Sign with private key"),