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
416 lines (415 loc) • 14.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BlockchainDataService = void 0;
/**
* Service for fetching blockchain data for visualization
*/
class BlockchainDataService {
constructor(rpc) {
this.cachedTxCount = 0;
this.lastBlockHeight = 0;
this.rpc = rpc;
}
/**
* Get basic chain information
*/
async getChainInfo() {
const info = await this.rpc.callRpc("getblockchaininfo");
return {
chain: info.chain,
blocks: info.blocks,
headers: info.headers,
bestBlockHash: info.bestblockhash,
difficulty: info.difficulty,
medianTime: info.mediantime,
verificationProgress: info.verificationprogress,
initialBlockDownload: info.initialblockdownload,
pruned: info.pruned,
sizeOnDisk: info.size_on_disk,
};
}
/**
* Get network information
*/
async getNetworkInfo() {
const info = await this.rpc.callRpc("getnetworkinfo");
return {
version: info.version,
subversion: info.subversion,
protocolVersion: info.protocolversion,
connections: info.connections,
connectionsIn: info.connections_in,
connectionsOut: info.connections_out,
localServices: info.localservices,
networkActive: info.networkactive,
warnings: info.warnings,
};
}
/**
* Get mempool information
*/
async getMempoolInfo() {
const info = await this.rpc.callRpc("getmempoolinfo");
return {
loaded: info.loaded,
size: info.size,
bytes: info.bytes,
usage: info.usage,
maxMempool: info.maxmempool,
mempoolMinFee: info.mempoolminfee,
minRelaytxFee: info.minrelaytxfee,
unbroadcastCount: info.unbroadcastcount || 0,
};
}
/**
* Get mempool transactions
*/
async getMempoolTransactions() {
return this.rpc.callRpc("getrawmempool");
}
/**
* Get block count
*/
async getBlockCount() {
return this.rpc.callRpc("getblockcount");
}
/**
* Get a specific block by hash
*/
async getBlock(hash, verbosity = 2) {
const block = await this.rpc.callRpc("getblock", [hash, verbosity]);
return {
hash: block.hash,
confirmations: block.confirmations,
size: block.size,
strippedSize: block.strippedsize,
weight: block.weight,
height: block.height,
version: block.version,
versionHex: block.versionHex,
merkleRoot: block.merkleroot,
tx: block.tx, // Array of transactions
time: block.time,
medianTime: block.mediantime,
nonce: block.nonce,
bits: block.bits,
difficulty: block.difficulty,
chainwork: block.chainwork,
previousBlockHash: block.previousblockhash,
nextBlockHash: block.nextblockhash,
};
}
/**
* Get a block by height
*/
async getBlockByHeight(height, verbosity = 2) {
const hash = await this.rpc.callRpc("getblockhash", [height]);
return this.getBlock(hash, verbosity);
}
/**
* Get the most recent blocks
*/
async getRecentBlocks(count = 10) {
const blockCount = await this.getBlockCount();
const blocks = [];
for (let i = 0; i < count && blockCount - i >= 0; i++) {
const block = await this.getBlockByHeight(blockCount - i);
blocks.push(block);
}
return blocks;
}
/**
* Get a new address from a wallet
*/
async getNewAddress(wallet) {
try {
return await this.rpc.callRpc("getnewaddress", [], wallet);
}
catch (error) {
console.error(`Error getting new address for wallet ${wallet}:`, error);
throw error;
}
}
/**
* Get detailed transaction information
*/
async getTransaction(txid) {
try {
// First try to get from wallet (which has more info)
const wallets = await this.rpc.listWallets();
if (wallets.length > 0) {
try {
const tx = await this.rpc.getTransaction(wallets[0], txid);
return {
txid: tx.txid,
hash: tx.hash || tx.txid,
version: tx.version,
size: tx.size,
vsize: tx.vsize,
weight: tx.weight,
locktime: tx.locktime,
vin: tx.vin,
vout: tx.vout,
hex: tx.hex,
blockhash: tx.blockhash,
confirmations: tx.confirmations,
time: tx.time,
blocktime: tx.blocktime,
};
}
catch (e) {
// Continue to try with getrawtransaction if wallet doesn't have this tx
}
}
// Fallback to getrawtransaction
const tx = await this.rpc.callRpc("getrawtransaction", [txid, true]);
return {
txid: tx.txid,
hash: tx.hash,
version: tx.version,
size: tx.size,
vsize: tx.vsize,
weight: tx.weight,
locktime: tx.locktime,
vin: tx.vin,
vout: tx.vout,
hex: tx.hex,
blockhash: tx.blockhash,
confirmations: tx.confirmations,
time: tx.time,
blocktime: tx.blocktime,
};
}
catch (error) {
console.error(`Error getting transaction ${txid}:`, error);
throw error;
}
}
/**
* Get mining info
*/
async getMiningInfo() {
return this.rpc.callRpc("getmininginfo");
}
/**
* Get wallet list
*/
async getWalletList() {
return this.rpc.callRpc("listwallets");
}
/**
* Get UTXO set statistics
*/
async getUTXOStats() {
return this.rpc.callRpc("gettxoutsetinfo");
}
/**
* Get blockchain data for visualization
* Combines multiple queries for a complete visualization dataset
*/
async getBlockchainVisualizationData(blockCount = 10) {
const [chainInfo, networkInfo, mempoolInfo, recentBlocks, mempoolTxs, miningInfo, utxoStats,] = await Promise.all([
this.getChainInfo(),
this.getNetworkInfo(),
this.getMempoolInfo(),
this.getRecentBlocks(blockCount),
this.getMempoolTransactions(),
this.getMiningInfo(),
this.getUTXOStats(),
]);
// Calculate total transaction count
const totalTxCount = this.calculateTotalTxCount(recentBlocks, this.cachedTxCount);
// Store for next time
this.cachedTxCount = totalTxCount;
// Create blockchain visualization response with enhanced data
return {
chain: chainInfo.chain,
blocks: recentBlocks.map((block) => ({
hash: block.hash,
height: block.height,
time: block.time,
txCount: block.tx?.length || 0,
size: block.size,
weight: block.weight,
confirmations: block.confirmations,
difficulty: block.difficulty,
miner: this.extractMinerInfo(block),
fee: this.calculateBlockFee(block),
})),
mempool: {
txCount: mempoolTxs.length,
txids: mempoolTxs,
size: mempoolInfo.bytes,
fees: this.calculateMempoolFees(mempoolInfo),
},
stats: {
difficulty: chainInfo.difficulty,
blockCount: chainInfo.blocks,
mempoolSize: mempoolInfo.size,
connections: networkInfo.connections,
totalTxCount: totalTxCount,
},
chainInfo,
networkInfo,
mempoolInfo,
miningInfo,
utxoStats,
timestamp: new Date().getTime(),
};
}
/**
* Extract miner information from coinbase transaction
*/
extractMinerInfo(block) {
try {
if (!block.tx || block.tx.length === 0)
return "Unknown";
// Get coinbase transaction (first tx in block)
const coinbaseTx = typeof block.tx[0] === "string"
? null // Need to fetch transaction if only have txid
: block.tx[0];
if (!coinbaseTx || !coinbaseTx.vin || coinbaseTx.vin.length === 0)
return "Unknown";
// Look for miner data in coinbase
const coinbaseData = coinbaseTx.vin[0].coinbase;
if (!coinbaseData)
return "Unknown";
// Convert hex to ASCII and look for recognizable patterns
const coinbaseAscii = Buffer.from(coinbaseData, "hex")
.toString("ascii")
.replace(/[^\x20-\x7E]/g, ""); // Remove non-printable chars
// Look for common miner strings
if (coinbaseAscii.includes("Bitmain"))
return "Bitmain";
if (coinbaseAscii.includes("AntPool"))
return "AntPool";
if (coinbaseAscii.includes("F2Pool"))
return "F2Pool";
if (coinbaseAscii.includes("ViaBTC"))
return "ViaBTC";
if (coinbaseAscii.includes("SlushPool"))
return "SlushPool";
// If nothing found, use a generic name with block height
return `Miner-${block.height}`;
}
catch (error) {
console.error("Error extracting miner info:", error);
return "Unknown";
}
}
/**
* Calculate estimated block fee
*/
calculateBlockFee(block) {
try {
// Simple estimation based on block reward for regtest
// In a full implementation, you would calculate this from the transactions
return 0; // Placeholder
}
catch (error) {
console.error("Error calculating block fee:", error);
return 0;
}
}
/**
* Calculate mempool fees
*/
calculateMempoolFees(mempoolInfo) {
// Simple estimate based on size and minimum fee rate
return (mempoolInfo.bytes * mempoolInfo.mempoolMinFee) / 100000000;
}
/**
* Calculate total transaction count (approximate)
*/
calculateTotalTxCount(blocks, cachedCount) {
try {
// Count transactions in provided blocks
const txCount = blocks.reduce((sum, block) => sum + (block.tx?.length || 0), 0);
// If we already have a cached count, use it as a base
if (cachedCount > 0) {
return cachedCount + txCount;
}
// Otherwise, estimate based on average transactions per block
const avgTxPerBlock = txCount / blocks.length;
return Math.round(avgTxPerBlock * blocks[0].height);
}
catch (error) {
console.error("Error calculating total tx count:", error);
return cachedCount;
}
}
/**
* Mine blocks - implementation for the real-time visualization
*/
async mineBlocks(numBlocks, address) {
try {
// Get an address if none provided
if (!address) {
const wallets = await this.rpc.listWallets();
if (wallets.length === 0) {
throw new Error("No wallets available for mining");
}
// Use the first wallet to generate an address
address = await this.rpc.callRpc("getnewaddress", [], wallets[0]);
}
// Generate blocks
const blockHashes = await this.rpc.generateToAddress(numBlocks, address);
return blockHashes;
}
catch (error) {
console.error("Error mining blocks:", error);
throw error;
}
}
/**
* Create transaction - implementation for the real-time visualization
*/
async createTransaction(fromWallet, toAddress, amount) {
try {
// Send to address
const txid = await this.rpc.callRpc("sendtoaddress", [toAddress, amount], fromWallet);
return { txid, success: true };
}
catch (error) {
console.error("Error creating transaction:", error);
throw error;
}
}
/**
* Check for new blocks since last check
* Returns the new block if one was found
*/
async checkForNewBlocks() {
try {
const currentHeight = await this.getBlockCount();
if (currentHeight > this.lastBlockHeight) {
// New block found
const newBlock = await this.getBlockByHeight(currentHeight);
this.lastBlockHeight = currentHeight;
return newBlock;
}
return null;
}
catch (error) {
console.error("Error checking for new blocks:", error);
return null;
}
}
/**
* Check for new mempool transactions
* Returns an array of new transaction IDs
*/
async checkForNewMempoolTransactions(previousTxids) {
try {
const currentTxids = await this.getMempoolTransactions();
// Find transactions in current mempool that weren't in previous mempool
const newTxids = currentTxids.filter((txid) => !previousTxids.includes(txid));
return newTxids;
}
catch (error) {
console.error("Error checking for new mempool transactions:", error);
return [];
}
}
}
exports.BlockchainDataService = BlockchainDataService;