2019-07-21 16:59:47 +02:00
|
|
|
const config = require('../../mempool-config.json');
|
2020-02-16 16:15:07 +01:00
|
|
|
import bitcoinApi from './bitcoin/electrs-api';
|
2020-06-10 18:52:14 +02:00
|
|
|
import { MempoolInfo, TransactionExtended, Transaction, VbytesPerSecond } from '../interfaces';
|
2020-10-13 10:27:52 +02:00
|
|
|
import logger from '../logger';
|
2020-09-25 21:11:30 +02:00
|
|
|
import { Common } from './common';
|
2019-07-21 16:59:47 +02:00
|
|
|
|
|
|
|
class Mempool {
|
2020-04-01 15:06:44 +02:00
|
|
|
private inSync: boolean = false;
|
2020-06-07 12:30:32 +02:00
|
|
|
private mempoolCache: { [txId: string]: TransactionExtended } = {};
|
2020-04-03 20:11:46 +02:00
|
|
|
private mempoolInfo: MempoolInfo = { size: 0, bytes: 0 };
|
2020-06-08 21:08:46 +02:00
|
|
|
private mempoolChangedCallback: ((newMempool: { [txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
|
|
|
|
deletedTransactions: TransactionExtended[]) => void) | undefined;
|
2019-07-21 16:59:47 +02:00
|
|
|
|
|
|
|
private txPerSecondArray: number[] = [];
|
|
|
|
private txPerSecond: number = 0;
|
|
|
|
|
2020-06-10 18:52:14 +02:00
|
|
|
private vBytesPerSecondArray: VbytesPerSecond[] = [];
|
2019-07-21 16:59:47 +02:00
|
|
|
private vBytesPerSecond: number = 0;
|
2020-06-18 08:54:54 +02:00
|
|
|
private mempoolProtection = 0;
|
2020-09-25 21:11:30 +02:00
|
|
|
private latestTransactions: any[] = [];
|
2019-07-21 16:59:47 +02:00
|
|
|
|
|
|
|
constructor() {
|
|
|
|
setInterval(this.updateTxPerSecond.bind(this), 1000);
|
|
|
|
}
|
|
|
|
|
2020-04-01 15:06:44 +02:00
|
|
|
public isInSync() {
|
|
|
|
return this.inSync;
|
|
|
|
}
|
|
|
|
|
2020-09-25 21:11:30 +02:00
|
|
|
public getLatestTransactions() {
|
|
|
|
return this.latestTransactions;
|
|
|
|
}
|
|
|
|
|
2020-06-08 21:08:46 +02:00
|
|
|
public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: TransactionExtended; },
|
|
|
|
newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]) => void) {
|
2019-07-21 16:59:47 +02:00
|
|
|
this.mempoolChangedCallback = fn;
|
|
|
|
}
|
|
|
|
|
2020-02-23 13:16:50 +01:00
|
|
|
public getMempool(): { [txid: string]: TransactionExtended } {
|
2020-02-16 16:15:07 +01:00
|
|
|
return this.mempoolCache;
|
2019-07-21 16:59:47 +02:00
|
|
|
}
|
|
|
|
|
2020-06-07 12:30:32 +02:00
|
|
|
public setMempool(mempoolData: { [txId: string]: TransactionExtended }) {
|
2020-02-16 16:15:07 +01:00
|
|
|
this.mempoolCache = mempoolData;
|
2020-07-03 18:45:19 +02:00
|
|
|
if (this.mempoolChangedCallback) {
|
|
|
|
this.mempoolChangedCallback(this.mempoolCache, [], []);
|
|
|
|
}
|
2019-07-21 16:59:47 +02:00
|
|
|
}
|
|
|
|
|
2020-02-17 14:39:20 +01:00
|
|
|
public async updateMemPoolInfo() {
|
|
|
|
try {
|
|
|
|
this.mempoolInfo = await bitcoinApi.getMempoolInfo();
|
2020-10-13 10:27:52 +02:00
|
|
|
} catch (e) {
|
|
|
|
logger.err('Error getMempoolInfo ' + e.message || e);
|
2020-02-17 14:39:20 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-16 16:15:07 +01:00
|
|
|
public getMempoolInfo(): MempoolInfo | undefined {
|
2019-07-21 16:59:47 +02:00
|
|
|
return this.mempoolInfo;
|
|
|
|
}
|
|
|
|
|
|
|
|
public getTxPerSecond(): number {
|
|
|
|
return this.txPerSecond;
|
|
|
|
}
|
|
|
|
|
|
|
|
public getVBytesPerSecond(): number {
|
|
|
|
return this.vBytesPerSecond;
|
|
|
|
}
|
|
|
|
|
2020-02-27 19:09:07 +01:00
|
|
|
public getFirstSeenForTransactions(txIds: string[]): number[] {
|
|
|
|
const txTimes: number[] = [];
|
|
|
|
txIds.forEach((txId: string) => {
|
|
|
|
if (this.mempoolCache[txId]) {
|
|
|
|
txTimes.push(this.mempoolCache[txId].firstSeen);
|
|
|
|
} else {
|
|
|
|
txTimes.push(0);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return txTimes;
|
|
|
|
}
|
|
|
|
|
2020-02-23 13:16:50 +01:00
|
|
|
public async getTransactionExtended(txId: string): Promise<TransactionExtended | false> {
|
2019-07-21 16:59:47 +02:00
|
|
|
try {
|
2020-02-16 16:15:07 +01:00
|
|
|
const transaction: Transaction = await bitcoinApi.getRawTransaction(txId);
|
2020-02-23 13:16:50 +01:00
|
|
|
return Object.assign({
|
2020-02-16 16:15:07 +01:00
|
|
|
vsize: transaction.weight / 4,
|
2020-07-23 19:20:59 +02:00
|
|
|
feePerVsize: (transaction.fee || 0) / (transaction.weight / 4),
|
2020-02-27 19:09:07 +01:00
|
|
|
firstSeen: Math.round((new Date().getTime() / 1000)),
|
2020-02-23 13:16:50 +01:00
|
|
|
}, transaction);
|
2019-07-21 16:59:47 +02:00
|
|
|
} catch (e) {
|
2020-10-13 14:54:47 +02:00
|
|
|
logger.debug(txId + ' not found');
|
2019-07-21 16:59:47 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public async updateMempool() {
|
2020-10-13 11:00:58 +02:00
|
|
|
logger.debug('Updating mempool');
|
2019-07-21 16:59:47 +02:00
|
|
|
const start = new Date().getTime();
|
|
|
|
let hasChange: boolean = false;
|
2020-06-18 08:54:54 +02:00
|
|
|
const currentMempoolSize = Object.keys(this.mempoolCache).length;
|
2019-07-21 16:59:47 +02:00
|
|
|
let txCount = 0;
|
|
|
|
try {
|
|
|
|
const transactions = await bitcoinApi.getRawMempool();
|
2020-06-18 08:54:54 +02:00
|
|
|
const diff = transactions.length - currentMempoolSize;
|
2020-02-23 13:16:50 +01:00
|
|
|
const newTransactions: TransactionExtended[] = [];
|
2020-02-16 16:15:07 +01:00
|
|
|
|
|
|
|
for (const txid of transactions) {
|
|
|
|
if (!this.mempoolCache[txid]) {
|
2020-02-23 13:16:50 +01:00
|
|
|
const transaction = await this.getTransactionExtended(txid);
|
2019-07-21 16:59:47 +02:00
|
|
|
if (transaction) {
|
2020-02-16 16:15:07 +01:00
|
|
|
this.mempoolCache[txid] = transaction;
|
2019-07-21 16:59:47 +02:00
|
|
|
txCount++;
|
2020-04-01 15:06:44 +02:00
|
|
|
if (this.inSync) {
|
|
|
|
this.txPerSecondArray.push(new Date().getTime());
|
|
|
|
this.vBytesPerSecondArray.push({
|
|
|
|
unixTime: new Date().getTime(),
|
|
|
|
vSize: transaction.vsize,
|
|
|
|
});
|
|
|
|
}
|
2019-07-21 16:59:47 +02:00
|
|
|
hasChange = true;
|
|
|
|
if (diff > 0) {
|
2020-10-13 11:00:58 +02:00
|
|
|
logger.debug('Fetched transaction ' + txCount + ' / ' + diff);
|
2019-07-21 16:59:47 +02:00
|
|
|
} else {
|
2020-10-13 11:00:58 +02:00
|
|
|
logger.debug('Fetched transaction ' + txCount);
|
2019-07-21 16:59:47 +02:00
|
|
|
}
|
2020-02-23 13:16:50 +01:00
|
|
|
newTransactions.push(transaction);
|
2019-07-21 16:59:47 +02:00
|
|
|
} else {
|
2020-10-13 14:54:47 +02:00
|
|
|
logger.debug('Error finding transaction in mempool.');
|
2019-07-21 16:59:47 +02:00
|
|
|
}
|
|
|
|
}
|
2019-11-15 10:27:12 +01:00
|
|
|
|
2020-06-23 09:49:06 +02:00
|
|
|
if ((new Date().getTime()) - start > config.MEMPOOL_REFRESH_RATE_MS * 10) {
|
2019-11-15 10:27:12 +01:00
|
|
|
break;
|
|
|
|
}
|
2019-07-21 16:59:47 +02:00
|
|
|
}
|
|
|
|
|
2020-06-18 08:54:54 +02:00
|
|
|
// Prevent mempool from clear on bitcoind restart by delaying the deletion
|
2020-10-15 06:54:21 +02:00
|
|
|
if ((config.NETWORK === 'mainnet' || !config.NETWORK)
|
2020-10-15 06:07:53 +02:00
|
|
|
&& this.mempoolProtection === 0 && transactions.length / currentMempoolSize <= 0.80) {
|
2020-06-18 08:54:54 +02:00
|
|
|
this.mempoolProtection = 1;
|
2020-06-18 09:34:56 +02:00
|
|
|
this.inSync = false;
|
2020-10-13 14:54:47 +02:00
|
|
|
logger.warn(`Mempool clear protection triggered because transactions.length: ${transactions.length} and currentMempoolSize: ${currentMempoolSize}.`);
|
2020-06-18 08:54:54 +02:00
|
|
|
setTimeout(() => {
|
|
|
|
this.mempoolProtection = 2;
|
2020-10-13 11:43:09 +02:00
|
|
|
logger.warn('Mempool clear protection resumed.');
|
2020-06-18 08:54:54 +02:00
|
|
|
}, 1000 * 60 * 2);
|
|
|
|
}
|
2020-06-08 21:32:24 +02:00
|
|
|
|
2020-06-18 08:54:54 +02:00
|
|
|
let newMempool = {};
|
2020-06-08 21:08:46 +02:00
|
|
|
const deletedTransactions: TransactionExtended[] = [];
|
2020-06-18 08:54:54 +02:00
|
|
|
|
|
|
|
if (this.mempoolProtection !== 1) {
|
|
|
|
this.mempoolProtection = 0;
|
|
|
|
// Index object for faster search
|
|
|
|
const transactionsObject = {};
|
|
|
|
transactions.forEach((txId) => transactionsObject[txId] = true);
|
|
|
|
|
|
|
|
// Replace mempool to separate deleted transactions
|
|
|
|
for (const tx in this.mempoolCache) {
|
|
|
|
if (transactionsObject[tx]) {
|
|
|
|
newMempool[tx] = this.mempoolCache[tx];
|
|
|
|
} else {
|
|
|
|
deletedTransactions.push(this.mempoolCache[tx]);
|
|
|
|
}
|
2019-07-21 16:59:47 +02:00
|
|
|
}
|
2020-06-18 08:54:54 +02:00
|
|
|
} else {
|
|
|
|
newMempool = this.mempoolCache;
|
2020-06-08 13:55:53 +02:00
|
|
|
}
|
2019-07-21 16:59:47 +02:00
|
|
|
|
2020-09-25 21:11:30 +02:00
|
|
|
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
|
|
|
|
this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6);
|
|
|
|
|
2020-04-01 15:06:44 +02:00
|
|
|
if (!this.inSync && transactions.length === Object.keys(newMempool).length) {
|
|
|
|
this.inSync = true;
|
2020-10-13 10:27:52 +02:00
|
|
|
logger.info('The mempool is now in sync!');
|
2020-04-01 15:06:44 +02:00
|
|
|
}
|
|
|
|
|
2020-06-08 13:55:53 +02:00
|
|
|
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
|
|
|
this.mempoolCache = newMempool;
|
|
|
|
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
|
2019-07-21 16:59:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const end = new Date().getTime();
|
|
|
|
const time = end - start;
|
2020-10-13 11:00:58 +02:00
|
|
|
logger.debug(`New mempool size: ${Object.keys(newMempool).length} Change: ${diff}`);
|
|
|
|
logger.debug('Mempool updated in ' + time / 1000 + ' seconds');
|
2019-07-21 16:59:47 +02:00
|
|
|
} catch (err) {
|
2020-10-13 10:27:52 +02:00
|
|
|
logger.err('getRawMempool error. ' + err.message || err);
|
2019-07-21 16:59:47 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private updateTxPerSecond() {
|
|
|
|
const nowMinusTimeSpan = new Date().getTime() - (1000 * config.TX_PER_SECOND_SPAN_SECONDS);
|
|
|
|
this.txPerSecondArray = this.txPerSecondArray.filter((unixTime) => unixTime > nowMinusTimeSpan);
|
|
|
|
this.txPerSecond = this.txPerSecondArray.length / config.TX_PER_SECOND_SPAN_SECONDS || 0;
|
|
|
|
|
|
|
|
this.vBytesPerSecondArray = this.vBytesPerSecondArray.filter((data) => data.unixTime > nowMinusTimeSpan);
|
|
|
|
if (this.vBytesPerSecondArray.length) {
|
|
|
|
this.vBytesPerSecond = Math.round(
|
|
|
|
this.vBytesPerSecondArray.map((data) => data.vSize).reduce((a, b) => a + b) / config.TX_PER_SECOND_SPAN_SECONDS
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default new Mempool();
|