From 2253dd570d83f4ab46607227f23855b641248e3f Mon Sep 17 00:00:00 2001 From: softsimon Date: Mon, 11 Jul 2022 19:15:28 +0200 Subject: [PATCH] Refactoring backend routes code --- backend/src/api/bisq/bisq.routes.ts | 381 ++++++ backend/src/api/bitcoin/bitcoin.routes.ts | 543 ++++++++ backend/src/api/blocks.ts | 2 +- backend/src/api/liquid/liquid.routes.ts | 73 ++ backend/src/api/mining/mining-routes.ts | 238 ++++ backend/src/api/{ => mining}/mining.ts | 24 +- .../statistics-api.ts} | 166 +-- .../src/api/statistics/statistics.routes.ts | 67 + backend/src/api/statistics/statistics.ts | 153 +++ backend/src/index.ts | 175 +-- backend/src/indexer.ts | 2 +- backend/src/routes.ts | 1105 ----------------- 12 files changed, 1488 insertions(+), 1441 deletions(-) create mode 100644 backend/src/api/bisq/bisq.routes.ts create mode 100644 backend/src/api/bitcoin/bitcoin.routes.ts create mode 100644 backend/src/api/liquid/liquid.routes.ts create mode 100644 backend/src/api/mining/mining-routes.ts rename backend/src/api/{ => mining}/mining.ts (96%) rename backend/src/api/{statistics.ts => statistics/statistics-api.ts} (69%) create mode 100644 backend/src/api/statistics/statistics.routes.ts create mode 100644 backend/src/api/statistics/statistics.ts delete mode 100644 backend/src/routes.ts diff --git a/backend/src/api/bisq/bisq.routes.ts b/backend/src/api/bisq/bisq.routes.ts new file mode 100644 index 000000000..8f002836f --- /dev/null +++ b/backend/src/api/bisq/bisq.routes.ts @@ -0,0 +1,381 @@ +import { Application, Request, Response } from 'express'; +import config from '../../config'; +import { RequiredSpec } from '../../mempool.interfaces'; +import bisq from './bisq'; +import { MarketsApiError } from './interfaces'; +import marketsApi from './markets-api'; + +class BisqRoutes { + public initRoutes(app: Application) { + app + .get(config.MEMPOOL.API_URL_PREFIX + 'bisq/stats', this.getBisqStats) + .get(config.MEMPOOL.API_URL_PREFIX + 'bisq/tx/:txId', this.getBisqTransaction) + .get(config.MEMPOOL.API_URL_PREFIX + 'bisq/block/:hash', this.getBisqBlock) + .get(config.MEMPOOL.API_URL_PREFIX + 'bisq/blocks/tip/height', this.getBisqTip) + .get(config.MEMPOOL.API_URL_PREFIX + 'bisq/blocks/:index/:length', this.getBisqBlocks) + .get(config.MEMPOOL.API_URL_PREFIX + 'bisq/address/:address', this.getBisqAddress) + .get(config.MEMPOOL.API_URL_PREFIX + 'bisq/txs/:index/:length', this.getBisqTransactions) + .get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/currencies', this.getBisqMarketCurrencies.bind(this)) + .get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/depth', this.getBisqMarketDepth.bind(this)) + .get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/hloc', this.getBisqMarketHloc.bind(this)) + .get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/markets', this.getBisqMarketMarkets.bind(this)) + .get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/offers', this.getBisqMarketOffers.bind(this)) + .get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/ticker', this.getBisqMarketTicker.bind(this)) + .get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/trades', this.getBisqMarketTrades.bind(this)) + .get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/volumes', this.getBisqMarketVolumes.bind(this)) + .get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/volumes/7d', this.getBisqMarketVolumes7d.bind(this)) + ; + } + + + private getBisqStats(req: Request, res: Response) { + const result = bisq.getStats(); + res.json(result); + } + + private getBisqTip(req: Request, res: Response) { + const result = bisq.getLatestBlockHeight(); + res.type('text/plain'); + res.send(result.toString()); + } + + private getBisqTransaction(req: Request, res: Response) { + const result = bisq.getTransaction(req.params.txId); + if (result) { + res.json(result); + } else { + res.status(404).send('Bisq transaction not found'); + } + } + + private getBisqTransactions(req: Request, res: Response) { + const types: string[] = []; + req.query.types = req.query.types || []; + if (!Array.isArray(req.query.types)) { + res.status(500).send('Types is not an array'); + return; + } + + for (const _type in req.query.types) { + if (typeof req.query.types[_type] === 'string') { + types.push(req.query.types[_type].toString()); + } + } + + const index = parseInt(req.params.index, 10) || 0; + const length = parseInt(req.params.length, 10) > 100 ? 100 : parseInt(req.params.length, 10) || 25; + const [transactions, count] = bisq.getTransactions(index, length, types); + res.header('X-Total-Count', count.toString()); + res.json(transactions); + } + + private getBisqBlock(req: Request, res: Response) { + const result = bisq.getBlock(req.params.hash); + if (result) { + res.json(result); + } else { + res.status(404).send('Bisq block not found'); + } + } + + private getBisqBlocks(req: Request, res: Response) { + const index = parseInt(req.params.index, 10) || 0; + const length = parseInt(req.params.length, 10) > 100 ? 100 : parseInt(req.params.length, 10) || 25; + const [transactions, count] = bisq.getBlocks(index, length); + res.header('X-Total-Count', count.toString()); + res.json(transactions); + } + + private getBisqAddress(req: Request, res: Response) { + const result = bisq.getAddress(req.params.address.substr(1)); + if (result) { + res.json(result); + } else { + res.status(404).send('Bisq address not found'); + } + } + + private getBisqMarketCurrencies(req: Request, res: Response) { + const constraints: RequiredSpec = { + 'type': { + required: false, + types: ['crypto', 'fiat', 'all'] + }, + }; + + const p = this.parseRequestParameters(req.query, constraints); + if (p.error) { + res.status(400).json(this.getBisqMarketErrorResponse(p.error)); + return; + } + + const result = marketsApi.getCurrencies(p.type); + if (result) { + res.json(result); + } else { + res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketCurrencies error')); + } + } + + private getBisqMarketDepth(req: Request, res: Response) { + const constraints: RequiredSpec = { + 'market': { + required: true, + types: ['@string'] + }, + }; + + const p = this.parseRequestParameters(req.query, constraints); + if (p.error) { + res.status(400).json(this.getBisqMarketErrorResponse(p.error)); + return; + } + + const result = marketsApi.getDepth(p.market); + if (result) { + res.json(result); + } else { + res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketDepth error')); + } + } + + private getBisqMarketMarkets(req: Request, res: Response) { + const result = marketsApi.getMarkets(); + if (result) { + res.json(result); + } else { + res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketMarkets error')); + } + } + + private getBisqMarketTrades(req: Request, res: Response) { + const constraints: RequiredSpec = { + 'market': { + required: true, + types: ['@string'] + }, + 'timestamp_from': { + required: false, + types: ['@number'] + }, + 'timestamp_to': { + required: false, + types: ['@number'] + }, + 'trade_id_to': { + required: false, + types: ['@string'] + }, + 'trade_id_from': { + required: false, + types: ['@string'] + }, + 'direction': { + required: false, + types: ['buy', 'sell'] + }, + 'limit': { + required: false, + types: ['@number'] + }, + 'sort': { + required: false, + types: ['asc', 'desc'] + } + }; + + const p = this.parseRequestParameters(req.query, constraints); + if (p.error) { + res.status(400).json(this.getBisqMarketErrorResponse(p.error)); + return; + } + + const result = marketsApi.getTrades(p.market, p.timestamp_from, + p.timestamp_to, p.trade_id_from, p.trade_id_to, p.direction, p.limit, p.sort); + if (result) { + res.json(result); + } else { + res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketTrades error')); + } + } + + private getBisqMarketOffers(req: Request, res: Response) { + const constraints: RequiredSpec = { + 'market': { + required: true, + types: ['@string'] + }, + 'direction': { + required: false, + types: ['buy', 'sell'] + }, + }; + + const p = this.parseRequestParameters(req.query, constraints); + if (p.error) { + res.status(400).json(this.getBisqMarketErrorResponse(p.error)); + return; + } + + const result = marketsApi.getOffers(p.market, p.direction); + if (result) { + res.json(result); + } else { + res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketOffers error')); + } + } + + private getBisqMarketVolumes(req: Request, res: Response) { + const constraints: RequiredSpec = { + 'market': { + required: false, + types: ['@string'] + }, + 'interval': { + required: false, + types: ['minute', 'half_hour', 'hour', 'half_day', 'day', 'week', 'month', 'year', 'auto'] + }, + 'timestamp_from': { + required: false, + types: ['@number'] + }, + 'timestamp_to': { + required: false, + types: ['@number'] + }, + 'milliseconds': { + required: false, + types: ['@boolean'] + }, + 'timestamp': { + required: false, + types: ['no', 'yes'] + }, + }; + + const p = this.parseRequestParameters(req.query, constraints); + if (p.error) { + res.status(400).json(this.getBisqMarketErrorResponse(p.error)); + return; + } + + const result = marketsApi.getVolumes(p.market, p.timestamp_from, p.timestamp_to, p.interval, p.milliseconds, p.timestamp); + if (result) { + res.json(result); + } else { + res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketVolumes error')); + } + } + + private getBisqMarketHloc(req: Request, res: Response) { + const constraints: RequiredSpec = { + 'market': { + required: true, + types: ['@string'] + }, + 'interval': { + required: false, + types: ['minute', 'half_hour', 'hour', 'half_day', 'day', 'week', 'month', 'year', 'auto'] + }, + 'timestamp_from': { + required: false, + types: ['@number'] + }, + 'timestamp_to': { + required: false, + types: ['@number'] + }, + 'milliseconds': { + required: false, + types: ['@boolean'] + }, + 'timestamp': { + required: false, + types: ['no', 'yes'] + }, + }; + + const p = this.parseRequestParameters(req.query, constraints); + if (p.error) { + res.status(400).json(this.getBisqMarketErrorResponse(p.error)); + return; + } + + const result = marketsApi.getHloc(p.market, p.interval, p.timestamp_from, p.timestamp_to, p.milliseconds, p.timestamp); + if (result) { + res.json(result); + } else { + res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketHloc error')); + } + } + + private getBisqMarketTicker(req: Request, res: Response) { + const constraints: RequiredSpec = { + 'market': { + required: false, + types: ['@string'] + }, + }; + + const p = this.parseRequestParameters(req.query, constraints); + if (p.error) { + res.status(400).json(this.getBisqMarketErrorResponse(p.error)); + return; + } + + const result = marketsApi.getTicker(p.market); + if (result) { + res.json(result); + } else { + res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketTicker error')); + } + } + + private getBisqMarketVolumes7d(req: Request, res: Response) { + const result = marketsApi.getVolumesByTime(604800); + if (result) { + res.json(result); + } else { + res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketVolumes7d error')); + } + } + + private parseRequestParameters(requestParams: object, params: RequiredSpec): { [name: string]: any; } { + const final = {}; + for (const i in params) { + if (params.hasOwnProperty(i)) { + if (params[i].required && requestParams[i] === undefined) { + return { error: i + ' parameter missing'}; + } + if (typeof requestParams[i] === 'string') { + const str = (requestParams[i] || '').toString().toLowerCase(); + if (params[i].types.indexOf('@number') > -1) { + const number = parseInt((str).toString(), 10); + final[i] = number; + } else if (params[i].types.indexOf('@string') > -1) { + final[i] = str; + } else if (params[i].types.indexOf('@boolean') > -1) { + final[i] = str === 'true' || str === 'yes'; + } else if (params[i].types.indexOf(str) > -1) { + final[i] = str; + } else { + return { error: i + ' parameter invalid'}; + } + } else if (typeof requestParams[i] === 'number') { + final[i] = requestParams[i]; + } + } + } + return final; + } + + private getBisqMarketErrorResponse(message: string): MarketsApiError { + return { + 'success': 0, + 'error': message + }; + } + +} + +export default new BisqRoutes; diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts new file mode 100644 index 000000000..a04a78117 --- /dev/null +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -0,0 +1,543 @@ +import { Application, Request, Response } from 'express'; +import axios from 'axios'; +import config from '../../config'; +import websocketHandler from '../websocket-handler'; +import mempool from '../mempool'; +import feeApi from '../fee-api'; +import mempoolBlocks from '../mempool-blocks'; +import bitcoinApi from './bitcoin-api-factory'; +import { Common } from '../common'; +import backendInfo from '../backend-info'; +import transactionUtils from '../transaction-utils'; +import { IEsploraApi } from './esplora-api.interface'; +import loadingIndicators from '../loading-indicators'; +import { TransactionExtended } from '../../mempool.interfaces'; +import logger from '../../logger'; +import blocks from '../blocks'; +import bitcoinClient from './bitcoin-client'; +import difficultyAdjustment from '../difficulty-adjustment'; + +class BitcoinRoutes { + public initRoutes(app: Application) { + app + .get(config.MEMPOOL.API_URL_PREFIX + 'transaction-times', this.getTransactionTimes) + .get(config.MEMPOOL.API_URL_PREFIX + 'outspends', this.$getBatchedOutspends) + .get(config.MEMPOOL.API_URL_PREFIX + 'cpfp/:txId', this.getCpfpInfo) + .get(config.MEMPOOL.API_URL_PREFIX + 'difficulty-adjustment', this.getDifficultyChange) + .get(config.MEMPOOL.API_URL_PREFIX + 'fees/recommended', this.getRecommendedFees) + .get(config.MEMPOOL.API_URL_PREFIX + 'fees/mempool-blocks', this.getMempoolBlocks) + .get(config.MEMPOOL.API_URL_PREFIX + 'backend-info', this.getBackendInfo) + .get(config.MEMPOOL.API_URL_PREFIX + 'init-data', this.getInitData) + .get(config.MEMPOOL.API_URL_PREFIX + 'validate-address/:address', this.validateAddress) + .post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', this.$postTransactionForm) + .get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => { + try { + const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations`, { responseType: 'stream', timeout: 10000 }); + response.data.pipe(res); + } catch (e) { + res.status(500).end(); + } + }) + .get(config.MEMPOOL.API_URL_PREFIX + 'donations/images/:id', async (req, res) => { + try { + const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations/images/${req.params.id}`, { + responseType: 'stream', timeout: 10000 + }); + response.data.pipe(res); + } catch (e) { + res.status(500).end(); + } + }) + .get(config.MEMPOOL.API_URL_PREFIX + 'contributors', async (req, res) => { + try { + const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors`, { responseType: 'stream', timeout: 10000 }); + response.data.pipe(res); + } catch (e) { + res.status(500).end(); + } + }) + .get(config.MEMPOOL.API_URL_PREFIX + 'contributors/images/:id', async (req, res) => { + try { + const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors/images/${req.params.id}`, { + responseType: 'stream', timeout: 10000 + }); + response.data.pipe(res); + } catch (e) { + res.status(500).end(); + } + }) + .get(config.MEMPOOL.API_URL_PREFIX + 'translators', async (req, res) => { + try { + const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators`, { responseType: 'stream', timeout: 10000 }); + response.data.pipe(res); + } catch (e) { + res.status(500).end(); + } + }) + .get(config.MEMPOOL.API_URL_PREFIX + 'translators/images/:id', async (req, res) => { + try { + const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators/images/${req.params.id}`, { + responseType: 'stream', timeout: 10000 + }); + response.data.pipe(res); + } catch (e) { + res.status(500).end(); + } + }) + .get(config.MEMPOOL.API_URL_PREFIX + 'blocks', this.getBlocks.bind(this)) + .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this)) + .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock) + .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions); + ; + + if (config.MEMPOOL.BACKEND !== 'esplora') { + app + .get(config.MEMPOOL.API_URL_PREFIX + 'mempool', this.getMempool) + .get(config.MEMPOOL.API_URL_PREFIX + 'mempool/txids', this.getMempoolTxIds) + .get(config.MEMPOOL.API_URL_PREFIX + 'mempool/recent', this.getRecentMempoolTransactions) + .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId', this.getTransaction) + .post(config.MEMPOOL.API_URL_PREFIX + 'tx', this.$postTransaction) + .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/hex', this.getRawTransaction) + .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', this.getTransactionStatus) + .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', this.getTransactionOutspends) + .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/header', this.getBlockHeader) + .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight) + .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/hash', this.getBlockTipHash) + .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs', this.getBlockTransactions) + .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs/:index', this.getBlockTransactions) + .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txids', this.getTxIdsForBlock) + .get(config.MEMPOOL.API_URL_PREFIX + 'block-height/:height', this.getBlockHeight) + .get(config.MEMPOOL.API_URL_PREFIX + 'address/:address', this.getAddress) + .get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs', this.getAddressTransactions) + .get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs/chain/:txId', this.getAddressTransactions) + .get(config.MEMPOOL.API_URL_PREFIX + 'address-prefix/:prefix', this.getAddressPrefix) + ; + } + } + + + private getInitData(req: Request, res: Response) { + try { + const result = websocketHandler.getInitData(); + res.json(result); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private getRecommendedFees(req: Request, res: Response) { + if (!mempool.isInSync()) { + res.statusCode = 503; + res.send('Service Unavailable'); + return; + } + const result = feeApi.getRecommendedFee(); + res.json(result); + } + + private getMempoolBlocks(req: Request, res: Response) { + try { + const result = mempoolBlocks.getMempoolBlocks(); + res.json(result); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private getTransactionTimes(req: Request, res: Response) { + if (!Array.isArray(req.query.txId)) { + res.status(500).send('Not an array'); + return; + } + const txIds: string[] = []; + for (const _txId in req.query.txId) { + if (typeof req.query.txId[_txId] === 'string') { + txIds.push(req.query.txId[_txId].toString()); + } + } + + const times = mempool.getFirstSeenForTransactions(txIds); + res.json(times); + } + + private async $getBatchedOutspends(req: Request, res: Response) { + if (!Array.isArray(req.query.txId)) { + res.status(500).send('Not an array'); + return; + } + if (req.query.txId.length > 50) { + res.status(400).send('Too many txids requested'); + return; + } + const txIds: string[] = []; + for (const _txId in req.query.txId) { + if (typeof req.query.txId[_txId] === 'string') { + txIds.push(req.query.txId[_txId].toString()); + } + } + + try { + const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txIds); + res.json(batchedOutspends); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private getCpfpInfo(req: Request, res: Response) { + if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) { + res.status(501).send(`Invalid transaction ID.`); + return; + } + + const tx = mempool.getMempool()[req.params.txId]; + if (!tx) { + res.status(404).send(`Transaction doesn't exist in the mempool.`); + return; + } + + if (tx.cpfpChecked) { + res.json({ + ancestors: tx.ancestors, + bestDescendant: tx.bestDescendant || null, + }); + return; + } + + const cpfpInfo = Common.setRelativesAndGetCpfpInfo(tx, mempool.getMempool()); + + res.json(cpfpInfo); + } + + private getBackendInfo(req: Request, res: Response) { + res.json(backendInfo.getBackendInfo()); + } + + private async getTransaction(req: Request, res: Response) { + try { + const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true); + res.json(transaction); + } catch (e) { + let statusCode = 500; + if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { + statusCode = 404; + } + res.status(statusCode).send(e instanceof Error ? e.message : e); + } + } + + private async getRawTransaction(req: Request, res: Response) { + try { + const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(req.params.txId, true); + res.setHeader('content-type', 'text/plain'); + res.send(transaction.hex); + } catch (e) { + let statusCode = 500; + if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { + statusCode = 404; + } + res.status(statusCode).send(e instanceof Error ? e.message : e); + } + } + + private async getTransactionStatus(req: Request, res: Response) { + try { + const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true); + res.json(transaction.status); + } catch (e) { + let statusCode = 500; + if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { + statusCode = 404; + } + res.status(statusCode).send(e instanceof Error ? e.message : e); + } + } + + private async getBlock(req: Request, res: Response) { + try { + const block = await blocks.$getBlock(req.params.hash); + + const blockAge = new Date().getTime() / 1000 - block.timestamp; + const day = 24 * 3600; + let cacheDuration; + if (blockAge > 365 * day) { + cacheDuration = 30 * day; + } else if (blockAge > 30 * day) { + cacheDuration = 10 * day; + } else { + cacheDuration = 600 + } + + res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString()); + res.json(block); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async getBlockHeader(req: Request, res: Response) { + try { + const blockHeader = await bitcoinApi.$getBlockHeader(req.params.hash); + res.setHeader('content-type', 'text/plain'); + res.send(blockHeader); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async getStrippedBlockTransactions(req: Request, res: Response) { + try { + const transactions = await blocks.$getStrippedBlockTransactions(req.params.hash); + res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); + res.json(transactions); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async getBlocks(req: Request, res: Response) { + try { + if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin + const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10); + res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); + res.json(await blocks.$getBlocks(height, 15)); + } else { // Liquid, Bisq + return await this.getLegacyBlocks(req, res); + } + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async getLegacyBlocks(req: Request, res: Response) { + try { + const returnBlocks: IEsploraApi.Block[] = []; + const fromHeight = parseInt(req.params.height, 10) || blocks.getCurrentBlockHeight(); + + // Check if block height exist in local cache to skip the hash lookup + const blockByHeight = blocks.getBlocks().find((b) => b.height === fromHeight); + let startFromHash: string | null = null; + if (blockByHeight) { + startFromHash = blockByHeight.id; + } else { + startFromHash = await bitcoinApi.$getBlockHash(fromHeight); + } + + let nextHash = startFromHash; + for (let i = 0; i < 10 && nextHash; i++) { + const localBlock = blocks.getBlocks().find((b) => b.id === nextHash); + if (localBlock) { + returnBlocks.push(localBlock); + nextHash = localBlock.previousblockhash; + } else { + const block = await bitcoinApi.$getBlock(nextHash); + returnBlocks.push(block); + nextHash = block.previousblockhash; + } + } + + res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); + res.json(returnBlocks); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async getBlockTransactions(req: Request, res: Response) { + try { + loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0); + + const txIds = await bitcoinApi.$getTxIdsForBlock(req.params.hash); + const transactions: TransactionExtended[] = []; + const startingIndex = Math.max(0, parseInt(req.params.index || '0', 10)); + + const endIndex = Math.min(startingIndex + 10, txIds.length); + for (let i = startingIndex; i < endIndex; i++) { + try { + const transaction = await transactionUtils.$getTransactionExtended(txIds[i], true, true); + transactions.push(transaction); + loadingIndicators.setProgress('blocktxs-' + req.params.hash, (i - startingIndex + 1) / (endIndex - startingIndex) * 100); + } catch (e) { + logger.debug('getBlockTransactions error: ' + (e instanceof Error ? e.message : e)); + } + } + res.json(transactions); + } catch (e) { + loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100); + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async getBlockHeight(req: Request, res: Response) { + try { + const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10)); + res.send(blockHash); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async getAddress(req: Request, res: Response) { + if (config.MEMPOOL.BACKEND === 'none') { + res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); + return; + } + + try { + const addressData = await bitcoinApi.$getAddress(req.params.address); + res.json(addressData); + } catch (e) { + if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { + return res.status(413).send(e instanceof Error ? e.message : e); + } + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async getAddressTransactions(req: Request, res: Response) { + if (config.MEMPOOL.BACKEND === 'none') { + res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); + return; + } + + try { + const transactions = await bitcoinApi.$getAddressTransactions(req.params.address, req.params.txId); + res.json(transactions); + } catch (e) { + if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { + return res.status(413).send(e instanceof Error ? e.message : e); + } + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async getAdressTxChain(req: Request, res: Response) { + res.status(501).send('Not implemented'); + } + + private async getAddressPrefix(req: Request, res: Response) { + try { + const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix); + res.send(blockHash); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async getRecentMempoolTransactions(req: Request, res: Response) { + const latestTransactions = Object.entries(mempool.getMempool()) + .sort((a, b) => (b[1].firstSeen || 0) - (a[1].firstSeen || 0)) + .slice(0, 10).map((tx) => Common.stripTransaction(tx[1])); + + res.json(latestTransactions); + } + + private async getMempool(req: Request, res: Response) { + const info = mempool.getMempoolInfo(); + res.json({ + count: info.size, + vsize: info.bytes, + total_fee: info.total_fee * 1e8, + fee_histogram: [] + }); + } + + private async getMempoolTxIds(req: Request, res: Response) { + try { + const rawMempool = await bitcoinApi.$getRawMempool(); + res.send(rawMempool); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async getBlockTipHeight(req: Request, res: Response) { + try { + const result = await bitcoinApi.$getBlockHeightTip(); + res.json(result); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async getBlockTipHash(req: Request, res: Response) { + try { + const result = await bitcoinApi.$getBlockHashTip(); + res.setHeader('content-type', 'text/plain'); + res.send(result); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async getTxIdsForBlock(req: Request, res: Response) { + try { + const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash); + res.json(result); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async validateAddress(req: Request, res: Response) { + try { + const result = await bitcoinClient.validateAddress(req.params.address); + res.json(result); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async getTransactionOutspends(req: Request, res: Response) { + try { + const result = await bitcoinApi.$getOutspends(req.params.txId); + res.json(result); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private getDifficultyChange(req: Request, res: Response) { + try { + res.json(difficultyAdjustment.getDifficultyAdjustment()); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async $postTransaction(req: Request, res: Response) { + res.setHeader('content-type', 'text/plain'); + try { + let rawTx; + if (typeof req.body === 'object') { + rawTx = Object.keys(req.body)[0]; + } else { + rawTx = req.body; + } + const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx); + res.send(txIdResult); + } catch (e: any) { + res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) + : (e.message || 'Error')); + } + } + + private async $postTransactionForm(req: Request, res: Response) { + res.setHeader('content-type', 'text/plain'); + const matches = /tx=([a-z0-9]+)/.exec(req.body); + let txHex = ''; + if (matches && matches[1]) { + txHex = matches[1]; + } + try { + const txIdResult = await bitcoinClient.sendRawTransaction(txHex); + res.send(txIdResult); + } catch (e: any) { + res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) + : (e.message || 'Error')); + } + } + +} + +export default new BitcoinRoutes(); diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 5d4602224..af317af14 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -19,7 +19,7 @@ import HashratesRepository from '../repositories/HashratesRepository'; import indexer from '../indexer'; import poolsParser from './pools-parser'; import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository'; -import mining from './mining'; +import mining from './mining/mining'; import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmentsRepository'; import difficultyAdjustment from './difficulty-adjustment'; diff --git a/backend/src/api/liquid/liquid.routes.ts b/backend/src/api/liquid/liquid.routes.ts new file mode 100644 index 000000000..b130373e1 --- /dev/null +++ b/backend/src/api/liquid/liquid.routes.ts @@ -0,0 +1,73 @@ +import axios from 'axios'; +import { Application, Request, Response } from 'express'; +import config from '../../config'; +import elementsParser from './elements-parser'; +import icons from './icons'; + +class LiquidRoutes { + public initRoutes(app: Application) { + app + .get(config.MEMPOOL.API_URL_PREFIX + 'assets/icons', this.getAllLiquidIcon) + .get(config.MEMPOOL.API_URL_PREFIX + 'assets/featured', this.$getAllFeaturedLiquidAssets) + .get(config.MEMPOOL.API_URL_PREFIX + 'asset/:assetId/icon', this.getLiquidIcon) + .get(config.MEMPOOL.API_URL_PREFIX + 'assets/group/:id', this.$getAssetGroup) + ; + + if (config.DATABASE.ENABLED) { + app + .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs/month', this.$getElementsPegsByMonth) + ; + } + } + + + private getLiquidIcon(req: Request, res: Response) { + const result = icons.getIconByAssetId(req.params.assetId); + if (result) { + res.setHeader('content-type', 'image/png'); + res.setHeader('content-length', result.length); + res.send(result); + } else { + res.status(404).send('Asset icon not found'); + } + } + + private getAllLiquidIcon(req: Request, res: Response) { + const result = icons.getAllIconIds(); + if (result) { + res.json(result); + } else { + res.status(404).send('Asset icons not found'); + } + } + + private async $getAllFeaturedLiquidAssets(req: Request, res: Response) { + try { + const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.LIQUID_API}/assets/featured`, { responseType: 'stream', timeout: 10000 }); + response.data.pipe(res); + } catch (e) { + res.status(500).end(); + } + } + + private async $getAssetGroup(req: Request, res: Response) { + try { + const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.LIQUID_API}/assets/group/${parseInt(req.params.id, 10)}`, + { responseType: 'stream', timeout: 10000 }); + response.data.pipe(res); + } catch (e) { + res.status(500).end(); + } + } + + private async $getElementsPegsByMonth(req: Request, res: Response) { + try { + const pegs = await elementsParser.$getPegDataByMonth(); + res.json(pegs); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } +} + +export default new LiquidRoutes(); diff --git a/backend/src/api/mining/mining-routes.ts b/backend/src/api/mining/mining-routes.ts new file mode 100644 index 000000000..02262353f --- /dev/null +++ b/backend/src/api/mining/mining-routes.ts @@ -0,0 +1,238 @@ +import { Application, Request, Response } from 'express'; +import config from "../../config"; +import logger from '../../logger'; +import BlocksAuditsRepository from '../../repositories/BlocksAuditsRepository'; +import BlocksRepository from '../../repositories/BlocksRepository'; +import DifficultyAdjustmentsRepository from '../../repositories/DifficultyAdjustmentsRepository'; +import HashratesRepository from '../../repositories/HashratesRepository'; +import bitcoinClient from '../bitcoin/bitcoin-client'; +import mining from "./mining"; + +class MiningRoutes { + public initRoutes(app: Application) { + app + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/:interval', this.$getPools) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/hashrate', this.$getPoolHistoricalHashrate) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/blocks', this.$getPoolBlocks) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/blocks/:height', this.$getPoolBlocks) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug', this.$getPool) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/pools/:interval', this.$getPoolsHistoricalHashrate) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/:interval', this.$getHistoricalHashrate) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments', this.$getDifficultyAdjustments) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/reward-stats/:blockCount', this.$getRewardStats) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fees/:interval', this.$getHistoricalBlockFees) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/rewards/:interval', this.$getHistoricalBlockRewards) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fee-rates/:interval', this.$getHistoricalBlockFeeRates) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/sizes-weights/:interval', this.$getHistoricalBlockSizeAndWeight) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments/:interval', this.$getDifficultyAdjustments) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/predictions/:interval', this.$getHistoricalBlockPrediction) + ; + } + + private async $getPool(req: Request, res: Response): Promise { + try { + const stats = await mining.$getPoolStat(req.params.slug); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); + res.json(stats); + } catch (e) { + if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { + res.status(404).send(e.message); + } else { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + } + + private async $getPoolBlocks(req: Request, res: Response) { + try { + const poolBlocks = await BlocksRepository.$getBlocksByPool( + req.params.slug, + req.params.height === undefined ? undefined : parseInt(req.params.height, 10), + ); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); + res.json(poolBlocks); + } catch (e) { + if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { + res.status(404).send(e.message); + } else { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + } + + private async $getPools(req: Request, res: Response) { + try { + const stats = await mining.$getPoolsStats(req.params.interval); + const blockCount = await BlocksRepository.$blockCount(null, null); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.header('X-total-count', blockCount.toString()); + res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); + res.json(stats); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async $getPoolsHistoricalHashrate(req: Request, res: Response) { + try { + const hashrates = await HashratesRepository.$getPoolsWeeklyHashrate(req.params.interval); + const blockCount = await BlocksRepository.$blockCount(null, null); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.header('X-total-count', blockCount.toString()); + res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); + res.json(hashrates); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async $getPoolHistoricalHashrate(req: Request, res: Response) { + try { + const hashrates = await HashratesRepository.$getPoolWeeklyHashrate(req.params.slug); + const blockCount = await BlocksRepository.$blockCount(null, null); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.header('X-total-count', blockCount.toString()); + res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); + res.json(hashrates); + } catch (e) { + if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { + res.status(404).send(e.message); + } else { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + } + + private async $getHistoricalHashrate(req: Request, res: Response) { + let currentHashrate = 0, currentDifficulty = 0; + try { + currentHashrate = await bitcoinClient.getNetworkHashPs(); + currentDifficulty = await bitcoinClient.getDifficulty(); + } catch (e) { + logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate and difficulty'); + } + + try { + const hashrates = await HashratesRepository.$getNetworkDailyHashrate(req.params.interval); + const difficulty = await DifficultyAdjustmentsRepository.$getAdjustments(req.params.interval, false); + const blockCount = await BlocksRepository.$blockCount(null, null); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.header('X-total-count', blockCount.toString()); + res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); + res.json({ + hashrates: hashrates, + difficulty: difficulty, + currentHashrate: currentHashrate, + currentDifficulty: currentDifficulty, + }); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async $getHistoricalBlockFees(req: Request, res: Response) { + try { + const blockFees = await mining.$getHistoricalBlockFees(req.params.interval); + const blockCount = await BlocksRepository.$blockCount(null, null); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.header('X-total-count', blockCount.toString()); + res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); + res.json(blockFees); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async $getHistoricalBlockRewards(req: Request, res: Response) { + try { + const blockRewards = await mining.$getHistoricalBlockRewards(req.params.interval); + const blockCount = await BlocksRepository.$blockCount(null, null); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.header('X-total-count', blockCount.toString()); + res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); + res.json(blockRewards); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async $getHistoricalBlockFeeRates(req: Request, res: Response) { + try { + const blockFeeRates = await mining.$getHistoricalBlockFeeRates(req.params.interval); + const blockCount = await BlocksRepository.$blockCount(null, null); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.header('X-total-count', blockCount.toString()); + res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); + res.json(blockFeeRates); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async $getHistoricalBlockSizeAndWeight(req: Request, res: Response) { + try { + const blockSizes = await mining.$getHistoricalBlockSizes(req.params.interval); + const blockWeights = await mining.$getHistoricalBlockWeights(req.params.interval); + const blockCount = await BlocksRepository.$blockCount(null, null); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.header('X-total-count', blockCount.toString()); + res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); + res.json({ + sizes: blockSizes, + weights: blockWeights + }); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async $getDifficultyAdjustments(req: Request, res: Response) { + try { + const difficulty = await DifficultyAdjustmentsRepository.$getRawAdjustments(req.params.interval, true); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); + res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment])); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async $getRewardStats(req: Request, res: Response) { + try { + const response = await mining.$getRewardStats(parseInt(req.params.blockCount, 10)); + res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); + res.json(response); + } catch (e) { + res.status(500).end(); + } + } + + private async $getHistoricalBlockPrediction(req: Request, res: Response) { + try { + const blockPredictions = await mining.$getBlockPredictionsHistory(req.params.interval); + const blockCount = await BlocksAuditsRepository.$getPredictionsCount(); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.header('X-total-count', blockCount.toString()); + res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); + res.json(blockPredictions.map(prediction => [prediction.time, prediction.height, prediction.match_rate])); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } +} + +export default new MiningRoutes(); diff --git a/backend/src/api/mining.ts b/backend/src/api/mining/mining.ts similarity index 96% rename from backend/src/api/mining.ts rename to backend/src/api/mining/mining.ts index 80726d07b..0ee8bcf00 100644 --- a/backend/src/api/mining.ts +++ b/backend/src/api/mining/mining.ts @@ -1,16 +1,16 @@ -import { IndexedDifficultyAdjustment, PoolInfo, PoolStats, RewardStats } from '../mempool.interfaces'; -import BlocksRepository from '../repositories/BlocksRepository'; -import PoolsRepository from '../repositories/PoolsRepository'; -import HashratesRepository from '../repositories/HashratesRepository'; -import bitcoinClient from './bitcoin/bitcoin-client'; -import logger from '../logger'; -import { Common } from './common'; -import loadingIndicators from './loading-indicators'; +import { IndexedDifficultyAdjustment, PoolInfo, PoolStats, RewardStats } from '../../mempool.interfaces'; +import BlocksRepository from '../../repositories/BlocksRepository'; +import PoolsRepository from '../../repositories/PoolsRepository'; +import HashratesRepository from '../../repositories/HashratesRepository'; +import bitcoinClient from '../bitcoin/bitcoin-client'; +import logger from '../../logger'; +import { Common } from '../common'; +import loadingIndicators from '../loading-indicators'; import { escape } from 'mysql2'; -import indexer from '../indexer'; -import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmentsRepository'; -import config from '../config'; -import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository'; +import indexer from '../../indexer'; +import DifficultyAdjustmentsRepository from '../../repositories/DifficultyAdjustmentsRepository'; +import config from '../../config'; +import BlocksAuditsRepository from '../../repositories/BlocksAuditsRepository'; class Mining { constructor() { diff --git a/backend/src/api/statistics.ts b/backend/src/api/statistics/statistics-api.ts similarity index 69% rename from backend/src/api/statistics.ts rename to backend/src/api/statistics/statistics-api.ts index bd93b4c6e..56a868f8f 100644 --- a/backend/src/api/statistics.ts +++ b/backend/src/api/statistics/statistics-api.ts @@ -1,160 +1,11 @@ -import memPool from './mempool'; -import DB from '../database'; -import logger from '../logger'; +import DB from '../../database'; +import logger from '../../logger'; +import { Statistic, OptimizedStatistic } from '../../mempool.interfaces'; -import { Statistic, TransactionExtended, OptimizedStatistic } from '../mempool.interfaces'; -import config from '../config'; -import { Common } from './common'; - -class Statistics { - protected intervalTimer: NodeJS.Timer | undefined; - protected newStatisticsEntryCallback: ((stats: OptimizedStatistic) => void) | undefined; +class StatisticsApi { protected queryTimeout = 120000; - public setNewStatisticsEntryCallback(fn: (stats: OptimizedStatistic) => void) { - this.newStatisticsEntryCallback = fn; - } - - constructor() { } - - public startStatistics(): void { - logger.info('Starting statistics service'); - - const now = new Date(); - const nextInterval = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), - Math.floor(now.getMinutes() / 1) * 1 + 1, 0, 0); - const difference = nextInterval.getTime() - now.getTime(); - - setTimeout(() => { - this.runStatistics(); - this.intervalTimer = setInterval(() => { - this.runStatistics(); - }, 1 * 60 * 1000); - }, difference); - } - - private async runStatistics(): Promise { - if (!memPool.isInSync()) { - return; - } - const currentMempool = memPool.getMempool(); - const txPerSecond = memPool.getTxPerSecond(); - const vBytesPerSecond = memPool.getVBytesPerSecond(); - - logger.debug('Running statistics'); - - let memPoolArray: TransactionExtended[] = []; - for (const i in currentMempool) { - if (currentMempool.hasOwnProperty(i)) { - memPoolArray.push(currentMempool[i]); - } - } - // Remove 0 and undefined - memPoolArray = memPoolArray.filter((tx) => tx.effectiveFeePerVsize); - - if (!memPoolArray.length) { - try { - const insertIdZeroed = await this.$createZeroedStatistic(); - if (this.newStatisticsEntryCallback && insertIdZeroed) { - const newStats = await this.$get(insertIdZeroed); - if (newStats) { - this.newStatisticsEntryCallback(newStats); - } - } - } catch (e) { - logger.err('Unable to insert zeroed statistics. ' + e); - } - return; - } - - memPoolArray.sort((a, b) => a.effectiveFeePerVsize - b.effectiveFeePerVsize); - const totalWeight = memPoolArray.map((tx) => tx.vsize).reduce((acc, curr) => acc + curr) * 4; - const totalFee = memPoolArray.map((tx) => tx.fee).reduce((acc, curr) => acc + curr); - - const logFees = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200, - 250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000]; - - const weightVsizeFees: { [feePerWU: number]: number } = {}; - const lastItem = logFees.length - 1; - - memPoolArray.forEach((transaction) => { - for (let i = 0; i < logFees.length; i++) { - if ( - (Common.isLiquid() && (i === lastItem || transaction.effectiveFeePerVsize * 10 < logFees[i + 1])) - || - (!Common.isLiquid() && (i === lastItem || transaction.effectiveFeePerVsize < logFees[i + 1])) - ) { - if (weightVsizeFees[logFees[i]]) { - weightVsizeFees[logFees[i]] += transaction.vsize; - } else { - weightVsizeFees[logFees[i]] = transaction.vsize; - } - break; - } - } - }); - - try { - const insertId = await this.$create({ - added: 'NOW()', - unconfirmed_transactions: memPoolArray.length, - tx_per_second: txPerSecond, - vbytes_per_second: Math.round(vBytesPerSecond), - mempool_byte_weight: totalWeight, - total_fee: totalFee, - fee_data: '', - vsize_1: weightVsizeFees['1'] || 0, - vsize_2: weightVsizeFees['2'] || 0, - vsize_3: weightVsizeFees['3'] || 0, - vsize_4: weightVsizeFees['4'] || 0, - vsize_5: weightVsizeFees['5'] || 0, - vsize_6: weightVsizeFees['6'] || 0, - vsize_8: weightVsizeFees['8'] || 0, - vsize_10: weightVsizeFees['10'] || 0, - vsize_12: weightVsizeFees['12'] || 0, - vsize_15: weightVsizeFees['15'] || 0, - vsize_20: weightVsizeFees['20'] || 0, - vsize_30: weightVsizeFees['30'] || 0, - vsize_40: weightVsizeFees['40'] || 0, - vsize_50: weightVsizeFees['50'] || 0, - vsize_60: weightVsizeFees['60'] || 0, - vsize_70: weightVsizeFees['70'] || 0, - vsize_80: weightVsizeFees['80'] || 0, - vsize_90: weightVsizeFees['90'] || 0, - vsize_100: weightVsizeFees['100'] || 0, - vsize_125: weightVsizeFees['125'] || 0, - vsize_150: weightVsizeFees['150'] || 0, - vsize_175: weightVsizeFees['175'] || 0, - vsize_200: weightVsizeFees['200'] || 0, - vsize_250: weightVsizeFees['250'] || 0, - vsize_300: weightVsizeFees['300'] || 0, - vsize_350: weightVsizeFees['350'] || 0, - vsize_400: weightVsizeFees['400'] || 0, - vsize_500: weightVsizeFees['500'] || 0, - vsize_600: weightVsizeFees['600'] || 0, - vsize_700: weightVsizeFees['700'] || 0, - vsize_800: weightVsizeFees['800'] || 0, - vsize_900: weightVsizeFees['900'] || 0, - vsize_1000: weightVsizeFees['1000'] || 0, - vsize_1200: weightVsizeFees['1200'] || 0, - vsize_1400: weightVsizeFees['1400'] || 0, - vsize_1600: weightVsizeFees['1600'] || 0, - vsize_1800: weightVsizeFees['1800'] || 0, - vsize_2000: weightVsizeFees['2000'] || 0, - }); - - if (this.newStatisticsEntryCallback && insertId) { - const newStats = await this.$get(insertId); - if (newStats) { - this.newStatisticsEntryCallback(newStats); - } - } - } catch (e) { - logger.err('Unable to insert statistics. ' + e); - } - } - - private async $createZeroedStatistic(): Promise { + public async $createZeroedStatistic(): Promise { try { const query = `INSERT INTO statistics( added, @@ -212,7 +63,7 @@ class Statistics { } } - private async $create(statistics: Statistic): Promise { + public async $create(statistics: Statistic): Promise { try { const query = `INSERT INTO statistics( added, @@ -413,7 +264,7 @@ class Statistics { ORDER BY statistics.added DESC;`; } - private async $get(id: number): Promise { + public async $get(id: number): Promise { try { const query = `SELECT *, UNIX_TIMESTAMP(added) as added FROM statistics WHERE id = ?`; const [rows] = await DB.query(query, [id]); @@ -574,7 +425,6 @@ class Statistics { }; }); } - } -export default new Statistics(); +export default new StatisticsApi(); diff --git a/backend/src/api/statistics/statistics.routes.ts b/backend/src/api/statistics/statistics.routes.ts new file mode 100644 index 000000000..4b1b91ce9 --- /dev/null +++ b/backend/src/api/statistics/statistics.routes.ts @@ -0,0 +1,67 @@ +import { Application, Request, Response } from 'express'; +import config from '../../config'; +import statisticsApi from './statistics-api'; + +class StatisticsRoutes { + public initRoutes(app: Application) { + app + .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2h', this.$getStatisticsByTime.bind(this, '2h')) + .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/24h', this.$getStatisticsByTime.bind(this, '24h')) + .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1w', this.$getStatisticsByTime.bind(this, '1w')) + .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1m', this.$getStatisticsByTime.bind(this, '1m')) + .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3m', this.$getStatisticsByTime.bind(this, '3m')) + .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/6m', this.$getStatisticsByTime.bind(this, '6m')) + .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1y', this.$getStatisticsByTime.bind(this, '1y')) + .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2y', this.$getStatisticsByTime.bind(this, '2y')) + .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3y', this.$getStatisticsByTime.bind(this, '3y')) + ; + } + + private async $getStatisticsByTime(time: '2h' | '24h' | '1w' | '1m' | '3m' | '6m' | '1y' | '2y' | '3y', req: Request, res: Response) { + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); + + try { + let result; + switch (time as string) { + case '2h': + result = await statisticsApi.$list2H(); + res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); + break; + case '24h': + result = await statisticsApi.$list24H(); + res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); + break; + case '1w': + result = await statisticsApi.$list1W(); + break; + case '1m': + result = await statisticsApi.$list1M(); + break; + case '3m': + result = await statisticsApi.$list3M(); + break; + case '6m': + result = await statisticsApi.$list6M(); + break; + case '1y': + result = await statisticsApi.$list1Y(); + break; + case '2y': + result = await statisticsApi.$list2Y(); + break; + case '3y': + result = await statisticsApi.$list3Y(); + break; + default: + result = await statisticsApi.$list2H(); + } + res.json(result); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } +} + +export default new StatisticsRoutes(); diff --git a/backend/src/api/statistics/statistics.ts b/backend/src/api/statistics/statistics.ts new file mode 100644 index 000000000..27554f36d --- /dev/null +++ b/backend/src/api/statistics/statistics.ts @@ -0,0 +1,153 @@ +import memPool from '../mempool'; +import logger from '../../logger'; +import { TransactionExtended, OptimizedStatistic } from '../../mempool.interfaces'; +import { Common } from '../common'; +import statisticsApi from './statistics-api'; + +class Statistics { + protected intervalTimer: NodeJS.Timer | undefined; + protected newStatisticsEntryCallback: ((stats: OptimizedStatistic) => void) | undefined; + + public setNewStatisticsEntryCallback(fn: (stats: OptimizedStatistic) => void) { + this.newStatisticsEntryCallback = fn; + } + + public startStatistics(): void { + logger.info('Starting statistics service'); + + const now = new Date(); + const nextInterval = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), + Math.floor(now.getMinutes() / 1) * 1 + 1, 0, 0); + const difference = nextInterval.getTime() - now.getTime(); + + setTimeout(() => { + this.runStatistics(); + this.intervalTimer = setInterval(() => { + this.runStatistics(); + }, 1 * 60 * 1000); + }, difference); + } + + private async runStatistics(): Promise { + if (!memPool.isInSync()) { + return; + } + const currentMempool = memPool.getMempool(); + const txPerSecond = memPool.getTxPerSecond(); + const vBytesPerSecond = memPool.getVBytesPerSecond(); + + logger.debug('Running statistics'); + + let memPoolArray: TransactionExtended[] = []; + for (const i in currentMempool) { + if (currentMempool.hasOwnProperty(i)) { + memPoolArray.push(currentMempool[i]); + } + } + // Remove 0 and undefined + memPoolArray = memPoolArray.filter((tx) => tx.effectiveFeePerVsize); + + if (!memPoolArray.length) { + try { + const insertIdZeroed = await statisticsApi.$createZeroedStatistic(); + if (this.newStatisticsEntryCallback && insertIdZeroed) { + const newStats = await statisticsApi.$get(insertIdZeroed); + if (newStats) { + this.newStatisticsEntryCallback(newStats); + } + } + } catch (e) { + logger.err('Unable to insert zeroed statistics. ' + e); + } + return; + } + + memPoolArray.sort((a, b) => a.effectiveFeePerVsize - b.effectiveFeePerVsize); + const totalWeight = memPoolArray.map((tx) => tx.vsize).reduce((acc, curr) => acc + curr) * 4; + const totalFee = memPoolArray.map((tx) => tx.fee).reduce((acc, curr) => acc + curr); + + const logFees = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200, + 250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000]; + + const weightVsizeFees: { [feePerWU: number]: number } = {}; + const lastItem = logFees.length - 1; + + memPoolArray.forEach((transaction) => { + for (let i = 0; i < logFees.length; i++) { + if ( + (Common.isLiquid() && (i === lastItem || transaction.effectiveFeePerVsize * 10 < logFees[i + 1])) + || + (!Common.isLiquid() && (i === lastItem || transaction.effectiveFeePerVsize < logFees[i + 1])) + ) { + if (weightVsizeFees[logFees[i]]) { + weightVsizeFees[logFees[i]] += transaction.vsize; + } else { + weightVsizeFees[logFees[i]] = transaction.vsize; + } + break; + } + } + }); + + try { + const insertId = await statisticsApi.$create({ + added: 'NOW()', + unconfirmed_transactions: memPoolArray.length, + tx_per_second: txPerSecond, + vbytes_per_second: Math.round(vBytesPerSecond), + mempool_byte_weight: totalWeight, + total_fee: totalFee, + fee_data: '', + vsize_1: weightVsizeFees['1'] || 0, + vsize_2: weightVsizeFees['2'] || 0, + vsize_3: weightVsizeFees['3'] || 0, + vsize_4: weightVsizeFees['4'] || 0, + vsize_5: weightVsizeFees['5'] || 0, + vsize_6: weightVsizeFees['6'] || 0, + vsize_8: weightVsizeFees['8'] || 0, + vsize_10: weightVsizeFees['10'] || 0, + vsize_12: weightVsizeFees['12'] || 0, + vsize_15: weightVsizeFees['15'] || 0, + vsize_20: weightVsizeFees['20'] || 0, + vsize_30: weightVsizeFees['30'] || 0, + vsize_40: weightVsizeFees['40'] || 0, + vsize_50: weightVsizeFees['50'] || 0, + vsize_60: weightVsizeFees['60'] || 0, + vsize_70: weightVsizeFees['70'] || 0, + vsize_80: weightVsizeFees['80'] || 0, + vsize_90: weightVsizeFees['90'] || 0, + vsize_100: weightVsizeFees['100'] || 0, + vsize_125: weightVsizeFees['125'] || 0, + vsize_150: weightVsizeFees['150'] || 0, + vsize_175: weightVsizeFees['175'] || 0, + vsize_200: weightVsizeFees['200'] || 0, + vsize_250: weightVsizeFees['250'] || 0, + vsize_300: weightVsizeFees['300'] || 0, + vsize_350: weightVsizeFees['350'] || 0, + vsize_400: weightVsizeFees['400'] || 0, + vsize_500: weightVsizeFees['500'] || 0, + vsize_600: weightVsizeFees['600'] || 0, + vsize_700: weightVsizeFees['700'] || 0, + vsize_800: weightVsizeFees['800'] || 0, + vsize_900: weightVsizeFees['900'] || 0, + vsize_1000: weightVsizeFees['1000'] || 0, + vsize_1200: weightVsizeFees['1200'] || 0, + vsize_1400: weightVsizeFees['1400'] || 0, + vsize_1600: weightVsizeFees['1600'] || 0, + vsize_1800: weightVsizeFees['1800'] || 0, + vsize_2000: weightVsizeFees['2000'] || 0, + }); + + if (this.newStatisticsEntryCallback && insertId) { + const newStats = await statisticsApi.$get(insertId); + if (newStats) { + this.newStatisticsEntryCallback(newStats); + } + } + } catch (e) { + logger.err('Unable to insert statistics. ' + e); + } + } +} + +export default new Statistics(); diff --git a/backend/src/index.ts b/backend/src/index.ts index 4e86060af..c5053bf12 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -3,15 +3,12 @@ import { Application, Request, Response, NextFunction } from 'express'; import * as http from 'http'; import * as WebSocket from 'ws'; import cluster from 'cluster'; -import axios from 'axios'; - import DB from './database'; import config from './config'; -import routes from './routes'; import blocks from './api/blocks'; import memPool from './api/mempool'; import diskCache from './api/disk-cache'; -import statistics from './api/statistics'; +import statistics from './api/statistics/statistics'; import websocketHandler from './api/websocket-handler'; import fiatConversion from './api/fiat-conversion'; import bisq from './api/bisq/bisq'; @@ -33,7 +30,11 @@ import channelsRoutes from './api/explorer/channels.routes'; import generalLightningRoutes from './api/explorer/general.routes'; import lightningStatsUpdater from './tasks/lightning/stats-updater.service'; import nodeSyncService from './tasks/lightning/node-sync.service'; -import BlocksAuditsRepository from './repositories/BlocksAuditsRepository'; +import statisticsRoutes from "./api/statistics/statistics.routes"; +import miningRoutes from "./api/mining/mining-routes"; +import bisqRoutes from "./api/bisq/bisq.routes"; +import liquidRoutes from "./api/liquid/liquid.routes"; +import bitcoinRoutes from "./api/bitcoin/bitcoin.routes"; class Server { private wss: WebSocket.Server | undefined; @@ -206,173 +207,19 @@ class Server { } setUpHttpApiRoutes() { - this.app - .get(config.MEMPOOL.API_URL_PREFIX + 'transaction-times', routes.getTransactionTimes) - .get(config.MEMPOOL.API_URL_PREFIX + 'outspends', routes.$getBatchedOutspends) - .get(config.MEMPOOL.API_URL_PREFIX + 'cpfp/:txId', routes.getCpfpInfo) - .get(config.MEMPOOL.API_URL_PREFIX + 'difficulty-adjustment', routes.getDifficultyChange) - .get(config.MEMPOOL.API_URL_PREFIX + 'fees/recommended', routes.getRecommendedFees) - .get(config.MEMPOOL.API_URL_PREFIX + 'fees/mempool-blocks', routes.getMempoolBlocks) - .get(config.MEMPOOL.API_URL_PREFIX + 'backend-info', routes.getBackendInfo) - .get(config.MEMPOOL.API_URL_PREFIX + 'init-data', routes.getInitData) - .get(config.MEMPOOL.API_URL_PREFIX + 'validate-address/:address', routes.validateAddress) - .post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', routes.$postTransactionForm) - .get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => { - try { - const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations`, { responseType: 'stream', timeout: 10000 }); - response.data.pipe(res); - } catch (e) { - res.status(500).end(); - } - }) - .get(config.MEMPOOL.API_URL_PREFIX + 'donations/images/:id', async (req, res) => { - try { - const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations/images/${req.params.id}`, { - responseType: 'stream', timeout: 10000 - }); - response.data.pipe(res); - } catch (e) { - res.status(500).end(); - } - }) - .get(config.MEMPOOL.API_URL_PREFIX + 'contributors', async (req, res) => { - try { - const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors`, { responseType: 'stream', timeout: 10000 }); - response.data.pipe(res); - } catch (e) { - res.status(500).end(); - } - }) - .get(config.MEMPOOL.API_URL_PREFIX + 'contributors/images/:id', async (req, res) => { - try { - const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors/images/${req.params.id}`, { - responseType: 'stream', timeout: 10000 - }); - response.data.pipe(res); - } catch (e) { - res.status(500).end(); - } - }) - .get(config.MEMPOOL.API_URL_PREFIX + 'translators', async (req, res) => { - try { - const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators`, { responseType: 'stream', timeout: 10000 }); - response.data.pipe(res); - } catch (e) { - res.status(500).end(); - } - }) - .get(config.MEMPOOL.API_URL_PREFIX + 'translators/images/:id', async (req, res) => { - try { - const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators/images/${req.params.id}`, { - responseType: 'stream', timeout: 10000 - }); - response.data.pipe(res); - } catch (e) { - res.status(500).end(); - } - }) - ; - + bitcoinRoutes.initRoutes(this.app); if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED) { - this.app - .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2h', routes.$getStatisticsByTime.bind(routes, '2h')) - .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/24h', routes.$getStatisticsByTime.bind(routes, '24h')) - .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1w', routes.$getStatisticsByTime.bind(routes, '1w')) - .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1m', routes.$getStatisticsByTime.bind(routes, '1m')) - .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3m', routes.$getStatisticsByTime.bind(routes, '3m')) - .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/6m', routes.$getStatisticsByTime.bind(routes, '6m')) - .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1y', routes.$getStatisticsByTime.bind(routes, '1y')) - .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2y', routes.$getStatisticsByTime.bind(routes, '2y')) - .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3y', routes.$getStatisticsByTime.bind(routes, '3y')) - ; + statisticsRoutes.initRoutes(this.app); } - if (Common.indexingEnabled()) { - this.app - .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/:interval', routes.$getPools) - .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/hashrate', routes.$getPoolHistoricalHashrate) - .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/blocks', routes.$getPoolBlocks) - .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/blocks/:height', routes.$getPoolBlocks) - .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug', routes.$getPool) - .get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/pools/:interval', routes.$getPoolsHistoricalHashrate) - .get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/:interval', routes.$getHistoricalHashrate) - .get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments', routes.$getDifficultyAdjustments) - .get(config.MEMPOOL.API_URL_PREFIX + 'mining/reward-stats/:blockCount', routes.$getRewardStats) - .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fees/:interval', routes.$getHistoricalBlockFees) - .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/rewards/:interval', routes.$getHistoricalBlockRewards) - .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fee-rates/:interval', routes.$getHistoricalBlockFeeRates) - .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/sizes-weights/:interval', routes.$getHistoricalBlockSizeAndWeight) - .get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments/:interval', routes.$getDifficultyAdjustments) - .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/predictions/:interval', routes.$getHistoricalBlockPrediction) - ; + miningRoutes.initRoutes(this.app); } - if (config.BISQ.ENABLED) { - this.app - .get(config.MEMPOOL.API_URL_PREFIX + 'bisq/stats', routes.getBisqStats) - .get(config.MEMPOOL.API_URL_PREFIX + 'bisq/tx/:txId', routes.getBisqTransaction) - .get(config.MEMPOOL.API_URL_PREFIX + 'bisq/block/:hash', routes.getBisqBlock) - .get(config.MEMPOOL.API_URL_PREFIX + 'bisq/blocks/tip/height', routes.getBisqTip) - .get(config.MEMPOOL.API_URL_PREFIX + 'bisq/blocks/:index/:length', routes.getBisqBlocks) - .get(config.MEMPOOL.API_URL_PREFIX + 'bisq/address/:address', routes.getBisqAddress) - .get(config.MEMPOOL.API_URL_PREFIX + 'bisq/txs/:index/:length', routes.getBisqTransactions) - .get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/currencies', routes.getBisqMarketCurrencies.bind(routes)) - .get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/depth', routes.getBisqMarketDepth.bind(routes)) - .get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/hloc', routes.getBisqMarketHloc.bind(routes)) - .get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/markets', routes.getBisqMarketMarkets.bind(routes)) - .get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/offers', routes.getBisqMarketOffers.bind(routes)) - .get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/ticker', routes.getBisqMarketTicker.bind(routes)) - .get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/trades', routes.getBisqMarketTrades.bind(routes)) - .get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/volumes', routes.getBisqMarketVolumes.bind(routes)) - .get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/volumes/7d', routes.getBisqMarketVolumes7d.bind(routes)) - ; + bisqRoutes.initRoutes(this.app); } - - this.app - .get(config.MEMPOOL.API_URL_PREFIX + 'blocks', routes.getBlocks.bind(routes)) - .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', routes.getBlocks.bind(routes)) - .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', routes.getBlock) - .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', routes.getStrippedBlockTransactions); - - if (config.MEMPOOL.BACKEND !== 'esplora') { - this.app - .get(config.MEMPOOL.API_URL_PREFIX + 'mempool', routes.getMempool) - .get(config.MEMPOOL.API_URL_PREFIX + 'mempool/txids', routes.getMempoolTxIds) - .get(config.MEMPOOL.API_URL_PREFIX + 'mempool/recent', routes.getRecentMempoolTransactions) - .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId', routes.getTransaction) - .post(config.MEMPOOL.API_URL_PREFIX + 'tx', routes.$postTransaction) - .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/hex', routes.getRawTransaction) - .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', routes.getTransactionStatus) - .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', routes.getTransactionOutspends) - .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/header', routes.getBlockHeader) - .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', routes.getBlockTipHeight) - .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/hash', routes.getBlockTipHash) - .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs', routes.getBlockTransactions) - .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs/:index', routes.getBlockTransactions) - .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txids', routes.getTxIdsForBlock) - .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.getAddressTransactions) - .get(config.MEMPOOL.API_URL_PREFIX + 'address-prefix/:prefix', routes.getAddressPrefix) - ; - } - if (Common.isLiquid()) { - this.app - .get(config.MEMPOOL.API_URL_PREFIX + 'assets/icons', routes.getAllLiquidIcon) - .get(config.MEMPOOL.API_URL_PREFIX + 'assets/featured', routes.$getAllFeaturedLiquidAssets) - .get(config.MEMPOOL.API_URL_PREFIX + 'asset/:assetId/icon', routes.getLiquidIcon) - .get(config.MEMPOOL.API_URL_PREFIX + 'assets/group/:id', routes.$getAssetGroup) - ; + liquidRoutes.initRoutes(this.app); } - - if (Common.isLiquid() && config.DATABASE.ENABLED) { - this.app - .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs/month', routes.$getElementsPegsByMonth) - ; - } - if (config.LIGHTNING.ENABLED) { generalLightningRoutes.initRoutes(this.app); nodesRoutes.initRoutes(this.app); diff --git a/backend/src/indexer.ts b/backend/src/indexer.ts index e34c43e8d..7523af94e 100644 --- a/backend/src/indexer.ts +++ b/backend/src/indexer.ts @@ -1,7 +1,7 @@ import { Common } from './api/common'; import blocks from './api/blocks'; import mempool from './api/mempool'; -import mining from './api/mining'; +import mining from './api/mining/mining'; import logger from './logger'; import HashratesRepository from './repositories/HashratesRepository'; import bitcoinClient from './api/bitcoin/bitcoin-client'; diff --git a/backend/src/routes.ts b/backend/src/routes.ts deleted file mode 100644 index 0bd3aa7cf..000000000 --- a/backend/src/routes.ts +++ /dev/null @@ -1,1105 +0,0 @@ -import config from './config'; -import { Request, Response } from 'express'; -import statistics from './api/statistics'; -import feeApi from './api/fee-api'; -import backendInfo from './api/backend-info'; -import mempoolBlocks from './api/mempool-blocks'; -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 { RequiredSpec, TransactionExtended } from './mempool.interfaces'; -import { MarketsApiError } from './api/bisq/interfaces'; -import { IEsploraApi } from './api/bitcoin/esplora-api.interface'; -import logger from './logger'; -import bitcoinApi from './api/bitcoin/bitcoin-api-factory'; -import transactionUtils from './api/transaction-utils'; -import blocks from './api/blocks'; -import loadingIndicators from './api/loading-indicators'; -import { Common } from './api/common'; -import bitcoinClient from './api/bitcoin/bitcoin-client'; -import elementsParser from './api/liquid/elements-parser'; -import icons from './api/liquid/icons'; -import miningStats from './api/mining'; -import axios from 'axios'; -import mining from './api/mining'; -import BlocksRepository from './repositories/BlocksRepository'; -import HashratesRepository from './repositories/HashratesRepository'; -import difficultyAdjustment from './api/difficulty-adjustment'; -import DifficultyAdjustmentsRepository from './repositories/DifficultyAdjustmentsRepository'; -import BlocksAuditsRepository from './repositories/BlocksAuditsRepository'; - -class Routes { - constructor() {} - - public async $getStatisticsByTime(time: '2h' | '24h' | '1w' | '1m' | '3m' | '6m' | '1y' | '2y' | '3y', req: Request, res: Response) { - res.header('Pragma', 'public'); - res.header('Cache-control', 'public'); - res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); - - try { - let result; - switch (time as string) { - case '2h': - result = await statistics.$list2H(); - res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); - break; - case '24h': - result = await statistics.$list24H(); - res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); - break; - case '1w': - result = await statistics.$list1W(); - break; - case '1m': - result = await statistics.$list1M(); - break; - case '3m': - result = await statistics.$list3M(); - break; - case '6m': - result = await statistics.$list6M(); - break; - case '1y': - result = await statistics.$list1Y(); - break; - case '2y': - result = await statistics.$list2Y(); - break; - case '3y': - result = await statistics.$list3Y(); - break; - default: - result = await statistics.$list2H(); - } - res.json(result); - } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - - public getInitData(req: Request, res: Response) { - try { - const result = websocketHandler.getInitData(); - res.json(result); - } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - - public getRecommendedFees(req: Request, res: Response) { - if (!mempool.isInSync()) { - res.statusCode = 503; - res.send('Service Unavailable'); - return; - } - const result = feeApi.getRecommendedFee(); - res.json(result); - } - - public getMempoolBlocks(req: Request, res: Response) { - try { - const result = mempoolBlocks.getMempoolBlocks(); - res.json(result); - } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - - public getTransactionTimes(req: Request, res: Response) { - if (!Array.isArray(req.query.txId)) { - res.status(500).send('Not an array'); - return; - } - const txIds: string[] = []; - for (const _txId in req.query.txId) { - if (typeof req.query.txId[_txId] === 'string') { - txIds.push(req.query.txId[_txId].toString()); - } - } - - const times = mempool.getFirstSeenForTransactions(txIds); - res.json(times); - } - - public async $getBatchedOutspends(req: Request, res: Response) { - if (!Array.isArray(req.query.txId)) { - res.status(500).send('Not an array'); - return; - } - if (req.query.txId.length > 50) { - res.status(400).send('Too many txids requested'); - return; - } - const txIds: string[] = []; - for (const _txId in req.query.txId) { - if (typeof req.query.txId[_txId] === 'string') { - txIds.push(req.query.txId[_txId].toString()); - } - } - - try { - const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txIds); - res.json(batchedOutspends); - } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - - public getCpfpInfo(req: Request, res: Response) { - if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) { - res.status(501).send(`Invalid transaction ID.`); - return; - } - - const tx = mempool.getMempool()[req.params.txId]; - if (!tx) { - res.status(404).send(`Transaction doesn't exist in the mempool.`); - return; - } - - if (tx.cpfpChecked) { - res.json({ - ancestors: tx.ancestors, - bestDescendant: tx.bestDescendant || null, - }); - return; - } - - const cpfpInfo = Common.setRelativesAndGetCpfpInfo(tx, mempool.getMempool()); - - res.json(cpfpInfo); - } - - public getBackendInfo(req: Request, res: Response) { - res.json(backendInfo.getBackendInfo()); - } - - public getBisqStats(req: Request, res: Response) { - const result = bisq.getStats(); - res.json(result); - } - - public getBisqTip(req: Request, res: Response) { - const result = bisq.getLatestBlockHeight(); - res.type('text/plain'); - res.send(result.toString()); - } - - public getBisqTransaction(req: Request, res: Response) { - const result = bisq.getTransaction(req.params.txId); - if (result) { - res.json(result); - } else { - res.status(404).send('Bisq transaction not found'); - } - } - - public getBisqTransactions(req: Request, res: Response) { - const types: string[] = []; - req.query.types = req.query.types || []; - if (!Array.isArray(req.query.types)) { - res.status(500).send('Types is not an array'); - return; - } - - for (const _type in req.query.types) { - if (typeof req.query.types[_type] === 'string') { - types.push(req.query.types[_type].toString()); - } - } - - const index = parseInt(req.params.index, 10) || 0; - const length = parseInt(req.params.length, 10) > 100 ? 100 : parseInt(req.params.length, 10) || 25; - const [transactions, count] = bisq.getTransactions(index, length, types); - res.header('X-Total-Count', count.toString()); - res.json(transactions); - } - - public getBisqBlock(req: Request, res: Response) { - const result = bisq.getBlock(req.params.hash); - if (result) { - res.json(result); - } else { - res.status(404).send('Bisq block not found'); - } - } - - public getBisqBlocks(req: Request, res: Response) { - const index = parseInt(req.params.index, 10) || 0; - const length = parseInt(req.params.length, 10) > 100 ? 100 : parseInt(req.params.length, 10) || 25; - const [transactions, count] = bisq.getBlocks(index, length); - res.header('X-Total-Count', count.toString()); - res.json(transactions); - } - - public getBisqAddress(req: Request, res: Response) { - const result = bisq.getAddress(req.params.address.substr(1)); - if (result) { - res.json(result); - } else { - res.status(404).send('Bisq address not found'); - } - } - - public getBisqMarketCurrencies(req: Request, res: Response) { - const constraints: RequiredSpec = { - 'type': { - required: false, - types: ['crypto', 'fiat', 'all'] - }, - }; - - const p = this.parseRequestParameters(req.query, constraints); - if (p.error) { - res.status(400).json(this.getBisqMarketErrorResponse(p.error)); - return; - } - - const result = bisqMarket.getCurrencies(p.type); - if (result) { - res.json(result); - } else { - res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketCurrencies error')); - } - } - - public getBisqMarketDepth(req: Request, res: Response) { - const constraints: RequiredSpec = { - 'market': { - required: true, - types: ['@string'] - }, - }; - - const p = this.parseRequestParameters(req.query, constraints); - if (p.error) { - res.status(400).json(this.getBisqMarketErrorResponse(p.error)); - return; - } - - const result = bisqMarket.getDepth(p.market); - if (result) { - res.json(result); - } else { - res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketDepth error')); - } - } - - public getBisqMarketMarkets(req: Request, res: Response) { - const result = bisqMarket.getMarkets(); - if (result) { - res.json(result); - } else { - res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketMarkets error')); - } - } - - public getBisqMarketTrades(req: Request, res: Response) { - const constraints: RequiredSpec = { - 'market': { - required: true, - types: ['@string'] - }, - 'timestamp_from': { - required: false, - types: ['@number'] - }, - 'timestamp_to': { - required: false, - types: ['@number'] - }, - 'trade_id_to': { - required: false, - types: ['@string'] - }, - 'trade_id_from': { - required: false, - types: ['@string'] - }, - 'direction': { - required: false, - types: ['buy', 'sell'] - }, - 'limit': { - required: false, - types: ['@number'] - }, - 'sort': { - required: false, - types: ['asc', 'desc'] - } - }; - - const p = this.parseRequestParameters(req.query, constraints); - if (p.error) { - res.status(400).json(this.getBisqMarketErrorResponse(p.error)); - return; - } - - const result = bisqMarket.getTrades(p.market, p.timestamp_from, - p.timestamp_to, p.trade_id_from, p.trade_id_to, p.direction, p.limit, p.sort); - if (result) { - res.json(result); - } else { - res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketTrades error')); - } - } - - public getBisqMarketOffers(req: Request, res: Response) { - const constraints: RequiredSpec = { - 'market': { - required: true, - types: ['@string'] - }, - 'direction': { - required: false, - types: ['buy', 'sell'] - }, - }; - - const p = this.parseRequestParameters(req.query, constraints); - if (p.error) { - res.status(400).json(this.getBisqMarketErrorResponse(p.error)); - return; - } - - const result = bisqMarket.getOffers(p.market, p.direction); - if (result) { - res.json(result); - } else { - res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketOffers error')); - } - } - - public getBisqMarketVolumes(req: Request, res: Response) { - const constraints: RequiredSpec = { - 'market': { - required: false, - types: ['@string'] - }, - 'interval': { - required: false, - types: ['minute', 'half_hour', 'hour', 'half_day', 'day', 'week', 'month', 'year', 'auto'] - }, - 'timestamp_from': { - required: false, - types: ['@number'] - }, - 'timestamp_to': { - required: false, - types: ['@number'] - }, - 'milliseconds': { - required: false, - types: ['@boolean'] - }, - 'timestamp': { - required: false, - types: ['no', 'yes'] - }, - }; - - const p = this.parseRequestParameters(req.query, constraints); - if (p.error) { - res.status(400).json(this.getBisqMarketErrorResponse(p.error)); - return; - } - - const result = bisqMarket.getVolumes(p.market, p.timestamp_from, p.timestamp_to, p.interval, p.milliseconds, p.timestamp); - if (result) { - res.json(result); - } else { - res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketVolumes error')); - } - } - - public getBisqMarketHloc(req: Request, res: Response) { - const constraints: RequiredSpec = { - 'market': { - required: true, - types: ['@string'] - }, - 'interval': { - required: false, - types: ['minute', 'half_hour', 'hour', 'half_day', 'day', 'week', 'month', 'year', 'auto'] - }, - 'timestamp_from': { - required: false, - types: ['@number'] - }, - 'timestamp_to': { - required: false, - types: ['@number'] - }, - 'milliseconds': { - required: false, - types: ['@boolean'] - }, - 'timestamp': { - required: false, - types: ['no', 'yes'] - }, - }; - - const p = this.parseRequestParameters(req.query, constraints); - if (p.error) { - res.status(400).json(this.getBisqMarketErrorResponse(p.error)); - return; - } - - const result = bisqMarket.getHloc(p.market, p.interval, p.timestamp_from, p.timestamp_to, p.milliseconds, p.timestamp); - if (result) { - res.json(result); - } else { - res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketHloc error')); - } - } - - public getBisqMarketTicker(req: Request, res: Response) { - const constraints: RequiredSpec = { - 'market': { - required: false, - types: ['@string'] - }, - }; - - const p = this.parseRequestParameters(req.query, constraints); - if (p.error) { - res.status(400).json(this.getBisqMarketErrorResponse(p.error)); - return; - } - - const result = bisqMarket.getTicker(p.market); - if (result) { - res.json(result); - } else { - res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketTicker error')); - } - } - - public getBisqMarketVolumes7d(req: Request, res: Response) { - const result = bisqMarket.getVolumesByTime(604800); - if (result) { - res.json(result); - } else { - res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketVolumes7d error')); - } - } - - private parseRequestParameters(requestParams: object, params: RequiredSpec): { [name: string]: any; } { - const final = {}; - for (const i in params) { - if (params.hasOwnProperty(i)) { - if (params[i].required && requestParams[i] === undefined) { - return { error: i + ' parameter missing'}; - } - if (typeof requestParams[i] === 'string') { - const str = (requestParams[i] || '').toString().toLowerCase(); - if (params[i].types.indexOf('@number') > -1) { - const number = parseInt((str).toString(), 10); - final[i] = number; - } else if (params[i].types.indexOf('@string') > -1) { - final[i] = str; - } else if (params[i].types.indexOf('@boolean') > -1) { - final[i] = str === 'true' || str === 'yes'; - } else if (params[i].types.indexOf(str) > -1) { - final[i] = str; - } else { - return { error: i + ' parameter invalid'}; - } - } else if (typeof requestParams[i] === 'number') { - final[i] = requestParams[i]; - } - } - } - return final; - } - - private getBisqMarketErrorResponse(message: string): MarketsApiError { - return { - 'success': 0, - 'error': message - }; - } - - public async getTransaction(req: Request, res: Response) { - try { - const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true); - res.json(transaction); - } catch (e) { - let statusCode = 500; - if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { - statusCode = 404; - } - res.status(statusCode).send(e instanceof Error ? e.message : e); - } - } - - public async getRawTransaction(req: Request, res: Response) { - try { - const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(req.params.txId, true); - res.setHeader('content-type', 'text/plain'); - res.send(transaction.hex); - } catch (e) { - let statusCode = 500; - if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { - statusCode = 404; - } - res.status(statusCode).send(e instanceof Error ? e.message : e); - } - } - - public async getTransactionStatus(req: Request, res: Response) { - try { - const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true); - res.json(transaction.status); - } catch (e) { - let statusCode = 500; - if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { - statusCode = 404; - } - res.status(statusCode).send(e instanceof Error ? e.message : e); - } - } - - public async $getPool(req: Request, res: Response) { - try { - const stats = await mining.$getPoolStat(req.params.slug); - res.header('Pragma', 'public'); - res.header('Cache-control', 'public'); - res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); - res.json(stats); - } catch (e) { - if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { - res.status(404).send(e.message); - } else { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - } - - public async $getPoolBlocks(req: Request, res: Response) { - try { - const poolBlocks = await BlocksRepository.$getBlocksByPool( - req.params.slug, - req.params.height === undefined ? undefined : parseInt(req.params.height, 10), - ); - res.header('Pragma', 'public'); - res.header('Cache-control', 'public'); - res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); - res.json(poolBlocks); - } catch (e) { - if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { - res.status(404).send(e.message); - } else { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - } - - public async $getPools(req: Request, res: Response) { - try { - const stats = await miningStats.$getPoolsStats(req.params.interval); - const blockCount = await BlocksRepository.$blockCount(null, null); - res.header('Pragma', 'public'); - res.header('Cache-control', 'public'); - res.header('X-total-count', blockCount.toString()); - res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); - res.json(stats); - } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - - public async $getPoolsHistoricalHashrate(req: Request, res: Response) { - try { - const hashrates = await HashratesRepository.$getPoolsWeeklyHashrate(req.params.interval); - const blockCount = await BlocksRepository.$blockCount(null, null); - res.header('Pragma', 'public'); - res.header('Cache-control', 'public'); - res.header('X-total-count', blockCount.toString()); - res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); - res.json(hashrates); - } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - - public async $getPoolHistoricalHashrate(req: Request, res: Response) { - try { - const hashrates = await HashratesRepository.$getPoolWeeklyHashrate(req.params.slug); - const blockCount = await BlocksRepository.$blockCount(null, null); - res.header('Pragma', 'public'); - res.header('Cache-control', 'public'); - res.header('X-total-count', blockCount.toString()); - res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); - res.json(hashrates); - } catch (e) { - if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { - res.status(404).send(e.message); - } else { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - } - - public async $getHistoricalHashrate(req: Request, res: Response) { - let currentHashrate = 0, currentDifficulty = 0; - try { - currentHashrate = await bitcoinClient.getNetworkHashPs(); - currentDifficulty = await bitcoinClient.getDifficulty(); - } catch (e) { - logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate and difficulty'); - } - - try { - const hashrates = await HashratesRepository.$getNetworkDailyHashrate(req.params.interval); - const difficulty = await DifficultyAdjustmentsRepository.$getAdjustments(req.params.interval, false); - const blockCount = await BlocksRepository.$blockCount(null, null); - res.header('Pragma', 'public'); - res.header('Cache-control', 'public'); - res.header('X-total-count', blockCount.toString()); - res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); - res.json({ - hashrates: hashrates, - difficulty: difficulty, - currentHashrate: currentHashrate, - currentDifficulty: currentDifficulty, - }); - } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - - public async $getHistoricalBlockFees(req: Request, res: Response) { - try { - const blockFees = await mining.$getHistoricalBlockFees(req.params.interval); - const blockCount = await BlocksRepository.$blockCount(null, null); - res.header('Pragma', 'public'); - res.header('Cache-control', 'public'); - res.header('X-total-count', blockCount.toString()); - res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); - res.json(blockFees); - } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - - public async $getHistoricalBlockRewards(req: Request, res: Response) { - try { - const blockRewards = await mining.$getHistoricalBlockRewards(req.params.interval); - const blockCount = await BlocksRepository.$blockCount(null, null); - res.header('Pragma', 'public'); - res.header('Cache-control', 'public'); - res.header('X-total-count', blockCount.toString()); - res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); - res.json(blockRewards); - } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - - public async $getHistoricalBlockFeeRates(req: Request, res: Response) { - try { - const blockFeeRates = await mining.$getHistoricalBlockFeeRates(req.params.interval); - const blockCount = await BlocksRepository.$blockCount(null, null); - res.header('Pragma', 'public'); - res.header('Cache-control', 'public'); - res.header('X-total-count', blockCount.toString()); - res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); - res.json(blockFeeRates); - } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - - public async $getHistoricalBlockSizeAndWeight(req: Request, res: Response) { - try { - const blockSizes = await mining.$getHistoricalBlockSizes(req.params.interval); - const blockWeights = await mining.$getHistoricalBlockWeights(req.params.interval); - const blockCount = await BlocksRepository.$blockCount(null, null); - res.header('Pragma', 'public'); - res.header('Cache-control', 'public'); - res.header('X-total-count', blockCount.toString()); - res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); - res.json({ - sizes: blockSizes, - weights: blockWeights - }); - } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - - public async $getDifficultyAdjustments(req: Request, res: Response) { - try { - const difficulty = await DifficultyAdjustmentsRepository.$getRawAdjustments(req.params.interval, true); - res.header('Pragma', 'public'); - res.header('Cache-control', 'public'); - res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); - res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment])); - } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - - public async $getHistoricalBlockPrediction(req: Request, res: Response) { - try { - const blockPredictions = await mining.$getBlockPredictionsHistory(req.params.interval); - const blockCount = await BlocksAuditsRepository.$getPredictionsCount(); - res.header('Pragma', 'public'); - res.header('Cache-control', 'public'); - res.header('X-total-count', blockCount.toString()); - res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); - res.json(blockPredictions.map(prediction => [prediction.time, prediction.height, prediction.match_rate])); - } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - - public async getBlock(req: Request, res: Response) { - try { - const block = await blocks.$getBlock(req.params.hash); - - const blockAge = new Date().getTime() / 1000 - block.timestamp; - const day = 24 * 3600; - let cacheDuration; - if (blockAge > 365 * day) { - cacheDuration = 30 * day; - } else if (blockAge > 30 * day) { - cacheDuration = 10 * day; - } else { - cacheDuration = 600 - } - - res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString()); - res.json(block); - } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - - public async getBlockHeader(req: Request, res: Response) { - try { - const blockHeader = await bitcoinApi.$getBlockHeader(req.params.hash); - res.setHeader('content-type', 'text/plain'); - res.send(blockHeader); - } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - - public async getStrippedBlockTransactions(req: Request, res: Response) { - try { - const transactions = await blocks.$getStrippedBlockTransactions(req.params.hash); - res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); - res.json(transactions); - } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - - public async getBlocks(req: Request, res: Response) { - try { - if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin - const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10); - res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); - res.json(await blocks.$getBlocks(height, 15)); - } else { // Liquid, Bisq - return await this.getLegacyBlocks(req, res); - } - } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - - public async getLegacyBlocks(req: Request, res: Response) { - try { - const returnBlocks: IEsploraApi.Block[] = []; - const fromHeight = parseInt(req.params.height, 10) || blocks.getCurrentBlockHeight(); - - // Check if block height exist in local cache to skip the hash lookup - const blockByHeight = blocks.getBlocks().find((b) => b.height === fromHeight); - let startFromHash: string | null = null; - if (blockByHeight) { - startFromHash = blockByHeight.id; - } else { - startFromHash = await bitcoinApi.$getBlockHash(fromHeight); - } - - let nextHash = startFromHash; - for (let i = 0; i < 10 && nextHash; i++) { - const localBlock = blocks.getBlocks().find((b) => b.id === nextHash); - if (localBlock) { - returnBlocks.push(localBlock); - nextHash = localBlock.previousblockhash; - } else { - const block = await bitcoinApi.$getBlock(nextHash); - returnBlocks.push(block); - nextHash = block.previousblockhash; - } - } - - res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); - res.json(returnBlocks); - } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - - public async getBlockTransactions(req: Request, res: Response) { - try { - loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0); - - const txIds = await bitcoinApi.$getTxIdsForBlock(req.params.hash); - const transactions: TransactionExtended[] = []; - const startingIndex = Math.max(0, parseInt(req.params.index || '0', 10)); - - const endIndex = Math.min(startingIndex + 10, txIds.length); - for (let i = startingIndex; i < endIndex; i++) { - try { - const transaction = await transactionUtils.$getTransactionExtended(txIds[i], true, true); - transactions.push(transaction); - loadingIndicators.setProgress('blocktxs-' + req.params.hash, (i - startingIndex + 1) / (endIndex - startingIndex) * 100); - } catch (e) { - logger.debug('getBlockTransactions error: ' + (e instanceof Error ? e.message : e)); - } - } - res.json(transactions); - } catch (e) { - loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100); - res.status(500).send(e instanceof Error ? e.message : e); - } - } - - public async getBlockHeight(req: Request, res: Response) { - try { - const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10)); - res.send(blockHash); - } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - - public async getAddress(req: Request, res: Response) { - if (config.MEMPOOL.BACKEND === 'none') { - res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); - return; - } - - try { - const addressData = await bitcoinApi.$getAddress(req.params.address); - res.json(addressData); - } catch (e) { - if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { - return res.status(413).send(e instanceof Error ? e.message : e); - } - res.status(500).send(e instanceof Error ? e.message : e); - } - } - - public async getAddressTransactions(req: Request, res: Response) { - if (config.MEMPOOL.BACKEND === 'none') { - res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); - return; - } - - try { - const transactions = await bitcoinApi.$getAddressTransactions(req.params.address, req.params.txId); - res.json(transactions); - } catch (e) { - if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { - return res.status(413).send(e instanceof Error ? e.message : e); - } - res.status(500).send(e instanceof Error ? e.message : e); - } - } - - public async getAdressTxChain(req: Request, res: Response) { - res.status(501).send('Not implemented'); - } - - public async getAddressPrefix(req: Request, res: Response) { - try { - const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix); - res.send(blockHash); - } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - - public async getRecentMempoolTransactions(req: Request, res: Response) { - const latestTransactions = Object.entries(mempool.getMempool()) - .sort((a, b) => (b[1].firstSeen || 0) - (a[1].firstSeen || 0)) - .slice(0, 10).map((tx) => Common.stripTransaction(tx[1])); - - res.json(latestTransactions); - } - - public async getMempool(req: Request, res: Response) { - const info = mempool.getMempoolInfo(); - res.json({ - count: info.size, - vsize: info.bytes, - total_fee: info.total_fee * 1e8, - fee_histogram: [] - }); - } - - public async getMempoolTxIds(req: Request, res: Response) { - try { - const rawMempool = await bitcoinApi.$getRawMempool(); - res.send(rawMempool); - } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - - public async getBlockTipHeight(req: Request, res: Response) { - try { - const result = await bitcoinApi.$getBlockHeightTip(); - res.json(result); - } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - - public async getBlockTipHash(req: Request, res: Response) { - try { - const result = await bitcoinApi.$getBlockHashTip(); - res.setHeader('content-type', 'text/plain'); - res.send(result); - } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - - public async getTxIdsForBlock(req: Request, res: Response) { - try { - const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash); - res.json(result); - } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - - public async validateAddress(req: Request, res: Response) { - try { - const result = await bitcoinClient.validateAddress(req.params.address); - res.json(result); - } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - - public async getTransactionOutspends(req: Request, res: Response) { - try { - const result = await bitcoinApi.$getOutspends(req.params.txId); - res.json(result); - } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - - public getDifficultyChange(req: Request, res: Response) { - try { - res.json(difficultyAdjustment.getDifficultyAdjustment()); - } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - - public async $getElementsPegsByMonth(req: Request, res: Response) { - try { - const pegs = await elementsParser.$getPegDataByMonth(); - res.json(pegs); - } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - - public async $postTransaction(req: Request, res: Response) { - res.setHeader('content-type', 'text/plain'); - try { - let rawTx; - if (typeof req.body === 'object') { - rawTx = Object.keys(req.body)[0]; - } else { - rawTx = req.body; - } - const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx); - res.send(txIdResult); - } catch (e: any) { - res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) - : (e.message || 'Error')); - } - } - - public async $postTransactionForm(req: Request, res: Response) { - res.setHeader('content-type', 'text/plain'); - const matches = /tx=([a-z0-9]+)/.exec(req.body); - let txHex = ''; - if (matches && matches[1]) { - txHex = matches[1]; - } - try { - const txIdResult = await bitcoinClient.sendRawTransaction(txHex); - res.send(txIdResult); - } catch (e: any) { - res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) - : (e.message || 'Error')); - } - } - - public getLiquidIcon(req: Request, res: Response) { - const result = icons.getIconByAssetId(req.params.assetId); - if (result) { - res.setHeader('content-type', 'image/png'); - res.setHeader('content-length', result.length); - res.send(result); - } else { - res.status(404).send('Asset icon not found'); - } - } - - public getAllLiquidIcon(req: Request, res: Response) { - const result = icons.getAllIconIds(); - if (result) { - res.json(result); - } else { - res.status(404).send('Asset icons not found'); - } - } - - public async $getAllFeaturedLiquidAssets(req: Request, res: Response) { - try { - const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.LIQUID_API}/assets/featured`, { responseType: 'stream', timeout: 10000 }); - response.data.pipe(res); - } catch (e) { - res.status(500).end(); - } - } - - public async $getAssetGroup(req: Request, res: Response) { - try { - const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.LIQUID_API}/assets/group/${parseInt(req.params.id, 10)}`, - { responseType: 'stream', timeout: 10000 }); - response.data.pipe(res); - } catch (e) { - res.status(500).end(); - } - } - - public async $getRewardStats(req: Request, res: Response) { - try { - const response = await mining.$getRewardStats(parseInt(req.params.blockCount, 10)); - res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); - res.json(response); - } catch (e) { - res.status(500).end(); - } - } -} - -export default new Routes();