Merge pull request #2076 from mempool/simon/backend-routes-refactor
This commit is contained in:
		
						commit
						75bb586f38
					
				
							
								
								
									
										381
									
								
								backend/src/api/bisq/bisq.routes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										381
									
								
								backend/src/api/bisq/bisq.routes.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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; | ||||
							
								
								
									
										543
									
								
								backend/src/api/bitcoin/bitcoin.routes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										543
									
								
								backend/src/api/bitcoin/bitcoin.routes.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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(); | ||||
| @ -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'; | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										73
									
								
								backend/src/api/liquid/liquid.routes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								backend/src/api/liquid/liquid.routes.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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(); | ||||
							
								
								
									
										238
									
								
								backend/src/api/mining/mining-routes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										238
									
								
								backend/src/api/mining/mining-routes.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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<void> { | ||||
|     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(); | ||||
| @ -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() { | ||||
| @ -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<void> { | ||||
|     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<number | undefined> { | ||||
|   public async $createZeroedStatistic(): Promise<number | undefined> { | ||||
|     try { | ||||
|       const query = `INSERT INTO statistics(
 | ||||
|               added, | ||||
| @ -212,7 +63,7 @@ class Statistics { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private async $create(statistics: Statistic): Promise<number | undefined> { | ||||
|   public async $create(statistics: Statistic): Promise<number | undefined> { | ||||
|     try { | ||||
|       const query = `INSERT INTO statistics(
 | ||||
|               added, | ||||
| @ -413,7 +264,7 @@ class Statistics { | ||||
|       ORDER BY statistics.added DESC;`;
 | ||||
|   } | ||||
| 
 | ||||
|   private async $get(id: number): Promise<OptimizedStatistic | undefined> { | ||||
|   public async $get(id: number): Promise<OptimizedStatistic | undefined> { | ||||
|     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(); | ||||
							
								
								
									
										67
									
								
								backend/src/api/statistics/statistics.routes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								backend/src/api/statistics/statistics.routes.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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(); | ||||
							
								
								
									
										153
									
								
								backend/src/api/statistics/statistics.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								backend/src/api/statistics/statistics.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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<void> { | ||||
|     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(); | ||||
| @ -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); | ||||
|  | ||||
| @ -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'; | ||||
|  | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user