Compare commits

..

2 Commits

Author SHA1 Message Date
Mononaut
d2f6741f40 use new effective fee stat fields in frontend 2025-01-03 17:24:17 +00:00
Mononaut
2befbec7a5 Add separate effective fee stats fields 2025-01-03 17:24:16 +00:00
27 changed files with 129 additions and 391 deletions

View File

@@ -244,7 +244,7 @@ class Blocks {
*/
private async $getBlockExtended(block: IEsploraApi.Block, transactions: TransactionExtended[]): Promise<BlockExtended> {
const coinbaseTx = transactionUtils.stripCoinbaseTransaction(transactions[0]);
const blk: Partial<BlockExtended> = Object.assign({}, block);
const extras: Partial<BlockExtension> = {};
@@ -268,15 +268,17 @@ class Blocks {
extras.segwitTotalWeight = 0;
} else {
const stats: IBitcoinApi.BlockStats = await bitcoinClient.getBlockStats(block.id);
let feeStats = {
const feeStats = {
medianFee: stats.feerate_percentiles[2], // 50th percentiles
feeRange: [stats.minfeerate, stats.feerate_percentiles, stats.maxfeerate].flat(),
};
if (transactions?.length > 1) {
feeStats = Common.calcEffectiveFeeStatistics(transactions);
}
extras.medianFee = feeStats.medianFee;
extras.feeRange = feeStats.feeRange;
if (transactions?.length > 1) {
const effectiveFeeStats = Common.calcEffectiveFeeStatistics(transactions);
extras.effectiveMedianFee = effectiveFeeStats.effective_median;
extras.effectiveFeeRange = effectiveFeeStats.effective_range;
}
extras.totalFees = stats.totalfee;
extras.avgFee = stats.avgfee;
extras.avgFeeRate = stats.avgfeerate;
@@ -296,7 +298,7 @@ class Blocks {
extras.medianFeeAmt = extras.feePercentiles[3];
}
}
extras.virtualSize = block.weight / 4.0;
if (coinbaseTx?.vout.length > 0) {
extras.coinbaseAddress = coinbaseTx.vout[0].scriptpubkey_address ?? null;
@@ -1316,6 +1318,8 @@ class Blocks {
avg_fee_rate: block.extras.avgFeeRate ?? null,
median_fee_rate: block.extras.medianFee ?? null,
fee_rate_percentiles: block.extras.feeRange ?? null,
effective_median_fee_rate: block.extras.effectiveMedianFee ?? null,
effective_fee_rate_percentiles: block.extras.effectiveFeeRange ?? null,
total_inputs: block.extras.totalInputs ?? null,
total_input_amt: block.extras.totalInputAmt ?? null,
total_outputs: block.extras.totalOutputs ?? null,
@@ -1378,6 +1382,17 @@ class Blocks {
'perc_90': cleanBlock.fee_rate_percentiles[5],
'max': cleanBlock.fee_rate_percentiles[6],
};
if (cleanBlock.effective_fee_rate_percentiles) {
cleanBlock.effective_fee_rate_percentiles = {
'min': cleanBlock.effective_fee_rate_percentiles[0],
'perc_10': cleanBlock.effective_fee_rate_percentiles[1],
'perc_25': cleanBlock.effective_fee_rate_percentiles[2],
'perc_50': cleanBlock.effective_fee_rate_percentiles[3],
'perc_75': cleanBlock.effective_fee_rate_percentiles[4],
'perc_90': cleanBlock.effective_fee_rate_percentiles[5],
'max': cleanBlock.effective_fee_rate_percentiles[6],
};
}
// Re-org can happen after indexing so we need to always get the
// latest state from core

View File

@@ -1,6 +1,6 @@
import * as bitcoinjs from 'bitcoinjs-lib';
import { Request } from 'express';
import { EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats, TransactionClassified, TransactionFlags } from '../mempool.interfaces';
import { EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats, TransactionClassified, TransactionFlags, FeeStats } from '../mempool.interfaces';
import config from '../config';
import { NodeSocket } from '../repositories/NodesSocketsRepository';
import { isIP } from 'net';
@@ -856,6 +856,15 @@ export class Common {
}
}
static calcFeeStatistics(transactions: { txid: string, feePerVsize: number }[]): FeeStats {
// skip the coinbase, then sort the remaining fee rates
const sortedRates = transactions.slice(1).map(tx => tx.feePerVsize).sort((a, b) => a - b);
return {
median: Math.round(Common.getNthPercentile(50, sortedRates)),
range: [0, 10, 25, 50, 75, 90, 100].map(n => Math.round(Common.getNthPercentile(n, sortedRates))),
};
}
static calcEffectiveFeeStatistics(transactions: { weight: number, fee: number, effectiveFeePerVsize?: number, txid: string, acceleration?: boolean }[]): EffectiveFeeStats {
const sortedTxs = transactions.map(tx => { return { txid: tx.txid, weight: tx.weight, rate: tx.effectiveFeePerVsize || ((tx.fee || 0) / (tx.weight / 4)) }; }).sort((a, b) => a.rate - b.rate);
@@ -898,8 +907,8 @@ export class Common {
);
return {
medianFee: medianFeeRate,
feeRange: [
effective_median: medianFeeRate,
effective_range: [
minFee,
[10,25,50,75,90].map(n => Common.getNthPercentile(n, sortedTxs).rate),
maxFee,
@@ -1150,16 +1159,16 @@ export class OnlineFeeStatsCalculator {
}
return {
minFee: this.feeRange[0].min,
medianFee: this.feeRange[Math.floor(this.feeRange.length / 2)].avg,
effective_median: this.feeRange[Math.floor(this.feeRange.length / 2)].avg,
maxFee: this.feeRange[this.feeRange.length - 1].max,
feeRange: this.feeRange.map(f => f.avg),
effective_range: this.feeRange.map(f => f.avg),
};
}
getFeeStats(): EffectiveFeeStats {
const stats = this.getRawFeeStats();
stats.feeRange[0] = stats.minFee;
stats.feeRange[stats.feeRange.length - 1] = stats.maxFee;
stats.effective_range[0] = stats.minFee;
stats.effective_range[stats.effective_range.length - 1] = stats.maxFee;
return stats;
}
}

View File

@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2';
class DatabaseMigration {
private static currentVersion = 94;
private static currentVersion = 95;
private queryTimeout = 3600_000;
private statisticsAddedIndexed = false;
private uniqueLogs: string[] = [];
@@ -1118,6 +1118,16 @@ class DatabaseMigration {
}
await this.updateToSchemaVersion(94);
}
if (databaseSchemaVersion < 95) {
// Version 95
await this.$executeQuery(`
ALTER TABLE \`blocks\`
ADD \`effective_median_fee\` BIGINT UNSIGNED NOT NULL DEFAULT 0,
ADD \`effective_fee_span\` JSON DEFAULT NULL;
`);
await this.updateToSchemaVersion(95);
}
}
/**

View File

@@ -63,7 +63,8 @@ class FeeApi {
}
private optimizeMedianFee(pBlock: MempoolBlock, nextBlock: MempoolBlock | undefined, previousFee?: number): number {
const useFee = previousFee ? (pBlock.medianFee + previousFee) / 2 : pBlock.medianFee;
const medianFee = pBlock.effectiveMedianFee ?? pBlock.medianFee;
const useFee = previousFee ? (medianFee + previousFee) / 2 : medianFee;
if (pBlock.blockVSize <= 500000) {
return this.defaultFee;
}

View File

@@ -1,6 +1,6 @@
import { GbtGenerator, GbtResult, ThreadTransaction as RustThreadTransaction, ThreadAcceleration as RustThreadAcceleration } from 'rust-gbt';
import logger from '../logger';
import { MempoolBlock, MempoolTransactionExtended, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats, TransactionClassified, TransactionCompressed, MempoolDeltaChange, GbtCandidates, PoolTag } from '../mempool.interfaces';
import { MempoolBlock, MempoolTransactionExtended, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, FeeStats, TransactionClassified, TransactionCompressed, MempoolDeltaChange, GbtCandidates, PoolTag, EffectiveFeeStats } from '../mempool.interfaces';
import { Common, OnlineFeeStatsCalculator } from './common';
import config from '../config';
import { Worker } from 'worker_threads';
@@ -33,6 +33,8 @@ class MempoolBlocks {
totalFees: block.totalFees,
medianFee: block.medianFee,
feeRange: block.feeRange,
effectiveMedianFee: block.effectiveMedianFee,
effectiveFeeRange: block.effectiveFeeRange,
};
});
}
@@ -527,7 +529,7 @@ class MempoolBlocks {
totalSize,
totalWeight,
totalFees,
(hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) ? feeStatsCalculator.getRawFeeStats() : undefined,
(hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) ? feeStatsCalculator.getFeeStats() : undefined,
);
};
@@ -541,17 +543,20 @@ class MempoolBlocks {
return mempoolBlocks;
}
private dataToMempoolBlocks(transactionIds: string[], transactions: MempoolTransactionExtended[], totalSize: number, totalWeight: number, totalFees: number, feeStats?: EffectiveFeeStats ): MempoolBlockWithTransactions {
if (!feeStats) {
feeStats = Common.calcEffectiveFeeStatistics(transactions);
private dataToMempoolBlocks(transactionIds: string[], transactions: MempoolTransactionExtended[], totalSize: number, totalWeight: number, totalFees: number, effectiveFeeStats?: EffectiveFeeStats ): MempoolBlockWithTransactions {
const feeStats = Common.calcFeeStatistics(transactions);
if (!effectiveFeeStats) {
effectiveFeeStats = Common.calcEffectiveFeeStatistics(transactions);
}
return {
blockSize: totalSize,
blockVSize: (totalWeight / 4), // fractional vsize to avoid rounding errors
nTx: transactionIds.length,
totalFees: totalFees,
medianFee: feeStats.medianFee, // Common.percentile(transactions.map((tx) => tx.effectiveFeePerVsize), config.MEMPOOL.RECOMMENDED_FEE_PERCENTILE),
feeRange: feeStats.feeRange, //Common.getFeesInRange(transactions, rangeLength),
medianFee: feeStats.median,
feeRange: feeStats.range,
effectiveMedianFee: effectiveFeeStats.effective_median,
effectiveFeeRange: effectiveFeeStats.effective_range,
transactionIds: transactionIds,
transactions: transactions.map((tx) => Common.classifyTransaction(tx)),
};

View File

@@ -73,6 +73,8 @@ export interface MempoolBlock {
medianFee: number;
totalFees: number;
feeRange: number[];
effectiveMedianFee?: number;
effectiveFeeRange?: number[];
}
export interface MempoolBlockWithTransactions extends MempoolBlock {
@@ -288,8 +290,10 @@ export const TransactionFlags = {
export interface BlockExtension {
totalFees: number;
medianFee: number; // median fee rate
feeRange: number[]; // fee rate percentiles
medianFee: number; // core median fee rate
feeRange: number[]; // core fee rate percentiles
effectiveMedianFee?: number; // effective median fee rate
effectiveFeeRange?: number[]; // effective fee rate percentiles
reward: number;
matchRate: number | null;
expectedFees: number | null;
@@ -369,9 +373,18 @@ export interface MempoolStats {
tx_count: number;
}
// Core fee stats
// measured in individual sats/vbyte
export interface FeeStats {
median: number; // median core fee rate
range: number[]; // 0th, 10th, 25th, 50th, 75th, 90th, 100th percentiles
}
// Mempool effective fee stats
// measured in effective sats/vbyte
export interface EffectiveFeeStats {
medianFee: number; // median effective fee rate
feeRange: number[]; // 2nd, 10th, 25th, 50th, 75th, 90th, 98th percentiles
effective_median: number; // median effective fee rate by weight
effective_range: number[]; // 2nd, 10th, 25th, 50th, 75th, 90th, 98th percentiles
}
export interface WorkingEffectiveFeeStats extends EffectiveFeeStats {

View File

@@ -315,12 +315,12 @@ class AccelerationRepository {
Infinity
);
const feeStats = Common.calcEffectiveFeeStatistics(template);
boostRate = feeStats.medianFee;
boostRate = feeStats.effective_median;
}
const accelerationSummaries = accelerations.map(acc => ({
...acc,
pools: acc.pools,
}))
}));
for (const acc of accelerations) {
if (blockTxs[acc.txid] && acc.pools.includes(block.extras.pool.id)) {
const tx = blockTxs[acc.txid];

View File

@@ -33,6 +33,8 @@ interface DatabaseBlock {
totalFees: number;
medianFee: number;
feeRange: string;
effectiveMedianFee?: number;
effectiveFeeRange?: string;
reward: number;
poolId: number;
poolName: string;
@@ -77,6 +79,8 @@ const BLOCK_DB_FIELDS = `
blocks.fees AS totalFees,
blocks.median_fee AS medianFee,
blocks.fee_span AS feeRange,
blocks.effective_median_fee AS effectiveMedianFee,
blocks.effective_fee_span AS effectiveFeeRange,
blocks.reward,
pools.unique_id AS poolId,
pools.name AS poolName,
@@ -108,7 +112,7 @@ class BlocksRepository {
/**
* Save indexed block data in the database
*/
public async $saveBlockInDatabase(block: BlockExtended) {
public async $saveBlockInDatabase(block: BlockExtended): Promise<void> {
const truncatedCoinbaseSignature = block?.extras?.coinbaseSignature?.substring(0, 500);
const truncatedCoinbaseSignatureAscii = block?.extras?.coinbaseSignatureAscii?.substring(0, 500);
@@ -117,6 +121,7 @@ class BlocksRepository {
height, hash, blockTimestamp, size,
weight, tx_count, coinbase_raw, difficulty,
pool_id, fees, fee_span, median_fee,
effective_fee_span, effective_median_fee,
reward, version, bits, nonce,
merkle_root, previous_block_hash, avg_fee, avg_fee_rate,
median_timestamp, header, coinbase_address, coinbase_addresses,
@@ -128,6 +133,7 @@ class BlocksRepository {
?, ?, FROM_UNIXTIME(?), ?,
?, ?, ?, ?,
?, ?, ?, ?,
?, ?,
?, ?, ?, ?,
?, ?, ?, ?,
FROM_UNIXTIME(?), ?, ?, ?,
@@ -155,6 +161,8 @@ class BlocksRepository {
block.extras.totalFees,
JSON.stringify(block.extras.feeRange),
block.extras.medianFee,
block.extras.effectiveFeeRange ? JSON.stringify(block.extras.effectiveFeeRange) : null,
block.extras.effectiveMedianFee,
block.extras.reward,
block.version,
block.bits,
@@ -968,16 +976,16 @@ class BlocksRepository {
/**
* Save indexed effective fee statistics
*
* @param id
* @param feeStats
*
* @param id
* @param feeStats
*/
public async $saveEffectiveFeeStats(id: string, feeStats: EffectiveFeeStats): Promise<void> {
try {
await DB.query(`
UPDATE blocks SET median_fee = ?, fee_span = ?
UPDATE blocks SET effective_median_fee = ?, effective_fee_span = ?
WHERE hash = ?`,
[feeStats.medianFee, JSON.stringify(feeStats.feeRange), id]
[feeStats.effective_median, JSON.stringify(feeStats.effective_range), id]
);
} catch (e) {
logger.err(`Cannot update block fee stats. Reason: ` + (e instanceof Error ? e.message : e));
@@ -1065,11 +1073,13 @@ class BlocksRepository {
blk.weight = dbBlk.weight;
blk.previousblockhash = dbBlk.previousblockhash;
blk.mediantime = dbBlk.mediantime;
// BlockExtension
extras.totalFees = dbBlk.totalFees;
extras.medianFee = dbBlk.medianFee;
extras.feeRange = JSON.parse(dbBlk.feeRange);
extras.effectiveMedianFee = dbBlk.effectiveMedianFee;
extras.effectiveFeeRange = dbBlk.effectiveFeeRange ? JSON.parse(dbBlk.effectiveFeeRange) : null;
extras.reward = dbBlk.reward;
extras.pool = {
id: dbBlk.poolId,

View File

@@ -38,12 +38,7 @@
}
},
{
"component": "simpleproof",
"mobileOrder": 6,
"props": {
"label": "Executive Decrees",
"key": "el_salvador_decretos"
}
"component": "blocks"
},
{
"component": "addressTransactions",

View File

@@ -32,9 +32,9 @@
<td i18n="block.weight">Weight</td>
<td [innerHTML]="'&lrm;' + (block?.weight | wuBytes: 2)"></td>
</tr>
<tr *ngIf="block?.extras?.medianFee != undefined">
<tr *ngIf="block?.extras?.medianFee != undefined && block?.extras?.effectiveMedianFee != undefined">
<td class="td-width" i18n="block.median-fee">Median fee</td>
<td>~<app-fee-rate [fee]="block?.extras?.medianFee" rounding="1.0-0"></app-fee-rate></td>
<td>~<app-fee-rate [fee]="block?.extras?.effectiveMedianFee ?? block?.extras?.medianFee" rounding="1.0-0"></app-fee-rate></td>
</tr>
<ng-template [ngIf]="fees !== undefined">
<tr>

View File

@@ -132,11 +132,11 @@
<td i18n="mempool-block.fee-span">Fee span</td>
<td><app-fee-rate [fee]="block.extras?.minFee" [showUnit]="false" rounding="1.0-0"></app-fee-rate> - <app-fee-rate [fee]="block.extras?.maxFee" rounding="1.0-0"></app-fee-rate></td>
</tr>
<tr *ngIf="block.extras?.medianFee != undefined">
<tr *ngIf="block.extras?.effectiveMedianFee != undefined || block.extras?.medianFee != undefined">
<td class="td-width" i18n="block.median-fee">Median fee</td>
<td>~<app-fee-rate [fee]="block.extras?.medianFee" rounding="1.0-0"></app-fee-rate>
<td>~<app-fee-rate [fee]="block.extras?.effectiveMedianFee ?? block.extras?.medianFee" rounding="1.0-0"></app-fee-rate>
<span class="fiat">
<app-fiat [blockConversion]="blockConversion" [value]="block.extras?.medianFee * 140" digitsInfo="1.2-2"
<app-fiat [blockConversion]="blockConversion" [value]="(block.extras?.effectiveMedianFee ?? block.extras?.medianFee) * 140" digitsInfo="1.2-2"
i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes"
placement="bottom"></app-fiat>
</span>

View File

@@ -792,6 +792,9 @@ export class BlockComponent implements OnInit, OnDestroy {
}
getMinBlockFee(block: BlockExtended): number {
if (block?.extras?.effectiveFeeRange) {
return block.extras.effectiveFeeRange[0];
}
if (block?.extras?.feeRange) {
// heuristic to check if feeRange is adjusted for effective rates
if (block.extras.medianFee === block.extras.feeRange[3]) {
@@ -804,6 +807,9 @@ export class BlockComponent implements OnInit, OnDestroy {
}
getMaxBlockFee(block: BlockExtended): number {
if (block?.extras?.effectiveFeeRange) {
return block.extras.effectiveFeeRange[block.extras.effectiveFeeRange.length - 1];
}
if (block?.extras?.feeRange) {
return block.extras.feeRange[block.extras.feeRange.length - 1];
}

View File

@@ -23,7 +23,7 @@
<div class="block-body">
<ng-container *ngIf="!minimal">
<div *ngIf="block?.extras; else emptyfees" [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fees">
~<app-fee-rate [fee]="block?.extras?.medianFee" unitClass="" rounding="1.0-0"></app-fee-rate>
~<app-fee-rate [fee]="block?.extras?.effectiveMedianFee ?? block?.extras?.medianFee" unitClass="" rounding="1.0-0"></app-fee-rate>
</div>
<ng-template #emptyfees>
<div [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fees">

View File

@@ -414,6 +414,9 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
}
getMinBlockFee(block: BlockExtended): number {
if (block?.extras?.effectiveFeeRange) {
return block.extras.effectiveFeeRange[0];
}
if (block?.extras?.feeRange) {
// heuristic to check if feeRange is adjusted for effective rates
if (block.extras.medianFee === block.extras.feeRange[3]) {
@@ -426,6 +429,9 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
}
getMaxBlockFee(block: BlockExtended): number {
if (block?.extras?.effectiveFeeRange) {
return block.extras.effectiveFeeRange[block.extras.effectiveFeeRange.length - 1];
}
if (block?.extras?.feeRange) {
return block.extras.feeRange[block.extras.feeRange.length - 1];
}

View File

@@ -305,20 +305,6 @@
</div>
</div>
}
@case ('simpleproof') {
<div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card">
<div class="card-body">
<a class="title-link" href="" [routerLink]="['/sp/verified' | relativeUrl]">
<h5 class="card-title d-inline" i18n="dashboard.recent-blocks">{{ widget.props?.label }}</h5>
<span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
</a>
<app-simpleproof-widget [label]="widget.props.label" [key]="widget.props.key" [widget]="true"></app-simpleproof-widget>
</div>
</div>
</div>
}
}
}
</div>

View File

@@ -57,6 +57,9 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
this.mempoolBlockIndex--;
}
const ordinal = this.getOrdinal(mempoolBlocks[this.mempoolBlockIndex]);
// prefer effective fee stats if available
mempoolBlocks[this.mempoolBlockIndex].feeRange = mempoolBlocks[this.mempoolBlockIndex].effectiveFeeRange ?? mempoolBlocks[this.mempoolBlockIndex].feeRange;
mempoolBlocks[this.mempoolBlockIndex].medianFee = mempoolBlocks[this.mempoolBlockIndex].effectiveMedianFee ?? mempoolBlocks[this.mempoolBlockIndex].medianFee;
this.ordinal$.next(ordinal);
this.seoService.setTitle(ordinal);
this.seoService.setDescription($localize`:@@meta.description.mempool-block:See stats for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} transactions in the mempool: fee range, aggregate size, and more. Mempool blocks are updated in real-time as the network receives new transactions.`);

View File

@@ -171,6 +171,8 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
block.index = this.blockIndex + i;
block.height = lastBlock.height + i + 1;
block.blink = specialBlocks[block.height]?.networks.includes(this.stateService.network || 'mainnet') ? true : false;
block.medianFee = block.effectiveMedianFee ?? block.medianFee;
block.feeRange = block.effectiveFeeRange ?? block.feeRange;
});
const stringifiedBlocks = JSON.stringify(mempoolBlocks);

View File

@@ -1,75 +0,0 @@
<div class="container-xl" style="min-height: 335px" [ngClass]="{'widget': widget, 'full-height': !widget}">
<div *ngIf="!widget" class="float-left" style="display: flex; width: 100%; align-items: center;">
<h1>{{ label }}</h1>
<div *ngIf="!widget && isLoading" class="spinner-border" role="status"></div>
</div>
<div class="clearfix"></div>
@if (isLoading) {
loading!
<div class="spinner-wrapper">
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
</div>
} @else if (error || !verified.length) {
<div class="error-wrapper">
<span>temporarily unavailable</span>
</div>
} @else {
<div style="min-height: 295px">
<table class="table table-borderless" [class.table-fixed]="widget">
<thead>
<th class="filename text-left" [ngClass]="{'widget': widget}" i18n="simpleproof.filename">Filename</th>
<th class="hash text-left" [ngClass]="{'widget': widget}" i18n="simpleproof.hash">Hash</th>
<th class="verified text-right" [ngClass]="{'widget': widget}" i18n="simpleproof.verified">Verified</th>
<th class="proof text-right" [ngClass]="{'widget': widget}" i18n="simpleproof.proof">Proof</th>
</thead>
<tbody *ngIf="verifiedPage; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<tr *ngFor="let item of verifiedPage">
<td class="filename text-left" [class]="widget ? 'widget' : ''">{{ item.file_name }}</td>
<td class="hash text-left" [class]="widget ? 'widget' : ''">{{ item.sha256 }}</td>
<td class="verified text-right" [class]="widget ? 'widget' : ''">
<app-timestamp [unixTime]="item.block_time" [customFormat]="'yyyy-MM-dd'" [hideTimeSince]="true"></app-timestamp>
</td>
<td class="proof text-right" [class]="widget ? 'widget' : ''">
<a [href]="item.sanitized_url" target="_blank" class="badge badge-primary badge-verify">
<span class="icon">
<img class="icon-img" src="/resources/sp.svg">
</span>
<span i18n="simpleproof.verify">Verify</span>
</a>
</td>
</tr>
</tbody>
<ng-template #skeleton>
<tbody>
<tr *ngFor="let item of [].constructor(itemsPerPage)">
<td class="filename text-left" [ngClass]="{'widget': widget}">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td class="hash text-left" [ngClass]="{'widget': widget}">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td class="verified text-right" [ngClass]="{'widget': widget}">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td class="proof text-right" [ngClass]="{'widget': widget}">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
</tr>
</tbody>
</ng-template>
</table>
<ngb-pagination *ngIf="!widget" class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''"
[collectionSize]="verified.length" [rotate]="true" [maxSize]="paginationMaxSize" [pageSize]="itemsPerPage" [(page)]="page"
(pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false">
</ngb-pagination>
<ng-template [ngIf]="!widget">
<div class="clearfix"></div>
<br>
</ng-template>
</div>
}
</div>

View File

@@ -1,114 +0,0 @@
.spinner-wrapper, .error-wrapper {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
}
.spinner-border {
height: 25px;
width: 25px;
margin-top: -10px;
margin-left: -13px;
flex-shrink: 0;
}
.container-xl {
max-width: 1400px;
}
.container-xl.widget {
padding-left: 0px;
padding-bottom: 0px;
padding-right: 0px;
}
.container-xl.legacy {
max-width: 1140px;
}
.container {
max-width: 100%;
}
tr, td, th {
border: 0px;
padding-top: 0.71rem !important;
padding-bottom: 0.7rem !important;
}
.clear-link {
color: white;
}
.disabled {
pointer-events: none;
opacity: 0.5;
}
.progress {
background-color: var(--secondary);
}
.filename {
width: 50%;
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hash {
width: 25%;
max-width: 700px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
td.hash {
font-family: monospace;
}
.widget .hash {
display: none;
}
@media (max-width: 1200px) {
.hash {
display: none;
}
}
.verified {
width: 25%;
}
td.verified {
color: var(--tertiary);
}
.proof {
width: 25%;
}
.badge-verify {
font-size: 1.05em;
font-weight: normal;
background: var(--nav-bg);
color: white;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: auto;
.icon {
margin: -0.25em;
margin-right: 0.5em;
.icon-img {
width: 16px;
font-size: 16px;
}
}
}

View File

@@ -1,86 +0,0 @@
import { Component, Input, SecurityContext, SimpleChanges, OnChanges } from '@angular/core';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
import { ServicesApiServices } from '@app/services/services-api.service';
import { catchError, of } from 'rxjs';
export interface SimpleProof {
file_name: string;
sha256: string;
ots_verification: string;
block_height: number;
block_hash: string;
block_time: number;
simpleproof_url: string;
key?: string;
sanitized_url?: SafeResourceUrl;
}
@Component({
selector: 'app-simpleproof-widget',
templateUrl: './simpleproof-widget.component.html',
styleUrls: ['./simpleproof-widget.component.scss'],
})
export class SimpleProofWidgetComponent implements OnChanges {
@Input() key: string = window['__env']?.customize?.dashboard.widgets?.find(w => w.component ==='simpleproof')?.props?.key ?? '';
@Input() label: string = window['__env']?.customize?.dashboard.widgets?.find(w => w.component ==='simpleproof')?.props?.label ?? 'Verified Documents';
@Input() widget: boolean = false;
@Input() width = 300;
@Input() height = 400;
verified: SimpleProof[] = [];
verifiedPage: SimpleProof[] = [];
isLoading: boolean = true;
error: boolean = false;
page = 1;
lastPage = 1;
itemsPerPage = 15;
paginationMaxSize = window.innerWidth <= 767.98 ? 3 : 5;
constructor(
private servicesApiService: ServicesApiServices,
public sanitizer: DomSanitizer,
) {}
ngOnInit(): void {
this.loadVerifications();
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.widget) {
this.itemsPerPage = this.widget ? 6 : 15;
}
if (changes.key) {
this.loadVerifications();
}
}
loadVerifications(): void {
if (this.key) {
this.isLoading = true;
this.servicesApiService.getSimpleProofs$(this.key).pipe(
catchError(() => {
this.isLoading = false;
this.error = true;
return of({});
}),
).subscribe((data: Record<string, SimpleProof>) => {
if (Object.keys(data).length) {
this.verified = Object.keys(data).map(key => ({
...data[key],
file_name: data[key].file_name.replace('source-', '').replace('_', ' '),
key,
sanitized_url: this.sanitizer.bypassSecurityTrustResourceUrl(this.sanitizer.sanitize(SecurityContext.URL, data[key]['simpleproof-url']) ?? ''),
})).sort((a, b) => b.key.localeCompare(a.key));
this.verifiedPage = this.verified.slice((this.page - 1) * this.itemsPerPage, this.page * this.itemsPerPage);
this.isLoading = false;
this.error = false;
}
});
}
}
pageChange(page: number): void {
this.page = page;
this.verifiedPage = this.verified.slice((this.page - 1) * this.itemsPerPage, this.page * this.itemsPerPage);
}
}

View File

@@ -30,7 +30,7 @@ export class TxFeeRatingComponent implements OnInit, OnChanges, OnDestroy {
ngOnInit() {
this.blocksSubscription = this.cacheService.loadedBlocks$.subscribe((block) => {
if (this.tx.status.confirmed && this.tx.status.block_height === block.height && block?.extras?.medianFee > 0) {
if (this.tx.status.confirmed && this.tx.status.block_height === block.height && (block?.extras?.effectiveMedianFee ?? block?.extras?.medianFee) > 0) {
this.calculateRatings(block);
this.cd.markForCheck();
}
@@ -45,7 +45,7 @@ export class TxFeeRatingComponent implements OnInit, OnChanges, OnDestroy {
this.cacheService.loadBlock(this.tx.status.block_height);
const foundBlock = this.cacheService.getCachedBlock(this.tx.status.block_height) || null;
if (foundBlock && foundBlock?.extras?.medianFee > 0) {
if (foundBlock && (foundBlock?.extras?.effectiveMedianFee ?? foundBlock?.extras?.medianFee) > 0) {
this.calculateRatings(foundBlock);
}
}
@@ -56,7 +56,7 @@ export class TxFeeRatingComponent implements OnInit, OnChanges, OnDestroy {
calculateRatings(block: BlockExtended) {
const feePervByte = this.tx.effectiveFeePerVsize || this.tx.fee / (this.tx.weight / 4);
this.medianFeeNeeded = block?.extras?.medianFee;
this.medianFeeNeeded = block?.extras?.effectiveMedianFee ?? block?.extras?.medianFee;
// Block not filled
if (block.weight < this.stateService.env.BLOCK_WEIGHT_UNITS * 0.95) {

View File

@@ -196,6 +196,8 @@ export interface BlockExtension {
minFee?: number;
maxFee?: number;
feeRange?: number[];
effectiveMedianFee?: number;
effectiveFeeRange?: number[];
reward?: number;
coinbaseRaw?: string;
matchRate?: number;

View File

@@ -61,8 +61,10 @@ export interface MempoolBlock {
blockVSize: number;
nTx: number;
medianFee: number;
effectiveMedianFee?: number;
totalFees: number;
feeRange: number[];
effectiveFeeRange?: number[];
index: number;
isStack?: boolean;
}

View File

@@ -13,7 +13,6 @@ import { RbfList } from '@components/rbf-list/rbf-list.component';
import { ServerHealthComponent } from '@components/server-health/server-health.component';
import { ServerStatusComponent } from '@components/server-health/server-status.component';
import { FaucetComponent } from '@components/faucet/faucet.component'
import { SimpleProofWidgetComponent } from './components/simpleproof-widget/simpleproof-widget.component';
const browserWindow = window || {};
// @ts-ignore
@@ -131,13 +130,6 @@ if (window['__env']?.OFFICIAL_MEMPOOL_SPACE) {
}
}
if (window['__env']?.customize?.dashboard.widgets?.some(w => w.component ==='simpleproof')) {
routes[0].children.push({
path: 'sp/verified',
component: SimpleProofWidgetComponent,
});
}
@NgModule({
imports: [
RouterModule.forChild(routes)

View File

@@ -8,7 +8,6 @@ import { Observable, of, ReplaySubject, tap, catchError, share, filter, switchMa
import { IBackendInfo } from '@interfaces/websocket.interface';
import { Acceleration, AccelerationHistoryParams } from '@interfaces/node-api.interface';
import { AccelerationStats } from '@components/acceleration/acceleration-stats/acceleration-stats.component';
import { SimpleProof } from '../components/simpleproof-widget/simpleproof-widget.component';
export interface IUser {
username: string;
@@ -218,8 +217,4 @@ export class ServicesApiServices {
getPaymentStatus$(orderId: string): Observable<any> {
return this.httpClient.get<any>(`${this.stateService.env.SERVICES_API}/payments/bitcoin/check?order_id=${orderId}`, { observe: 'response' });
}
getSimpleProofs$(key: string): Observable<Record<string, SimpleProof>> {
return this.httpClient.get<Record<string, SimpleProof>>(`${this.stateService.env.SERVICES_API}/sp/verified/${key}`);
}
}

View File

@@ -116,7 +116,6 @@ import { CalculatorComponent } from '@components/calculator/calculator.component
import { BitcoinsatoshisPipe } from '@app/shared/pipes/bitcoinsatoshis.pipe';
import { HttpErrorComponent } from '@app/shared/components/http-error/http-error.component';
import { TwitterWidgetComponent } from '@components/twitter-widget/twitter-widget.component';
import { SimpleProofWidgetComponent } from '@components/simpleproof-widget/simpleproof-widget.component';
import { FaucetComponent } from '@components/faucet/faucet.component';
import { TwitterLogin } from '@components/twitter-login/twitter-login.component';
import { BitcoinInvoiceComponent } from '@components/bitcoin-invoice/bitcoin-invoice.component';
@@ -236,7 +235,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/
OrdDataComponent,
HttpErrorComponent,
TwitterWidgetComponent,
SimpleProofWidgetComponent,
FaucetComponent,
TwitterLogin,
BitcoinInvoiceComponent,
@@ -371,7 +369,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/
OrdDataComponent,
HttpErrorComponent,
TwitterWidgetComponent,
SimpleProofWidgetComponent,
TwitterLogin,
BitcoinInvoiceComponent,
BitcoinsatoshisPipe,

View File

@@ -1,36 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="Layer_1"
version="1.1"
viewBox="0 0 492.10001 575.79999"
width="492.10001"
height="575.79999"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<!-- Generator: Adobe Illustrator 29.0.0, SVG Export Plug-In . SVG Version: 2.1.0 Build 186) -->
<defs
id="defs184">
<style
id="style182">
.st0 {
fill: #fff;
}
.st1 {
fill: #f88e2b;
}
</style>
</defs>
<g
id="g216"
transform="translate(-159.5,-152.1)">
<polygon
class="st0"
points="296.6,375.6 296.6,459.1 404.9,524.2 651.6,378.5 651.6,294.6 651.6,294.5 405.3,440.4 "
id="polygon212" />
<polygon
class="st1"
points="405.5,644.5 231.3,542 231.3,335.6 405.5,235 520.9,301.7 592.1,259.8 405.5,152.1 159.5,294.1 159.5,583.1 405.5,727.9 651.6,583.1 651.6,447.1 579.7,489.4 579.7,542 "
id="polygon214" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 996 B