mempool/backend/src/routes.ts

721 lines
21 KiB
TypeScript
Raw Normal View History

import config from './config';
import { Request, Response } from 'express';
2019-07-21 16:59:47 +02:00
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';
2020-09-10 09:46:23 +02:00
import bisq from './api/bisq/bisq';
import websocketHandler from './api/websocket-handler';
2020-09-10 09:46:23 +02:00
import bisqMarket from './api/bisq/markets-api';
import { RequiredSpec, TransactionExtended } from './mempool.interfaces';
2020-09-10 09:46:23 +02:00
import { MarketsApiError } from './api/bisq/interfaces';
2020-12-29 14:41:16 +01:00
import { IEsploraApi } from './api/bitcoin/esplora-api.interface';
import logger from './logger';
2020-12-21 17:08:34 +01:00
import bitcoinApi from './api/bitcoin/bitcoin-api-factory';
import transactionUtils from './api/transaction-utils';
2020-12-29 14:41:16 +01:00
import blocks from './api/blocks';
import loadingIndicators from './api/loading-indicators';
import { Common } from './api/common';
2019-07-21 16:59:47 +02:00
class Routes {
constructor() {}
2019-07-21 16:59:47 +02:00
2020-06-07 12:30:32 +02:00
public async get2HStatistics(req: Request, res: Response) {
2019-07-21 16:59:47 +02:00
const result = await statistics.$list2H();
2020-07-18 13:46:33 +02:00
res.json(result);
2019-07-21 16:59:47 +02:00
}
2020-06-07 12:30:32 +02:00
public get24HStatistics(req: Request, res: Response) {
res.json(statistics.getCache()['24h']);
2019-07-21 16:59:47 +02:00
}
2020-06-07 12:30:32 +02:00
public get1WHStatistics(req: Request, res: Response) {
res.json(statistics.getCache()['1w']);
2019-07-21 16:59:47 +02:00
}
2020-06-07 12:30:32 +02:00
public get1MStatistics(req: Request, res: Response) {
res.json(statistics.getCache()['1m']);
2019-07-21 16:59:47 +02:00
}
2020-06-07 12:30:32 +02:00
public get3MStatistics(req: Request, res: Response) {
res.json(statistics.getCache()['3m']);
2019-07-21 16:59:47 +02:00
}
2020-06-07 12:30:32 +02:00
public get6MStatistics(req: Request, res: Response) {
res.json(statistics.getCache()['6m']);
2019-07-21 16:59:47 +02:00
}
2020-06-07 12:30:32 +02:00
public get1YStatistics(req: Request, res: Response) {
res.json(statistics.getCache()['1y']);
2020-02-16 18:26:57 +01:00
}
public getInitData(req: Request, res: Response) {
try {
const result = websocketHandler.getInitData();
res.json(result);
} catch (e) {
res.status(500).send(e.message);
}
}
2020-06-07 12:30:32 +02:00
public async getRecommendedFees(req: Request, res: Response) {
if (!mempool.isInSync()) {
res.statusCode = 503;
res.send('Service Unavailable');
return;
}
2019-07-21 16:59:47 +02:00
const result = feeApi.getRecommendedFee();
2020-07-18 13:46:33 +02:00
res.json(result);
2019-07-21 16:59:47 +02:00
}
2020-06-07 12:30:32 +02:00
public getMempoolBlocks(req: Request, res: Response) {
2019-11-06 08:35:02 +01:00
try {
2020-06-07 12:30:32 +02:00
const result = mempoolBlocks.getMempoolBlocks();
2020-07-18 13:46:33 +02:00
res.json(result);
2019-11-06 08:35:02 +01:00
} catch (e) {
res.status(500).send(e.message);
}
}
2020-06-07 12:30:32 +02:00
public getTransactionTimes(req: Request, res: Response) {
if (!Array.isArray(req.query.txId)) {
res.status(500).send('Not an array');
return;
}
2020-06-07 12:30:32 +02:00
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);
2020-07-18 13:46:33 +02:00
res.json(times);
}
2020-05-26 13:06:14 +02:00
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);
}
2020-06-07 12:30:32 +02:00
public getBackendInfo(req: Request, res: Response) {
2020-07-18 13:46:33 +02:00
res.json(backendInfo.getBackendInfo());
2020-05-26 13:06:14 +02:00
}
2020-07-14 09:38:52 +02:00
public getBisqStats(req: Request, res: Response) {
const result = bisq.getStats();
2020-07-18 13:46:33 +02:00
res.json(result);
2020-07-14 09:38:52 +02:00
}
public getBisqTip(req: Request, res: Response) {
const result = bisq.getLatestBlockHeight();
2020-07-18 13:46:33 +02:00
res.type('text/plain');
res.send(result.toString());
}
public getBisqTransaction(req: Request, res: Response) {
const result = bisq.getTransaction(req.params.txId);
if (result) {
2020-07-18 13:46:33 +02:00
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);
2020-07-10 09:13:07 +02:00
res.header('X-Total-Count', count.toString());
2020-07-18 13:46:33 +02:00
res.json(transactions);
}
public getBisqBlock(req: Request, res: Response) {
const result = bisq.getBlock(req.params.hash);
if (result) {
2020-07-18 13:46:33 +02:00
res.json(result);
} else {
res.status(404).send('Bisq block not found');
}
}
2020-07-10 09:13:07 +02:00
public getBisqBlocks(req: Request, res: Response) {
2020-07-10 09:13:07 +02:00
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);
2020-07-10 09:13:07 +02:00
res.header('X-Total-Count', count.toString());
2020-07-18 13:46:33 +02:00
res.json(transactions);
2020-07-10 09:13:07 +02:00
}
2020-07-13 16:46:25 +02:00
public getBisqAddress(req: Request, res: Response) {
const result = bisq.getAddress(req.params.address.substr(1));
if (result) {
2020-07-18 13:46:33 +02:00
res.json(result);
2020-07-13 16:46:25 +02:00
} else {
res.status(404).send('Bisq address not found');
}
}
2020-09-10 09:46:23 +02:00
public getBisqMarketCurrencies(req: Request, res: Response) {
const constraints: RequiredSpec = {
'type': {
required: false,
types: ['crypto', 'fiat', 'all']
},
};
const p = this.parseRequestParameters(req.query, constraints);
2020-09-10 09:46:23 +02:00
if (p.error) {
2020-09-14 22:11:52 +02:00
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
2020-09-10 09:46:23 +02:00
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);
2020-09-10 09:46:23 +02:00
if (p.error) {
2020-09-14 22:11:52 +02:00
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
2020-09-10 09:46:23 +02:00
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);
2020-09-10 09:46:23 +02:00
if (p.error) {
2020-09-14 22:11:52 +02:00
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
2020-09-10 09:46:23 +02:00
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']
2020-09-10 09:46:23 +02:00
},
};
const p = this.parseRequestParameters(req.query, constraints);
2020-09-10 09:46:23 +02:00
if (p.error) {
2020-09-14 22:11:52 +02:00
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
2020-09-10 09:46:23 +02:00
return;
}
const result = bisqMarket.getOffers(p.market, p.direction);
2020-09-10 09:46:23 +02:00
if (result) {
res.json(result);
} else {
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketOffers error'));
}
}
public getBisqMarketVolumes(req: Request, res: Response) {
const constraints: RequiredSpec = {
'market': {
2020-09-14 22:11:52 +02:00
required: false,
2020-09-10 09:46:23 +02:00
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']
},
2020-09-14 22:11:52 +02:00
'milliseconds': {
required: false,
types: ['@boolean']
},
'timestamp': {
required: false,
types: ['no', 'yes']
},
2020-09-10 09:46:23 +02:00
};
const p = this.parseRequestParameters(req.query, constraints);
2020-09-10 09:46:23 +02:00
if (p.error) {
2020-09-14 22:11:52 +02:00
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
2020-09-10 09:46:23 +02:00
return;
}
const result = bisqMarket.getVolumes(p.market, p.timestamp_from, p.timestamp_to, p.interval, p.milliseconds, p.timestamp);
2020-09-10 09:46:23 +02:00
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']
},
2020-09-13 12:51:53 +02:00
'milliseconds': {
required: false,
types: ['@boolean']
},
'timestamp': {
required: false,
types: ['no', 'yes']
},
2020-09-10 09:46:23 +02:00
};
const p = this.parseRequestParameters(req.query, constraints);
2020-09-10 09:46:23 +02:00
if (p.error) {
2020-09-14 22:11:52 +02:00
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
2020-09-10 09:46:23 +02:00
return;
}
const result = bisqMarket.getHloc(p.market, p.interval, p.timestamp_from, p.timestamp_to, p.milliseconds, p.timestamp);
2020-09-10 09:46:23 +02:00
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);
2020-09-10 09:46:23 +02:00
if (p.error) {
2020-09-14 22:11:52 +02:00
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
2020-09-10 09:46:23 +02:00
return;
}
2020-09-14 22:11:52 +02:00
const result = bisqMarket.getTicker(p.market);
2020-09-10 09:46:23 +02:00
if (result) {
res.json(result);
} else {
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketTicker error'));
}
}
2021-03-10 17:02:55 +01:00
public getBisqMarketVolumes7d(req: Request, res: Response) {
const result = bisqMarket.getVolumesByTime(604800);
if (result) {
res.json(result);
} else {
2021-03-10 17:02:55 +01:00
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;
2020-09-13 12:51:53 +02:00
} 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;
}
2020-09-10 09:46:23 +02:00
private getBisqMarketErrorResponse(message: string): MarketsApiError {
return {
'success': 0,
'error': message
};
}
2020-12-21 17:08:34 +01:00
public async getTransaction(req: Request, res: Response) {
try {
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true);
res.json(transaction);
2020-12-21 17:08:34 +01:00
} catch (e) {
let statusCode = 500;
if (e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404;
}
res.status(statusCode).send(e.message || e);
2020-12-21 17:08:34 +01:00
}
}
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.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404;
}
res.status(statusCode).send(e.message || e);
}
}
2020-12-21 17:08:34 +01:00
public async getBlock(req: Request, res: Response) {
try {
const result = await bitcoinApi.$getBlock(req.params.hash);
res.json(result);
} catch (e) {
res.status(500).send(e.message || e);
2020-12-21 17:08:34 +01:00
}
}
public async getBlockHeader(req: Request, res: Response) {
try {
const blockHeader = await bitcoinApi.$getBlockHeader(req.params.hash);
res.send(blockHeader);
} catch (e) {
res.status(500).send(e.message || e);
}
}
2020-12-21 17:08:34 +01:00
public async getBlocks(req: Request, res: Response) {
2020-12-29 14:41:16 +01:00
try {
loadingIndicators.setProgress('blocks', 0);
2020-12-29 14:41:16 +01:00
const returnBlocks: IEsploraApi.Block[] = [];
2020-12-29 19:47:07 +01:00
const fromHeight = parseInt(req.params.height, 10) || blocks.getCurrentBlockHeight();
2020-12-29 14:41:16 +01:00
2020-12-29 19:47:07 +01:00
// Check if block height exist in local cache to skip the hash lookup
const blockByHeight = blocks.getBlocks().find((b) => b.height === fromHeight);
2020-12-29 14:41:16 +01:00
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; i++) {
2020-12-29 19:47:07 +01:00
const localBlock = blocks.getBlocks().find((b) => b.id === nextHash);
2020-12-29 14:41:16 +01:00
if (localBlock) {
returnBlocks.push(localBlock);
nextHash = localBlock.previousblockhash;
} else {
const block = await bitcoinApi.$getBlock(nextHash);
returnBlocks.push(block);
nextHash = block.previousblockhash;
}
loadingIndicators.setProgress('blocks', i / 10 * 100);
2020-12-29 14:41:16 +01:00
}
res.json(returnBlocks);
} catch (e) {
loadingIndicators.setProgress('blocks', 100);
res.status(500).send(e.message || e);
2020-12-29 14:41:16 +01:00
}
2020-12-21 17:08:34 +01:00
}
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);
transactions.push(transaction);
loadingIndicators.setProgress('blocktxs-' + req.params.hash, (i + 1) / endIndex * 100);
} catch (e) {
logger.debug('getBlockTransactions error: ' + e.message || e);
}
}
res.json(transactions);
} catch (e) {
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100);
res.status(500).send(e.message || e);
}
2020-12-21 17:08:34 +01:00
}
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.message || e);
}
2020-12-21 17:08:34 +01:00
}
public async getAddress(req: Request, res: Response) {
if (config.MEMPOOL.BACKEND === 'none') {
2020-12-22 00:04:31 +01:00
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);
2020-12-21 17:08:34 +01:00
} catch (e) {
if (e.message && e.message.indexOf('exceeds') > 0) {
return res.status(413).send(e.message);
}
res.status(500).send(e.message || e);
2020-12-21 17:08:34 +01:00
}
}
public async getAddressTransactions(req: Request, res: Response) {
if (config.MEMPOOL.BACKEND === 'none') {
2020-12-22 00:04:31 +01:00
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);
2020-12-22 00:04:31 +01:00
res.json(transactions);
} catch (e) {
if (e.message && e.message.indexOf('exceeds') > 0) {
return res.status(413).send(e.message);
}
res.status(500).send(e.message || e);
2020-12-22 00:04:31 +01:00
}
2020-12-21 17:08:34 +01:00
}
public async getAdressTxChain(req: Request, res: Response) {
res.status(501).send('Not implemented');
2020-12-21 17:08:34 +01:00
}
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.message || e);
}
2020-12-21 17:08:34 +01:00
}
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) {
res.status(501).send('Not implemented');
}
public async getMempoolTxIds(req: Request, res: Response) {
try {
const rawMempool = await bitcoinApi.$getRawMempool();
res.send(rawMempool);
} catch (e) {
res.status(500).send(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.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.message || e);
}
}
2020-12-21 17:08:34 +01:00
public getTransactionOutspends(req: Request, res: Response) {
res.status(501).send('Not implemented');
2020-12-21 17:08:34 +01:00
}
public getDifficultyChange(req: Request, res: Response) {
try {
const now = new Date().getTime() / 1000;
const DATime=blocks.getLastDifficultyAdjustmentTime();
const diff = now - DATime;
const blockHeight=blocks.getCurrentBlockHeight();
const blocksInEpoch = blockHeight % 2016;
const estimatedBlocks = Math.round(diff / 60 / 10);
const difficultyChange = (600 / (diff / blocksInEpoch ) - 1) * 100;
const timeAvgDiff = difficultyChange * 0.1;
let timeAvgMins = 10;
if (timeAvgDiff > 0 ){
timeAvgMins -= Math.abs(timeAvgDiff);
} else {
timeAvgMins += Math.abs(timeAvgDiff);
}
const remainingBlocks = 2016 - blocksInEpoch;
const timeAvgSeconds = timeAvgMins * 60;
const remainingTime = remainingBlocks * timeAvgSeconds;
const estimatedRetargetDate=(remainingTime + now);
const totalTime=estimatedRetargetDate-DATime;
const progressPercent=100-((remainingTime*100)/totalTime);
const result={
progressPercent,
difficultyChange,
estimatedRetargetDate,
remainingBlocks,
remainingTime,
}
res.json(result);
} catch (e) {
res.status(500).send(e.message || e);
}
}
2019-07-21 16:59:47 +02:00
}
export default new Routes();