import * as bitcoinjs from 'bitcoinjs-lib'; import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory'; import { IBitcoinApi } from './bitcoin-api.interface'; import { IEsploraApi } from './esplora-api.interface'; import blocks from '../blocks'; import mempool from '../mempool'; import { TransactionExtended } from '../../mempool.interfaces'; import transactionUtils from '../transaction-utils'; class BitcoinApi implements AbstractBitcoinApi { private rawMempoolCache: IBitcoinApi.RawMempool | null = null; protected bitcoindClient: any; constructor(bitcoinClient: any) { this.bitcoindClient = bitcoinClient; } static convertBlock(block: IBitcoinApi.Block): IEsploraApi.Block { return { id: block.hash, height: block.height, version: block.version, timestamp: block.time, bits: parseInt(block.bits, 16), nonce: block.nonce, difficulty: block.difficulty, merkle_root: block.merkleroot, tx_count: block.nTx, size: block.size, weight: block.weight, previousblockhash: block.previousblockhash, mediantime: block.mediantime, stale: block.confirmations === -1, }; } $getRawTransaction(txId: string, skipConversion = false, addPrevout = false, lazyPrevouts = false): Promise { // If the transaction is in the mempool we already converted and fetched the fee. Only prevouts are missing const txInMempool = mempool.getMempool()[txId]; if (txInMempool && addPrevout) { return this.$addPrevouts(txInMempool); } return this.bitcoindClient.getRawTransaction(txId, true) .then((transaction: IBitcoinApi.Transaction) => { if (skipConversion) { transaction.vout.forEach((vout) => { vout.value = Math.round(vout.value * 100000000); }); return transaction; } return this.$convertTransaction(transaction, addPrevout, lazyPrevouts); }) .catch((e: Error) => { if (e.message.startsWith('The genesis block coinbase')) { return this.$returnCoinbaseTransaction(); } throw e; }); } async $getRawTransactions(txids: string[]): Promise { const txs: IEsploraApi.Transaction[] = []; for (const txid of txids) { try { const tx = await this.$getRawTransaction(txid, false, true); txs.push(tx); } catch (err) { // skip failures } } return txs; } $getMempoolTransactions(txids: string[]): Promise { throw new Error('Method getMempoolTransactions not supported by the Bitcoin RPC API.'); } $getAllMempoolTransactions(lastTxid: string): Promise { throw new Error('Method getAllMempoolTransactions not supported by the Bitcoin RPC API.'); } async $getTransactionHex(txId: string): Promise { const txInMempool = mempool.getMempool()[txId]; if (txInMempool && txInMempool.hex) { return txInMempool.hex; } return this.bitcoindClient.getRawTransaction(txId, true) .then((transaction: IBitcoinApi.Transaction) => { return transaction.hex; }); } $getBlockHeightTip(): Promise { return this.bitcoindClient.getBlockCount(); } $getBlockHashTip(): Promise { return this.bitcoindClient.getBestBlockHash(); } $getTxIdsForBlock(hash: string): Promise { return this.bitcoindClient.getBlock(hash, 1) .then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx); } $getTxsForBlock(hash: string): Promise { throw new Error('Method getTxsForBlock not supported by the Bitcoin RPC API.'); } $getRawBlock(hash: string): Promise { return this.bitcoindClient.getBlock(hash, 0) .then((raw: string) => Buffer.from(raw, "hex")); } $getBlockHash(height: number): Promise { return this.bitcoindClient.getBlockHash(height); } $getBlockHeader(hash: string): Promise { return this.bitcoindClient.getBlockHeader(hash, false); } async $getBlock(hash: string): Promise { const foundBlock = blocks.getBlocks().find((block) => block.id === hash); if (foundBlock) { return foundBlock; } return this.bitcoindClient.getBlock(hash) .then((block: IBitcoinApi.Block) => BitcoinApi.convertBlock(block)); } $getAddress(address: string): Promise { throw new Error('Method getAddress not supported by the Bitcoin RPC API.'); } $getAddressTransactions(address: string, lastSeenTxId: string): Promise { throw new Error('Method getAddressTransactions not supported by the Bitcoin RPC API.'); } $getScriptHash(scripthash: string): Promise { throw new Error('Method getScriptHash not supported by the Bitcoin RPC API.'); } $getScriptHashTransactions(scripthash: string, lastSeenTxId: string): Promise { throw new Error('Method getScriptHashTransactions not supported by the Bitcoin RPC API.'); } $getRawMempool(): Promise { return this.bitcoindClient.getRawMemPool(); } $getAddressPrefix(prefix: string): string[] { const found: { [address: string]: string } = {}; const mp = mempool.getMempool(); for (const tx in mp) { for (const vout of mp[tx].vout) { if (vout.scriptpubkey_address.indexOf(prefix) === 0) { found[vout.scriptpubkey_address] = ''; if (Object.keys(found).length >= 10) { return Object.keys(found); } } } } return Object.keys(found); } $sendRawTransaction(rawTransaction: string): Promise { return this.bitcoindClient.sendRawTransaction(rawTransaction); } async $getOutspend(txId: string, vout: number): Promise { const txOut = await this.bitcoindClient.getTxOut(txId, vout, false); return { spent: txOut === null, status: { confirmed: true, } }; } async $getOutspends(txId: string): Promise { const outSpends: IEsploraApi.Outspend[] = []; const tx = await this.$getRawTransaction(txId, true, false); for (let i = 0; i < tx.vout.length; i++) { if (tx.status && tx.status.block_height === 0) { outSpends.push({ spent: false }); } else { const txOut = await this.bitcoindClient.getTxOut(txId, i); outSpends.push({ spent: txOut === null, }); } } return outSpends; } async $getBatchedOutspends(txId: string[]): Promise { const outspends: IEsploraApi.Outspend[][] = []; for (const tx of txId) { const outspend = await this.$getOutspends(tx); outspends.push(outspend); } return outspends; } async $getBatchedOutspendsInternal(txId: string[]): Promise { return this.$getBatchedOutspends(txId); } async $getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise { const outspends: IEsploraApi.Outspend[] = []; for (const outpoint of outpoints) { const outspend = await this.$getOutspend(outpoint.txid, outpoint.vout); outspends.push(outspend); } return outspends; } $getEstimatedHashrate(blockHeight: number): Promise { // 120 is the default block span in Core return this.bitcoindClient.getNetworkHashPs(120, blockHeight); } protected async $convertTransaction(transaction: IBitcoinApi.Transaction, addPrevout: boolean, lazyPrevouts = false): Promise { let esploraTransaction: IEsploraApi.Transaction = { txid: transaction.txid, version: transaction.version, locktime: transaction.locktime, size: transaction.size, weight: transaction.weight, fee: 0, vin: [], vout: [], status: { confirmed: false }, }; esploraTransaction.vout = transaction.vout.map((vout) => { return { value: Math.round(vout.value * 100000000), scriptpubkey: vout.scriptPubKey.hex, scriptpubkey_address: vout.scriptPubKey && vout.scriptPubKey.address ? vout.scriptPubKey.address : vout.scriptPubKey.addresses ? vout.scriptPubKey.addresses[0] : '', scriptpubkey_asm: vout.scriptPubKey.asm ? transactionUtils.convertScriptSigAsm(vout.scriptPubKey.hex) : '', scriptpubkey_type: this.translateScriptPubKeyType(vout.scriptPubKey.type), }; }); esploraTransaction.vin = transaction.vin.map((vin) => { return { is_coinbase: !!vin.coinbase, prevout: null, scriptsig: vin.scriptSig && vin.scriptSig.hex || vin.coinbase || '', scriptsig_asm: vin.scriptSig && transactionUtils.convertScriptSigAsm(vin.scriptSig.hex) || '', sequence: vin.sequence, txid: vin.txid || '', vout: vin.vout || 0, witness: vin.txinwitness || [], inner_redeemscript_asm: '', inner_witnessscript_asm: '', }; }); if (transaction.confirmations) { esploraTransaction.status = { confirmed: true, block_height: blocks.getCurrentBlockHeight() - transaction.confirmations + 1, block_hash: transaction.blockhash, block_time: transaction.blocktime, }; } if (addPrevout) { esploraTransaction = await this.$calculateFeeFromInputs(esploraTransaction, false, lazyPrevouts); } else if (!transaction.confirmations) { esploraTransaction = await this.$appendMempoolFeeData(esploraTransaction); } return esploraTransaction; } private translateScriptPubKeyType(outputType: string): string { const map = { 'pubkey': 'p2pk', 'pubkeyhash': 'p2pkh', 'scripthash': 'p2sh', 'witness_v0_keyhash': 'v0_p2wpkh', 'witness_v0_scripthash': 'v0_p2wsh', 'witness_v1_taproot': 'v1_p2tr', 'nonstandard': 'nonstandard', 'multisig': 'multisig', 'nulldata': 'op_return' }; if (map[outputType]) { return map[outputType]; } else { return 'unknown'; } } private async $appendMempoolFeeData(transaction: IEsploraApi.Transaction): Promise { if (transaction.fee) { return transaction; } let mempoolEntry: IBitcoinApi.MempoolEntry; if (!mempool.isInSync() && !this.rawMempoolCache) { this.rawMempoolCache = await this.$getRawMempoolVerbose(); } if (this.rawMempoolCache && this.rawMempoolCache[transaction.txid]) { mempoolEntry = this.rawMempoolCache[transaction.txid]; } else { mempoolEntry = await this.$getMempoolEntry(transaction.txid); } transaction.fee = Math.round(mempoolEntry.fees.base * 100000000); return transaction; } protected async $addPrevouts(transaction: TransactionExtended): Promise { for (const vin of transaction.vin) { if (vin.prevout) { continue; } const innerTx = await this.$getRawTransaction(vin.txid, false, false); vin.prevout = innerTx.vout[vin.vout]; transactionUtils.addInnerScriptsToVin(vin); } return transaction; } protected $returnCoinbaseTransaction(): Promise { return this.bitcoindClient.getBlockHash(0).then((hash: string) => this.bitcoindClient.getBlock(hash, 2) .then((block: IBitcoinApi.Block) => { return this.$convertTransaction(Object.assign(block.tx[0], { confirmations: blocks.getCurrentBlockHeight() + 1, blocktime: block.time }), false); }) ); } private $getMempoolEntry(txid: string): Promise { return this.bitcoindClient.getMempoolEntry(txid); } private $getRawMempoolVerbose(): Promise { return this.bitcoindClient.getRawMemPool(true); } private async $calculateFeeFromInputs(transaction: IEsploraApi.Transaction, addPrevout: boolean, lazyPrevouts: boolean): Promise { if (transaction.vin[0].is_coinbase) { transaction.fee = 0; return transaction; } let totalIn = 0; for (let i = 0; i < transaction.vin.length; i++) { if (lazyPrevouts && i > 12) { transaction.vin[i].lazy = true; continue; } const innerTx = await this.$getRawTransaction(transaction.vin[i].txid, false, false); transaction.vin[i].prevout = innerTx.vout[transaction.vin[i].vout]; transactionUtils.addInnerScriptsToVin(transaction.vin[i]); totalIn += innerTx.vout[transaction.vin[i].vout].value; } if (lazyPrevouts && transaction.vin.length > 12) { transaction.fee = -1; } else { const totalOut = transaction.vout.reduce((p, output) => p + output.value, 0); transaction.fee = parseFloat((totalIn - totalOut).toFixed(8)); } return transaction; } public startHealthChecks(): void {}; } export default BitcoinApi;