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
217 lines (216 loc) • 8.2 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.CaravanService = void 0;
const fs = __importStar(require("fs-extra"));
const path = __importStar(require("path"));
const crypto = __importStar(require("crypto"));
const caravan_1 = require("../types/caravan");
/**
* Service for managing Caravan wallets
*/
class CaravanService {
constructor(rpc, caravanDir, keysDir) {
this.rpc = rpc;
this.caravanDir = caravanDir;
this.keysDir = keysDir;
// Ensure directories exist
fs.ensureDirSync(this.caravanDir);
fs.ensureDirSync(this.keysDir);
}
/**
* Get the path to the Caravan configuration directory
*/
getCaravanDir() {
return this.caravanDir;
}
/**
* Convert BIP32 path from 'h' notation to apostrophe notation
* Example: "m/84h/1h/0h" becomes "m/84'/1'/0'"
*/
convertBip32PathFormat(path) {
// If the path already uses apostrophes, return it as is
if (path.includes("'")) {
return path;
}
// Replace 'h' with apostrophes
return path.replace(/h/g, "'");
}
/**
* Format a Caravan wallet config for export
* This handles any needed conversions (like BIP32 path format)
*/
formatCaravanConfigForExport(config) {
const formattedConfig = { ...config };
// Convert BIP32 paths in extended public keys
if (formattedConfig.extendedPublicKeys) {
formattedConfig.extendedPublicKeys =
formattedConfig.extendedPublicKeys.map((key) => ({
...key,
bip32Path: this.convertBip32PathFormat(key.bip32Path),
}));
}
return formattedConfig;
}
/**
* List all Caravan wallet configurations
*/
async listCaravanWallets() {
try {
const files = await fs.readdir(this.caravanDir);
const jsonFiles = files.filter((file) => file.endsWith(".json"));
const configs = [];
for (const file of jsonFiles) {
try {
const configPath = path.join(this.caravanDir, file);
const config = await fs.readJson(configPath);
if (config.name && config.quorum && config.extendedPublicKeys) {
configs.push({
...config,
filename: file,
});
}
}
catch (error) {
console.error(`Error reading ${file}:`, error);
}
}
return configs;
}
catch (error) {
console.error("Error listing Caravan wallet configs:", error);
return [];
}
}
/**
* Get a specific Caravan wallet by name
*/
async getCaravanWallet(name) {
const wallets = await this.listCaravanWallets();
return wallets.find((wallet) => wallet.name === name) || null;
}
/**
* Save a Caravan wallet configuration
*/
async saveCaravanWalletConfig(config) {
const filename = `${config.name.replace(/[^a-zA-Z0-9]/g, "_")}.json`;
const configPath = path.join(this.caravanDir, filename);
await fs.writeJson(configPath, config, { spaces: 2 });
return filename;
}
/**
* Create a new Caravan wallet configuration
*/
async createCaravanWalletConfig({ name, addressType, network, requiredSigners, totalSigners, extendedPublicKeys, startingAddressIndex = 0, }) {
// Generate UUID for the wallet (optional but useful for consistent wallet identification)
const uuid = crypto.randomBytes(16).toString("hex");
const config = {
name,
addressType,
network,
quorum: {
requiredSigners,
totalSigners,
},
extendedPublicKeys,
startingAddressIndex,
uuid,
};
await this.saveCaravanWalletConfig(config);
return config;
}
/**
* Create a watch-only wallet for a Caravan wallet configuration
*/
async createWatchWalletForCaravan(caravanConfig) {
// Create a safe wallet name
const safeWalletName = `${caravanConfig.name.replace(/\s+/g, "_").toLowerCase()}_watch`;
try {
// Create the wallet
await this.rpc.createWallet(safeWalletName, true, false);
// Build descriptors from the Caravan config
const descriptors = this.buildDescriptorsFromCaravanConfig(caravanConfig);
// Import descriptors into the wallet
await this.rpc.importDescriptors(safeWalletName, descriptors);
return safeWalletName;
}
catch (error) {
console.error(`Error creating watch-only wallet for ${caravanConfig.name}:`, error);
throw error;
}
}
/**
* Build wallet descriptors from a Caravan configuration
*/
buildDescriptorsFromCaravanConfig(caravanConfig) {
const { requiredSigners } = caravanConfig.quorum;
const xpubs = caravanConfig.extendedPublicKeys.map((key) => `${key.xpub}/0/*`);
const changeXpubs = caravanConfig.extendedPublicKeys.map((key) => `${key.xpub}/1/*`);
let receiveDescriptor;
let changeDescriptor;
// Build descriptors based on address type
switch (caravanConfig.addressType) {
case caravan_1.AddressType.P2WSH:
receiveDescriptor = `wsh(multi(${requiredSigners},${xpubs.join(",")}))`;
changeDescriptor = `wsh(multi(${requiredSigners},${changeXpubs.join(",")}))`;
break;
case caravan_1.AddressType.P2SH_P2WSH:
receiveDescriptor = `sh(wsh(multi(${requiredSigners},${xpubs.join(",")})))`;
changeDescriptor = `sh(wsh(multi(${requiredSigners},${changeXpubs.join(",")})))`;
break;
case caravan_1.AddressType.P2SH:
receiveDescriptor = `sh(multi(${requiredSigners},${xpubs.join(",")}))`;
changeDescriptor = `sh(multi(${requiredSigners},${changeXpubs.join(",")}))`;
break;
default:
throw new Error(`Unsupported address type: ${caravanConfig.addressType}`);
}
return [
{
desc: receiveDescriptor,
timestamp: "now",
active: true,
internal: false,
},
{
desc: changeDescriptor,
timestamp: "now",
active: true,
internal: true,
},
];
}
}
exports.CaravanService = CaravanService;