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
344 lines (343 loc) • 13.8 kB
JavaScript
;
/**
* Snapshot Service for Caravan-X
* Handles saving and restoring blockchain states in regtest mode
*/
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.SnapshotService = void 0;
const fs = __importStar(require("fs-extra"));
const path = __importStar(require("path"));
const crypto = __importStar(require("crypto"));
const util_1 = require("util");
const child_process_1 = require("child_process");
const chalk_1 = __importDefault(require("chalk"));
const ora_1 = __importDefault(require("ora"));
const execAsync = (0, util_1.promisify)(child_process_1.exec);
class SnapshotService {
constructor(rpc, snapshotsDir, bitcoinDataDir) {
this.rpc = rpc;
this.snapshotsDir = snapshotsDir;
this.bitcoinDataDir = bitcoinDataDir;
// Ensure snapshots directory exists
fs.ensureDirSync(this.snapshotsDir);
}
/**
* Create a snapshot of the current blockchain state
*/
async createSnapshot(options) {
const spinner = (0, ora_1.default)(`Creating snapshot: ${options.name}`).start();
try {
// Get current blockchain info
const blockchainInfo = await this.rpc.callRpc("getblockchaininfo");
const blockHash = await this.rpc.callRpc("getblockhash", [
blockchainInfo.blocks,
]);
// Get list of loaded wallets
const wallets = options.includeWallets || (await this.rpc.listWallets());
// Generate snapshot ID
const id = this.generateSnapshotId(options.name);
// Create snapshot metadata
const snapshot = {
id,
name: options.name,
description: options.description,
createdAt: new Date().toISOString(),
blockHeight: blockchainInfo.blocks,
blockHash,
wallets,
filePath: path.join(this.snapshotsDir, `${id}.tar.gz`),
metadata: {
tags: options.tags,
scenario: options.scenario,
},
};
// Create temporary directory for snapshot
const tempDir = path.join(this.snapshotsDir, `temp_${id}`);
await fs.ensureDir(tempDir);
try {
// Copy regtest blockchain data
spinner.text = "Copying blockchain data...";
const regtestDir = path.join(this.bitcoinDataDir, "regtest");
const snapshotRegtestDir = path.join(tempDir, "regtest");
await fs.ensureDir(snapshotRegtestDir);
// Copy only necessary files (blocks, chainstate, wallets)
await this.copyDirectory(path.join(regtestDir, "blocks"), path.join(snapshotRegtestDir, "blocks"));
await this.copyDirectory(path.join(regtestDir, "chainstate"), path.join(snapshotRegtestDir, "chainstate"));
// Copy wallet files if they exist
if (wallets.length > 0) {
const walletsDir = path.join(regtestDir, "wallets");
const snapshotWalletsDir = path.join(snapshotRegtestDir, "wallets");
for (const wallet of wallets) {
const walletPath = path.join(walletsDir, wallet);
if (await fs.pathExists(walletPath)) {
await this.copyDirectory(walletPath, path.join(snapshotWalletsDir, wallet));
}
}
}
// Save snapshot metadata
spinner.text = "Saving snapshot metadata...";
await fs.writeJson(path.join(tempDir, "snapshot.json"), snapshot, {
spaces: 2,
});
// Create compressed archive
spinner.text = "Compressing snapshot...";
await this.createTarGz(tempDir, snapshot.filePath);
// Save snapshot reference
await this.saveSnapshotReference(snapshot);
spinner.succeed(`Snapshot created: ${snapshot.name} (${id})`);
return snapshot;
}
finally {
// Clean up temporary directory
await fs.remove(tempDir);
}
}
catch (error) {
spinner.fail("Failed to create snapshot");
throw error;
}
}
/**
* Restore a snapshot
*/
async restoreSnapshot(snapshotId, options = {}) {
const spinner = (0, ora_1.default)("Restoring snapshot...").start();
try {
// Get snapshot metadata
const snapshot = await this.getSnapshot(snapshotId);
if (!snapshot) {
throw new Error(`Snapshot not found: ${snapshotId}`);
}
// Verify snapshot file exists
if (!(await fs.pathExists(snapshot.filePath))) {
throw new Error(`Snapshot file not found: ${snapshot.filePath}`);
}
// Stop Bitcoin Core if requested
if (options.stopBitcoin) {
spinner.text = "Stopping Bitcoin Core...";
try {
await this.rpc.callRpc("stop");
// Wait for Bitcoin to stop
await new Promise((resolve) => setTimeout(resolve, 2000));
}
catch (error) {
// Bitcoin might already be stopped
}
}
// Create temporary directory for extraction
const tempDir = path.join(this.snapshotsDir, `restore_${Date.now()}`);
await fs.ensureDir(tempDir);
try {
// Extract snapshot
spinner.text = "Extracting snapshot...";
await this.extractTarGz(snapshot.filePath, tempDir);
// Backup current data
spinner.text = "Backing up current data...";
const backupDir = path.join(this.snapshotsDir, `backup_${Date.now()}`);
const regtestDir = path.join(this.bitcoinDataDir, "regtest");
if (await fs.pathExists(regtestDir)) {
await this.copyDirectory(regtestDir, backupDir);
}
// Restore data
spinner.text = "Restoring blockchain data...";
const snapshotRegtestDir = path.join(tempDir, "regtest");
// Remove current data
if (await fs.pathExists(regtestDir)) {
await fs.remove(regtestDir);
}
// Copy snapshot data
await this.copyDirectory(snapshotRegtestDir, regtestDir);
// Restart Bitcoin Core if requested
if (options.restartBitcoin) {
spinner.text = "Restarting Bitcoin Core...";
// This would require integration with the Docker service or system service
// For now, just inform the user
spinner.info("Please restart Bitcoin Core manually to apply the snapshot");
}
spinner.succeed(`Snapshot restored: ${snapshot.name}`);
console.log(chalk_1.default.cyan(`\nBlock height: ${snapshot.blockHeight}`));
console.log(chalk_1.default.cyan(`Block hash: ${snapshot.blockHash}`));
console.log(chalk_1.default.cyan(`Wallets: ${snapshot.wallets.join(", ")}`));
}
finally {
// Clean up temporary directory
await fs.remove(tempDir);
}
}
catch (error) {
spinner.fail("Failed to restore snapshot");
throw error;
}
}
/**
* List all snapshots
*/
async listSnapshots() {
try {
const snapshotsFile = path.join(this.snapshotsDir, "snapshots.json");
if (!(await fs.pathExists(snapshotsFile))) {
return [];
}
const snapshots = await fs.readJson(snapshotsFile);
return snapshots || [];
}
catch (error) {
console.error("Error listing snapshots:", error);
return [];
}
}
/**
* Get a specific snapshot by ID or name
*/
async getSnapshot(idOrName) {
const snapshots = await this.listSnapshots();
return (snapshots.find((s) => s.id === idOrName || s.name === idOrName) || null);
}
/**
* Delete a snapshot
*/
async deleteSnapshot(snapshotId) {
const spinner = (0, ora_1.default)("Deleting snapshot...").start();
try {
const snapshot = await this.getSnapshot(snapshotId);
if (!snapshot) {
throw new Error(`Snapshot not found: ${snapshotId}`);
}
// Delete snapshot file
if (await fs.pathExists(snapshot.filePath)) {
await fs.remove(snapshot.filePath);
}
// Remove from snapshots list
const snapshots = await this.listSnapshots();
const updatedSnapshots = snapshots.filter((s) => s.id !== snapshot.id);
await this.saveSnapshotsList(updatedSnapshots);
spinner.succeed(`Snapshot deleted: ${snapshot.name}`);
}
catch (error) {
spinner.fail("Failed to delete snapshot");
throw error;
}
}
/**
* Compare two snapshots
*/
async diffSnapshots(snapshot1Id, snapshot2Id) {
const snapshot1 = await this.getSnapshot(snapshot1Id);
const snapshot2 = await this.getSnapshot(snapshot2Id);
if (!snapshot1 || !snapshot2) {
throw new Error("One or both snapshots not found");
}
const wallets1Set = new Set(snapshot1.wallets);
const wallets2Set = new Set(snapshot2.wallets);
const added = snapshot2.wallets.filter((w) => !wallets1Set.has(w));
const removed = snapshot1.wallets.filter((w) => !wallets2Set.has(w));
const common = snapshot1.wallets.filter((w) => wallets2Set.has(w));
return {
snapshot1,
snapshot2,
blocksDiff: snapshot2.blockHeight - snapshot1.blockHeight,
walletsDiff: {
added,
removed,
common,
},
heightDiff: snapshot2.blockHeight - snapshot1.blockHeight,
};
}
/**
* Generate a unique snapshot ID
*/
generateSnapshotId(name) {
const timestamp = Date.now();
const hash = crypto
.createHash("sha256")
.update(`${name}_${timestamp}`)
.digest("hex")
.substring(0, 8);
return `snapshot_${hash}_${timestamp}`;
}
/**
* Save snapshot reference
*/
async saveSnapshotReference(snapshot) {
const snapshots = await this.listSnapshots();
snapshots.push(snapshot);
await this.saveSnapshotsList(snapshots);
}
/**
* Save snapshots list
*/
async saveSnapshotsList(snapshots) {
const snapshotsFile = path.join(this.snapshotsDir, "snapshots.json");
await fs.writeJson(snapshotsFile, snapshots, { spaces: 2 });
}
/**
* Copy directory recursively
*/
async copyDirectory(src, dest) {
await fs.copy(src, dest, {
overwrite: true,
errorOnExist: false,
});
}
/**
* Create tar.gz archive
*/
async createTarGz(sourceDir, outputFile) {
await execAsync(`tar -czf "${outputFile}" -C "${sourceDir}" .`);
}
/**
* Extract tar.gz archive
*/
async extractTarGz(archiveFile, outputDir) {
await execAsync(`tar -xzf "${archiveFile}" -C "${outputDir}"`);
}
/**
* Auto-snapshot based on interval
*/
async createAutoSnapshot() {
const blockchainInfo = await this.rpc.callRpc("getblockchaininfo");
return this.createSnapshot({
name: `auto_${Date.now()}`,
description: `Auto-snapshot at block ${blockchainInfo.blocks}`,
tags: ["auto"],
});
}
}
exports.SnapshotService = SnapshotService;