Refactoring backend routes code
This commit is contained in:
		
							parent
							
								
									97046a7dc4
								
							
						
					
					
						commit
						2253dd570d
					
				
							
								
								
									
										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 indexer from '../indexer'; | ||||||
| import poolsParser from './pools-parser'; | import poolsParser from './pools-parser'; | ||||||
| import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository'; | import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository'; | ||||||
| import mining from './mining'; | import mining from './mining/mining'; | ||||||
| import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmentsRepository'; | import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmentsRepository'; | ||||||
| import difficultyAdjustment from './difficulty-adjustment'; | 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 { IndexedDifficultyAdjustment, PoolInfo, PoolStats, RewardStats } from '../../mempool.interfaces'; | ||||||
| import BlocksRepository from '../repositories/BlocksRepository'; | import BlocksRepository from '../../repositories/BlocksRepository'; | ||||||
| import PoolsRepository from '../repositories/PoolsRepository'; | import PoolsRepository from '../../repositories/PoolsRepository'; | ||||||
| import HashratesRepository from '../repositories/HashratesRepository'; | import HashratesRepository from '../../repositories/HashratesRepository'; | ||||||
| import bitcoinClient from './bitcoin/bitcoin-client'; | import bitcoinClient from '../bitcoin/bitcoin-client'; | ||||||
| import logger from '../logger'; | import logger from '../../logger'; | ||||||
| import { Common } from './common'; | import { Common } from '../common'; | ||||||
| import loadingIndicators from './loading-indicators'; | import loadingIndicators from '../loading-indicators'; | ||||||
| import { escape } from 'mysql2'; | import { escape } from 'mysql2'; | ||||||
| import indexer from '../indexer'; | import indexer from '../../indexer'; | ||||||
| import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmentsRepository'; | import DifficultyAdjustmentsRepository from '../../repositories/DifficultyAdjustmentsRepository'; | ||||||
| import config from '../config'; | import config from '../../config'; | ||||||
| import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository'; | import BlocksAuditsRepository from '../../repositories/BlocksAuditsRepository'; | ||||||
| 
 | 
 | ||||||
