@btc-vision/bitcoin-rpc
Version:
The one and only fully typed Bitcoin RPC client for Node.js
719 lines (718 loc) • 23 kB
JavaScript
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}`);
}
}
}