record & display transaction first seen times

This commit is contained in:
Mononaut
2022-11-28 13:20:51 +09:00
parent 5905eebaa6
commit 639294d319
10 changed files with 141 additions and 12 deletions

View File

@@ -25,6 +25,7 @@ class BitcoinRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'transaction-times', this.getTransactionTimes)
.get(config.MEMPOOL.API_URL_PREFIX + 'outspends', this.$getBatchedOutspends)
.get(config.MEMPOOL.API_URL_PREFIX + 'cpfp/:txId', this.$getCpfpInfo)
.get(config.MEMPOOL.API_URL_PREFIX + 'extras/:txId', this.$getTransactionExtras)
.get(config.MEMPOOL.API_URL_PREFIX + 'difficulty-adjustment', this.getDifficultyChange)
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/recommended', this.getRecommendedFees)
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/mempool-blocks', this.getMempoolBlocks)
@@ -221,6 +222,42 @@ class BitcoinRoutes {
res.status(404).send(`Transaction has no CPFP info available.`);
}
private async $getTransactionExtras(req: Request, res: Response): Promise<void> {
if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
res.status(501).send(`Invalid transaction ID.`);
return;
}
const tx = mempool.getMempool()[req.params.txId];
if (tx) {
if (tx?.cpfpChecked) {
res.json({
ancestors: tx.ancestors,
bestDescendant: tx.bestDescendant || null,
descendants: tx.descendants || null,
effectiveFeePerVsize: tx.effectiveFeePerVsize || null,
firstSeen: tx.firstSeen,
});
return;
}
const cpfpInfo = Common.setRelativesAndGetCpfpInfo(tx, mempool.getMempool());
res.json({
...cpfpInfo,
firstSeen: tx.firstSeen,
});
return;
} else {
const extras = await transactionRepository.$getTransactionExtras(req.params.txId);
if (extras) {
res.json(extras);
return;
}
}
res.status(404).send(`Transaction has no extra info available.`);
}
private getBackendInfo(req: Request, res: Response) {
res.json(backendInfo.getBackendInfo());
}

View File

@@ -4,7 +4,7 @@ import logger from '../logger';
import { Common } from './common';
class DatabaseMigration {
private static currentVersion = 49;
private static currentVersion = 50;
private queryTimeout = 3600_000;
private statisticsAddedIndexed = false;
private uniqueLogs: string[] = [];
@@ -442,6 +442,11 @@ class DatabaseMigration {
await this.$executeQuery('TRUNCATE TABLE `blocks_audits`');
await this.updateToSchemaVersion(49);
}
if (databaseSchemaVersion < 50 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `transactions` ADD first_seen datetime DEFAULT NULL');
await this.updateToSchemaVersion(49);
}
}
/**

View File

@@ -9,6 +9,7 @@ import loadingIndicators from './loading-indicators';
import bitcoinClient from './bitcoin/bitcoin-client';
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import rbfCache from './rbf-cache';
import transactionRepository from '../repositories/TransactionRepository';
class Mempool {
private static WEBSOCKET_REFRESH_RATE_MS = 10000;
@@ -217,6 +218,14 @@ class Mempool {
}
}
public async $saveTxFirstSeenTimes(transactions: TransactionExtended[], mempool: { [txid: string]: TransactionExtended }) {
for (const tx of transactions) {
if (mempool[tx.txid]) {
await transactionRepository.$saveTxFirstSeen(tx.txid, tx.firstSeen || Date.now());
}
}
}
private updateTxPerSecond() {
const nowMinusTimeSpan = new Date().getTime() - (1000 * config.STATISTICS.TX_PER_SECOND_SAMPLE_PERIOD);
this.txPerSecondArray = this.txPerSecondArray.filter((unixTime) => unixTime > nowMinusTimeSpan);

View File

@@ -19,6 +19,7 @@ import feeApi from './fee-api';
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
import Audit from './audit';
import mempool from './mempool';
class WebsocketHandler {
private wss: WebSocket.Server | undefined;
@@ -462,6 +463,8 @@ class WebsocketHandler {
}
}
await mempool.$saveTxFirstSeenTimes(transactions, _memPool);
const removed: string[] = [];
// Update mempool to remove transactions included in the new block
for (const txId of txIds) {

View File

@@ -136,6 +136,10 @@ export interface CpfpInfo {
effectiveFeePerVsize?: number;
}
export interface TransactionExtras extends CpfpInfo {
firstSeen?: number;
}
export interface TransactionStripped {
txid: string;
fee: number;

View File

@@ -1,14 +1,15 @@
import DB from '../database';
import logger from '../logger';
import { Ancestor, CpfpInfo } from '../mempool.interfaces';
import { Ancestor, CpfpInfo, TransactionExtended, TransactionExtras } from '../mempool.interfaces';
interface CpfpSummary {
interface TxInfo {
txid: string;
cluster: string;
root: string;
txs: Ancestor[];
height: number;
fee_rate: number;
firstSeen: number;
}
class TransactionRepository {
@@ -33,6 +34,46 @@ class TransactionRepository {
}
}
public async $saveTxFirstSeen(txid: string, seenAt: number) {
try {
await DB.query(
`
INSERT INTO transactions
(
txid,
first_seen
)
VALUE (?, FROM_UNIXTIME(?))
ON DUPLICATE KEY UPDATE
first_seen = FROM_UNIXTIME(?)
;`,
[txid, seenAt, seenAt]
);
} catch (e: any) {
logger.err(`Cannot save transaction first seen time into db. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getTransactionExtras(txid: string): Promise<TransactionExtras | void> {
try {
let query = `
SELECT *, UNIX_TIMESTAMP(first_seen) as firstSeen
FROM transactions
LEFT JOIN cpfp_clusters AS cluster ON cluster.root = transactions.cluster
WHERE transactions.txid = ?
`;
const [rows]: any = await DB.query(query, [txid]);
if (rows.length) {
rows[0].txs = JSON.parse(rows[0].txs) as Ancestor[];
return this.convertCpfp(rows[0]);
}
} catch (e) {
logger.err('Cannot get transaction cpfp info from db. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getCpfpInfo(txid: string): Promise<CpfpInfo | void> {
try {
let query = `
@@ -54,12 +95,12 @@ class TransactionRepository {
}
}
private convertCpfp(cpfp: CpfpSummary): CpfpInfo {
private convertCpfp(info: TxInfo): TransactionExtras {
const descendants: Ancestor[] = [];
const ancestors: Ancestor[] = [];
let matched = false;
for (const tx of cpfp.txs) {
if (tx.txid === cpfp.txid) {
for (const tx of (info.txs || [])) {
if (tx.txid === info.txid) {
matched = true;
} else if (!matched) {
descendants.push(tx);
@@ -68,9 +109,10 @@ class TransactionRepository {
}
}
return {
descendants,
ancestors,
effectiveFeePerVsize: cpfp.fee_rate
descendants: descendants?.length ? descendants : undefined,
ancestors: ancestors,
effectiveFeePerVsize: info.fee_rate,
firstSeen: info.firstSeen || undefined,
};
}
}