UNPKG

@btc-vision/bitcoin-rpc

Version:

The one and only fully typed Bitcoin RPC client for Node.js

719 lines (718 loc) 23 kB
import { Logger } from '@btc-vision/bsi-common'; import { RPCClient, } from './external/rpc.js'; import { BitcoinVerbosity } from './types/BitcoinVerbosity.js'; import { FeeEstimation } from './types/FeeEstimation.js'; export class BitcoinRPC extends Logger { constructor(cacheClearInterval = 1000, enableDebug = false) { super(); this.cacheClearInterval = cacheClearInterval; this.enableDebug = enableDebug; this.logColor = '#fa9600'; this.rpc = null; this.blockchainInfo = null; this.currentBlockInfo = null; this.purgeInterval = null; this.purgeCachedData(); } destroy() { if (this.purgeInterval) { clearInterval(this.purgeInterval); } this.rpc = null; this.blockchainInfo = null; this.currentBlockInfo = null; } getRpcConfigFromBlockchainConfig(rpcInfo) { return { url: `http://${rpcInfo.BITCOIND_HOST}`, port: rpcInfo.BITCOIND_PORT, user: rpcInfo.BITCOIND_USERNAME, pass: rpcInfo.BITCOIND_PASSWORD, }; } async getBestBlockHash() { this.debugMessage('getBestBlockHash'); if (!this.rpc) { throw new Error('RPC not initialized'); } const bestBlockHash = (await this.rpc.getbestblockhash().catch((e) => { this.error(`Error getting best block hash: ${e}`); return; })); return bestBlockHash || null; } async getBlockBatch(blockHashes) { this.debugMessage('getBlockBatch'); if (!this.rpc) { throw new Error('RPC not initialized'); } const blockData = (await this.rpc .getblockBatch(blockHashes) .catch((e) => { this.error(`Error getting block batch: ${e}`); return null; })); return blockData || null; } async getBlockAsHexString(blockHash) { this.debugMessage('getBlockAsHexString'); if (!this.rpc) { throw new Error('RPC not initialized'); } const param = { blockhash: blockHash, verbosity: BitcoinVerbosity.NONE, }; const blockData = (await this.rpc.getblock(param).catch((e) => { this.error(`Error getting block data: ${e}`); return null; })); return blockData == '' ? null : blockData; } async estimateSmartFee(confTarget, estimateMode = FeeEstimation.CONSERVATIVE) { this.debugMessage('estimateSmartFee'); if (!this.rpc) { throw new Error('RPC not initialized'); } const opts = { conf_target: confTarget, estimate_mode: estimateMode, }; return (await this.rpc.estimatesmartfee(opts)); } async joinPSBTs(psbts) { this.debugMessage('joinPSBTs'); if (!this.rpc) { throw new Error('RPC not initialized'); } const result = (await this.rpc .joinpsbts({ txs: psbts, }) .catch((e) => { this.error(`Error joining PSBTs: ${e}`); return ''; })); return result || null; } async getBlockInfoOnly(blockHash) { this.debugMessage('getBlockInfoOnly'); if (!this.rpc) { throw new Error('RPC not initialized'); } const param = { blockhash: blockHash, verbosity: BitcoinVerbosity.RAW, }; const blockData = (await this.rpc.getblock(param).catch((e) => { this.error(`Error getting block data: ${e}`); return null; })); return blockData || null; } async getBlockInfoWithTransactionData(blockHash) { this.debugMessage(`getBlockInfoWithTransactionData ${blockHash}`); if (!this.rpc) { throw new Error('RPC not initialized'); } const param = { blockhash: blockHash, verbosity: 2, }; const blockData = (await this.rpc .getblock(param) .catch((e) => { this.error(`Error getting block data: ${e}`); return null; })); return blockData || null; } async getBlockHashes(height, count) { this.debugMessage(`getBlockHashes ${height} ${count}`); if (!this.rpc) { throw new Error('RPC not initialized'); } const param = { height, }; const blockHashes = (await this.rpc .getblockhashes(param, count) .catch((e) => { this.error(`Error getting block hashes: ${e}`); return []; })); return blockHashes || null; } async getBlocksInfoWithTransactionData(blockHashes) { if (!this.rpc) { throw new Error('RPC not initialized'); } const blockData = (await this.rpc .getblockBatch(blockHashes, 2) .catch((e) => { this.error(`Error getting block data: ${e}`); return null; })); return blockData || null; } async getBlockCount() { this.debugMessage('getBlockCount'); if (!this.rpc) { throw new Error('RPC not initialized'); } const blockCount = (await this.rpc.getblockcount().catch((e) => { this.error(`Error getting block count: ${e}`); return 0; })); return blockCount ?? null; } async getBlockFilter(blockHash, filterType) { this.debugMessage(`getBlockFilter ${blockHash}`); if (!this.rpc) { throw new Error('RPC not initialized'); } const param = { blockhash: blockHash, filtertype: filterType, }; const result = (await this.rpc .getblockfilter(param) .catch((e) => { this.error(`Error getting block filter: ${e}`); return null; })); return result || null; } async getBlockHash(height) { this.debugMessage(`getBlockHash ${height}`); if (!this.rpc) { throw new Error('RPC not initialized'); } const param = { height: height, }; const result = (await this.rpc.getblockhash(param).catch((e) => { this.error(`Error getting block hash: ${e}`); return ''; })); return result || null; } async getChainInfo() { this.debugMessage('getChainInfo'); if (!this.rpc) { throw new Error('RPC not initialized'); } this.blockchainInfo = (await this.rpc.getblockchaininfo()); if (this.blockchainInfo) { this.currentBlockInfo = { blockHeight: this.blockchainInfo.blocks, blockHash: this.blockchainInfo.bestblockhash, }; } return this.blockchainInfo; } async getWalletInfo(walletName) { this.debugMessage(`getWalletInfo ${walletName}`); if (!this.rpc) { throw new Error('RPC not initialized'); } const walletInfo = (await this.rpc .getwalletinfo(walletName) .catch((e) => { this.error(`Error getting wallet info: ${e}`); return null; })); return walletInfo || null; } async createWallet(params) { this.debugMessage('createWallet'); if (!this.rpc) { throw new Error('RPC not initialized'); } const wallet = (await this.rpc .createwallet(params) .catch((e) => { this.error(`Error creating wallet: ${e}`); return ''; })); return wallet || null; } async loadWallet(filename) { this.debugMessage(`loadWallet ${filename}`); if (!this.rpc) { throw new Error('RPC not initialized'); } const params = { filename, }; const wallet = (await this.rpc .loadwallet(params) .catch((e) => { this.error(`Error loading wallet: ${e}`); return ''; })); return wallet || null; } async listWallets() { this.debugMessage('listWallets'); if (!this.rpc) { throw new Error('RPC not initialized'); } const wallets = (await this.rpc.listwallets().catch((e) => { this.error(`Error listing wallets: ${e}`); return []; })); return wallets || null; } async getNewAddress(label, wallet) { this.debugMessage(`getNewAddress ${label}`); if (!this.rpc) { throw new Error('RPC not initialized'); } const params = { label, }; const address = (await this.rpc .getnewaddress(params, wallet) .catch((e) => { this.error(`Error getting new address: ${e}`); return ''; })); return address || null; } async generateToAddress(nBlock, address, wallet) { this.debugMessage(`generateToAddress ${nBlock} ${address}`); if (!this.rpc) { throw new Error('RPC not initialized'); } const params = { nblocks: nBlock, address, }; const blockHashes = (await this.rpc .generatetoaddress(params, wallet) .catch((e) => { this.error(`Error generating to address: ${e}`); return []; })); return blockHashes || null; } async importPrivateKey(privateKey, label, rescan, wallet) { this.debugMessage(`importPrivateKey ${label}`); if (!this.rpc) { throw new Error('RPC not initialized'); } const params = { privkey: privateKey, label, rescan, }; await this.rpc.importprivkey(params, wallet).catch((e) => { this.error(`Error importing private key: ${e}`); }); } async getAddressByLabel(label, wallet) { this.debugMessage(`getAddressByLabel ${label}`); if (!this.rpc) { throw new Error('RPC not initialized'); } const params = { label, }; const address = (await this.rpc .getaddressesbylabel(params, wallet) .catch((e) => { this.error(`Error getting address by label: ${e}`); throw e; })); return address || null; } async sendRawTransaction(params) { this.debugMessage('sendRawTransaction'); if (!this.rpc) { throw new Error('RPC not initialized'); } const txId = (await this.rpc .sendrawtransaction(params) .catch((e) => { throw e; })); return txId || null; } async dumpPrivateKey(address, wallet) { this.debugMessage(`dumpPrivateKey ${address}`); if (!this.rpc) { throw new Error('RPC not initialized'); } const privateKey = (await this.rpc .dumpprivkey({ address, }, wallet) .catch((e) => { this.error(`Error dumping private key: ${e}`); return ''; })); return privateKey || null; } async getRawTransaction(parameters) { this.debugMessage(`getRawTransaction ${parameters.txId}`); if (!this.rpc) { throw new Error('RPC not initialized'); } const params = { txid: parameters.txId, verbose: parameters.verbose !== BitcoinVerbosity.RAW, }; if (parameters.blockHash) { params.blockhash = parameters.blockHash; } const rawTx = (await this.rpc .getrawtransaction(params) .catch((e) => { this.error(`Error getting raw transaction: ${e}`); return null; })); return rawTx || null; } async getRawTransactions(txs, verbose) { this.debugMessage(`getRawTransactions ${txs}`); if (!this.rpc) { throw new Error('RPC not initialized'); } const txsInfo = (await this.rpc .getrawtransactionBatch(txs, verbose !== BitcoinVerbosity.RAW) .catch((e) => { this.error(`Error getting raw transactions: ${e}`); return null; })); return txsInfo || null; } async getBlockHeight() { this.debugMessage('getBlockHeight'); if (!this.rpc) { throw new Error('RPC not initialized'); } if (!this.currentBlockInfo) { await this.getChainInfo(); } return this.currentBlockInfo; } async getBlockHeader(blockHash, verbose) { this.debugMessage(`getBlockHeader ${blockHash} ${verbose}`); if (!this.rpc) { throw new Error('RPC not initialized'); } const param = { blockhash: blockHash, verbose: verbose, }; const header = (await this.rpc .getblockheader(param) .catch((e) => { this.error(`Error getting block header: ${e}`); return ''; })); return header || null; } async getBlockStatsByHeight(height, stats) { this.debugMessage(`getBlockStatsByHeight ${height} ${stats}`); if (!this.rpc) { throw new Error('RPC not initialized'); } const param = { hash_or_height: height, stats: stats, }; const blockStats = (await this.rpc .getblockstats(param) .catch((e) => { this.error(`Error getting block stats: ${e}`); return null; })); return blockStats || null; } async getBlockStatsByHash(blockHash, stats) { this.debugMessage(`getBlockStatsByHash ${blockHash} ${stats}`); if (!this.rpc) { throw new Error('RPC not initialized'); } const param = { hash_or_height: blockHash, stats: stats, }; const blockStats = (await this.rpc .getblockstats(param) .catch((e) => { this.error(`Error getting block stats: ${e}`); return null; })); return blockStats || null; } async getChainTips() { this.debugMessage('getChainTips'); if (!this.rpc) { throw new Error('RPC not initialized'); } const tips = (await this.rpc.getchaintips().catch((e) => { this.error(`Error getting chain tips: ${e}`); return null; })); return tips || null; } async getChainTxStats(param) { this.debugMessage('getChainTxStats'); if (!this.rpc) { throw new Error('RPC not initialized'); } const chainTxStats = (await this.rpc .getchaintxstats(param) .catch((e) => { this.error(`Error getting chain tx stats: ${e}`); return null; })); return chainTxStats || null; } async getDifficulty() { this.debugMessage('getDifficulty'); if (!this.rpc) { throw new Error('RPC not initialized'); } const difficulty = (await this.rpc.getdifficulty().catch((e) => { this.error(`Error getting difficulty: ${e}`); return 0; })); return difficulty ?? null; } async getMempoolAncestors(txId, verbose) { this.debugMessage(`getMempoolAncestors ${txId}`); if (!this.rpc) { throw new Error('RPC not initialized'); } const param = { txid: txId, verbose: verbose !== BitcoinVerbosity.RAW, }; const transactionInfo = (await this.rpc .getmempoolancestors(param) .catch((e) => { this.error(`Error getting mempool ancestors: ${e}`); return null; })); return transactionInfo || null; } async getMempoolDescendants(txid, verbose) { this.debugMessage(`getMempoolDescendants ${txid}`); if (!this.rpc) { throw new Error('RPC not initialized'); } const param = { txid: txid, verbose: verbose !== BitcoinVerbosity.RAW, }; const transactionInfo = (await this.rpc .getmempooldescendants(param) .catch((e) => { this.error(`Error getting mempool descendants: ${e}`); return null; })); return transactionInfo || null; } async getMempoolEntry(txid) { this.debugMessage(`getMempoolEntry ${txid}`); if (!this.rpc) { throw new Error('RPC not initialized'); } const param = { txid: txid, }; const transactionInfo = (await this.rpc .getmempoolentry(param) .catch((e) => { this.error(`Error getting mempool entry: ${e}`); return null; })); return transactionInfo || null; } async getMempoolInfo() { this.debugMessage('getMempoolInfo'); if (!this.rpc) { throw new Error('RPC not initialized'); } const mempoolInfo = (await this.rpc .getmempoolinfo() .catch((e) => { this.error(`Error getting mempool info: ${e}`); return null; })); return mempoolInfo || null; } async getRawMempool(verbose) { this.debugMessage('getRawMempool'); if (!this.rpc) { throw new Error('RPC not initialized'); } const param = { verbose: verbose !== BitcoinVerbosity.RAW, }; const mempoolInfo = (await this.rpc .getrawmempool(param) .catch((e) => { this.error(`Error getting raw mempool: ${e}`); return null; })); return mempoolInfo || null; } async getTxOut(txid, voutNumber, includeMempool) { this.debugMessage(`getTxOut ${txid} ${voutNumber}`); if (!this.rpc) { throw new Error('RPC not initialized'); } const param = { n: voutNumber, txid: txid, include_mempool: includeMempool, }; const txOuputInfo = (await this.rpc .gettxout(param) .catch((e) => { this.error(`Error getting tx out: ${e}`); return null; })); return txOuputInfo || null; } async getTxOutProof(txids, blockHash) { this.debugMessage(`getTxOutProof ${txids} ${blockHash}`); if (!this.rpc) { throw new Error('RPC not initialized'); } const param = { txids: txids, blockhash: blockHash, }; const txOuputProof = (await this.rpc .gettxoutproof(param) .catch((e) => { this.error(`Error getting tx out proof: ${e}`); return ''; })); return txOuputProof || null; } async getTxOutSetInfo() { this.debugMessage('getTxOutSetInfo'); if (!this.rpc) { throw new Error('RPC not initialized'); } const txOuputSetInfo = (await this.rpc .gettxoutsetinfo() .catch((e) => { this.error(`Error getting tx out set info: ${e}`); return null; })); return txOuputSetInfo || null; } async preciousBlock(blockHash) { this.debugMessage(`preciousBlock ${blockHash}`); if (!this.rpc) { throw new Error('RPC not initialized'); } const param = { blockhash: blockHash, }; await this.rpc.preciousblock(param).catch((e) => { this.error(`Error precious block: ${e}`); }); } async pruneBlockChain(height) { this.debugMessage(`pruneBlockChain ${height}`); if (!this.rpc) { throw new Error('RPC not initialized'); } const param = { height: height, }; const prunedHeight = (await this.rpc .pruneblockchain(param) .catch((e) => { this.error(`Error pruning blockchain: ${e}`); return 0; })); return prunedHeight ?? null; } async saveMempool() { this.debugMessage('saveMempool'); if (!this.rpc) { throw new Error('RPC not initialized'); } await this.rpc.savemempool().catch((e) => { this.error(`Error saving mempool: ${e}`); }); } async verifyChain(checkLevel, nblocks) { this.debugMessage(`verifyChain ${checkLevel} ${nblocks}`); if (!this.rpc) { throw new Error('RPC not initialized'); } const param = { checklevel: checkLevel, nblocks: nblocks, }; const checked = (await this.rpc.verifychain(param).catch((e) => { this.error(`Error verifying chain: ${e}`); return false; })); return checked ?? null; } async verifyTxOutProof(proof) { this.debugMessage(`verifyTxOutProof ${proof}`); if (!this.rpc) { throw new Error('RPC not initialized'); } const param = { proof: proof, }; const proofs = (await this.rpc .verifytxoutproof(param) .catch((e) => { this.error(`Error verifying tx out proof: ${e}`); return []; })); return proofs || null; } async init(rpcInfo) { if (this.rpc) { throw new Error('RPC already initialized'); } const rpcConfig = this.getRpcConfigFromBlockchainConfig(rpcInfo); this.rpc = new RPCClient(rpcConfig); await this.testRPC(); } error(...args) { if (this.enableDebug) { super.error(...args); } } debugMessage(message) { if (this.enableDebug) { this.log(message); } } purgeCachedData() { this.purgeInterval = setInterval(() => { this.blockchainInfo = null; this.currentBlockInfo = null; }, this.cacheClearInterval); } async testRPC() { try { const chainInfo = await this.getChainInfo(); if (!chainInfo) { this.error('RPC errored. Please check your configuration.'); process.exit(1); } } catch (e) { const error = e; this.error(`RPC errored. Please check your configuration. ${error.message}`); } } }