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

344 lines (343 loc) 13.8 kB
"use strict"; /** * 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;