diff --git a/backend/mempool-config.sample.json b/backend/mempool-config.sample.json index 473622ef5..786aaddc8 100644 --- a/backend/mempool-config.sample.json +++ b/backend/mempool-config.sample.json @@ -1,7 +1,7 @@ { "MEMPOOL": { "NETWORK": "mainnet", - "BACKEND": "electrs", + "BACKEND": "electrum", "HTTP_PORT": 8999, "SPAWN_CLUSTER_PROCS": 0, "API_URL_PREFIX": "/api/v1/", diff --git a/backend/src/api/bisq/bisq.ts b/backend/src/api/bisq/bisq.ts index 8c4d89ec6..7824d30ff 100644 --- a/backend/src/api/bisq/bisq.ts +++ b/backend/src/api/bisq/bisq.ts @@ -3,7 +3,7 @@ import * as fs from 'fs'; import axios from 'axios'; import { BisqBlocks, BisqBlock, BisqTransaction, BisqStats, BisqTrade } from './interfaces'; import { Common } from '../common'; -import { Block } from '../../interfaces'; +import { BlockExtended } from '../../mempool.interfaces'; import { StaticPool } from 'node-worker-threads-pool'; import logger from '../../logger'; @@ -42,7 +42,7 @@ class Bisq { this.startSubDirectoryWatcher(); } - handleNewBitcoinBlock(block: Block): void { + handleNewBitcoinBlock(block: BlockExtended): void { if (block.height - 2 > this.latestBlockHeight && this.latestBlockHeight !== 0) { logger.warn(`Bitcoin block height (#${block.height}) has diverged from the latest Bisq block height (#${this.latestBlockHeight}). Restarting watchers...`); this.startTopDirectoryWatcher(); diff --git a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts index cce6a352b..4dcd4b4d4 100644 --- a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts +++ b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts @@ -1,21 +1,13 @@ -import { MempoolInfo, Transaction, Block, MempoolEntries, MempoolEntry, Address, AddressInformation, - ScriptHashBalance, ScriptHashHistory } from '../../interfaces'; +import { IEsploraApi } from './esplora-api.interface'; export interface AbstractBitcoinApi { - $getMempoolInfo(): Promise; - $getRawMempool(): Promise; - $getRawTransaction(txId: string): Promise; + $getRawMempool(): Promise; + $getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean): Promise; + $getRawTransactionBitcoind(txId: string, skipConversion?: boolean, addPrevout?: boolean): Promise; $getBlockHeightTip(): Promise; $getTxIdsForBlock(hash: string): Promise; $getBlockHash(height: number): Promise; - $getBlock(hash: string): Promise; - $getMempoolEntry(txid: string): Promise; - $getAddress(address: string): Promise
; - $validateAddress(address: string): Promise; - $getScriptHashBalance(scriptHash: string): Promise; - $getScriptHashHistory(scriptHash: string): Promise; - - // Custom - $getRawMempoolVerbose(): Promise; - $getRawTransactionBitcond(txId: string): Promise; + $getBlock(hash: string): Promise; + $getAddress(address: string): Promise; + $getAddressTransactions(address: string, lastSeenTxId: string): Promise; } diff --git a/backend/src/api/bitcoin/bitcoin-api-factory.ts b/backend/src/api/bitcoin/bitcoin-api-factory.ts index f625e99e1..a16521a0e 100644 --- a/backend/src/api/bitcoin/bitcoin-api-factory.ts +++ b/backend/src/api/bitcoin/bitcoin-api-factory.ts @@ -1,18 +1,18 @@ import config from '../../config'; import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory'; -import BitcoindElectrsApi from './bitcoind-electrs-api'; -import BitcoindApi from './bitcoind-api'; -import ElectrsApi from './electrs-api'; +import EsploraApi from './esplora-api'; +import BitcoinApi from './bitcoin-api'; +import ElectrumApi from './electrum-api'; function bitcoinApiFactory(): AbstractBitcoinApi { switch (config.MEMPOOL.BACKEND) { - case 'electrs': - return new ElectrsApi(); - case 'bitcoind-electrs': - return new BitcoindElectrsApi(); - case 'bitcoind': + case 'esplora': + return new EsploraApi(); + case 'electrum': + return new ElectrumApi(); + case 'none': default: - return new BitcoindApi(); + return new BitcoinApi(); } } diff --git a/backend/src/api/bitcoin/bitcoin-api.interface.ts b/backend/src/api/bitcoin/bitcoin-api.interface.ts new file mode 100644 index 000000000..194beebee --- /dev/null +++ b/backend/src/api/bitcoin/bitcoin-api.interface.ts @@ -0,0 +1,116 @@ +export namespace IBitcoinApi { + export interface MempoolInfo { + loaded: boolean; // (boolean) True if the mempool is fully loaded + size: number; // (numeric) Current tx count + bytes: number; // (numeric) Sum of all virtual transaction sizes as defined in BIP 141. + usage: number; // (numeric) Total memory usage for the mempool + maxmempool: number; // (numeric) Maximum memory usage for the mempool + mempoolminfee: number; // (numeric) Minimum fee rate in BTC/kB for tx to be accepted. + minrelaytxfee: number; // (numeric) Current minimum relay fee for transactions + } + + export interface RawMempool { [txId: string]: MempoolEntry; } + + export interface MempoolEntry { + vsize: number; // (numeric) virtual transaction size as defined in BIP 141. + weight: number; // (numeric) transaction weight as defined in BIP 141. + time: number; // (numeric) local time transaction entered pool in seconds since 1 Jan 1970 GMT + height: number; // (numeric) block height when transaction entered pool + descendantcount: number; // (numeric) number of in-mempool descendant transactions (including this one) + descendantsize: number; // (numeric) virtual transaction size of in-mempool descendants (including this one) + ancestorcount: number; // (numeric) number of in-mempool ancestor transactions (including this one) + ancestorsize: number; // (numeric) virtual transaction size of in-mempool ancestors (including this one) + wtxid: string; // (string) hash of serialized transactionumber; including witness data + fees: { + base: number; // (numeric) transaction fee in BTC + modified: number; // (numeric) transaction fee with fee deltas used for mining priority in BTC + ancestor: number; // (numeric) modified fees (see above) of in-mempool ancestors (including this one) in BTC + descendant: number; // (numeric) modified fees (see above) of in-mempool descendants (including this one) in BTC + }; + depends: string[]; // (string) parent transaction id + spentby: string[]; // (array) unconfirmed transactions spending outputs from this transaction + 'bip125-replaceable': boolean; // (boolean) Whether this transaction could be replaced due to BIP125 (replace-by-fee) + } + + export interface Block { + hash: string; // (string) the block hash (same as provided) + confirmations: number; // (numeric) The number of confirmations, or -1 if the block is not on the main chain + size: number; // (numeric) The block size + strippedsize: number; // (numeric) The block size excluding witness data + weight: number; // (numeric) The block weight as defined in BIP 141 + height: number; // (numeric) The block height or index + version: number; // (numeric) The block version + versionHex: string; // (string) The block version formatted in hexadecimal + merkleroot: string; // (string) The merkle root + tx: Transaction[]; + time: number; // (numeric) The block time expressed in UNIX epoch time + mediantime: number; // (numeric) The median block time expressed in UNIX epoch time + nonce: number; // (numeric) The nonce + bits: string; // (string) The bits + difficulty: number; // (numeric) The difficulty + chainwork: string; // (string) Expected number of hashes required to produce the chain up to this block (in hex) + nTx: number; // (numeric) The number of transactions in the block + previousblockhash: string; // (string) The hash of the previous block + nextblockhash: string; // (string) The hash of the next block + } + + export interface Transaction { + in_active_chain: boolean; // (boolean) Whether specified block is in the active chain or not + hex: string; // (string) The serialized, hex-encoded data for 'txid' + txid: string; // (string) The transaction id (same as provided) + hash: string; // (string) The transaction hash (differs from txid for witness transactions) + size: number; // (numeric) The serialized transaction size + vsize: number; // (numeric) The virtual transaction size (differs from size for witness transactions) + weight: number; // (numeric) The transaction's weight (between vsize*4-3 and vsize*4) + version: number; // (numeric) The version + locktime: number; // (numeric) The lock time + vin: Vin[]; + vout: Vout[]; + blockhash: string; // (string) the block hash + confirmations: number; // (numeric) The confirmations + blocktime: number; // (numeric) The block time expressed in UNIX epoch time + time: number; // (numeric) Same as blocktime + } + + interface Vin { + txid?: string; // (string) The transaction id + vout?: number; // (string) + scriptSig?: { // (json object) The script + asm: string; // (string) asm + hex: string; // (string) hex + }; + sequence: number; // (numeric) The script sequence number + txinwitness?: string[]; // (string) hex-encoded witness data + coinbase?: string; + } + + interface Vout { + value: number; // (numeric) The value in BTC + n: number; // (numeric) index + scriptPubKey: { // (json object) + asm: string; // (string) the asm + hex: string; // (string) the hex + reqSigs: number; // (numeric) The required sigs + type: string; // (string) The type, eg 'pubkeyhash' + addresses: string[] // (string) bitcoin address + }; + } + + export interface AddressInformation { + isvalid: boolean; // (boolean) If the address is valid or not. If not, this is the only property returned. + address: string; // (string) The bitcoin address validated + scriptPubKey: string; // (string) The hex-encoded scriptPubKey generated by the address + isscript: boolean; // (boolean) If the key is a script + iswitness: boolean; // (boolean) If the address is a witness + witness_version?: boolean; // (numeric, optional) The version number of the witness program + witness_program: string; // (string, optional) The hex value of the witness program + } + + export interface ChainTips { + height: number; // (numeric) height of the chain tip + hash: string; // (string) block hash of the tip + branchlen: number; // (numeric) zero for main chain, otherwise length of branch connecting the tip to the main chain + status: 'invalid' | 'headers-only' | 'valid-headers' | 'valid-fork' | 'active'; + } + +} diff --git a/backend/src/api/bitcoin/bitcoin-api.ts b/backend/src/api/bitcoin/bitcoin-api.ts new file mode 100644 index 000000000..c5452a533 --- /dev/null +++ b/backend/src/api/bitcoin/bitcoin-api.ts @@ -0,0 +1,199 @@ +import config from '../../config'; +import * as bitcoin from '@mempool/bitcoin'; +import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory'; +import { IBitcoinApi } from './bitcoin-api.interface'; +import { IEsploraApi } from './esplora-api.interface'; +import blocks from '../blocks'; +import bitcoinBaseApi from './bitcoin-base.api'; +import mempool from '../mempool'; + +class BitcoinApi implements AbstractBitcoinApi { + private rawMempoolCache: IBitcoinApi.RawMempool | null = null; + private bitcoindClient: any; + + constructor() { + this.bitcoindClient = new bitcoin.Client({ + host: config.BITCOIND.HOST, + port: config.BITCOIND.PORT, + user: config.BITCOIND.USERNAME, + pass: config.BITCOIND.PASSWORD, + timeout: 60000, + }); + } + + $getRawTransactionBitcoind(txId: string, skipConversion = false, addPrevout = false): Promise { + return this.bitcoindClient.getRawTransaction(txId, true) + .then((transaction: IBitcoinApi.Transaction) => { + if (skipConversion) { + return transaction; + } + return this.$convertTransaction(transaction, addPrevout); + }); + } + + $getRawTransaction(txId: string, skipConversion = false, addPrevout = false): Promise { + return this.bitcoindClient.getRawTransaction(txId, true) + .then((transaction: IBitcoinApi.Transaction) => { + if (skipConversion) { + return transaction; + } + return this.$convertTransaction(transaction, addPrevout); + }); + } + + $getBlockHeightTip(): Promise { + return this.bitcoindClient.getChainTips() + .then((result: IBitcoinApi.ChainTips[]) => result[0].height); + } + + $getTxIdsForBlock(hash: string): Promise { + return this.bitcoindClient.getBlock(hash, 1) + .then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx); + } + + $getBlockHash(height: number): Promise { + return this.bitcoindClient.getBlockHash(height); + } + + $getBlock(hash: string): Promise { + return this.bitcoindClient.getBlock(hash) + .then((block: IBitcoinApi.Block) => this.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.'); + } + + $getRawMempool(): Promise { + return this.bitcoindClient.getRawMemPool(); + } + + protected async $convertTransaction(transaction: IBitcoinApi.Transaction, addPrevout: boolean): 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: vout.value * 100000000, + scriptpubkey: vout.scriptPubKey.hex, + scriptpubkey_address: vout.scriptPubKey && vout.scriptPubKey.addresses ? vout.scriptPubKey.addresses[0] : '', + scriptpubkey_asm: vout.scriptPubKey.asm, + 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 && vin.scriptSig.asm || '', + sequence: vin.sequence, + txid: vin.txid || '', + vout: vin.vout || 0, + witness: vin.txinwitness, + }; + }); + + if (transaction.confirmations) { + esploraTransaction.status = { + confirmed: true, + block_height: blocks.getCurrentBlockHeight() - transaction.confirmations + 1, + block_hash: transaction.blockhash, + block_time: transaction.blocktime, + }; + } + + if (transaction.confirmations) { + esploraTransaction = await this.$calculateFeeFromInputs(esploraTransaction, addPrevout); + } else { + esploraTransaction = await this.$appendMempoolFeeData(esploraTransaction); + } + + return esploraTransaction; + } + + private 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, + }; + } + + 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', + 'nulldata': 'op_return' + }; + + if (map[outputType]) { + return map[outputType]; + } else { + return ''; + } + } + + private async $appendMempoolFeeData(transaction: IEsploraApi.Transaction): Promise { + let mempoolEntry: IBitcoinApi.MempoolEntry; + if (!mempool.isInSync() && !this.rawMempoolCache) { + this.rawMempoolCache = await bitcoinBaseApi.$getRawMempoolVerbose(); + } + if (this.rawMempoolCache && this.rawMempoolCache[transaction.txid]) { + mempoolEntry = this.rawMempoolCache[transaction.txid]; + } else { + mempoolEntry = await bitcoinBaseApi.$getMempoolEntry(transaction.txid); + } + transaction.fee = mempoolEntry.fees.base * 100000000; + return transaction; + } + + private async $calculateFeeFromInputs(transaction: IEsploraApi.Transaction, addPrevout: boolean): Promise { + if (transaction.vin[0].is_coinbase) { + transaction.fee = 0; + return transaction; + } + let totalIn = 0; + for (const vin of transaction.vin) { + const innerTx = await this.$getRawTransaction(vin.txid, !addPrevout); + if (addPrevout) { + vin.prevout = innerTx.vout[vin.vout]; + } + totalIn += innerTx.vout[vin.vout].value; + } + const totalOut = transaction.vout.reduce((prev, output) => prev + output.value, 0); + transaction.fee = parseFloat((totalIn - totalOut).toFixed(8)); + return transaction; + } + +} + +export default BitcoinApi; diff --git a/backend/src/api/bitcoin/bitcoin-base.api.ts b/backend/src/api/bitcoin/bitcoin-base.api.ts new file mode 100644 index 000000000..22c8daaed --- /dev/null +++ b/backend/src/api/bitcoin/bitcoin-base.api.ts @@ -0,0 +1,40 @@ +import config from '../../config'; +import * as bitcoin from '@mempool/bitcoin'; +import { IBitcoinApi } from './bitcoin-api.interface'; + +class BitcoinBaseApi { + bitcoindClient: any; + + constructor() { + this.bitcoindClient = new bitcoin.Client({ + host: config.BITCOIND.HOST, + port: config.BITCOIND.PORT, + user: config.BITCOIND.USERNAME, + pass: config.BITCOIND.PASSWORD, + timeout: 60000, + }); + } + + $getMempoolInfo(): Promise { + return this.bitcoindClient.getMempoolInfo(); + } + + $getRawTransaction(txId: string): Promise { + return this.bitcoindClient.getRawTransaction(txId, true); + } + + $getMempoolEntry(txid: string): Promise { + return this.bitcoindClient.getMempoolEntry(txid); + } + + $getRawMempoolVerbose(): Promise { + return this.bitcoindClient.getRawMemPool(true); + } + + $validateAddress(address: string): Promise { + return this.bitcoindClient.validateAddress(address); + } + +} + +export default new BitcoinBaseApi(); diff --git a/backend/src/api/bitcoin/bitcoind-api.ts b/backend/src/api/bitcoin/bitcoind-api.ts deleted file mode 100644 index d008e95b2..000000000 --- a/backend/src/api/bitcoin/bitcoind-api.ts +++ /dev/null @@ -1,101 +0,0 @@ -import config from '../../config'; -import { Transaction, Block, MempoolInfo, RpcBlock, MempoolEntries, MempoolEntry, Address, - AddressInformation, ScriptHashBalance, ScriptHashHistory } from '../../interfaces'; -import * as bitcoin from '@mempool/bitcoin'; -import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory'; - -class BitcoindApi implements AbstractBitcoinApi { - bitcoindClient: any; - - constructor() { - this.bitcoindClient = new bitcoin.Client({ - host: config.BITCOIND.HOST, - port: config.BITCOIND.PORT, - user: config.BITCOIND.USERNAME, - pass: config.BITCOIND.PASSWORD, - timeout: 60000, - }); - } - - $getMempoolInfo(): Promise { - return this.bitcoindClient.getMempoolInfo(); - } - - $getRawMempool(): Promise { - return this.bitcoindClient.getRawMemPool(); - } - - $getRawMempoolVerbose(): Promise { - return this.bitcoindClient.getRawMemPool(true); - } - - $getMempoolEntry(txid: string): Promise { - return this.bitcoindClient.getMempoolEntry(txid); - } - - $getRawTransaction(txId: string): Promise { - return this.bitcoindClient.getRawTransaction(txId, true) - .then((transaction: Transaction) => { - transaction.vout.forEach((vout) => vout.value = vout.value * 100000000); - return transaction; - }); - } - - $getBlockHeightTip(): Promise { - return this.bitcoindClient.getChainTips() - .then((result) => result[0].height); - } - - $getTxIdsForBlock(hash: string): Promise { - return this.bitcoindClient.getBlock(hash, 1) - .then((rpcBlock: RpcBlock) => { - return rpcBlock.tx; - }); - } - - $getBlockHash(height: number): Promise { - return this.bitcoindClient.getBlockHash(height); - } - - $getBlock(hash: string): Promise { - return this.bitcoindClient.getBlock(hash) - .then((rpcBlock: RpcBlock) => { - return { - id: rpcBlock.hash, - height: rpcBlock.height, - version: rpcBlock.version, - timestamp: rpcBlock.time, - bits: rpcBlock.bits, - nonce: rpcBlock.nonce, - difficulty: rpcBlock.difficulty, - merkle_root: rpcBlock.merkleroot, - tx_count: rpcBlock.nTx, - size: rpcBlock.size, - weight: rpcBlock.weight, - previousblockhash: rpcBlock.previousblockhash, - }; - }); - } - - $getRawTransactionBitcond(txId: string): Promise { - throw new Error('Method not implemented.'); - } - - $getAddress(address: string): Promise
{ - throw new Error('Method not implemented.'); - } - - $validateAddress(address: string): Promise { - return this.bitcoindClient.validateAddress(address); - } - - $getScriptHashBalance(scriptHash: string): Promise { - throw new Error('Method not implemented.'); - } - - $getScriptHashHistory(scriptHash: string): Promise { - throw new Error('Method not implemented.'); - } -} - -export default BitcoindApi; diff --git a/backend/src/api/bitcoin/bitcoind-electrs-api.ts b/backend/src/api/bitcoin/bitcoind-electrs-api.ts deleted file mode 100644 index cbbdec8f2..000000000 --- a/backend/src/api/bitcoin/bitcoind-electrs-api.ts +++ /dev/null @@ -1,141 +0,0 @@ -import config from '../../config'; -import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory'; -import { Transaction, Block, MempoolInfo, RpcBlock, MempoolEntries, MempoolEntry, Address, - AddressInformation, ScriptHashBalance, ScriptHashHistory } from '../../interfaces'; -import * as bitcoin from '@mempool/bitcoin'; -import * as ElectrumClient from '@codewarriorr/electrum-client-js'; -import logger from '../../logger'; -import transactionUtils from '../transaction-utils'; -import * as sha256 from 'crypto-js/sha256'; -import * as hexEnc from 'crypto-js/enc-hex'; -class BitcoindElectrsApi implements AbstractBitcoinApi { - bitcoindClient: any; - electrumClient: any; - - constructor() { - this.bitcoindClient = new bitcoin.Client({ - host: config.BITCOIND.HOST, - port: config.BITCOIND.PORT, - user: config.BITCOIND.USERNAME, - pass: config.BITCOIND.PASSWORD, - timeout: 60000, - }); - - this.electrumClient = new ElectrumClient( - config.ELECTRS.HOST, - config.ELECTRS.PORT, - 'ssl' - ); - - this.electrumClient.connect( - 'electrum-client-js', - '1.4' - ); - } - - $getMempoolInfo(): Promise { - return this.bitcoindClient.getMempoolInfo(); - } - - $getRawMempool(): Promise { - return this.bitcoindClient.getRawMemPool(); - } - - $getRawMempoolVerbose(): Promise { - return this.bitcoindClient.getRawMemPool(true); - } - - $getMempoolEntry(txid: string): Promise { - return this.bitcoindClient.getMempoolEntry(txid); - } - - async $getRawTransaction(txId: string): Promise { - try { - const transaction: Transaction = await this.electrumClient.blockchain_transaction_get(txId, true); - if (!transaction) { - throw new Error(txId + ' not found!'); - } - transactionUtils.bitcoindToElectrsTransaction(transaction); - return transaction; - } catch (e) { - logger.debug('getRawTransaction error: ' + (e.message || e)); - throw new Error(e); - } - } - - $getRawTransactionBitcond(txId: string): Promise { - return this.bitcoindClient.getRawTransaction(txId, true) - .then((transaction: Transaction) => { - transactionUtils.bitcoindToElectrsTransaction(transaction); - return transaction; - }); - } - - $getBlockHeightTip(): Promise { - return this.bitcoindClient.getChainTips() - .then((result) => result[0].height); - } - - $getTxIdsForBlock(hash: string): Promise { - return this.bitcoindClient.getBlock(hash, 1) - .then((rpcBlock: RpcBlock) => { - return rpcBlock.tx; - }); - } - - $getBlockHash(height: number): Promise { - return this.bitcoindClient.getBlockHash(height); - } - - $getBlock(hash: string): Promise { - return this.bitcoindClient.getBlock(hash) - .then((rpcBlock: RpcBlock) => { - return { - id: rpcBlock.hash, - height: rpcBlock.height, - version: rpcBlock.version, - timestamp: rpcBlock.time, - bits: rpcBlock.bits, - nonce: rpcBlock.nonce, - difficulty: rpcBlock.difficulty, - merkle_root: rpcBlock.merkleroot, - tx_count: rpcBlock.nTx, - size: rpcBlock.size, - weight: rpcBlock.weight, - previousblockhash: rpcBlock.previousblockhash, - }; - }); - } - - async $getAddress(address: string): Promise
{ - try { - const addressInfo: Address = await this.electrumClient.blockchain_scripthash_getBalance(address); - if (!address) { - throw new Error('not found'); - } - return addressInfo; - } catch (e) { - logger.debug('getRawTransaction error: ' + (e.message || e)); - throw new Error(e); - } - } - - $validateAddress(address: string): Promise { - return this.bitcoindClient.validateAddress(address); - } - - $getScriptHashBalance(scriptHash: string): Promise { - return this.electrumClient.blockchain_scripthash_getBalance(this.encodeScriptHash(scriptHash)); - } - - $getScriptHashHistory(scriptHash: string): Promise { - return this.electrumClient.blockchain_scripthash_getHistory(this.encodeScriptHash(scriptHash)); - } - - private encodeScriptHash(scriptPubKey: string): string { - const addrScripthash = hexEnc.stringify(sha256(hexEnc.parse(scriptPubKey))); - return addrScripthash.match(/.{2}/g).reverse().join(''); - } -} - -export default BitcoindElectrsApi; diff --git a/backend/src/api/bitcoin/electrs-api.ts b/backend/src/api/bitcoin/electrs-api.ts deleted file mode 100644 index 521ff7ab5..000000000 --- a/backend/src/api/bitcoin/electrs-api.ts +++ /dev/null @@ -1,82 +0,0 @@ -import config from '../../config'; -import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory'; -import { Transaction, Block, MempoolInfo, MempoolEntry, MempoolEntries, Address, - AddressInformation, ScriptHashBalance, ScriptHashHistory } from '../../interfaces'; -import axios from 'axios'; - -class ElectrsApi implements AbstractBitcoinApi { - - constructor() { - } - - $getMempoolInfo(): Promise { - return axios.get(config.ELECTRS.REST_API_URL + '/mempool', { timeout: 10000 }) - .then((response) => { - return { - size: response.data.count, - bytes: response.data.vsize, - }; - }); - } - - $getRawMempool(): Promise { - return axios.get(config.ELECTRS.REST_API_URL + '/mempool/txids') - .then((response) => response.data); - } - - $getRawTransaction(txId: string): Promise { - return axios.get(config.ELECTRS.REST_API_URL + '/tx/' + txId) - .then((response) => response.data); - } - - $getBlockHeightTip(): Promise { - return axios.get(config.ELECTRS.REST_API_URL + '/blocks/tip/height') - .then((response) => response.data); - } - - $getTxIdsForBlock(hash: string): Promise { - return axios.get(config.ELECTRS.REST_API_URL + '/block/' + hash + '/txids') - .then((response) => response.data); - } - - $getBlockHash(height: number): Promise { - return axios.get(config.ELECTRS.REST_API_URL + '/block-height/' + height) - .then((response) => response.data); - } - - $getBlock(hash: string): Promise { - return axios.get(config.ELECTRS.REST_API_URL + '/block/' + hash) - .then((response) => response.data); - } - - $getRawMempoolVerbose(): Promise { - throw new Error('Method not implemented.'); - } - - $getMempoolEntry(): Promise { - throw new Error('Method not implemented.'); - } - - $getRawTransactionBitcond(txId: string): Promise { - throw new Error('Method not implemented.'); - } - - $getAddress(address: string): Promise
{ - throw new Error('Method not implemented.'); - } - - $getScriptHashBalance(scriptHash: string): Promise { - throw new Error('Method not implemented.'); - } - - $getScriptHashHistory(scriptHash: string): Promise { - throw new Error('Method not implemented.'); - } - - $validateAddress(address: string): Promise { - throw new Error('Method not implemented.'); - } - -} - -export default ElectrsApi; diff --git a/backend/src/api/bitcoin/electrum-api.interface.ts b/backend/src/api/bitcoin/electrum-api.interface.ts new file mode 100644 index 000000000..633de3cbc --- /dev/null +++ b/backend/src/api/bitcoin/electrum-api.interface.ts @@ -0,0 +1,12 @@ +export namespace IElectrumApi { + export interface ScriptHashBalance { + confirmed: number; + unconfirmed: number; + } + + export interface ScriptHashHistory { + height: number; + tx_hash: string; + fee?: number; + } +} diff --git a/backend/src/api/bitcoin/electrum-api.ts b/backend/src/api/bitcoin/electrum-api.ts new file mode 100644 index 000000000..697db749e --- /dev/null +++ b/backend/src/api/bitcoin/electrum-api.ts @@ -0,0 +1,119 @@ +import config from '../../config'; +import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory'; +import * as ElectrumClient from '@codewarriorr/electrum-client-js'; +import { IBitcoinApi } from './bitcoin-api.interface'; +import { IEsploraApi } from './esplora-api.interface'; +import { IElectrumApi } from './electrum-api.interface'; +import * as sha256 from 'crypto-js/sha256'; +import * as hexEnc from 'crypto-js/enc-hex'; +import BitcoinApi from './bitcoin-api'; +import bitcoinBaseApi from './bitcoin-base.api'; +class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi { + private electrumClient: any; + + constructor() { + super(); + + this.electrumClient = new ElectrumClient( + config.ELECTRS.HOST, + config.ELECTRS.PORT, + 'ssl' + ); + + this.electrumClient.connect( + 'electrum-client-js', + '1.4' + ); + } + + async $getRawTransaction(txId: string, skipConversion = false, addPrevout = false): Promise { + const transaction: IBitcoinApi.Transaction = await this.electrumClient.blockchain_transaction_get(txId, true); + if (!transaction) { + throw new Error('Unable to get transaction: ' + txId); + } + if (skipConversion) { + // @ts-ignore + return transaction; + } + return this.$convertTransaction(transaction, addPrevout); + } + + async $getAddress(address: string): Promise { + const addressInfo = await bitcoinBaseApi.$validateAddress(address); + if (!addressInfo || !addressInfo.isvalid) { + return ({ + 'address': address, + 'chain_stats': { + 'funded_txo_count': 0, + 'funded_txo_sum': 0, + 'spent_txo_count': 0, + 'spent_txo_sum': 0, + 'tx_count': 0 + }, + 'mempool_stats': { + 'funded_txo_count': 0, + 'funded_txo_sum': 0, + 'spent_txo_count': 0, + 'spent_txo_sum': 0, + 'tx_count': 0 + } + }); + } + + const balance = await this.$getScriptHashBalance(addressInfo.scriptPubKey); + const history = await this.$getScriptHashHistory(addressInfo.scriptPubKey); + + const unconfirmed = history.filter((h) => h.fee).length; + + return { + 'address': addressInfo.address, + 'chain_stats': { + 'funded_txo_count': 0, + 'funded_txo_sum': balance.confirmed ? balance.confirmed : 0, + 'spent_txo_count': 0, + 'spent_txo_sum': balance.confirmed < 0 ? balance.confirmed : 0, + 'tx_count': history.length - unconfirmed, + }, + 'mempool_stats': { + 'funded_txo_count': 0, + 'funded_txo_sum': balance.unconfirmed > 0 ? balance.unconfirmed : 0, + 'spent_txo_count': 0, + 'spent_txo_sum': balance.unconfirmed < 0 ? -balance.unconfirmed : 0, + 'tx_count': unconfirmed, + } + }; + + } + + async $getAddressTransactions(address: string, lastSeenTxId: string): Promise { + const addressInfo = await bitcoinBaseApi.$validateAddress(address); + if (!addressInfo || !addressInfo.isvalid) { + return []; + } + const history = await this.$getScriptHashHistory(addressInfo.scriptPubKey); + const transactions: IEsploraApi.Transaction[] = []; + for (const h of history) { + const tx = await this.$getRawTransaction(h.tx_hash); + if (tx) { + transactions.push(tx); + } + } + return transactions; + } + + private $getScriptHashBalance(scriptHash: string): Promise { + return this.electrumClient.blockchain_scripthash_getBalance(this.encodeScriptHash(scriptHash)); + } + + private $getScriptHashHistory(scriptHash: string): Promise { + return this.electrumClient.blockchain_scripthash_getHistory(this.encodeScriptHash(scriptHash)); + } + + private encodeScriptHash(scriptPubKey: string): string { + const addrScripthash = hexEnc.stringify(sha256(hexEnc.parse(scriptPubKey))); + return addrScripthash.match(/.{2}/g).reverse().join(''); + } + +} + +export default BitcoindElectrsApi; diff --git a/backend/src/api/bitcoin/esplora-api.interface.ts b/backend/src/api/bitcoin/esplora-api.interface.ts new file mode 100644 index 000000000..cfabe40bc --- /dev/null +++ b/backend/src/api/bitcoin/esplora-api.interface.ts @@ -0,0 +1,168 @@ +export namespace IEsploraApi { + export interface Transaction { + txid: string; + version: number; + locktime: number; + size: number; + weight: number; + fee: number; + vin: Vin[]; + vout: Vout[]; + status: Status; + } + + export interface Recent { + txid: string; + fee: number; + vsize: number; + value: number; + } + + export interface Vin { + txid: string; + vout: number; + is_coinbase: boolean; + scriptsig: string; + scriptsig_asm: string; + inner_redeemscript_asm?: string; + inner_witnessscript_asm?: string; + sequence: any; + witness?: string[]; + prevout: Vout | null; + // Elements + is_pegin?: boolean; + issuance?: Issuance; + } + + interface Issuance { + asset_id: string; + is_reissuance: string; + asset_blinding_nonce: string; + asset_entropy: string; + contract_hash: string; + assetamount?: number; + assetamountcommitment?: string; + tokenamount?: number; + tokenamountcommitment?: string; + } + + export interface Vout { + scriptpubkey: string; + scriptpubkey_asm: string; + scriptpubkey_type: string; + scriptpubkey_address: string; + value: number; + // Elements + valuecommitment?: number; + asset?: string; + pegout?: Pegout; + } + + interface Pegout { + genesis_hash: string; + scriptpubkey: string; + scriptpubkey_asm: string; + scriptpubkey_address: string; + } + + export interface Status { + confirmed: boolean; + block_height?: number; + block_hash?: string; + block_time?: number; + } + + export interface Block { + id: string; + height: number; + version: number; + timestamp: number; + bits: number; + nonce: number; + difficulty: number; + merkle_root: string; + tx_count: number; + size: number; + weight: number; + previousblockhash: string; + } + + export interface Address { + address: string; + chain_stats: ChainStats; + mempool_stats: MempoolStats; + } + + export interface ChainStats { + funded_txo_count: number; + funded_txo_sum: number; + spent_txo_count: number; + spent_txo_sum: number; + tx_count: number; + } + + export interface MempoolStats { + funded_txo_count: number; + funded_txo_sum: number; + spent_txo_count: number; + spent_txo_sum: number; + tx_count: number; + } + + export interface Outspend { + spent: boolean; + txid: string; + vin: number; + status: Status; + } + + export interface Asset { + asset_id: string; + issuance_txin: IssuanceTxin; + issuance_prevout: IssuancePrevout; + reissuance_token: string; + contract_hash: string; + status: Status; + chain_stats: AssetStats; + mempool_stats: AssetStats; + } + + export interface AssetExtended extends Asset { + name: string; + ticker: string; + precision: number; + entity: Entity; + version: number; + issuer_pubkey: string; + } + + export interface Entity { + domain: string; + } + + interface IssuanceTxin { + txid: string; + vin: number; + } + + interface IssuancePrevout { + txid: string; + vout: number; + } + + interface AssetStats { + tx_count: number; + issuance_count: number; + issued_amount: number; + burned_amount: number; + has_blinded_issuances: boolean; + reissuance_tokens: number; + burned_reissuance_tokens: number; + peg_in_count: number; + peg_in_amount: number; + peg_out_count: number; + peg_out_amount: number; + burn_count: number; + } + +} diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts new file mode 100644 index 000000000..a7bedbbec --- /dev/null +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -0,0 +1,55 @@ +import config from '../../config'; +import axios from 'axios'; +import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory'; +import { IEsploraApi } from './esplora-api.interface'; + +class ElectrsApi implements AbstractBitcoinApi { + + constructor() { } + + $getRawMempool(): Promise { + return axios.get(config.ELECTRS.REST_API_URL + '/mempool/txids') + .then((response) => response.data); + } + + $getRawTransaction(txId: string): Promise { + return axios.get(config.ELECTRS.REST_API_URL + '/tx/' + txId) + .then((response) => response.data); + } + + $getBlockHeightTip(): Promise { + return axios.get(config.ELECTRS.REST_API_URL + '/blocks/tip/height') + .then((response) => response.data); + } + + $getTxIdsForBlock(hash: string): Promise { + return axios.get(config.ELECTRS.REST_API_URL + '/block/' + hash + '/txids') + .then((response) => response.data); + } + + $getBlockHash(height: number): Promise { + return axios.get(config.ELECTRS.REST_API_URL + '/block-height/' + height) + .then((response) => response.data); + } + + $getBlock(hash: string): Promise { + return axios.get(config.ELECTRS.REST_API_URL + '/block/' + hash) + .then((response) => response.data); + } + + $getAddress(address: string): Promise { + throw new Error('Method getAddress not implemented.'); + } + + $getAddressTransactions(address: string, txId?: string): Promise { + throw new Error('Method getAddressTransactions not implemented.'); + } + + $getRawTransactionBitcoind(txId: string): Promise { + return axios.get(config.ELECTRS.REST_API_URL + '/tx/' + txId) + .then((response) => response.data); + } + +} + +export default ElectrsApi; diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 76213f86b..80c77b7c5 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -2,29 +2,29 @@ import config from '../config'; import bitcoinApi from './bitcoin/bitcoin-api-factory'; import logger from '../logger'; import memPool from './mempool'; -import { Block, TransactionExtended, TransactionMinerInfo } from '../interfaces'; +import { BlockExtended, TransactionExtended, TransactionMinerInfo } from '../mempool.interfaces'; import { Common } from './common'; import diskCache from './disk-cache'; import transactionUtils from './transaction-utils'; class Blocks { private static KEEP_BLOCK_AMOUNT = 8; - private blocks: Block[] = []; + private blocks: BlockExtended[] = []; private currentBlockHeight = 0; private lastDifficultyAdjustmentTime = 0; - private newBlockCallbacks: ((block: Block, txIds: string[], transactions: TransactionExtended[]) => void)[] = []; + private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = []; constructor() { } - public getBlocks(): Block[] { + public getBlocks(): BlockExtended[] { return this.blocks; } - public setBlocks(blocks: Block[]) { + public setBlocks(blocks: BlockExtended[]) { this.blocks = blocks; } - public setNewBlockCallback(fn: (block: Block, txIds: string[], transactions: TransactionExtended[]) => void) { + public setNewBlockCallback(fn: (block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void) { this.newBlockCallbacks.push(fn); } @@ -68,9 +68,8 @@ class Blocks { for (let i = 0; i < txIds.length; i++) { // When using bitcoind, just fetch the coinbase tx for now - if ((config.MEMPOOL.BACKEND === 'bitcoind' || - config.MEMPOOL.BACKEND === 'bitcoind-electrs') && i === 0) { - const tx = await transactionUtils.getTransactionExtended(txIds[i], true); + if (config.MEMPOOL.BACKEND !== 'none' && i === 0) { + const tx = await transactionUtils.$getTransactionExtended(txIds[i]); if (tx) { transactions.push(tx); } @@ -78,9 +77,9 @@ class Blocks { if (mempool[txIds[i]]) { transactions.push(mempool[txIds[i]]); found++; - } else if (config.MEMPOOL.BACKEND === 'electrs') { + } else if (config.MEMPOOL.BACKEND === 'esplora') { logger.debug(`Fetching block tx ${i} of ${txIds.length}`); - const tx = await transactionUtils.getTransactionExtended(txIds[i]); + const tx = await transactionUtils.$getTransactionExtended(txIds[i]); if (tx) { transactions.push(tx); } @@ -89,23 +88,24 @@ class Blocks { logger.debug(`${found} of ${txIds.length} found in mempool. ${txIds.length - found} not found.`); - block.reward = transactions[0].vout.reduce((acc, curr) => acc + curr.value, 0); - block.coinbaseTx = this.stripCoinbaseTransaction(transactions[0]); + const blockExtended: BlockExtended = Object.assign({}, block); + blockExtended.reward = transactions[0].vout.reduce((acc, curr) => acc + curr.value, 0); + blockExtended.coinbaseTx = this.stripCoinbaseTransaction(transactions[0]); transactions.sort((a, b) => b.feePerVsize - a.feePerVsize); - block.medianFee = transactions.length > 1 ? Common.median(transactions.map((tx) => tx.feePerVsize)) : 0; - block.feeRange = transactions.length > 1 ? Common.getFeesInRange(transactions.slice(0, transactions.length - 1), 8) : [0, 0]; + blockExtended.medianFee = transactions.length > 1 ? Common.median(transactions.map((tx) => tx.feePerVsize)) : 0; + blockExtended.feeRange = transactions.length > 1 ? Common.getFeesInRange(transactions.slice(0, transactions.length - 1), 8) : [0, 0]; if (block.height % 2016 === 0) { this.lastDifficultyAdjustmentTime = block.timestamp; } - this.blocks.push(block); + this.blocks.push(blockExtended); if (this.blocks.length > Blocks.KEEP_BLOCK_AMOUNT) { this.blocks = this.blocks.slice(-Blocks.KEEP_BLOCK_AMOUNT); } if (this.newBlockCallbacks.length) { - this.newBlockCallbacks.forEach((cb) => cb(block, txIds, transactions)); + this.newBlockCallbacks.forEach((cb) => cb(blockExtended, txIds, transactions)); } diskCache.$saveCacheToDisk(); } diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index fdd50a11a..0bb90c6df 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -1,4 +1,4 @@ -import { TransactionExtended, TransactionStripped } from '../interfaces'; +import { TransactionExtended, TransactionStripped } from '../mempool.interfaces'; export class Common { static median(numbers: number[]) { diff --git a/backend/src/api/fee-api.ts b/backend/src/api/fee-api.ts index 551501240..8293520bf 100644 --- a/backend/src/api/fee-api.ts +++ b/backend/src/api/fee-api.ts @@ -1,5 +1,5 @@ import config from '../config'; -import { MempoolBlock } from '../interfaces'; +import { MempoolBlock } from '../mempool.interfaces'; import projectedBlocks from './mempool-blocks'; class FeeApi { diff --git a/backend/src/api/mempool-blocks.ts b/backend/src/api/mempool-blocks.ts index 681c4f26e..d3dbfcb16 100644 --- a/backend/src/api/mempool-blocks.ts +++ b/backend/src/api/mempool-blocks.ts @@ -1,4 +1,4 @@ -import { MempoolBlock, TransactionExtended, MempoolBlockWithTransactions } from '../interfaces'; +import { MempoolBlock, TransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces'; import { Common } from './common'; class MempoolBlocks { diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index cb13fb874..7f066f1a6 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -1,15 +1,18 @@ import config from '../config'; import bitcoinApi from './bitcoin/bitcoin-api-factory'; -import { MempoolInfo, TransactionExtended, Transaction, VbytesPerSecond, MempoolEntry, MempoolEntries } from '../interfaces'; +import { TransactionExtended, VbytesPerSecond } from '../mempool.interfaces'; import logger from '../logger'; import { Common } from './common'; import transactionUtils from './transaction-utils'; +import { IBitcoinApi } from './bitcoin/bitcoin-api.interface'; +import bitcoinBaseApi from './bitcoin/bitcoin-base.api'; class Mempool { private inSync: boolean = false; private mempoolCache: { [txId: string]: TransactionExtended } = {}; - private mempoolInfo: MempoolInfo = { size: 0, bytes: 0 }; - private mempoolChangedCallback: ((newMempool: { [txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[], + private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, + maxmempool: 0, mempoolminfee: 0, minrelaytxfee: 0 }; + private mempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]) => void) | undefined; private txPerSecondArray: number[] = []; @@ -49,10 +52,10 @@ class Mempool { } public async $updateMemPoolInfo() { - this.mempoolInfo = await bitcoinApi.$getMempoolInfo(); + this.mempoolInfo = await bitcoinBaseApi.$getMempoolInfo(); } - public getMempoolInfo(): MempoolInfo | undefined { + public getMempoolInfo(): IBitcoinApi.MempoolInfo | undefined { return this.mempoolInfo; } @@ -67,8 +70,9 @@ class Mempool { public getFirstSeenForTransactions(txIds: string[]): number[] { const txTimes: number[] = []; txIds.forEach((txId: string) => { - if (this.mempoolCache[txId]) { - txTimes.push(this.mempoolCache[txId].firstSeen); + const tx = this.mempoolCache[txId]; + if (tx && tx.firstSeen) { + txTimes.push(tx.firstSeen); } else { txTimes.push(0); } @@ -88,7 +92,7 @@ class Mempool { for (const txid of transactions) { if (!this.mempoolCache[txid]) { - const transaction = await transactionUtils.getTransactionExtended(txid, false, true); + const transaction = await transactionUtils.$getTransactionExtended(txid, true); if (transaction) { this.mempoolCache[txid] = transaction; txCount++; @@ -118,6 +122,7 @@ class Mempool { // Prevent mempool from clear on bitcoind restart by delaying the deletion if (this.mempoolProtection === 0 + && config.MEMPOOL.BACKEND === 'esplora' && currentMempoolSize > 20000 && transactions.length / currentMempoolSize <= 0.80 ) { diff --git a/backend/src/api/statistics.ts b/backend/src/api/statistics.ts index ebdd85a9b..2681933ee 100644 --- a/backend/src/api/statistics.ts +++ b/backend/src/api/statistics.ts @@ -2,7 +2,7 @@ import memPool from './mempool'; import { DB } from '../database'; import logger from '../logger'; -import { Statistic, TransactionExtended, OptimizedStatistic } from '../interfaces'; +import { Statistic, TransactionExtended, OptimizedStatistic } from '../mempool.interfaces'; class Statistics { protected intervalTimer: NodeJS.Timer | undefined; diff --git a/backend/src/api/transaction-utils.ts b/backend/src/api/transaction-utils.ts index fc150d40b..9d0ff990c 100644 --- a/backend/src/api/transaction-utils.ts +++ b/backend/src/api/transaction-utils.ts @@ -1,16 +1,15 @@ import bitcoinApi from './bitcoin/bitcoin-api-factory'; -import { MempoolEntries, MempoolEntry, Transaction, TransactionExtended, TransactionMinerInfo } from '../interfaces'; -import config from '../config'; import logger from '../logger'; -import mempool from './mempool'; -import blocks from './blocks'; +import { TransactionExtended, TransactionMinerInfo } from '../mempool.interfaces'; +import { IEsploraApi } from './bitcoin/esplora-api.interface'; class TransactionUtils { - private mempoolEntriesCache: MempoolEntries | null = null; - constructor() { } public async $addPrevoutsToTransaction(transaction: TransactionExtended): Promise { + if (transaction.vin[0].is_coinbase) { + return transaction; + } for (const vin of transaction.vin) { const innerTx = await bitcoinApi.$getRawTransaction(vin.txid); vin.prevout = innerTx.vout[vin.vout]; @@ -18,26 +17,10 @@ class TransactionUtils { return transaction; } - public async $calculateFeeFromInputs(transaction: Transaction): Promise { - if (transaction.vin[0]['coinbase']) { - transaction.fee = 0; - // @ts-ignore - return transaction; - } - let totalIn = 0; - for (const vin of transaction.vin) { - const innerTx = await bitcoinApi.$getRawTransaction(vin.txid); - totalIn += innerTx.vout[vin.vout].value; - } - const totalOut = transaction.vout.reduce((prev, output) => prev + output.value, 0); - transaction.fee = parseFloat((totalIn - totalOut).toFixed(8)); - return this.extendTransaction(transaction); - } - - public extendTransaction(transaction: Transaction): TransactionExtended { + public extendTransaction(transaction: IEsploraApi.Transaction): TransactionExtended { transaction['vsize'] = Math.round(transaction.weight / 4); transaction['feePerVsize'] = Math.max(1, (transaction.fee || 0) / (transaction.weight / 4)); - if (!transaction.in_active_chain) { + if (!transaction.status.confirmed) { transaction['firstSeen'] = Math.round((new Date().getTime() / 1000)); } // @ts-ignore @@ -58,20 +41,13 @@ class TransactionUtils { }; } - public async getTransactionExtended(txId: string, isCoinbase = false, inMempool = false): Promise { + public async $getTransactionExtended(txId: string, inMempool = false, addPrevouts = false): Promise { try { - let transaction: Transaction; + let transaction: IEsploraApi.Transaction; if (inMempool) { - transaction = await bitcoinApi.$getRawTransactionBitcond(txId); + transaction = await bitcoinApi.$getRawTransactionBitcoind(txId, false, addPrevouts); } else { - transaction = await bitcoinApi.$getRawTransaction(txId); - } - if (config.MEMPOOL.BACKEND !== 'electrs' && !isCoinbase) { - if (inMempool) { - transaction = await this.$appendFeeData(transaction); - } else { - transaction = await this.$calculateFeeFromInputs(transaction); - } + transaction = await bitcoinApi.$getRawTransaction(txId, false, addPrevouts); } return this.extendTransaction(transaction); } catch (e) { @@ -81,64 +57,6 @@ class TransactionUtils { } } - public bitcoindToElectrsTransaction(transaction: any): void { - try { - transaction.vout = transaction.vout.map((vout) => { - return { - value: vout.value * 100000000, - scriptpubkey: vout.scriptPubKey.hex, - scriptpubkey_address: vout.scriptPubKey && vout.scriptPubKey.addresses ? vout.scriptPubKey.addresses[0] : null, - scriptpubkey_asm: vout.scriptPubKey.asm, - scriptpubkey_type: this.translateScriptPubKeyType(vout.scriptPubKey.type), - }; - }); - if (transaction.confirmations) { - transaction['status'] = { - confirmed: true, - block_height: blocks.getCurrentBlockHeight() - transaction.confirmations, - block_hash: transaction.blockhash, - block_time: transaction.blocktime, - }; - } else { - transaction['status'] = { confirmed: false }; - } - } catch (e) { - console.log('augment failed: ' + (e.message || e)); - } - } - - 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', - 'nulldata': 'nulldata' - }; - - if (map[outputType]) { - return map[outputType]; - } else { - return ''; - } - } - - private async $appendFeeData(transaction: Transaction): Promise { - let mempoolEntry: MempoolEntry; - if (!mempool.isInSync() && !this.mempoolEntriesCache) { - this.mempoolEntriesCache = await bitcoinApi.$getRawMempoolVerbose(); - } - if (this.mempoolEntriesCache && this.mempoolEntriesCache[transaction.txid]) { - mempoolEntry = this.mempoolEntriesCache[transaction.txid]; - } else { - mempoolEntry = await bitcoinApi.$getMempoolEntry(transaction.txid); - } - transaction.fee = mempoolEntry.fees.base * 100000000; - return transaction; - } } export default new TransactionUtils(); diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index 76fb91583..e696cad51 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -1,6 +1,6 @@ import logger from '../logger'; import * as WebSocket from 'ws'; -import { Block, TransactionExtended, WebsocketResponse, MempoolBlock, OptimizedStatistic } from '../interfaces'; +import { BlockExtended, TransactionExtended, WebsocketResponse, MempoolBlock, OptimizedStatistic } from '../mempool.interfaces'; import blocks from './blocks'; import memPool from './mempool'; import backendInfo from './backend-info'; @@ -117,7 +117,7 @@ class WebsocketHandler { }); } - getInitData(_blocks?: Block[]) { + getInitData(_blocks?: BlockExtended[]) { if (!_blocks) { _blocks = blocks.getBlocks(); } @@ -256,7 +256,7 @@ class WebsocketHandler { }); } - handleNewBlock(block: Block, txIds: string[], transactions: TransactionExtended[]) { + handleNewBlock(block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) { if (!this.wss) { throw new Error('WebSocket.Server is not set'); } diff --git a/backend/src/config.ts b/backend/src/config.ts index 6e2e3dc9d..b8af02efc 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -3,7 +3,7 @@ const configFile = require('../mempool-config.json'); interface IConfig { MEMPOOL: { NETWORK: 'mainnet' | 'testnet' | 'liquid'; - BACKEND: 'electrs' | 'bitcoind' | 'bitcoind-electrs'; + BACKEND: 'esplora' | 'electrum' | 'none'; HTTP_PORT: number; SPAWN_CLUSTER_PROCS: number; API_URL_PREFIX: string; @@ -20,7 +20,7 @@ interface IConfig { PORT: number; USERNAME: string; PASSWORD: string; - }, + }; DATABASE: { ENABLED: boolean; HOST: string, @@ -53,7 +53,7 @@ interface IConfig { const defaults: IConfig = { 'MEMPOOL': { 'NETWORK': 'mainnet', - 'BACKEND': 'electrs', + 'BACKEND': 'none', 'HTTP_PORT': 8999, 'SPAWN_CLUSTER_PROCS': 0, 'API_URL_PREFIX': '/api/v1/', @@ -66,10 +66,10 @@ const defaults: IConfig = { 'PORT': 3306 }, 'BITCOIND': { - 'HOST': "127.0.0.1", + 'HOST': '127.0.0.1', 'PORT': 8332, - 'USERNAME': "mempoo", - 'PASSWORD': "mempool" + 'USERNAME': 'mempool', + 'PASSWORD': 'mempool' }, 'DATABASE': { 'ENABLED': true, diff --git a/backend/src/index.ts b/backend/src/index.ts index 11878460f..0ffb2d1e4 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -209,7 +209,7 @@ class Server { }); } - if (config.MEMPOOL.BACKEND === 'bitcoind' || config.MEMPOOL.BACKEND === 'bitcoind-electrs') { + if (config.MEMPOOL.BACKEND !== 'esplora') { this.app .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId', routes.getTransaction) .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', routes.getTransactionOutspends) @@ -219,7 +219,7 @@ class Server { .get(config.MEMPOOL.API_URL_PREFIX + 'block-height/:height', routes.getBlockHeight) .get(config.MEMPOOL.API_URL_PREFIX + 'address/:address', routes.getAddress) .get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs', routes.getAddressTransactions) - .get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs/chain/:txId', routes.getAdressTxChain) + .get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs/chain/:txId', routes.getAddressTransactions) .get(config.MEMPOOL.API_URL_PREFIX + 'address-prefix/:prefix', routes.getAddressPrefix) ; } diff --git a/backend/src/interfaces.ts b/backend/src/interfaces.ts deleted file mode 100644 index e7a77f670..000000000 --- a/backend/src/interfaces.ts +++ /dev/null @@ -1,320 +0,0 @@ -export interface MempoolInfo { - size: number; - bytes: number; - usage?: number; - maxmempool?: number; - mempoolminfee?: number; - minrelaytxfee?: number; -} - -export interface MempoolBlock { - blockSize: number; - blockVSize: number; - nTx: number; - medianFee: number; - totalFees: number; - feeRange: number[]; -} - -export interface MempoolBlockWithTransactions extends MempoolBlock { - transactionIds: string[]; -} - -export interface Transaction { - txid: string; - version: number; - locktime: number; - fee: number; - size: number; - weight: number; - vin: Vin[]; - vout: Vout[]; - status: Status; - - // bitcoind (temp?) - in_active_chain?: boolean; -} - -export interface TransactionMinerInfo { - vin: VinStrippedToScriptsig[]; - vout: VoutStrippedToScriptPubkey[]; -} - -interface VinStrippedToScriptsig { - scriptsig: string; -} - -interface VoutStrippedToScriptPubkey { - scriptpubkey_address: string | undefined; - value: number; -} - -export interface TransactionExtended extends Transaction { - vsize: number; - feePerVsize: number; - firstSeen: number; -} - -export interface TransactionStripped { - txid: string; - fee: number; - weight: number; - value: number; -} - -export interface Vin { - txid: string; - vout: number; - is_coinbase: boolean; - scriptsig: string; - scriptsig_asm: string; - inner_redeemscript_asm?: string; - inner_witnessscript_asm?: string; - sequence: any; - witness?: string[]; - prevout: Vout; - // Elements - is_pegin?: boolean; - issuance?: Issuance; -} - -interface Issuance { - asset_id: string; - is_reissuance: string; - asset_blinding_nonce: string; - asset_entropy: string; - contract_hash: string; - assetamount?: number; - assetamountcommitment?: string; - tokenamount?: number; - tokenamountcommitment?: string; -} - -export interface Vout { - scriptpubkey: string; - scriptpubkey_asm: string; - scriptpubkey_type: string; - scriptpubkey_address: string; - value: number; - // Elements - valuecommitment?: number; - asset?: string; - pegout?: Pegout; -} - -interface Pegout { - genesis_hash: string; - scriptpubkey: string; - scriptpubkey_asm: string; - scriptpubkey_address: string; -} - -export interface Status { - confirmed: boolean; - block_height?: number; - block_hash?: string; - block_time?: number; -} - -export interface Block { - id: string; - height: number; - version: number; - timestamp: number; - bits: number; - nonce: number; - difficulty: number; - merkle_root: string; - tx_count: number; - size: number; - weight: number; - previousblockhash: string; - - // Custom properties - medianFee?: number; - feeRange?: number[]; - reward?: number; - coinbaseTx?: TransactionMinerInfo; - matchRate?: number; -} - -export interface RpcBlock { - hash: string; - confirmations: number; - size: number; - strippedsize: number; - weight: number; - height: number; - version: number; - versionHex: string; - merkleroot: string; - tx: Transaction[]; - time: number; - mediantime: number; - nonce: number; - bits: number; - difficulty: number; - chainwork: string; - nTx: number; - previousblockhash: string; - nextblockhash: string; -} - -export interface MempoolEntries { [txId: string]: MempoolEntry; } - -export interface MempoolEntry { - fees: Fees; - vsize: number; - weight: number; - fee: number; - modifiedfee: number; - time: number; - height: number; - descendantcount: number; - descendantsize: number; - descendantfees: number; - ancestorcount: number; - ancestorsize: number; - ancestorfees: number; - wtxid: string; - depends: any[]; - spentby: any[]; - 'bip125-replaceable': boolean; -} - -export interface Fees { - base: number; - modified: number; - ancestor: number; - descendant: number; -} - -export interface Address { - address: string; - chain_stats: ChainStats; - mempool_stats: MempoolStats; -} - -export interface ChainStats { - funded_txo_count: number; - funded_txo_sum: number; - spent_txo_count: number; - spent_txo_sum: number; - tx_count: number; -} - -export interface MempoolStats { - funded_txo_count: number; - funded_txo_sum: number; - spent_txo_count: number; - spent_txo_sum: number; - tx_count: number; -} - -export interface Statistic { - id?: number; - added: string; - unconfirmed_transactions: number; - tx_per_second: number; - vbytes_per_second: number; - total_fee: number; - mempool_byte_weight: number; - fee_data: string; - - vsize_1: number; - vsize_2: number; - vsize_3: number; - vsize_4: number; - vsize_5: number; - vsize_6: number; - vsize_8: number; - vsize_10: number; - vsize_12: number; - vsize_15: number; - vsize_20: number; - vsize_30: number; - vsize_40: number; - vsize_50: number; - vsize_60: number; - vsize_70: number; - vsize_80: number; - vsize_90: number; - vsize_100: number; - vsize_125: number; - vsize_150: number; - vsize_175: number; - vsize_200: number; - vsize_250: number; - vsize_300: number; - vsize_350: number; - vsize_400: number; - vsize_500: number; - vsize_600: number; - vsize_700: number; - vsize_800: number; - vsize_900: number; - vsize_1000: number; - vsize_1200: number; - vsize_1400: number; - vsize_1600: number; - vsize_1800: number; - vsize_2000: number; -} - -export interface OptimizedStatistic { - id: number; - added: string; - unconfirmed_transactions: number; - tx_per_second: number; - vbytes_per_second: number; - total_fee: number; - mempool_byte_weight: number; - vsizes: number[]; -} - -export interface Outspend { - spent: boolean; - txid: string; - vin: number; - status: Status; -} -export interface WebsocketResponse { - action: string; - data: string[]; - 'track-tx': string; - 'track-address': string; - 'watch-mempool': boolean; -} - -export interface VbytesPerSecond { - unixTime: number; - vSize: number; -} - -export interface RequiredSpec { [name: string]: RequiredParams; } - -interface RequiredParams { - required: boolean; - types: ('@string' | '@number' | '@boolean' | string)[]; -} - -export interface AddressInformation { - isvalid: boolean; - address: string; - scriptPubKey: string; - isscript: boolean; - iswitness: boolean; - witness_version?: boolean; - witness_program: string; -} - -export interface ScriptHashBalance { - confirmed: number; - unconfirmed: number; -} - -export interface ScriptHashHistory { - height: number; - tx_hash: string; - fee?: number; -} diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts new file mode 100644 index 000000000..1058df7f0 --- /dev/null +++ b/backend/src/mempool.interfaces.ts @@ -0,0 +1,138 @@ +import { IEsploraApi } from './api/bitcoin/esplora-api.interface'; + +export interface MempoolBlock { + blockSize: number; + blockVSize: number; + nTx: number; + medianFee: number; + totalFees: number; + feeRange: number[]; +} + +export interface MempoolBlockWithTransactions extends MempoolBlock { + transactionIds: string[]; +} + +interface VinStrippedToScriptsig { + scriptsig: string; +} + +interface VoutStrippedToScriptPubkey { + scriptpubkey_address: string | undefined; + value: number; +} + +export interface TransactionExtended extends IEsploraApi.Transaction { + vsize: number; + feePerVsize: number; + firstSeen?: number; +} + +export interface TransactionStripped { + txid: string; + fee: number; + weight: number; + value: number; +} +export interface BlockExtended extends IEsploraApi.Block { + medianFee?: number; + feeRange?: number[]; + reward?: number; + coinbaseTx?: TransactionMinerInfo; + matchRate?: number; +} + +export interface TransactionMinerInfo { + vin: VinStrippedToScriptsig[]; + vout: VoutStrippedToScriptPubkey[]; +} + +export interface MempoolStats { + funded_txo_count: number; + funded_txo_sum: number; + spent_txo_count: number; + spent_txo_sum: number; + tx_count: number; +} + +export interface Statistic { + id?: number; + added: string; + unconfirmed_transactions: number; + tx_per_second: number; + vbytes_per_second: number; + total_fee: number; + mempool_byte_weight: number; + fee_data: string; + + vsize_1: number; + vsize_2: number; + vsize_3: number; + vsize_4: number; + vsize_5: number; + vsize_6: number; + vsize_8: number; + vsize_10: number; + vsize_12: number; + vsize_15: number; + vsize_20: number; + vsize_30: number; + vsize_40: number; + vsize_50: number; + vsize_60: number; + vsize_70: number; + vsize_80: number; + vsize_90: number; + vsize_100: number; + vsize_125: number; + vsize_150: number; + vsize_175: number; + vsize_200: number; + vsize_250: number; + vsize_300: number; + vsize_350: number; + vsize_400: number; + vsize_500: number; + vsize_600: number; + vsize_700: number; + vsize_800: number; + vsize_900: number; + vsize_1000: number; + vsize_1200: number; + vsize_1400: number; + vsize_1600: number; + vsize_1800: number; + vsize_2000: number; +} + +export interface OptimizedStatistic { + id: number; + added: string; + unconfirmed_transactions: number; + tx_per_second: number; + vbytes_per_second: number; + total_fee: number; + mempool_byte_weight: number; + vsizes: number[]; +} + +export interface WebsocketResponse { + action: string; + data: string[]; + 'track-tx': string; + 'track-address': string; + 'watch-mempool': boolean; +} + +export interface VbytesPerSecond { + unixTime: number; + vSize: number; +} + +export interface RequiredSpec { [name: string]: RequiredParams; } + +interface RequiredParams { + required: boolean; + types: ('@string' | '@number' | '@boolean' | string)[]; +} + diff --git a/backend/src/routes.ts b/backend/src/routes.ts index 0a2c39465..a130e6025 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -1,5 +1,5 @@ import config from './config'; -import { json, Request, Response } from 'express'; +import { Request, Response } from 'express'; import statistics from './api/statistics'; import feeApi from './api/fee-api'; import backendInfo from './api/backend-info'; @@ -8,7 +8,7 @@ import mempool from './api/mempool'; import bisq from './api/bisq/bisq'; import websocketHandler from './api/websocket-handler'; import bisqMarket from './api/bisq/markets-api'; -import { OptimizedStatistic, RequiredSpec, Transaction, TransactionExtended } from './interfaces'; +import { OptimizedStatistic, RequiredSpec, TransactionExtended } from './mempool.interfaces'; import { MarketsApiError } from './api/bisq/interfaces'; import donations from './api/donations'; import logger from './logger'; @@ -533,10 +533,10 @@ class Routes { if (txInMempool) { transaction = txInMempool; } else { - transaction = await transactionUtils.getTransactionExtended(req.params.txId); + transaction = await transactionUtils.$getTransactionExtended(req.params.txId, false, true); } + if (transaction) { - transaction = await transactionUtils.$addPrevoutsToTransaction(transaction); res.json(transaction); } else { res.status(500).send('Error fetching transaction.'); @@ -560,7 +560,20 @@ class Routes { } public async getBlockTransactions(req: Request, res: Response) { - res.status(404).send('Not implemented'); + try { + const txIds = await bitcoinApi.$getTxIdsForBlock(req.params.hash); + const transactions: TransactionExtended[] = []; + + for (let i = 0; i < Math.min(15, txIds.length); i++) { + const transaction = await transactionUtils.$getTransactionExtended(txIds[i], false, true); + if (transaction) { + transactions.push(transaction); + } + } + res.json(transactions); + } catch (e) { + res.status(500).send(e.message); + } } public async getBlockHeight(req: Request, res: Response) { @@ -568,81 +581,27 @@ class Routes { } public async getAddress(req: Request, res: Response) { - if (config.MEMPOOL.BACKEND === 'bitcoind') { + if (config.MEMPOOL.BACKEND === 'none') { res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); return; } + try { - const addressInfo = await bitcoinApi.$validateAddress(req.params.address); - if (!addressInfo || !addressInfo.isvalid) { - res.json({ - 'address': req.params.address, - 'chain_stats': { - 'funded_txo_count': 0, - 'funded_txo_sum': 0, - 'spent_txo_count': 0, - 'spent_txo_sum': 0, - 'tx_count': 0 - }, - 'mempool_stats': { - 'funded_txo_count': 0, - 'funded_txo_sum': 0, - 'spent_txo_count': 0, - 'spent_txo_sum': 0, - 'tx_count': 0 - } - }); - return; - } - - const balance = await bitcoinApi.$getScriptHashBalance(addressInfo.scriptPubKey); - const history = await bitcoinApi.$getScriptHashHistory(addressInfo.scriptPubKey); - - const unconfirmed = history.filter((h) => h.fee).length; - - res.json({ - 'address': addressInfo.address, - 'chain_stats': { - 'funded_txo_count': 0, - 'funded_txo_sum': balance.confirmed ? balance.confirmed : 0, - 'spent_txo_count': 0, - 'spent_txo_sum': balance.confirmed < 0 ? balance.confirmed : 0, - 'tx_count': history.length - unconfirmed, - }, - 'mempool_stats': { - 'funded_txo_count': 0, - 'funded_txo_sum': balance.unconfirmed > 0 ? balance.unconfirmed : 0, - 'spent_txo_count': 0, - 'spent_txo_sum': balance.unconfirmed < 0 ? -balance.unconfirmed : 0, - 'tx_count': unconfirmed, - } - }); - + const addressData = await bitcoinApi.$getAddress(req.params.address); + res.json(addressData); } catch (e) { res.status(500).send(e.message); } } public async getAddressTransactions(req: Request, res: Response) { - if (config.MEMPOOL.BACKEND === 'bitcoind') { + if (config.MEMPOOL.BACKEND === 'none') { res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); return; } try { - const addressInfo = await bitcoinApi.$validateAddress(req.params.address); - if (!addressInfo || !addressInfo.isvalid) { - res.json([]); - } - const history = await bitcoinApi.$getScriptHashHistory(addressInfo.scriptPubKey); - const transactions: TransactionExtended[] = []; - for (const h of history) { - let tx = await transactionUtils.getTransactionExtended(h.tx_hash); - if (tx) { - tx = await transactionUtils.$addPrevoutsToTransaction(tx); - transactions.push(tx); - } - } + const transactions = await bitcoinApi.$getAddressTransactions(req.params.address, req.params.txId); res.json(transactions); } catch (e) { res.status(500).send(e.message);