2019-07-21 16:59:47 +02:00
|
|
|
const config = require('../mempool-config.json');
|
|
|
|
import * as fs from 'fs';
|
|
|
|
import * as express from 'express';
|
|
|
|
import * as compression from 'compression';
|
|
|
|
import * as http from 'http';
|
|
|
|
import * as https from 'https';
|
|
|
|
import * as WebSocket from 'ws';
|
|
|
|
|
2019-10-22 11:09:07 +02:00
|
|
|
import bitcoinApi from './api/bitcoin/bitcoin-api-factory';
|
2019-07-21 16:59:47 +02:00
|
|
|
import diskCache from './api/disk-cache';
|
|
|
|
import memPool from './api/mempool';
|
|
|
|
import blocks from './api/blocks';
|
|
|
|
import projectedBlocks from './api/projected-blocks';
|
|
|
|
import statistics from './api/statistics';
|
2019-07-26 11:48:32 +02:00
|
|
|
import { IBlock, IMempool, ITransaction, IMempoolStats } from './interfaces';
|
2019-07-21 16:59:47 +02:00
|
|
|
|
|
|
|
import routes from './routes';
|
|
|
|
import fiatConversion from './api/fiat-conversion';
|
|
|
|
|
|
|
|
class MempoolSpace {
|
|
|
|
private wss: WebSocket.Server;
|
|
|
|
private server: https.Server | http.Server;
|
|
|
|
private app: any;
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
this.app = express();
|
|
|
|
this.app
|
|
|
|
.use((req, res, next) => {
|
|
|
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
|
|
next();
|
|
|
|
})
|
|
|
|
.use(compression());
|
|
|
|
if (config.ENV === 'dev') {
|
|
|
|
this.server = http.createServer(this.app);
|
|
|
|
this.wss = new WebSocket.Server({ server: this.server });
|
|
|
|
} else {
|
|
|
|
const credentials = {
|
|
|
|
cert: fs.readFileSync('/etc/letsencrypt/live/mempool.space/fullchain.pem'),
|
|
|
|
key: fs.readFileSync('/etc/letsencrypt/live/mempool.space/privkey.pem'),
|
|
|
|
};
|
|
|
|
this.server = https.createServer(credentials, this.app);
|
|
|
|
this.wss = new WebSocket.Server({ server: this.server });
|
|
|
|
}
|
|
|
|
|
|
|
|
this.setUpRoutes();
|
|
|
|
this.setUpWebsocketHandling();
|
|
|
|
this.setUpMempoolCache();
|
|
|
|
this.runMempoolIntervalFunctions();
|
|
|
|
|
|
|
|
statistics.startStatistics();
|
|
|
|
fiatConversion.startService();
|
|
|
|
|
2019-08-29 01:05:46 +02:00
|
|
|
const opts = {
|
|
|
|
host: '127.0.0.1',
|
2019-08-23 15:27:52 +02:00
|
|
|
port: 8999
|
|
|
|
};
|
|
|
|
this.server.listen(opts, () => {
|
2019-10-22 11:09:07 +02:00
|
|
|
console.log(`Server started on ${opts.host}:${opts.port}`);
|
2019-07-21 16:59:47 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private async runMempoolIntervalFunctions() {
|
|
|
|
await blocks.updateBlocks();
|
2019-07-26 11:48:32 +02:00
|
|
|
await memPool.updateMemPoolInfo();
|
2019-07-21 16:59:47 +02:00
|
|
|
await memPool.updateMempool();
|
|
|
|
setTimeout(this.runMempoolIntervalFunctions.bind(this), config.MEMPOOL_REFRESH_RATE_MS);
|
|
|
|
}
|
|
|
|
|
|
|
|
private setUpMempoolCache() {
|
|
|
|
const cacheData = diskCache.loadData();
|
|
|
|
if (cacheData) {
|
|
|
|
memPool.setMempool(JSON.parse(cacheData));
|
|
|
|
}
|
|
|
|
|
|
|
|
process.on('SIGINT', (options) => {
|
|
|
|
console.log('SIGINT');
|
|
|
|
diskCache.saveData(JSON.stringify(memPool.getMempool()));
|
|
|
|
process.exit(2);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private setUpWebsocketHandling() {
|
|
|
|
this.wss.on('connection', (client: WebSocket) => {
|
|
|
|
let theBlocks = blocks.getBlocks();
|
|
|
|
theBlocks = theBlocks.concat([]).splice(theBlocks.length - config.INITIAL_BLOCK_AMOUNT);
|
|
|
|
const formatedBlocks = theBlocks.map((b) => blocks.formatBlock(b));
|
|
|
|
|
|
|
|
client.send(JSON.stringify({
|
|
|
|
'mempoolInfo': memPool.getMempoolInfo(),
|
|
|
|
'blocks': formatedBlocks,
|
|
|
|
'projectedBlocks': projectedBlocks.getProjectedBlocks(),
|
|
|
|
'txPerSecond': memPool.getTxPerSecond(),
|
|
|
|
'vBytesPerSecond': memPool.getVBytesPerSecond(),
|
|
|
|
'conversions': fiatConversion.getTickers()['BTCUSD'],
|
|
|
|
}));
|
|
|
|
|
|
|
|
client.on('message', async (message: any) => {
|
|
|
|
try {
|
|
|
|
const parsedMessage = JSON.parse(message);
|
2019-07-26 11:48:32 +02:00
|
|
|
|
|
|
|
if (parsedMessage.action === 'want') {
|
|
|
|
client['want-stats'] = parsedMessage.data.indexOf('stats') > -1;
|
|
|
|
client['want-blocks'] = parsedMessage.data.indexOf('blocks') > -1;
|
|
|
|
client['want-projected-blocks'] = parsedMessage.data.indexOf('projected-blocks') > -1;
|
|
|
|
client['want-live-2h-chart'] = parsedMessage.data.indexOf('live-2h-chart') > -1;
|
|
|
|
}
|
|
|
|
|
2019-07-21 16:59:47 +02:00
|
|
|
if (parsedMessage.action === 'track-tx' && parsedMessage.txId && /^[a-fA-F0-9]{64}$/.test(parsedMessage.txId)) {
|
|
|
|
const tx = await memPool.getRawTransaction(parsedMessage.txId);
|
|
|
|
if (tx) {
|
|
|
|
console.log('Now tracking: ' + parsedMessage.txId);
|
|
|
|
client['trackingTx'] = true;
|
|
|
|
client['txId'] = parsedMessage.txId;
|
|
|
|
client['tx'] = tx;
|
|
|
|
|
|
|
|
if (tx.blockhash) {
|
|
|
|
const currentBlocks = blocks.getBlocks();
|
|
|
|
const foundBlock = currentBlocks.find((block) => block.tx && block.tx.some((i: string) => i === parsedMessage.txId));
|
|
|
|
if (foundBlock) {
|
|
|
|
console.log('Found block by looking in local cache');
|
|
|
|
client['blockHeight'] = foundBlock.height;
|
|
|
|
} else {
|
|
|
|
const theBlock = await bitcoinApi.getBlock(tx.blockhash);
|
|
|
|
if (theBlock) {
|
|
|
|
client['blockHeight'] = theBlock.height;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
client['blockHeight'] = 0;
|
|
|
|
}
|
|
|
|
client.send(JSON.stringify({
|
|
|
|
'projectedBlocks': projectedBlocks.getProjectedBlocks(client['txId']),
|
|
|
|
'track-tx': {
|
|
|
|
tracking: true,
|
|
|
|
blockHeight: client['blockHeight'],
|
|
|
|
tx: client['tx'],
|
|
|
|
}
|
|
|
|
}));
|
|
|
|
} else {
|
|
|
|
console.log('TX NOT FOUND, NOT TRACKING');
|
2019-07-25 13:20:02 +02:00
|
|
|
client['trackingTx'] = false;
|
|
|
|
client['blockHeight'] = 0;
|
|
|
|
client['tx'] = null;
|
2019-07-21 16:59:47 +02:00
|
|
|
client.send(JSON.stringify({
|
|
|
|
'track-tx': {
|
|
|
|
tracking: false,
|
|
|
|
blockHeight: 0,
|
|
|
|
message: 'not-found',
|
|
|
|
}
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (parsedMessage.action === 'stop-tracking-tx') {
|
|
|
|
console.log('STOP TRACKING');
|
|
|
|
client['trackingTx'] = false;
|
|
|
|
client.send(JSON.stringify({
|
|
|
|
'track-tx': {
|
|
|
|
tracking: false,
|
|
|
|
blockHeight: 0,
|
|
|
|
message: 'not-found',
|
|
|
|
}
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
console.log(e);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
client.on('close', () => {
|
|
|
|
client['trackingTx'] = false;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
blocks.setNewBlockCallback((block: IBlock) => {
|
|
|
|
const formattedBlocks = blocks.formatBlock(block);
|
|
|
|
|
|
|
|
this.wss.clients.forEach((client) => {
|
|
|
|
if (client.readyState !== WebSocket.OPEN) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-07-26 11:48:32 +02:00
|
|
|
const response = {};
|
|
|
|
|
2019-07-21 16:59:47 +02:00
|
|
|
if (client['trackingTx'] === true && client['blockHeight'] === 0) {
|
2019-07-26 11:48:32 +02:00
|
|
|
if (block.tx.some((tx: ITransaction) => tx === client['txId'])) {
|
2019-07-21 16:59:47 +02:00
|
|
|
client['blockHeight'] = block.height;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-26 11:48:32 +02:00
|
|
|
response['track-tx'] = {
|
|
|
|
tracking: client['trackingTx'] || false,
|
|
|
|
blockHeight: client['blockHeight'],
|
|
|
|
};
|
|
|
|
|
|
|
|
response['block'] = formattedBlocks;
|
|
|
|
|
|
|
|
client.send(JSON.stringify(response));
|
2019-07-21 16:59:47 +02:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
memPool.setMempoolChangedCallback((newMempool: IMempool) => {
|
|
|
|
projectedBlocks.updateProjectedBlocks(newMempool);
|
|
|
|
|
2019-07-26 11:48:32 +02:00
|
|
|
const pBlocks = projectedBlocks.getProjectedBlocks();
|
2019-07-21 16:59:47 +02:00
|
|
|
const mempoolInfo = memPool.getMempoolInfo();
|
|
|
|
const txPerSecond = memPool.getTxPerSecond();
|
|
|
|
const vBytesPerSecond = memPool.getVBytesPerSecond();
|
|
|
|
|
|
|
|
this.wss.clients.forEach((client: WebSocket) => {
|
|
|
|
if (client.readyState !== WebSocket.OPEN) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-07-26 11:48:32 +02:00
|
|
|
const response = {};
|
2019-07-21 16:59:47 +02:00
|
|
|
|
2019-07-26 11:48:32 +02:00
|
|
|
if (client['want-stats']) {
|
|
|
|
response['mempoolInfo'] = mempoolInfo;
|
|
|
|
response['txPerSecond'] = txPerSecond;
|
|
|
|
response['vBytesPerSecond'] = vBytesPerSecond;
|
|
|
|
response['track-tx'] = {
|
2019-07-21 16:59:47 +02:00
|
|
|
tracking: client['trackingTx'] || false,
|
|
|
|
blockHeight: client['blockHeight'],
|
2019-07-26 11:48:32 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (client['want-projected-blocks'] && client['trackingTx'] && client['blockHeight'] === 0) {
|
|
|
|
response['projectedBlocks'] = projectedBlocks.getProjectedBlocks(client['txId']);
|
|
|
|
} else if (client['want-projected-blocks']) {
|
|
|
|
response['projectedBlocks'] = pBlocks;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (Object.keys(response).length) {
|
|
|
|
client.send(JSON.stringify(response));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
statistics.setNewStatisticsEntryCallback((stats: IMempoolStats) => {
|
|
|
|
this.wss.clients.forEach((client: WebSocket) => {
|
|
|
|
if (client.readyState !== WebSocket.OPEN) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (client['want-live-2h-chart']) {
|
|
|
|
client.send(JSON.stringify({
|
|
|
|
'live-2h-chart': stats
|
|
|
|
}));
|
|
|
|
}
|
2019-07-21 16:59:47 +02:00
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private setUpRoutes() {
|
|
|
|
this.app
|
|
|
|
.get(config.API_ENDPOINT + 'transactions/height/:id', routes.$getgetTransactionsForBlock)
|
|
|
|
.get(config.API_ENDPOINT + 'transactions/projected/:id', routes.getgetTransactionsForProjectedBlock)
|
|
|
|
.get(config.API_ENDPOINT + 'fees/recommended', routes.getRecommendedFees)
|
2019-07-26 16:51:12 +02:00
|
|
|
.get(config.API_ENDPOINT + 'fees/projected-blocks', routes.getProjectedBlocks)
|
2019-07-21 16:59:47 +02:00
|
|
|
.get(config.API_ENDPOINT + 'statistics/2h', routes.get2HStatistics)
|
2019-08-15 13:06:08 +02:00
|
|
|
.get(config.API_ENDPOINT + 'statistics/24h', routes.get24HStatistics.bind(routes))
|
|
|
|
.get(config.API_ENDPOINT + 'statistics/1w', routes.get1WHStatistics.bind(routes))
|
|
|
|
.get(config.API_ENDPOINT + 'statistics/1m', routes.get1MStatistics.bind(routes))
|
|
|
|
.get(config.API_ENDPOINT + 'statistics/3m', routes.get3MStatistics.bind(routes))
|
|
|
|
.get(config.API_ENDPOINT + 'statistics/6m', routes.get6MStatistics.bind(routes))
|
2019-07-21 16:59:47 +02:00
|
|
|
;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const mempoolSpace = new MempoolSpace();
|