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
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.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"),