2022-12-13 17:11:37 -06:00
|
|
|
import { TransactionExtended, TransactionStripped } from "../mempool.interfaces";
|
|
|
|
import { Common } from "./common";
|
|
|
|
|
|
|
|
interface RbfTransaction extends TransactionStripped {
|
|
|
|
rbf?: boolean;
|
|
|
|
}
|
2022-12-09 10:32:58 -06:00
|
|
|
|
2022-03-08 14:49:25 +01:00
|
|
|
class RbfCache {
|
2022-12-09 14:35:51 -06:00
|
|
|
private replacedBy: { [txid: string]: string; } = {};
|
|
|
|
private replaces: { [txid: string]: string[] } = {};
|
2022-12-13 17:11:37 -06:00
|
|
|
private rbfChains: { [root: string]: { tx: TransactionStripped, time: number, mined?: boolean }[] } = {}; // sequences of consecutive replacements
|
|
|
|
private chainMap: { [txid: string]: string } = {}; // map of txids to sequence ids
|
2022-12-09 10:32:58 -06:00
|
|
|
private txs: { [txid: string]: TransactionExtended } = {};
|
2022-12-09 14:35:51 -06:00
|
|
|
private expiring: { [txid: string]: Date } = {};
|
2022-03-08 14:49:25 +01:00
|
|
|
|
|
|
|
constructor() {
|
|
|
|
setInterval(this.cleanup.bind(this), 1000 * 60 * 60);
|
|
|
|
}
|
|
|
|
|
2022-12-13 17:11:37 -06:00
|
|
|
public add(replacedTxExtended: TransactionExtended, newTxExtended: TransactionExtended): void {
|
|
|
|
const replacedTx = Common.stripTransaction(replacedTxExtended) as RbfTransaction;
|
|
|
|
replacedTx.rbf = replacedTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
|
|
|
|
const newTx = Common.stripTransaction(newTxExtended) as RbfTransaction;
|
|
|
|
newTx.rbf = newTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
|
|
|
|
|
|
|
|
this.replacedBy[replacedTx.txid] = newTx.txid;
|
|
|
|
this.txs[replacedTx.txid] = replacedTxExtended;
|
|
|
|
if (!this.replaces[newTx.txid]) {
|
|
|
|
this.replaces[newTx.txid] = [];
|
|
|
|
}
|
|
|
|
this.replaces[newTx.txid].push(replacedTx.txid);
|
|
|
|
|
|
|
|
// maintain rbf chains
|
|
|
|
if (this.chainMap[replacedTx.txid]) {
|
|
|
|
// add to an existing chain
|
|
|
|
const chainRoot = this.chainMap[replacedTx.txid];
|
|
|
|
this.rbfChains[chainRoot].push({ tx: newTx, time: newTxExtended.firstSeen || Date.now() });
|
|
|
|
this.chainMap[newTx.txid] = chainRoot;
|
|
|
|
} else {
|
|
|
|
// start a new chain
|
|
|
|
this.rbfChains[replacedTx.txid] = [
|
|
|
|
{ tx: replacedTx, time: replacedTxExtended.firstSeen || Date.now() },
|
|
|
|
{ tx: newTx, time: newTxExtended.firstSeen || Date.now() },
|
|
|
|
];
|
|
|
|
this.chainMap[replacedTx.txid] = replacedTx.txid;
|
|
|
|
this.chainMap[newTx.txid] = replacedTx.txid;
|
2022-12-09 10:32:58 -06:00
|
|
|
}
|
2022-03-08 14:49:25 +01:00
|
|
|
}
|
|
|
|
|
2022-12-09 14:35:51 -06:00
|
|
|
public getReplacedBy(txId: string): string | undefined {
|
|
|
|
return this.replacedBy[txId];
|
2022-12-09 10:32:58 -06:00
|
|
|
}
|
|
|
|
|
2022-12-09 14:35:51 -06:00
|
|
|
public getReplaces(txId: string): string[] | undefined {
|
2022-12-09 10:32:58 -06:00
|
|
|
return this.replaces[txId];
|
|
|
|
}
|
|
|
|
|
|
|
|
public getTx(txId: string): TransactionExtended | undefined {
|
|
|
|
return this.txs[txId];
|
2022-03-08 14:49:25 +01:00
|
|
|
}
|
|
|
|
|
2022-12-13 17:11:37 -06:00
|
|
|
public getRbfChain(txId: string): { tx: TransactionStripped, time: number }[] {
|
|
|
|
return this.rbfChains[this.chainMap[txId]] || [];
|
|
|
|
}
|
|
|
|
|
2022-12-09 14:35:51 -06:00
|
|
|
// flag a transaction as removed from the mempool
|
|
|
|
public evict(txid): void {
|
|
|
|
this.expiring[txid] = new Date(Date.now() + 1000 * 86400); // 24 hours
|
|
|
|
}
|
|
|
|
|
2022-03-08 14:49:25 +01:00
|
|
|
private cleanup(): void {
|
|
|
|
const currentDate = new Date();
|
2022-12-09 14:35:51 -06:00
|
|
|
for (const txid in this.expiring) {
|
|
|
|
if (this.expiring[txid] < currentDate) {
|
|
|
|
delete this.expiring[txid];
|
|
|
|
this.remove(txid);
|
2022-12-09 10:32:58 -06:00
|
|
|
}
|
|
|
|
}
|
2022-12-09 14:35:51 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
// remove a transaction & all previous versions from the cache
|
|
|
|
private remove(txid): void {
|
2022-12-13 17:11:37 -06:00
|
|
|
// don't remove a transaction if a newer version remains in the mempool
|
|
|
|
if (!this.replacedBy[txid]) {
|
2022-12-09 14:35:51 -06:00
|
|
|
const replaces = this.replaces[txid];
|
|
|
|
delete this.replaces[txid];
|
2022-12-13 17:11:37 -06:00
|
|
|
delete this.chainMap[txid];
|
|
|
|
delete this.txs[txid];
|
|
|
|
delete this.expiring[txid];
|
2022-12-09 14:35:51 -06:00
|
|
|
for (const tx of replaces) {
|
|
|
|
// recursively remove prior versions from the cache
|
|
|
|
delete this.replacedBy[tx];
|
2022-12-13 17:11:37 -06:00
|
|
|
// if this is the root of a chain, remove that too
|
|
|
|
if (this.chainMap[tx] === tx) {
|
|
|
|
delete this.rbfChains[tx];
|
|
|
|
}
|
2022-12-09 14:35:51 -06:00
|
|
|
this.remove(tx);
|
2022-03-08 14:49:25 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default new RbfCache();
|