Merge pull request #2824 from mempool/mononaut/more-rbf-info
cache, serve & display more comprehensive RBF info
This commit is contained in:
@@ -18,6 +18,7 @@ import blocks from '../blocks';
|
||||
import bitcoinClient from './bitcoin-client';
|
||||
import difficultyAdjustment from '../difficulty-adjustment';
|
||||
import transactionRepository from '../../repositories/TransactionRepository';
|
||||
import rbfCache from '../rbf-cache';
|
||||
|
||||
class BitcoinRoutes {
|
||||
public initRoutes(app: Application) {
|
||||
@@ -31,6 +32,8 @@ class BitcoinRoutes {
|
||||
.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)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/replaces', this.getRbfHistory)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/cached', this.getCachedTx)
|
||||
.post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', this.$postTransactionForm)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
|
||||
try {
|
||||
@@ -589,6 +592,28 @@ class BitcoinRoutes {
|
||||
}
|
||||
}
|
||||
|
||||
private async getRbfHistory(req: Request, res: Response) {
|
||||
try {
|
||||
const result = rbfCache.getReplaces(req.params.txId);
|
||||
res.json(result || []);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async getCachedTx(req: Request, res: Response) {
|
||||
try {
|
||||
const result = rbfCache.getTx(req.params.txId);
|
||||
if (result) {
|
||||
res.json(result);
|
||||
} else {
|
||||
res.status(404).send('not found');
|
||||
}
|
||||
} 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);
|
||||
|
||||
@@ -60,8 +60,6 @@ export class Common {
|
||||
static findRbfTransactions(added: TransactionExtended[], deleted: TransactionExtended[]): { [txid: string]: TransactionExtended } {
|
||||
const matches: { [txid: string]: TransactionExtended } = {};
|
||||
deleted
|
||||
// The replaced tx must have at least one input with nSequence < maxint-1 (That’s the opt-in)
|
||||
.filter((tx) => tx.vin.some((vin) => vin.sequence < 0xfffffffe))
|
||||
.forEach((deletedTx) => {
|
||||
const foundMatches = added.find((addedTx) => {
|
||||
// The new tx must, absolutely speaking, pay at least as much fee as the replaced tx.
|
||||
@@ -70,7 +68,7 @@ export class Common {
|
||||
&& addedTx.feePerVsize > deletedTx.feePerVsize
|
||||
// Spends one or more of the same inputs
|
||||
&& deletedTx.vin.some((deletedVin) =>
|
||||
addedTx.vin.some((vin) => vin.txid === deletedVin.txid));
|
||||
addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
|
||||
});
|
||||
if (foundMatches) {
|
||||
matches[deletedTx.txid] = foundMatches;
|
||||
|
||||
@@ -210,7 +210,7 @@ class Mempool {
|
||||
for (const rbfTransaction in rbfTransactions) {
|
||||
if (this.mempoolCache[rbfTransaction]) {
|
||||
// Store replaced transactions
|
||||
rbfCache.add(rbfTransaction, rbfTransactions[rbfTransaction].txid);
|
||||
rbfCache.add(this.mempoolCache[rbfTransaction], rbfTransactions[rbfTransaction].txid);
|
||||
// Erase the replaced transactions from the local mempool
|
||||
delete this.mempoolCache[rbfTransaction];
|
||||
}
|
||||
@@ -236,6 +236,7 @@ class Mempool {
|
||||
const lazyDeleteAt = this.mempoolCache[tx].deleteAfter;
|
||||
if (lazyDeleteAt && lazyDeleteAt < now) {
|
||||
delete this.mempoolCache[tx];
|
||||
rbfCache.evict(tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,62 @@
|
||||
export interface CachedRbf {
|
||||
txid: string;
|
||||
expires: Date;
|
||||
}
|
||||
import { TransactionExtended } from "../mempool.interfaces";
|
||||
|
||||
class RbfCache {
|
||||
private cache: { [txid: string]: CachedRbf; } = {};
|
||||
private replacedBy: { [txid: string]: string; } = {};
|
||||
private replaces: { [txid: string]: string[] } = {};
|
||||
private txs: { [txid: string]: TransactionExtended } = {};
|
||||
private expiring: { [txid: string]: Date } = {};
|
||||
|
||||
constructor() {
|
||||
setInterval(this.cleanup.bind(this), 1000 * 60 * 60);
|
||||
}
|
||||
|
||||
public add(replacedTxId: string, newTxId: string): void {
|
||||
this.cache[replacedTxId] = {
|
||||
expires: new Date(Date.now() + 1000 * 604800), // 1 week
|
||||
txid: newTxId,
|
||||
};
|
||||
public add(replacedTx: TransactionExtended, newTxId: string): void {
|
||||
this.replacedBy[replacedTx.txid] = newTxId;
|
||||
this.txs[replacedTx.txid] = replacedTx;
|
||||
if (!this.replaces[newTxId]) {
|
||||
this.replaces[newTxId] = [];
|
||||
}
|
||||
this.replaces[newTxId].push(replacedTx.txid);
|
||||
}
|
||||
|
||||
public get(txId: string): CachedRbf | undefined {
|
||||
return this.cache[txId];
|
||||
public getReplacedBy(txId: string): string | undefined {
|
||||
return this.replacedBy[txId];
|
||||
}
|
||||
|
||||
public getReplaces(txId: string): string[] | undefined {
|
||||
return this.replaces[txId];
|
||||
}
|
||||
|
||||
public getTx(txId: string): TransactionExtended | undefined {
|
||||
return this.txs[txId];
|
||||
}
|
||||
|
||||
// flag a transaction as removed from the mempool
|
||||
public evict(txid): void {
|
||||
this.expiring[txid] = new Date(Date.now() + 1000 * 86400); // 24 hours
|
||||
}
|
||||
|
||||
private cleanup(): void {
|
||||
const currentDate = new Date();
|
||||
for (const c in this.cache) {
|
||||
if (this.cache[c].expires < currentDate) {
|
||||
delete this.cache[c];
|
||||
for (const txid in this.expiring) {
|
||||
if (this.expiring[txid] < currentDate) {
|
||||
delete this.expiring[txid];
|
||||
this.remove(txid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove a transaction & all previous versions from the cache
|
||||
private remove(txid): void {
|
||||
// don't remove a transaction while a newer version remains in the mempool
|
||||
if (this.replaces[txid] && !this.replacedBy[txid]) {
|
||||
const replaces = this.replaces[txid];
|
||||
delete this.replaces[txid];
|
||||
for (const tx of replaces) {
|
||||
// recursively remove prior versions from the cache
|
||||
delete this.replacedBy[tx];
|
||||
delete this.txs[tx];
|
||||
this.remove(tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,10 +58,10 @@ class WebsocketHandler {
|
||||
client['track-tx'] = parsedMessage['track-tx'];
|
||||
// Client is telling the transaction wasn't found
|
||||
if (parsedMessage['watch-mempool']) {
|
||||
const rbfCacheTx = rbfCache.get(client['track-tx']);
|
||||
if (rbfCacheTx) {
|
||||
const rbfCacheTxid = rbfCache.getReplacedBy(client['track-tx']);
|
||||
if (rbfCacheTxid) {
|
||||
response['txReplaced'] = {
|
||||
txid: rbfCacheTx.txid,
|
||||
txid: rbfCacheTxid,
|
||||
};
|
||||
client['track-tx'] = null;
|
||||
} else {
|
||||
@@ -467,6 +467,7 @@ class WebsocketHandler {
|
||||
for (const txId of txIds) {
|
||||
delete _memPool[txId];
|
||||
removed.push(txId);
|
||||
rbfCache.evict(txId);
|
||||
}
|
||||
|
||||
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
|
||||
|
||||
Reference in New Issue
Block a user