diff --git a/backend/package.json b/backend/package.json index fa2c79c11..3796c340e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -30,6 +30,7 @@ "@codewarriorr/electrum-client-js": "^0.1.1", "@mempool/bitcoin": "^3.0.2", "axios": "^0.21.0", + "crypto-js": "^4.0.0", "express": "^4.17.1", "locutus": "^2.0.12", "mysql2": "^1.6.1", diff --git a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts index 94332b560..cce6a352b 100644 --- a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts +++ b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts @@ -1,4 +1,5 @@ -import { MempoolInfo, Transaction, Block, MempoolEntries, MempoolEntry, Address } from '../../interfaces'; +import { MempoolInfo, Transaction, Block, MempoolEntries, MempoolEntry, Address, AddressInformation, + ScriptHashBalance, ScriptHashHistory } from '../../interfaces'; export interface AbstractBitcoinApi { $getMempoolInfo(): Promise; @@ -10,6 +11,9 @@ export interface AbstractBitcoinApi { $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; diff --git a/backend/src/api/bitcoin/bitcoind-api.ts b/backend/src/api/bitcoin/bitcoind-api.ts index 7f01f5f55..d008e95b2 100644 --- a/backend/src/api/bitcoin/bitcoind-api.ts +++ b/backend/src/api/bitcoin/bitcoind-api.ts @@ -1,8 +1,10 @@ import config from '../../config'; -import { Transaction, Block, MempoolInfo, RpcBlock, MempoolEntries, MempoolEntry, Address } from '../../interfaces'; +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 { +class BitcoindApi implements AbstractBitcoinApi { bitcoindClient: any; constructor() { @@ -82,6 +84,18 @@ class BitcoindApi { $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 index f80baa0c2..cbbdec8f2 100644 --- a/backend/src/api/bitcoin/bitcoind-electrs-api.ts +++ b/backend/src/api/bitcoin/bitcoind-electrs-api.ts @@ -1,11 +1,13 @@ import config from '../../config'; import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory'; -import { Transaction, Block, MempoolInfo, RpcBlock, MempoolEntries, MempoolEntry, Address } from '../../interfaces'; +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; @@ -117,6 +119,23 @@ class BitcoindElectrsApi implements AbstractBitcoinApi { 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 index a2dc86ad4..521ff7ab5 100644 --- a/backend/src/api/bitcoin/electrs-api.ts +++ b/backend/src/api/bitcoin/electrs-api.ts @@ -1,6 +1,7 @@ import config from '../../config'; import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory'; -import { Transaction, Block, MempoolInfo, MempoolEntry, MempoolEntries, Address } from '../../interfaces'; +import { Transaction, Block, MempoolInfo, MempoolEntry, MempoolEntries, Address, + AddressInformation, ScriptHashBalance, ScriptHashHistory } from '../../interfaces'; import axios from 'axios'; class ElectrsApi implements AbstractBitcoinApi { @@ -63,6 +64,19 @@ class ElectrsApi implements AbstractBitcoinApi { $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/blocks.ts b/backend/src/api/blocks.ts index a941f8fa2..76213f86b 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -67,17 +67,22 @@ class Blocks { let found = 0; 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 (tx) { + transactions.push(tx); + } + } if (mempool[txIds[i]]) { transactions.push(mempool[txIds[i]]); found++; - } else { - // When using bitcoind, just skip parsing past block tx's for now except for coinbase - if (config.MEMPOOL.BACKEND === 'electrs' || i === 0) { // - logger.debug(`Fetching block tx ${i} of ${txIds.length}`); - const tx = await transactionUtils.getTransactionExtended(txIds[i]); - if (tx) { - transactions.push(tx); - } + } else if (config.MEMPOOL.BACKEND === 'electrs') { + logger.debug(`Fetching block tx ${i} of ${txIds.length}`); + const tx = await transactionUtils.getTransactionExtended(txIds[i]); + if (tx) { + transactions.push(tx); } } } diff --git a/backend/src/api/transaction-utils.ts b/backend/src/api/transaction-utils.ts index c94a903be..fc150d40b 100644 --- a/backend/src/api/transaction-utils.ts +++ b/backend/src/api/transaction-utils.ts @@ -34,13 +34,14 @@ class TransactionUtils { return this.extendTransaction(transaction); } - public extendTransaction(transaction: Transaction | MempoolEntry): TransactionExtended { + public extendTransaction(transaction: 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) { + transaction['firstSeen'] = Math.round((new Date().getTime() / 1000)); + } // @ts-ignore - return Object.assign({ - vsize: Math.round(transaction.weight / 4), - feePerVsize: Math.max(1, (transaction.fee || 0) / (transaction.weight / 4)), - firstSeen: Math.round((new Date().getTime() / 1000)), - }, transaction); + return transaction; } public stripCoinbaseTransaction(tx: TransactionExtended): TransactionMinerInfo { @@ -88,7 +89,7 @@ class TransactionUtils { scriptpubkey: vout.scriptPubKey.hex, scriptpubkey_address: vout.scriptPubKey && vout.scriptPubKey.addresses ? vout.scriptPubKey.addresses[0] : null, scriptpubkey_asm: vout.scriptPubKey.asm, - scriptpubkey_type: vout.scriptPubKey.type, + scriptpubkey_type: this.translateScriptPubKeyType(vout.scriptPubKey.type), }; }); if (transaction.confirmations) { @@ -106,6 +107,25 @@ class TransactionUtils { } } + 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) { diff --git a/backend/src/interfaces.ts b/backend/src/interfaces.ts index 302d43e12..e7a77f670 100644 --- a/backend/src/interfaces.ts +++ b/backend/src/interfaces.ts @@ -30,6 +30,9 @@ export interface Transaction { vin: Vin[]; vout: Vout[]; status: Status; + + // bitcoind (temp?) + in_active_chain?: boolean; } export interface TransactionMinerInfo { @@ -294,3 +297,24 @@ 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/routes.ts b/backend/src/routes.ts index bc24c7b6b..0a2c39465 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -1,5 +1,5 @@ import config from './config'; -import { Request, Response } from 'express'; +import { json, Request, Response } from 'express'; import statistics from './api/statistics'; import feeApi from './api/fee-api'; import backendInfo from './api/backend-info'; @@ -568,16 +568,85 @@ class Routes { } public async getAddress(req: Request, res: Response) { + if (config.MEMPOOL.BACKEND === 'bitcoind') { + res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); + return; + } try { - const result = await bitcoinApi.$getAddress(req.params.hash); - res.json(result); + 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, + } + }); + } catch (e) { res.status(500).send(e.message); } } public async getAddressTransactions(req: Request, res: Response) { - res.status(404).send('Not implemented'); + if (config.MEMPOOL.BACKEND === 'bitcoind') { + 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); + } + } + res.json(transactions); + } catch (e) { + res.status(500).send(e.message); + } } public async getAdressTxChain(req: Request, res: Response) {