| class Mining { | class Mining { | ||||||
|   constructor() { |   constructor() { | ||||||
| @ -1,160 +1,11 @@ | |||||||
| import memPool from './mempool'; | import DB from '../../database'; | ||||||
| import DB from '../database'; | import logger from '../../logger'; | ||||||
| import logger from '../logger'; | import { Statistic, OptimizedStatistic } from '../../mempool.interfaces'; | ||||||
| 
 | 
 | ||||||
| import { Statistic, TransactionExtended, OptimizedStatistic } from '../mempool.interfaces'; | class StatisticsApi { | ||||||
| import config from '../config'; |  | ||||||
| import { Common } from './common'; |  | ||||||
| 
 |  | ||||||
| class Statistics { |  | ||||||
|   protected intervalTimer: NodeJS.Timer | undefined; |  | ||||||
|   protected newStatisticsEntryCallback: ((stats: OptimizedStatistic) => void) | undefined; |  | ||||||
|   protected queryTimeout = 120000; |   protected queryTimeout = 120000; | ||||||
| 
 | 
 | ||||||
|   public setNewStatisticsEntryCallback(fn: (stats: OptimizedStatistic) => void) { |   public async $createZeroedStatistic(): Promise<number | undefined> { | ||||||
|     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> { |  | ||||||
|     try { |     try { | ||||||
|       const query = `INSERT INTO statistics(
 |       const query = `INSERT INTO statistics(
 | ||||||
|               added, |               added, | ||||||
| @ -212,7 +63,7 @@ class Statistics { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private async $create(statistics: Statistic): Promise<number | undefined> { |   public async $create(statistics: Statistic): Promise<number | undefined> { | ||||||
|     try { |     try { | ||||||
|       const query = `INSERT INTO statistics(
 |       const query = `INSERT INTO statistics(
 | ||||||
|               added, |               added, | ||||||
| @ -413,7 +264,7 @@ class Statistics { | |||||||
|       ORDER BY statistics.added DESC;`;
 |       ORDER BY statistics.added DESC;`;
 | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private async $get(id: number): Promise<OptimizedStatistic | undefined> { |   public async $get(id: number): Promise<OptimizedStatistic | undefined> { | ||||||
|     try { |     try { | ||||||
|       const query = `SELECT *, UNIX_TIMESTAMP(added) as added FROM statistics WHERE id = ?`; |       const query = `SELECT *, UNIX_TIMESTAMP(added) as added FROM statistics WHERE id = ?`; | ||||||
|       const [rows] = await DB.query(query, [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 http from 'http'; | ||||||
| import * as WebSocket from 'ws'; | import * as WebSocket from 'ws'; | ||||||
| import cluster from 'cluster'; | import cluster from 'cluster'; | ||||||
| import axios from 'axios'; |  | ||||||
| 
 |  | ||||||
| import DB from './database'; | import DB from './database'; | ||||||
| import config from './config'; | import config from './config'; | ||||||
| import routes from './routes'; |  | ||||||
| import blocks from './api/blocks'; | import blocks from './api/blocks'; | ||||||
| import memPool from './api/mempool'; | import memPool from './api/mempool'; | ||||||
| import diskCache from './api/disk-cache'; | import diskCache from './api/disk-cache'; | ||||||
| import statistics from './api/statistics'; | import statistics from './api/statistics/statistics'; | ||||||
| import websocketHandler from './api/websocket-handler'; | import websocketHandler from './api/websocket-handler'; | ||||||
| import fiatConversion from './api/fiat-conversion'; | import fiatConversion from './api/fiat-conversion'; | ||||||
| import bisq from './api/bisq/bisq'; | 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 generalLightningRoutes from './api/explorer/general.routes'; | ||||||
| import lightningStatsUpdater from './tasks/lightning/stats-updater.service'; | import lightningStatsUpdater from './tasks/lightning/stats-updater.service'; | ||||||
| import nodeSyncService from './tasks/lightning/node-sync.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 { | class Server { | ||||||
|   private wss: WebSocket.Server | undefined; |   private wss: WebSocket.Server | undefined; | ||||||
| @ -206,173 +207,19 @@ class Server { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   setUpHttpApiRoutes() { |   setUpHttpApiRoutes() { | ||||||
|     this.app |     bitcoinRoutes.initRoutes(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(); |  | ||||||
|         } |  | ||||||
|       }) |  | ||||||
|       ; |  | ||||||
| 
 |  | ||||||
|     if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED) { |     if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED) { | ||||||
|       this.app |       statisticsRoutes.initRoutes(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')) |  | ||||||
|         ; |  | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|     if (Common.indexingEnabled()) { |     if (Common.indexingEnabled()) { | ||||||
|       this.app |       miningRoutes.initRoutes(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) |  | ||||||
|         ; |  | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|     if (config.BISQ.ENABLED) { |     if (config.BISQ.ENABLED) { | ||||||
|       this.app |       bisqRoutes.initRoutes(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)) |  | ||||||
|         ; |  | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|     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()) { |     if (Common.isLiquid()) { | ||||||
|       this.app |       liquidRoutes.initRoutes(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) |  | ||||||
|         ; |  | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|     if (Common.isLiquid() && config.DATABASE.ENABLED) { |  | ||||||
|       this.app |  | ||||||
|         .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs/month', routes.$getElementsPegsByMonth) |  | ||||||
|         ; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (config.LIGHTNING.ENABLED) { |     if (config.LIGHTNING.ENABLED) { | ||||||
|       generalLightningRoutes.initRoutes(this.app); |       generalLightningRoutes.initRoutes(this.app); | ||||||
|       nodesRoutes.initRoutes(this.app); |       nodesRoutes.initRoutes(this.app); | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| import { Common } from './api/common'; | import { Common } from './api/common'; | ||||||
| import blocks from './api/blocks'; | import blocks from './api/blocks'; | ||||||
| import mempool from './api/mempool'; | import mempool from './api/mempool'; | ||||||
| import mining from './api/mining'; | import mining from './api/mining/mining'; | ||||||
| import logger from './logger'; | import logger from './logger'; | ||||||
| import HashratesRepository from './repositories/HashratesRepository'; | import HashratesRepository from './repositories/HashratesRepository'; | ||||||
| import bitcoinClient from './api/bitcoin/bitcoin-client'; | 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