2023-05-12 16:31:01 -06:00
|
|
|
import config from "../config";
|
2023-05-04 22:59:34 -04:00
|
|
|
import logger from "../logger";
|
2023-05-29 15:56:29 -04:00
|
|
|
import { MempoolTransactionExtended, TransactionStripped } from "../mempool.interfaces";
|
2023-03-05 03:02:46 -06:00
|
|
|
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
2022-12-13 17:11:37 -06:00
|
|
|
import { Common } from "./common";
|
2023-05-12 16:31:01 -06:00
|
|
|
import redisCache from "./redis-cache";
|
2022-12-13 17:11:37 -06:00
|
|
|
|
2023-05-12 16:31:01 -06:00
|
|
|
export interface RbfTransaction extends TransactionStripped {
|
2022-12-13 17:11:37 -06:00
|
|
|
rbf?: boolean;
|
2022-12-17 09:39:06 -06:00
|
|
|
mined?: boolean;
|
2023-07-14 16:08:57 +09:00
|
|
|
fullRbf?: boolean;
|
2022-12-13 17:11:37 -06:00
|
|
|
}
|
2022-12-09 10:32:58 -06:00
|
|
|
|
2023-05-12 16:31:01 -06:00
|
|
|
export interface RbfTree {
|
2022-12-17 09:39:06 -06:00
|
|
|
tx: RbfTransaction;
|
|
|
|
time: number;
|
|
|
|
interval?: number;
|
|
|
|
mined?: boolean;
|
|
|
|
fullRbf: boolean;
|
|
|
|
replaces: RbfTree[];
|
|
|
|
}
|
2022-12-14 08:49:35 -06:00
|
|
|
|
2023-07-14 16:08:57 +09:00
|
|
|
export interface ReplacementInfo {
|
|
|
|
mined: boolean;
|
|
|
|
fullRbf: boolean;
|
|
|
|
txid: string;
|
|
|
|
oldFee: number;
|
|
|
|
oldVsize: number;
|
|
|
|
newFee: number;
|
|
|
|
newVsize: number;
|
|
|
|
}
|
|
|
|
|
2023-05-12 16:31:01 -06:00
|
|
|
enum CacheOp {
|
|
|
|
Remove = 0,
|
|
|
|
Add = 1,
|
|
|
|
Change = 2,
|
|
|
|
}
|
|
|
|
|
|
|
|
interface CacheEvent {
|
|
|
|
op: CacheOp;
|
|
|
|
type: 'tx' | 'tree' | 'exp';
|
|
|
|
txid: string,
|
|
|
|
value?: any,
|
|
|
|
}
|
|
|
|
|
2022-03-08 14:49:25 +01:00
|
|
|
class RbfCache {
|
2022-12-14 08:49:35 -06:00
|
|
|
private replacedBy: Map<string, string> = new Map();
|
|
|
|
private replaces: Map<string, string[]> = new Map();
|
2022-12-17 09:39:06 -06:00
|
|
|
private rbfTrees: Map<string, RbfTree> = new Map(); // sequences of consecutive replacements
|
|
|
|
private dirtyTrees: Set<string> = new Set();
|
|
|
|
private treeMap: Map<string, string> = new Map(); // map of txids to sequence ids
|
2023-05-29 15:56:29 -04:00
|
|
|
private txs: Map<string, MempoolTransactionExtended> = new Map();
|
2023-05-04 22:59:34 -04:00
|
|
|
private expiring: Map<string, number> = new Map();
|
2023-05-12 16:31:01 -06:00
|
|
|
private cacheQueue: CacheEvent[] = [];
|
2022-03-08 14:49:25 +01:00
|
|
|
|
|
|
|
constructor() {
|
2023-05-04 22:59:34 -04:00
|
|
|
setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
|
2022-03-08 14:49:25 +01:00
|
|
|
}
|
|
|
|
|
2023-05-12 16:31:01 -06:00
|
|
|
private addTx(txid: string, tx: MempoolTransactionExtended): void {
|
|
|
|
this.txs.set(txid, tx);
|
|
|
|
this.cacheQueue.push({ op: CacheOp.Add, type: 'tx', txid });
|
|
|
|
}
|
|
|
|
|
|
|
|
private addTree(txid: string, tree: RbfTree): void {
|
|
|
|
this.rbfTrees.set(txid, tree);
|
|
|
|
this.dirtyTrees.add(txid);
|
|
|
|
this.cacheQueue.push({ op: CacheOp.Add, type: 'tree', txid });
|
|
|
|
}
|
|
|
|
|
|
|
|
private addExpiration(txid: string, expiry: number): void {
|
|
|
|
this.expiring.set(txid, expiry);
|
|
|
|
this.cacheQueue.push({ op: CacheOp.Add, type: 'exp', txid, value: expiry });
|
|
|
|
}
|
|
|
|
|
|
|
|
private removeTx(txid: string): void {
|
|
|
|
this.txs.delete(txid);
|
|
|
|
this.cacheQueue.push({ op: CacheOp.Remove, type: 'tx', txid });
|
|
|
|
}
|
|
|
|
|
|
|
|
private removeTree(txid: string): void {
|
|
|
|
this.rbfTrees.delete(txid);
|
|
|
|
this.cacheQueue.push({ op: CacheOp.Remove, type: 'tree', txid });
|
|
|
|
}
|
|
|
|
|
|
|
|
private removeExpiration(txid: string): void {
|
|
|
|
this.expiring.delete(txid);
|
|
|
|
this.cacheQueue.push({ op: CacheOp.Remove, type: 'exp', txid });
|
|
|
|
}
|
|
|
|
|
2023-05-29 15:56:29 -04:00
|
|
|
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
|
2023-05-18 09:51:41 -04:00
|
|
|
if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
|
2022-12-17 09:39:06 -06:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-12-13 17:11:37 -06:00
|
|
|
const newTx = Common.stripTransaction(newTxExtended) as RbfTransaction;
|
2023-05-11 08:57:12 -06:00
|
|
|
const newTime = newTxExtended.firstSeen || (Date.now() / 1000);
|
2022-12-13 17:11:37 -06:00
|
|
|
newTx.rbf = newTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
|
2023-05-12 16:31:01 -06:00
|
|
|
this.addTx(newTx.txid, newTxExtended);
|
2022-12-17 09:39:06 -06:00
|
|
|
|
|
|
|
// maintain rbf trees
|
2023-07-14 16:08:57 +09:00
|
|
|
let txFullRbf = false;
|
|
|
|
let treeFullRbf = false;
|
2022-12-17 09:39:06 -06:00
|
|
|
const replacedTrees: RbfTree[] = [];
|
|
|
|
for (const replacedTxExtended of replaced) {
|
|
|
|
const replacedTx = Common.stripTransaction(replacedTxExtended) as RbfTransaction;
|
|
|
|
replacedTx.rbf = replacedTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
|
2023-07-14 16:08:57 +09:00
|
|
|
if (!replacedTx.rbf) {
|
|
|
|
txFullRbf = true;
|
|
|
|
}
|
2022-12-17 09:39:06 -06:00
|
|
|
this.replacedBy.set(replacedTx.txid, newTx.txid);
|
|
|
|
if (this.treeMap.has(replacedTx.txid)) {
|
|
|
|
const treeId = this.treeMap.get(replacedTx.txid);
|
|
|
|
if (treeId) {
|
|
|
|
const tree = this.rbfTrees.get(treeId);
|
2023-05-12 16:31:01 -06:00
|
|
|
this.removeTree(treeId);
|
2022-12-17 09:39:06 -06:00
|
|
|
if (tree) {
|
|
|
|
tree.interval = newTime - tree?.time;
|
|
|
|
replacedTrees.push(tree);
|
2023-07-14 16:08:57 +09:00
|
|
|
treeFullRbf = treeFullRbf || tree.fullRbf || !tree.tx.rbf;
|
2022-12-17 09:39:06 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
2023-05-11 08:57:12 -06:00
|
|
|
const replacedTime = replacedTxExtended.firstSeen || (Date.now() / 1000);
|
2022-12-17 09:39:06 -06:00
|
|
|
replacedTrees.push({
|
|
|
|
tx: replacedTx,
|
|
|
|
time: replacedTime,
|
|
|
|
interval: newTime - replacedTime,
|
|
|
|
fullRbf: !replacedTx.rbf,
|
|
|
|
replaces: [],
|
|
|
|
});
|
2023-07-14 16:08:57 +09:00
|
|
|
treeFullRbf = treeFullRbf || !replacedTx.rbf;
|
2023-05-12 16:31:01 -06:00
|
|
|
this.addTx(replacedTx.txid, replacedTxExtended);
|
2022-12-17 09:39:06 -06:00
|
|
|
}
|
2022-12-09 10:32:58 -06:00
|
|
|
}
|
2023-07-14 16:08:57 +09:00
|
|
|
newTx.fullRbf = txFullRbf;
|
2022-12-17 09:39:06 -06:00
|
|
|
const treeId = replacedTrees[0].tx.txid;
|
|
|
|
const newTree = {
|
|
|
|
tx: newTx,
|
2023-05-11 08:57:12 -06:00
|
|
|
time: newTime,
|
2023-07-14 16:08:57 +09:00
|
|
|
fullRbf: treeFullRbf,
|
2022-12-17 09:39:06 -06:00
|
|
|
replaces: replacedTrees
|
|
|
|
};
|
2023-05-12 16:31:01 -06:00
|
|
|
this.addTree(treeId, newTree);
|
2022-12-17 09:39:06 -06:00
|
|
|
this.updateTreeMap(treeId, newTree);
|
|
|
|
this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid));
|
2022-03-08 14:49:25 +01:00
|
|
|
}
|
|
|
|
|
2023-07-25 14:00:17 +09:00
|
|
|
public has(txId: string): boolean {
|
|
|
|
return this.txs.has(txId);
|
|
|
|
}
|
|
|
|
|
|
|
|
public anyInSameTree(txId: string, predicate: (tx: RbfTransaction) => boolean): boolean {
|
|
|
|
const tree = this.getRbfTree(txId);
|
|
|
|
if (!tree) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
const txs = this.getTransactionsInTree(tree);
|
|
|
|
for (const tx of txs) {
|
|
|
|
if (predicate(tx)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-12-09 14:35:51 -06:00
|
|
|
public getReplacedBy(txId: string): string | undefined {
|
2022-12-14 08:49:35 -06:00
|
|
|
return this.replacedBy.get(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-14 08:49:35 -06:00
|
|
|
return this.replaces.get(txId);
|
2022-12-09 10:32:58 -06:00
|
|
|
}
|
|
|
|
|
2023-05-29 15:56:29 -04:00
|
|
|
public getTx(txId: string): MempoolTransactionExtended | undefined {
|
2022-12-14 08:49:35 -06:00
|
|
|
return this.txs.get(txId);
|
2022-03-08 14:49:25 +01:00
|
|
|
}
|
|
|
|
|
2022-12-17 09:39:06 -06:00
|
|
|
public getRbfTree(txId: string): RbfTree | void {
|
|
|
|
return this.rbfTrees.get(this.treeMap.get(txId) || '');
|
2022-12-13 17:11:37 -06:00
|
|
|
}
|
|
|
|
|
2022-12-17 09:39:06 -06:00
|
|
|
// get a paginated list of RbfTrees
|
2022-12-14 16:51:53 -06:00
|
|
|
// ordered by most recent replacement time
|
2022-12-17 09:39:06 -06:00
|
|
|
public getRbfTrees(onlyFullRbf: boolean, after?: string): RbfTree[] {
|
2022-12-14 16:51:53 -06:00
|
|
|
const limit = 25;
|
2022-12-17 09:39:06 -06:00
|
|
|
const trees: RbfTree[] = [];
|
2022-12-14 16:51:53 -06:00
|
|
|
const used = new Set<string>();
|
|
|
|
const replacements: string[][] = Array.from(this.replacedBy).reverse();
|
2022-12-17 09:39:06 -06:00
|
|
|
const afterTree = after ? this.treeMap.get(after) : null;
|
|
|
|
let ready = !afterTree;
|
|
|
|
for (let i = 0; i < replacements.length && trees.length <= limit - 1; i++) {
|
2022-12-14 16:51:53 -06:00
|
|
|
const txid = replacements[i][1];
|
2022-12-17 09:39:06 -06:00
|
|
|
const treeId = this.treeMap.get(txid) || '';
|
|
|
|
if (treeId === afterTree) {
|
2022-12-14 16:51:53 -06:00
|
|
|
ready = true;
|
|
|
|
} else if (ready) {
|
2022-12-17 09:39:06 -06:00
|
|
|
if (!used.has(treeId)) {
|
|
|
|
const tree = this.rbfTrees.get(treeId);
|
|
|
|
used.add(treeId);
|
|
|
|
if (tree && (!onlyFullRbf || tree.fullRbf)) {
|
|
|
|
trees.push(tree);
|
2022-12-14 16:51:53 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-12-17 09:39:06 -06:00
|
|
|
return trees;
|
2022-12-14 16:51:53 -06:00
|
|
|
}
|
|
|
|
|
2022-12-17 09:39:06 -06:00
|
|
|
// get map of rbf trees that have been updated since the last call
|
|
|
|
public getRbfChanges(): { trees: {[id: string]: RbfTree }, map: { [txid: string]: string }} {
|
|
|
|
const changes: { trees: {[id: string]: RbfTree }, map: { [txid: string]: string }} = {
|
|
|
|
trees: {},
|
2022-12-14 08:49:35 -06:00
|
|
|
map: {},
|
|
|
|
};
|
2022-12-17 09:39:06 -06:00
|
|
|
this.dirtyTrees.forEach(id => {
|
|
|
|
const tree = this.rbfTrees.get(id);
|
|
|
|
if (tree) {
|
|
|
|
changes.trees[id] = tree;
|
|
|
|
this.getTransactionsInTree(tree).forEach(tx => {
|
|
|
|
changes.map[tx.txid] = id;
|
2022-12-14 08:49:35 -06:00
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
2022-12-17 09:39:06 -06:00
|
|
|
this.dirtyTrees = new Set();
|
2022-12-14 08:49:35 -06:00
|
|
|
return changes;
|
|
|
|
}
|
|
|
|
|
2022-12-14 16:51:53 -06:00
|
|
|
public mined(txid): void {
|
2023-05-04 22:59:34 -04:00
|
|
|
if (!this.txs.has(txid)) {
|
|
|
|
return;
|
|
|
|
}
|
2022-12-17 09:39:06 -06:00
|
|
|
const treeId = this.treeMap.get(txid);
|
|
|
|
if (treeId && this.rbfTrees.has(treeId)) {
|
|
|
|
const tree = this.rbfTrees.get(treeId);
|
|
|
|
if (tree) {
|
|
|
|
this.setTreeMined(tree, txid);
|
|
|
|
tree.mined = true;
|
|
|
|
this.dirtyTrees.add(treeId);
|
2023-05-12 16:31:01 -06:00
|
|
|
this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId });
|
2022-12-14 16:51:53 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
this.evict(txid);
|
|
|
|
}
|
2022-12-14 08:49:35 -06:00
|
|
|
|
2022-12-09 14:35:51 -06:00
|
|
|
// flag a transaction as removed from the mempool
|
2023-05-04 19:10:53 -04:00
|
|
|
public evict(txid: string, fast: boolean = false): void {
|
2023-05-04 22:59:34 -04:00
|
|
|
if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) {
|
2023-05-12 16:31:01 -06:00
|
|
|
const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours
|
|
|
|
this.addExpiration(txid, expiryTime);
|
2023-05-04 22:59:34 -04:00
|
|
|
}
|
2022-12-09 14:35:51 -06:00
|
|
|
}
|
|
|
|
|
2023-06-19 18:14:09 -04:00
|
|
|
// is the transaction involved in a full rbf replacement?
|
|
|
|
public isFullRbf(txid: string): boolean {
|
|
|
|
const treeId = this.treeMap.get(txid);
|
|
|
|
if (!treeId) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
const tree = this.rbfTrees.get(treeId);
|
|
|
|
if (!tree) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return tree?.fullRbf;
|
|
|
|
}
|
|
|
|
|
2022-03-08 14:49:25 +01:00
|
|
|
private cleanup(): void {
|
2023-05-04 22:59:34 -04:00
|
|
|
const now = Date.now();
|
|
|
|
for (const txid of this.expiring.keys()) {
|
|
|
|
if ((this.expiring.get(txid) || 0) < now) {
|
2023-05-12 16:31:01 -06:00
|
|
|
this.removeExpiration(txid);
|
2022-12-09 14:35:51 -06:00
|
|
|
this.remove(txid);
|
2022-12-09 10:32:58 -06:00
|
|
|
}
|
|
|
|
}
|
2023-05-12 16:31:01 -06:00
|
|
|
logger.debug(`rbf cache contains ${this.txs.size} txs, ${this.rbfTrees.size} trees, ${this.expiring.size} due to expire`);
|
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
|
2022-12-14 08:49:35 -06:00
|
|
|
if (!this.replacedBy.has(txid)) {
|
|
|
|
const replaces = this.replaces.get(txid);
|
|
|
|
this.replaces.delete(txid);
|
2022-12-17 09:39:06 -06:00
|
|
|
this.treeMap.delete(txid);
|
2023-05-12 16:31:01 -06:00
|
|
|
this.removeTx(txid);
|
|
|
|
this.removeExpiration(txid);
|
2022-12-14 08:49:35 -06:00
|
|
|
for (const tx of (replaces || [])) {
|
2022-12-09 14:35:51 -06:00
|
|
|
// recursively remove prior versions from the cache
|
2022-12-14 08:49:35 -06:00
|
|
|
this.replacedBy.delete(tx);
|
2022-12-17 09:39:06 -06:00
|
|
|
// if this is the id of a tree, remove that too
|
|
|
|
if (this.treeMap.get(tx) === tx) {
|
2023-05-12 16:31:01 -06:00
|
|
|
this.removeTree(tx);
|
2022-12-13 17:11:37 -06:00
|
|
|
}
|
2022-12-09 14:35:51 -06:00
|
|
|
this.remove(tx);
|
2022-03-08 14:49:25 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-12-17 09:39:06 -06:00
|
|
|
|
|
|
|
private updateTreeMap(newId: string, tree: RbfTree): void {
|
|
|
|
this.treeMap.set(tree.tx.txid, newId);
|
|
|
|
tree.replaces.forEach(subtree => {
|
|
|
|
this.updateTreeMap(newId, subtree);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private getTransactionsInTree(tree: RbfTree, txs: RbfTransaction[] = []): RbfTransaction[] {
|
|
|
|
txs.push(tree.tx);
|
|
|
|
tree.replaces.forEach(subtree => {
|
|
|
|
this.getTransactionsInTree(subtree, txs);
|
|
|
|
});
|
|
|
|
return txs;
|
|
|
|
}
|
|
|
|
|
|
|
|
private setTreeMined(tree: RbfTree, txid: string): void {
|
|
|
|
if (tree.tx.txid === txid) {
|
|
|
|
tree.tx.mined = true;
|
|
|
|
} else {
|
|
|
|
tree.replaces.forEach(subtree => {
|
|
|
|
this.setTreeMined(subtree, txid);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2023-03-05 03:02:46 -06:00
|
|
|
|
2023-05-12 16:31:01 -06:00
|
|
|
public async updateCache(): Promise<void> {
|
|
|
|
if (!config.REDIS.ENABLED) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Update the Redis cache by replaying queued events
|
|
|
|
for (const e of this.cacheQueue) {
|
|
|
|
if (e.op === CacheOp.Add || e.op === CacheOp.Change) {
|
|
|
|
let value = e.value;
|
|
|
|
switch(e.type) {
|
|
|
|
case 'tx': {
|
|
|
|
value = this.txs.get(e.txid);
|
|
|
|
} break;
|
|
|
|
case 'tree': {
|
|
|
|
const tree = this.rbfTrees.get(e.txid);
|
|
|
|
value = tree ? this.exportTree(tree) : null;
|
|
|
|
} break;
|
|
|
|
}
|
|
|
|
if (value != null) {
|
|
|
|
await redisCache.$setRbfEntry(e.type, e.txid, value);
|
|
|
|
}
|
|
|
|
} else if (e.op === CacheOp.Remove) {
|
|
|
|
await redisCache.$removeRbfEntry(e.type, e.txid);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.cacheQueue = [];
|
|
|
|
}
|
|
|
|
|
2023-03-05 03:02:46 -06:00
|
|
|
public dump(): any {
|
|
|
|
const trees = Array.from(this.rbfTrees.values()).map((tree: RbfTree) => { return this.exportTree(tree); });
|
|
|
|
|
|
|
|
return {
|
|
|
|
txs: Array.from(this.txs.entries()),
|
|
|
|
trees,
|
|
|
|
expiring: Array.from(this.expiring.entries()),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
public async load({ txs, trees, expiring }): Promise<void> {
|
|
|
|
txs.forEach(txEntry => {
|
2023-07-30 16:01:03 +09:00
|
|
|
this.txs.set(txEntry.key, txEntry.value);
|
2023-03-05 03:02:46 -06:00
|
|
|
});
|
|
|
|
for (const deflatedTree of trees) {
|
|
|
|
await this.importTree(deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
|
|
|
|
}
|
|
|
|
expiring.forEach(expiringEntry => {
|
2023-07-30 16:01:03 +09:00
|
|
|
if (this.txs.has(expiringEntry.key)) {
|
|
|
|
this.expiring.set(expiringEntry.key, new Date(expiringEntry.value).getTime());
|
2023-05-04 22:59:34 -04:00
|
|
|
}
|
2023-03-05 03:02:46 -06:00
|
|
|
});
|
|
|
|
this.cleanup();
|
|
|
|
}
|
|
|
|
|
|
|
|
exportTree(tree: RbfTree, deflated: any = null) {
|
|
|
|
if (!deflated) {
|
|
|
|
deflated = {
|
|
|
|
root: tree.tx.txid,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
deflated[tree.tx.txid] = {
|
|
|
|
tx: tree.tx.txid,
|
|
|
|
txMined: tree.tx.mined,
|
|
|
|
time: tree.time,
|
|
|
|
interval: tree.interval,
|
|
|
|
mined: tree.mined,
|
|
|
|
fullRbf: tree.fullRbf,
|
|
|
|
replaces: tree.replaces.map(child => child.tx.txid),
|
|
|
|
};
|
|
|
|
tree.replaces.forEach(child => {
|
|
|
|
this.exportTree(child, deflated);
|
|
|
|
});
|
|
|
|
return deflated;
|
|
|
|
}
|
|
|
|
|
2023-05-29 15:56:29 -04:00
|
|
|
async importTree(root, txid, deflated, txs: Map<string, MempoolTransactionExtended>, mined: boolean = false): Promise<RbfTree | void> {
|
2023-03-05 03:02:46 -06:00
|
|
|
const treeInfo = deflated[txid];
|
|
|
|
const replaces: RbfTree[] = [];
|
|
|
|
|
|
|
|
// check if any transactions in this tree have already been confirmed
|
|
|
|
mined = mined || treeInfo.mined;
|
2023-05-04 22:59:34 -04:00
|
|
|
let exists = mined;
|
2023-03-05 03:02:46 -06:00
|
|
|
if (!mined) {
|
|
|
|
try {
|
|
|
|
const apiTx = await bitcoinApi.$getRawTransaction(txid);
|
2023-05-04 22:59:34 -04:00
|
|
|
if (apiTx) {
|
|
|
|
exists = true;
|
|
|
|
}
|
2023-03-05 03:02:46 -06:00
|
|
|
if (apiTx?.status?.confirmed) {
|
|
|
|
mined = true;
|
2023-05-04 22:59:34 -04:00
|
|
|
treeInfo.txMined = true;
|
|
|
|
this.evict(txid, true);
|
2023-03-05 03:02:46 -06:00
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
// most transactions do not exist
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-04 22:59:34 -04:00
|
|
|
// if the root tx is not in the mempool or the blockchain
|
|
|
|
// evict this tree as soon as possible
|
|
|
|
if (root === txid && !exists) {
|
|
|
|
this.evict(txid, true);
|
|
|
|
}
|
|
|
|
|
2023-03-05 03:02:46 -06:00
|
|
|
// recursively reconstruct child trees
|
|
|
|
for (const childId of treeInfo.replaces) {
|
|
|
|
const replaced = await this.importTree(root, childId, deflated, txs, mined);
|
|
|
|
if (replaced) {
|
|
|
|
this.replacedBy.set(replaced.tx.txid, txid);
|
|
|
|
replaces.push(replaced);
|
|
|
|
if (replaced.mined) {
|
|
|
|
mined = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.replaces.set(txid, replaces.map(t => t.tx.txid));
|
|
|
|
|
|
|
|
const tx = txs.get(txid);
|
|
|
|
if (!tx) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const strippedTx = Common.stripTransaction(tx) as RbfTransaction;
|
|
|
|
strippedTx.rbf = tx.vin.some((v) => v.sequence < 0xfffffffe);
|
|
|
|
strippedTx.mined = treeInfo.txMined;
|
|
|
|
const tree = {
|
|
|
|
tx: strippedTx,
|
|
|
|
time: treeInfo.time,
|
|
|
|
interval: treeInfo.interval,
|
|
|
|
mined: mined,
|
|
|
|
fullRbf: treeInfo.fullRbf,
|
|
|
|
replaces,
|
|
|
|
};
|
|
|
|
this.treeMap.set(txid, root);
|
|
|
|
if (root === txid) {
|
2023-05-12 16:31:01 -06:00
|
|
|
this.addTree(root, tree);
|
2023-03-05 03:02:46 -06:00
|
|
|
}
|
|
|
|
return tree;
|
|
|
|
}
|
2023-07-14 16:08:57 +09:00
|
|
|
|
|
|
|
public getLatestRbfSummary(): ReplacementInfo[] {
|
|
|
|
const rbfList = this.getRbfTrees(false);
|
|
|
|
return rbfList.slice(0, 6).map(rbfTree => {
|
|
|
|
let oldFee = 0;
|
|
|
|
let oldVsize = 0;
|
|
|
|
for (const replaced of rbfTree.replaces) {
|
|
|
|
oldFee += replaced.tx.fee;
|
|
|
|
oldVsize += replaced.tx.vsize;
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
txid: rbfTree.tx.txid,
|
|
|
|
mined: !!rbfTree.tx.mined,
|
|
|
|
fullRbf: !!rbfTree.tx.fullRbf,
|
|
|
|
oldFee,
|
|
|
|
oldVsize,
|
|
|
|
newFee: rbfTree.tx.fee,
|
|
|
|
newVsize: rbfTree.tx.vsize,
|
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
2022-03-08 14:49:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
export default new RbfCache();
|