record & display transaction first seen times
This commit is contained in:
parent
5905eebaa6
commit
639294d319
@ -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());
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -136,6 +136,10 @@ export interface CpfpInfo {
|
||||
effectiveFeePerVsize?: number;
|
||||
}
|
||||
|
||||
export interface TransactionExtras extends CpfpInfo {
|
||||
firstSeen?: number;
|
||||
}
|
||||
|
||||
export interface TransactionStripped {
|
||||
txid: string;
|
||||
fee: number;
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -54,6 +54,18 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<ng-template [ngIf]="transactionTime !== 0">
|
||||
<tr *ngIf="transactionTime === -1; else firstSeenTmpl">
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<ng-template #firstSeenTmpl>
|
||||
<tr>
|
||||
<td i18n="transaction.first-seen|Transaction first seen">First seen</td>
|
||||
<td><i><app-time-since [time]="transactionTime" [fastRender]="true"></app-time-since></i></td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
<tr *ngIf="latestBlock && tx.status.block_height <= latestBlock.height - 8">
|
||||
<td class="td-width" i18n="transaction.included-in-block|Transaction included in block">Included in block</td>
|
||||
<td>
|
||||
@ -66,7 +78,7 @@
|
||||
<td><app-time-span [time]="tx.status.block_time - transactionTime" [fastRender]="true"></app-time-span></td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet' && cpfpInfo && (cpfpInfo?.bestDescendant || cpfpInfo?.descendants?.length || cpfpInfo?.ancestors?.length)">
|
||||
<td class="td-width" i18n="transaction.features|Transaction features">Features</td>
|
||||
<td>
|
||||
<app-tx-features [tx]="tx"></app-tx-features>
|
||||
@ -497,6 +509,12 @@
|
||||
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right" (click)="showCpfpDetails = !showCpfpDetails">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="tx?.status?.confirmed && (!cpfpInfo || (!cpfpInfo?.bestDescendant && !cpfpInfo?.descendants?.length && !cpfpInfo?.ancestors?.length)) && network !== 'liquid' && network !== 'liquidtestnet'">
|
||||
<td class="td-width" i18n="transaction.features|Transaction Features">Features</td>
|
||||
<td>
|
||||
<app-tx-features [tx]="tx"></app-tx-features>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-template>
|
||||
|
@ -110,7 +110,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
.pipe(
|
||||
switchMap((txId) =>
|
||||
this.apiService
|
||||
.getCpfpinfo$(txId)
|
||||
.getTransactionExtras$(txId)
|
||||
.pipe(retryWhen((errors) => errors.pipe(
|
||||
mergeMap((error) => {
|
||||
if (!this.tx?.status || this.tx.status.confirmed) {
|
||||
@ -156,6 +156,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
txFeePerVSize: this.tx.effectiveFeePerVsize,
|
||||
});
|
||||
}
|
||||
if (cpfpInfo.firstSeen) {
|
||||
this.transactionTime = cpfpInfo.firstSeen;
|
||||
}
|
||||
this.cpfpInfo = cpfpInfo;
|
||||
});
|
||||
|
||||
|
@ -27,6 +27,10 @@ export interface CpfpInfo {
|
||||
effectiveFeePerVsize?: number;
|
||||
}
|
||||
|
||||
export interface TransactionExtras extends CpfpInfo {
|
||||
firstSeen?: number;
|
||||
}
|
||||
|
||||
export interface DifficultyAdjustment {
|
||||
progressPercent: number;
|
||||
difficultyChange: number;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators,
|
||||
PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore } from '../interfaces/node-api.interface';
|
||||
PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, TransactionExtras } from '../interfaces/node-api.interface';
|
||||
import { Observable } from 'rxjs';
|
||||
import { StateService } from './state.service';
|
||||
import { WebsocketResponse } from '../interfaces/websocket.interface';
|
||||
@ -115,6 +115,10 @@ export class ApiService {
|
||||
return this.httpClient.get<CpfpInfo>(this.apiBaseUrl + this.apiBasePath + '/api/v1/cpfp/' + txid);
|
||||
}
|
||||
|
||||
getTransactionExtras$(txid: string): Observable<TransactionExtras> {
|
||||
return this.httpClient.get<TransactionExtras>(this.apiBaseUrl + this.apiBasePath + '/api/v1/extras/' + txid);
|
||||
}
|
||||
|
||||
validateAddress$(address: string): Observable<AddressInformation> {
|
||||
return this.httpClient.get<AddressInformation>(this.apiBaseUrl + this.apiBasePath + '/api/v1/validate-address/' + address);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user