2021-03-18 23:47:40 +07:00
|
|
|
import logger from '../logger';
|
2022-12-06 05:51:44 +09:00
|
|
|
import { MempoolBlock, TransactionExtended, ThreadTransaction, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor } from '../mempool.interfaces';
|
2020-05-24 16:29:30 +07:00
|
|
|
import { Common } from './common';
|
2021-03-18 05:52:46 +00:00
|
|
|
import config from '../config';
|
2022-12-06 05:51:44 +09:00
|
|
|
import { Worker } from 'worker_threads';
|
2022-11-16 18:17:07 -06:00
|
|
|
import path from 'path';
|
2020-02-16 22:15:07 +07:00
|
|
|
|
|
|
|
class MempoolBlocks {
|
2020-06-08 02:08:51 +07:00
|
|
|
private mempoolBlocks: MempoolBlockWithTransactions[] = [];
|
2022-05-31 21:36:42 +00:00
|
|
|
private mempoolBlockDeltas: MempoolBlockDelta[] = [];
|
2022-12-06 05:51:44 +09:00
|
|
|
private txSelectionWorker: Worker | null = null;
|
2020-02-16 22:15:07 +07:00
|
|
|
|
|
|
|
constructor() {}
|
|
|
|
|
|
|
|
public getMempoolBlocks(): MempoolBlock[] {
|
2020-06-08 02:08:51 +07:00
|
|
|
return this.mempoolBlocks.map((block) => {
|
|
|
|
return {
|
|
|
|
blockSize: block.blockSize,
|
|
|
|
blockVSize: block.blockVSize,
|
|
|
|
nTx: block.nTx,
|
|
|
|
totalFees: block.totalFees,
|
|
|
|
medianFee: block.medianFee,
|
|
|
|
feeRange: block.feeRange,
|
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
public getMempoolBlocksWithTransactions(): MempoolBlockWithTransactions[] {
|
2020-02-16 22:15:07 +07:00
|
|
|
return this.mempoolBlocks;
|
|
|
|
}
|
|
|
|
|
2022-05-31 21:36:42 +00:00
|
|
|
public getMempoolBlockDeltas(): MempoolBlockDelta[] {
|
2022-06-02 01:29:03 +04:00
|
|
|
return this.mempoolBlockDeltas;
|
2022-05-31 21:36:42 +00:00
|
|
|
}
|
|
|
|
|
2023-01-30 16:26:37 -06:00
|
|
|
public updateMempoolBlocks(memPool: { [txid: string]: TransactionExtended }, saveResults: boolean = false): MempoolBlockWithTransactions[] {
|
2020-02-16 22:15:07 +07:00
|
|
|
const latestMempool = memPool;
|
2020-02-23 19:16:50 +07:00
|
|
|
const memPoolArray: TransactionExtended[] = [];
|
2020-02-16 22:15:07 +07:00
|
|
|
for (const i in latestMempool) {
|
|
|
|
if (latestMempool.hasOwnProperty(i)) {
|
|
|
|
memPoolArray.push(latestMempool[i]);
|
|
|
|
}
|
|
|
|
}
|
2021-03-18 23:47:40 +07:00
|
|
|
const start = new Date().getTime();
|
|
|
|
|
|
|
|
// Clear bestDescendants & ancestors
|
|
|
|
memPoolArray.forEach((tx) => {
|
|
|
|
tx.bestDescendant = null;
|
|
|
|
tx.ancestors = [];
|
|
|
|
tx.cpfpChecked = false;
|
|
|
|
if (!tx.effectiveFeePerVsize) {
|
|
|
|
tx.effectiveFeePerVsize = tx.feePerVsize;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// First sort
|
2020-02-16 22:15:07 +07:00
|
|
|
memPoolArray.sort((a, b) => b.feePerVsize - a.feePerVsize);
|
2021-03-18 23:47:40 +07:00
|
|
|
|
|
|
|
// Loop through and traverse all ancestors and sum up all the sizes + fees
|
|
|
|
// Pass down size + fee to all unconfirmed children
|
|
|
|
let sizes = 0;
|
2023-03-12 14:51:29 +09:00
|
|
|
memPoolArray.forEach((tx) => {
|
2021-04-10 21:26:05 +04:00
|
|
|
sizes += tx.weight;
|
2021-03-18 23:47:40 +07:00
|
|
|
if (sizes > 4000000 * 8) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
Common.setRelativesAndGetCpfpInfo(tx, memPool);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Final sort, by effective fee
|
|
|
|
memPoolArray.sort((a, b) => b.effectiveFeePerVsize - a.effectiveFeePerVsize);
|
|
|
|
|
|
|
|
const end = new Date().getTime();
|
|
|
|
const time = end - start;
|
|
|
|
logger.debug('Mempool blocks calculated in ' + time / 1000 + ' seconds');
|
|
|
|
|
2023-03-12 14:51:29 +09:00
|
|
|
const blocks = this.calculateMempoolBlocks(memPoolArray);
|
2022-10-27 10:21:39 -06:00
|
|
|
|
2023-01-30 16:26:37 -06:00
|
|
|
if (saveResults) {
|
|
|
|
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, blocks);
|
|
|
|
this.mempoolBlocks = blocks;
|
|
|
|
this.mempoolBlockDeltas = deltas;
|
|
|
|
}
|
|
|
|
|
|
|
|
return blocks;
|
2020-02-16 22:15:07 +07:00
|
|
|
}
|
|
|
|
|
2023-03-12 14:51:29 +09:00
|
|
|
private calculateMempoolBlocks(transactionsSorted: TransactionExtended[]): MempoolBlockWithTransactions[] {
|
2020-06-08 02:08:51 +07:00
|
|
|
const mempoolBlocks: MempoolBlockWithTransactions[] = [];
|
2021-08-14 03:24:31 +03:00
|
|
|
let blockWeight = 0;
|
2020-02-23 19:16:50 +07:00
|
|
|
let transactions: TransactionExtended[] = [];
|
2020-02-16 22:15:07 +07:00
|
|
|
transactionsSorted.forEach((tx) => {
|
2021-10-05 04:11:13 +04:00
|
|
|
if (blockWeight + tx.weight <= config.MEMPOOL.BLOCK_WEIGHT_UNITS
|
|
|
|
|| mempoolBlocks.length === config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT - 1) {
|
2021-08-14 03:24:31 +03:00
|
|
|
blockWeight += tx.weight;
|
2020-02-16 22:15:07 +07:00
|
|
|
transactions.push(tx);
|
|
|
|
} else {
|
2023-03-12 14:51:29 +09:00
|
|
|
mempoolBlocks.push(this.dataToMempoolBlocks(transactions));
|
2021-08-14 03:24:31 +03:00
|
|
|
blockWeight = tx.weight;
|
2020-06-08 18:55:53 +07:00
|
|
|
transactions = [tx];
|
2020-02-16 22:15:07 +07:00
|
|
|
}
|
|
|
|
});
|
|
|
|
if (transactions.length) {
|
2023-03-12 14:51:29 +09:00
|
|
|
mempoolBlocks.push(this.dataToMempoolBlocks(transactions));
|
2020-02-16 22:15:07 +07:00
|
|
|
}
|
2022-10-10 22:13:04 +00:00
|
|
|
|
2022-11-16 18:17:07 -06:00
|
|
|
return mempoolBlocks;
|
|
|
|
}
|
|
|
|
|
|
|
|
private calculateMempoolDeltas(prevBlocks: MempoolBlockWithTransactions[], mempoolBlocks: MempoolBlockWithTransactions[]): MempoolBlockDelta[] {
|
|
|
|
const mempoolBlockDeltas: MempoolBlockDelta[] = [];
|
2022-05-31 21:36:42 +00:00
|
|
|
for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) {
|
2022-06-02 01:29:03 +04:00
|
|
|
let added: TransactionStripped[] = [];
|
|
|
|
let removed: string[] = [];
|
2022-05-31 21:36:42 +00:00
|
|
|
if (mempoolBlocks[i] && !prevBlocks[i]) {
|
2022-06-02 01:29:03 +04:00
|
|
|
added = mempoolBlocks[i].transactions;
|
2022-05-31 21:36:42 +00:00
|
|
|
} else if (!mempoolBlocks[i] && prevBlocks[i]) {
|
2022-06-02 01:29:03 +04:00
|
|
|
removed = prevBlocks[i].transactions.map(tx => tx.txid);
|
2022-05-31 21:36:42 +00:00
|
|
|
} else if (mempoolBlocks[i] && prevBlocks[i]) {
|
2022-06-02 01:29:03 +04:00
|
|
|
const prevIds = {};
|
|
|
|
const newIds = {};
|
2022-05-31 21:36:42 +00:00
|
|
|
prevBlocks[i].transactions.forEach(tx => {
|
2022-06-02 01:29:03 +04:00
|
|
|
prevIds[tx.txid] = true;
|
|
|
|
});
|
2022-05-31 21:36:42 +00:00
|
|
|
mempoolBlocks[i].transactions.forEach(tx => {
|
2022-06-02 01:29:03 +04:00
|
|
|
newIds[tx.txid] = true;
|
|
|
|
});
|
2022-05-31 21:36:42 +00:00
|
|
|
prevBlocks[i].transactions.forEach(tx => {
|
2022-06-02 01:29:03 +04:00
|
|
|
if (!newIds[tx.txid]) {
|
|
|
|
removed.push(tx.txid);
|
|
|
|
}
|
|
|
|
});
|
2022-05-31 21:36:42 +00:00
|
|
|
mempoolBlocks[i].transactions.forEach(tx => {
|
2022-06-02 01:29:03 +04:00
|
|
|
if (!prevIds[tx.txid]) {
|
|
|
|
added.push(tx);
|
|
|
|
}
|
|
|
|
});
|
2022-05-31 21:36:42 +00:00
|
|
|
}
|
|
|
|
mempoolBlockDeltas.push({
|
|
|
|
added,
|
|
|
|
removed
|
2022-06-02 01:29:03 +04:00
|
|
|
});
|
2022-05-31 21:36:42 +00:00
|
|
|
}
|
2022-11-16 18:17:07 -06:00
|
|
|
return mempoolBlockDeltas;
|
2020-02-16 22:15:07 +07:00
|
|
|
}
|
|
|
|
|
2023-04-30 15:28:34 -06:00
|
|
|
public async $makeBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, saveResults: boolean = false): Promise<MempoolBlockWithTransactions[]> {
|
2022-12-06 05:51:44 +09:00
|
|
|
// prepare a stripped down version of the mempool with only the minimum necessary data
|
|
|
|
// to reduce the overhead of passing this data to the worker thread
|
|
|
|
const strippedMempool: { [txid: string]: ThreadTransaction } = {};
|
2023-03-24 09:47:08 +09:00
|
|
|
Object.values(newMempool).filter(tx => !tx.deleteAfter).forEach(entry => {
|
2022-12-06 05:51:44 +09:00
|
|
|
strippedMempool[entry.txid] = {
|
|
|
|
txid: entry.txid,
|
|
|
|
fee: entry.fee,
|
|
|
|
weight: entry.weight,
|
|
|
|
feePerVsize: entry.fee / (entry.weight / 4),
|
|
|
|
effectiveFeePerVsize: entry.fee / (entry.weight / 4),
|
|
|
|
vin: entry.vin.map(v => v.txid),
|
|
|
|
};
|
|
|
|
});
|
2022-10-10 22:13:04 +00:00
|
|
|
|
2022-12-07 14:51:26 -06:00
|
|
|
// (re)initialize tx selection worker thread
|
2022-12-06 05:51:44 +09:00
|
|
|
if (!this.txSelectionWorker) {
|
|
|
|
this.txSelectionWorker = new Worker(path.resolve(__dirname, './tx-selection-worker.js'));
|
2022-12-07 14:51:26 -06:00
|
|
|
// if the thread throws an unexpected error, or exits for any other reason,
|
|
|
|
// reset worker state so that it will be re-initialized on the next run
|
2022-12-06 05:51:44 +09:00
|
|
|
this.txSelectionWorker.once('error', () => {
|
|
|
|
this.txSelectionWorker = null;
|
|
|
|
});
|
|
|
|
this.txSelectionWorker.once('exit', () => {
|
|
|
|
this.txSelectionWorker = null;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// run the block construction algorithm in a separate thread, and wait for a result
|
2022-12-07 14:51:26 -06:00
|
|
|
let threadErrorListener;
|
|
|
|
try {
|
|
|
|
const workerResultPromise = new Promise<{ blocks: ThreadTransaction[][], clusters: { [root: string]: string[] } }>((resolve, reject) => {
|
|
|
|
threadErrorListener = reject;
|
|
|
|
this.txSelectionWorker?.once('message', (result): void => {
|
|
|
|
resolve(result);
|
|
|
|
});
|
|
|
|
this.txSelectionWorker?.once('error', reject);
|
2022-12-06 05:51:44 +09:00
|
|
|
});
|
2022-12-07 14:51:26 -06:00
|
|
|
this.txSelectionWorker.postMessage({ type: 'set', mempool: strippedMempool });
|
2023-03-24 09:47:08 +09:00
|
|
|
let { blocks, clusters } = await workerResultPromise;
|
|
|
|
// filter out stale transactions
|
2023-03-26 05:41:31 +09:00
|
|
|
const unfilteredCount = blocks.reduce((total, block) => { return total + block.length; }, 0);
|
2023-03-24 09:47:08 +09:00
|
|
|
blocks = blocks.map(block => block.filter(tx => (tx.txid && tx.txid in newMempool)));
|
2023-03-26 05:41:31 +09:00
|
|
|
const filteredCount = blocks.reduce((total, block) => { return total + block.length; }, 0);
|
|
|
|
if (filteredCount < unfilteredCount) {
|
|
|
|
logger.warn(`tx selection worker thread returned ${unfilteredCount - filteredCount} stale transactions from makeBlockTemplates`);
|
|
|
|
}
|
2022-10-27 10:21:39 -06:00
|
|
|
|
2022-12-07 14:51:26 -06:00
|
|
|
// clean up thread error listener
|
|
|
|
this.txSelectionWorker?.removeListener('error', threadErrorListener);
|
2023-01-30 16:26:37 -06:00
|
|
|
|
|
|
|
return this.processBlockTemplates(newMempool, blocks, clusters, saveResults);
|
2022-12-07 14:51:26 -06:00
|
|
|
} catch (e) {
|
|
|
|
logger.err('makeBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
|
|
|
|
}
|
2023-01-30 16:26:37 -06:00
|
|
|
return this.mempoolBlocks;
|
2022-12-06 05:51:44 +09:00
|
|
|
}
|
|
|
|
|
2023-04-30 15:28:34 -06:00
|
|
|
public async $updateBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, added: TransactionExtended[], removed: string[], saveResults: boolean = false): Promise<void> {
|
2022-12-06 05:51:44 +09:00
|
|
|
if (!this.txSelectionWorker) {
|
|
|
|
// need to reset the worker
|
2023-04-30 15:28:34 -06:00
|
|
|
await this.$makeBlockTemplates(newMempool, saveResults);
|
2023-01-30 16:26:37 -06:00
|
|
|
return;
|
2022-12-06 05:51:44 +09:00
|
|
|
}
|
|
|
|
// prepare a stripped down version of the mempool with only the minimum necessary data
|
|
|
|
// to reduce the overhead of passing this data to the worker thread
|
|
|
|
const addedStripped: ThreadTransaction[] = added.map(entry => {
|
|
|
|
return {
|
|
|
|
txid: entry.txid,
|
|
|
|
fee: entry.fee,
|
|
|
|
weight: entry.weight,
|
|
|
|
feePerVsize: entry.fee / (entry.weight / 4),
|
|
|
|
effectiveFeePerVsize: entry.fee / (entry.weight / 4),
|
|
|
|
vin: entry.vin.map(v => v.txid),
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
// run the block construction algorithm in a separate thread, and wait for a result
|
2022-12-07 14:51:26 -06:00
|
|
|
let threadErrorListener;
|
|
|
|
try {
|
|
|
|
const workerResultPromise = new Promise<{ blocks: ThreadTransaction[][], clusters: { [root: string]: string[] } }>((resolve, reject) => {
|
|
|
|
threadErrorListener = reject;
|
|
|
|
this.txSelectionWorker?.once('message', (result): void => {
|
|
|
|
resolve(result);
|
|
|
|
});
|
|
|
|
this.txSelectionWorker?.once('error', reject);
|
2022-12-06 05:51:44 +09:00
|
|
|
});
|
2022-12-07 14:51:26 -06:00
|
|
|
this.txSelectionWorker.postMessage({ type: 'update', added: addedStripped, removed });
|
2023-03-24 09:47:08 +09:00
|
|
|
let { blocks, clusters } = await workerResultPromise;
|
|
|
|
// filter out stale transactions
|
2023-03-26 05:41:31 +09:00
|
|
|
const unfilteredCount = blocks.reduce((total, block) => { return total + block.length; }, 0);
|
2023-03-24 09:47:08 +09:00
|
|
|
blocks = blocks.map(block => block.filter(tx => (tx.txid && tx.txid in newMempool)));
|
2023-03-26 05:41:31 +09:00
|
|
|
const filteredCount = blocks.reduce((total, block) => { return total + block.length; }, 0);
|
|
|
|
if (filteredCount < unfilteredCount) {
|
|
|
|
logger.warn(`tx selection worker thread returned ${unfilteredCount - filteredCount} stale transactions from updateBlockTemplates`);
|
|
|
|
}
|
2022-12-06 05:51:44 +09:00
|
|
|
|
2022-12-07 14:51:26 -06:00
|
|
|
// clean up thread error listener
|
|
|
|
this.txSelectionWorker?.removeListener('error', threadErrorListener);
|
2023-01-30 16:26:37 -06:00
|
|
|
|
|
|
|
this.processBlockTemplates(newMempool, blocks, clusters, saveResults);
|
2022-12-07 14:51:26 -06:00
|
|
|
} catch (e) {
|
|
|
|
logger.err('updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
|
|
|
|
}
|
2022-12-06 05:51:44 +09:00
|
|
|
}
|
|
|
|
|
2023-01-30 16:26:37 -06:00
|
|
|
private processBlockTemplates(mempool, blocks, clusters, saveResults): MempoolBlockWithTransactions[] {
|
2022-12-06 05:51:44 +09:00
|
|
|
// update this thread's mempool with the results
|
|
|
|
blocks.forEach(block => {
|
|
|
|
block.forEach(tx => {
|
2023-03-24 09:47:08 +09:00
|
|
|
if (tx.txid && tx.txid in mempool) {
|
2022-12-06 05:51:44 +09:00
|
|
|
if (tx.effectiveFeePerVsize != null) {
|
|
|
|
mempool[tx.txid].effectiveFeePerVsize = tx.effectiveFeePerVsize;
|
|
|
|
}
|
|
|
|
if (tx.cpfpRoot && tx.cpfpRoot in clusters) {
|
|
|
|
const ancestors: Ancestor[] = [];
|
|
|
|
const descendants: Ancestor[] = [];
|
|
|
|
const cluster = clusters[tx.cpfpRoot];
|
|
|
|
let matched = false;
|
|
|
|
cluster.forEach(txid => {
|
2023-03-24 09:47:08 +09:00
|
|
|
if (!txid || !mempool[txid]) {
|
2023-03-26 05:41:31 +09:00
|
|
|
logger.warn('projected transaction ancestor missing from mempool cache');
|
2023-03-24 09:47:08 +09:00
|
|
|
return;
|
|
|
|
}
|
2022-12-06 05:51:44 +09:00
|
|
|
if (txid === tx.txid) {
|
|
|
|
matched = true;
|
|
|
|
} else {
|
|
|
|
const relative = {
|
|
|
|
txid: txid,
|
|
|
|
fee: mempool[txid].fee,
|
|
|
|
weight: mempool[txid].weight,
|
|
|
|
};
|
|
|
|
if (matched) {
|
|
|
|
descendants.push(relative);
|
|
|
|
} else {
|
|
|
|
ancestors.push(relative);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
mempool[tx.txid].ancestors = ancestors;
|
|
|
|
mempool[tx.txid].descendants = descendants;
|
|
|
|
mempool[tx.txid].bestDescendant = null;
|
|
|
|
}
|
|
|
|
mempool[tx.txid].cpfpChecked = tx.cpfpChecked;
|
2023-03-26 05:41:31 +09:00
|
|
|
} else {
|
|
|
|
logger.warn('projected transaction missing from mempool cache');
|
2022-12-06 05:51:44 +09:00
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
// unpack the condensed blocks into proper mempool blocks
|
2023-03-12 14:51:29 +09:00
|
|
|
const mempoolBlocks = blocks.map((transactions) => {
|
2022-12-06 05:51:44 +09:00
|
|
|
return this.dataToMempoolBlocks(transactions.map(tx => {
|
|
|
|
return mempool[tx.txid] || null;
|
2023-03-12 14:51:29 +09:00
|
|
|
}).filter(tx => !!tx));
|
2022-12-06 05:51:44 +09:00
|
|
|
});
|
|
|
|
|
2023-01-30 16:26:37 -06:00
|
|
|
if (saveResults) {
|
|
|
|
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks);
|
|
|
|
this.mempoolBlocks = mempoolBlocks;
|
|
|
|
this.mempoolBlockDeltas = deltas;
|
|
|
|
}
|
2022-12-06 05:51:44 +09:00
|
|
|
|
2023-01-30 16:26:37 -06:00
|
|
|
return mempoolBlocks;
|
2022-10-10 22:13:04 +00:00
|
|
|
}
|
|
|
|
|
2023-03-12 14:51:29 +09:00
|
|
|
private dataToMempoolBlocks(transactions: TransactionExtended[]): MempoolBlockWithTransactions {
|
2023-02-17 18:58:05 -06:00
|
|
|
let totalSize = 0;
|
|
|
|
let totalWeight = 0;
|
|
|
|
const fitTransactions: TransactionExtended[] = [];
|
|
|
|
transactions.forEach(tx => {
|
|
|
|
totalSize += tx.size;
|
|
|
|
totalWeight += tx.weight;
|
|
|
|
if ((totalWeight + tx.weight) <= config.MEMPOOL.BLOCK_WEIGHT_UNITS * 1.2) {
|
|
|
|
fitTransactions.push(tx);
|
|
|
|
}
|
|
|
|
});
|
2023-03-12 14:51:06 +09:00
|
|
|
const feeStats = Common.calcEffectiveFeeStatistics(transactions);
|
2020-02-16 22:15:07 +07:00
|
|
|
return {
|
2022-12-06 05:51:44 +09:00
|
|
|
blockSize: totalSize,
|
|
|
|
blockVSize: totalWeight / 4,
|
2020-02-16 22:15:07 +07:00
|
|
|
nTx: transactions.length,
|
2020-03-17 21:53:20 +07:00
|
|
|
totalFees: transactions.reduce((acc, cur) => acc + cur.fee, 0),
|
2023-03-12 14:51:06 +09:00
|
|
|
medianFee: feeStats.medianFee, // Common.percentile(transactions.map((tx) => tx.effectiveFeePerVsize), config.MEMPOOL.RECOMMENDED_FEE_PERCENTILE),
|
|
|
|
feeRange: feeStats.feeRange, //Common.getFeesInRange(transactions, rangeLength),
|
2020-06-08 02:08:51 +07:00
|
|
|
transactionIds: transactions.map((tx) => tx.txid),
|
2023-02-17 18:58:05 -06:00
|
|
|
transactions: fitTransactions.map((tx) => Common.stripTransaction(tx)),
|
2020-02-16 22:15:07 +07:00
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default new MempoolBlocks();
|