Compare commits

..

3 Commits

Author SHA1 Message Date
Mononaut
a62bc22549 reduce number of GBT candidates 2023-05-05 15:38:00 -07:00
Mononaut
8bbe8a976b add missing rbf eviction 2023-05-05 15:38:00 -07:00
Mononaut
1c40c64de1 Fix new block mempool deletion race condition 2023-05-05 15:38:00 -07:00
41 changed files with 408 additions and 1401 deletions

View File

@@ -1,6 +1,7 @@
import logger from '../logger';
import { MempoolBlock, TransactionExtended, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction } from '../mempool.interfaces';
import { MempoolBlock, TransactionExtended, ThreadTransaction, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor } from '../mempool.interfaces';
import { Common } from './common';
import Mempool from './mempool';
import config from '../config';
import { Worker } from 'worker_threads';
import path from 'path';
@@ -10,8 +11,8 @@ class MempoolBlocks {
private mempoolBlockDeltas: MempoolBlockDelta[] = [];
private txSelectionWorker: Worker | null = null;
private nextUid: number = 1;
private uidMap: Map<number, string> = new Map(); // map short numerical uids to full txids
private filteredTxs: Map<string, TransactionExtended> = new Map();
private minFee: number = 0;
constructor() {}
@@ -97,6 +98,7 @@ class MempoolBlocks {
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, blocks);
this.mempoolBlocks = blocks;
this.mempoolBlockDeltas = deltas;
this.updateMinFee(this.mempoolBlocks);
}
return blocks;
@@ -104,12 +106,8 @@ class MempoolBlocks {
private calculateMempoolBlocks(transactionsSorted: TransactionExtended[]): MempoolBlockWithTransactions[] {
const mempoolBlocks: MempoolBlockWithTransactions[] = [];
let blockSize = 0;
let blockWeight = 0;
let blockVsize = 0;
let blockFees = 0;
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
let transactionIds: string[] = [];
let transactions: TransactionExtended[] = [];
transactionsSorted.forEach((tx) => {
if (blockWeight + tx.weight <= config.MEMPOOL.BLOCK_WEIGHT_UNITS
@@ -120,14 +118,9 @@ class MempoolBlocks {
};
blockWeight += tx.weight;
blockVsize += tx.vsize;
blockSize += tx.size;
blockFees += tx.fee;
if (blockVsize <= sizeLimit) {
transactions.push(tx);
}
transactionIds.push(tx.txid);
transactions.push(tx);
} else {
mempoolBlocks.push(this.dataToMempoolBlocks(transactionIds, transactions, blockSize, blockWeight, blockFees));
mempoolBlocks.push(this.dataToMempoolBlocks(transactions));
blockVsize = 0;
tx.position = {
block: mempoolBlocks.length,
@@ -135,14 +128,11 @@ class MempoolBlocks {
};
blockVsize += tx.vsize;
blockWeight = tx.weight;
blockSize = tx.size;
blockFees = tx.fee;
transactionIds = [tx.txid];
transactions = [tx];
}
});
if (transactions.length) {
mempoolBlocks.push(this.dataToMempoolBlocks(transactionIds, transactions, blockSize, blockWeight, blockFees));
mempoolBlocks.push(this.dataToMempoolBlocks(transactions));
}
return mempoolBlocks;
@@ -190,27 +180,28 @@ class MempoolBlocks {
}
public async $makeBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, saveResults: boolean = false): Promise<MempoolBlockWithTransactions[]> {
const start = Date.now();
// reset mempool short ids
this.resetUids();
for (const tx of Object.values(newMempool)) {
this.setUid(tx);
}
// Identify txs that can't be CPFP'd
this.markRelatives(newMempool);
// Track filtered transactions
this.filteredTxs.clear();
const filterFee = this.getFilterFee();
// prepare a stripped down version of the mempool with only the minimum necessary data
// to reduce the overhead of passing this data to the worker thread
const strippedMempool: Map<number, CompactThreadTransaction> = new Map();
const strippedMempool: { [txid: string]: ThreadTransaction } = {};
Object.values(newMempool).forEach(entry => {
if (entry.uid != null) {
strippedMempool.set(entry.uid, {
uid: entry.uid,
if (entry.hasRelatives || entry.feePerVsize >= filterFee) {
strippedMempool[entry.txid] = {
txid: entry.txid,
fee: entry.fee,
weight: entry.weight,
feePerVsize: entry.fee / (entry.weight / 4),
effectiveFeePerVsize: entry.effectiveFeePerVsize || (entry.fee / (entry.weight / 4)),
inputs: entry.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => uid != null) as number[],
});
effectiveFeePerVsize: entry.fee / (entry.weight / 4),
vin: entry.vin.map(v => v.txid),
};
} else {
this.filteredTxs.set(entry.txid, entry);
}
});
@@ -230,7 +221,7 @@ class MempoolBlocks {
// run the block construction algorithm in a separate thread, and wait for a result
let threadErrorListener;
try {
const workerResultPromise = new Promise<{ blocks: number[][], rates: Map<number, number>, clusters: Map<number, number[]> }>((resolve, reject) => {
const workerResultPromise = new Promise<{ blocks: ThreadTransaction[][], clusters: { [root: string]: string[] } }>((resolve, reject) => {
threadErrorListener = reject;
this.txSelectionWorker?.once('message', (result): void => {
resolve(result);
@@ -238,223 +229,253 @@ class MempoolBlocks {
this.txSelectionWorker?.once('error', reject);
});
this.txSelectionWorker.postMessage({ type: 'set', mempool: strippedMempool });
const { blocks, rates, clusters } = this.convertResultTxids(await workerResultPromise);
let { blocks, clusters } = await workerResultPromise;
// filter out stale transactions
const unfilteredCount = blocks.reduce((total, block) => { return total + block.length; }, 0);
blocks = blocks.map(block => block.filter(tx => (tx.txid && tx.txid in newMempool)));
const filteredCount = blocks.reduce((total, block) => { return total + block.length; }, 0);
if (filteredCount < unfilteredCount) {
logger.warn(`tx selection worker thread returned ${unfilteredCount - filteredCount} stale transactions from makeBlockTemplates`);
}
// clean up thread error listener
this.txSelectionWorker?.removeListener('error', threadErrorListener);
const processed = this.processBlockTemplates(newMempool, blocks, rates, clusters, saveResults);
logger.debug(`makeBlockTemplates completed in ${(Date.now() - start)/1000} seconds`);
return processed;
return this.processBlockTemplates(newMempool, blocks, clusters, saveResults);
} catch (e) {
logger.err('makeBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
}
return this.mempoolBlocks;
}
public async $updateBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, added: TransactionExtended[], removed: TransactionExtended[], saveResults: boolean = false): Promise<void> {
public async $updateBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, added: TransactionExtended[], removed: string[], saveResults: boolean = false): Promise<void> {
if (!this.txSelectionWorker) {
// need to reset the worker
await this.$makeBlockTemplates(newMempool, saveResults);
return;
}
const start = Date.now();
for (const tx of Object.values(added)) {
this.setUid(tx);
}
const removedUids = removed.map(tx => this.getUid(tx)).filter(uid => uid != null) as number[];
this.updateMarkedRelatives(newMempool, added);
const filterDiff = this.updateFilteredTxs(newMempool, this.getFilterFee());
// prepare a stripped down version of the mempool with only the minimum necessary data
// to reduce the overhead of passing this data to the worker thread
const addedStripped: CompactThreadTransaction[] = added.filter(entry => entry.uid != null).map(entry => {
const addedStripped: ThreadTransaction[] = added.concat(filterDiff.included).map(entry => {
return {
uid: entry.uid || 0,
txid: entry.txid,
fee: entry.fee,
weight: entry.weight,
feePerVsize: entry.fee / (entry.weight / 4),
effectiveFeePerVsize: entry.effectiveFeePerVsize || (entry.fee / (entry.weight / 4)),
inputs: entry.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => uid != null) as number[],
effectiveFeePerVsize: entry.fee / (entry.weight / 4),
vin: entry.vin.map(v => v.txid),
};
});
const removedIds = removed.concat(filterDiff.filtered);
// run the block construction algorithm in a separate thread, and wait for a result
let threadErrorListener;
try {
const workerResultPromise = new Promise<{ blocks: number[][], rates: Map<number, number>, clusters: Map<number, number[]> }>((resolve, reject) => {
const workerResultPromise = new Promise<{ blocks: ThreadTransaction[][], clusters: { [root: string]: string[] } }>((resolve, reject) => {
threadErrorListener = reject;
this.txSelectionWorker?.once('message', (result): void => {
resolve(result);
});
this.txSelectionWorker?.once('error', reject);
});
this.txSelectionWorker.postMessage({ type: 'update', added: addedStripped, removed: removedUids });
const { blocks, rates, clusters } = this.convertResultTxids(await workerResultPromise);
this.removeUids(removedUids);
this.txSelectionWorker.postMessage({ type: 'update', added: addedStripped, removed: removedIds });
let { blocks, clusters } = await workerResultPromise;
// filter out stale transactions
const unfilteredCount = blocks.reduce((total, block) => { return total + block.length; }, 0);
blocks = blocks.map(block => block.filter(tx => (tx.txid && tx.txid in newMempool)));
const filteredCount = blocks.reduce((total, block) => { return total + block.length; }, 0);
if (filteredCount < unfilteredCount) {
logger.warn(`tx selection worker thread returned ${unfilteredCount - filteredCount} stale transactions from updateBlockTemplates`);
}
// clean up thread error listener
this.txSelectionWorker?.removeListener('error', threadErrorListener);
this.processBlockTemplates(newMempool, blocks, rates, clusters, saveResults);
logger.debug(`updateBlockTemplates completed in ${(Date.now() - start) / 1000} seconds`);
this.processBlockTemplates(newMempool, blocks, clusters, saveResults);
} catch (e) {
logger.err('updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
}
}
private processBlockTemplates(mempool, blocks: string[][], rates: { [root: string]: number }, clusters: { [root: string]: string[] }, saveResults): MempoolBlockWithTransactions[] {
for (const txid of Object.keys(rates)) {
if (txid in mempool) {
mempool[txid].effectiveFeePerVsize = rates[txid];
}
}
const readyBlocks: { transactionIds, transactions, totalSize, totalWeight, totalFees }[] = [];
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
private processBlockTemplates(mempool, blocks, clusters, saveResults): MempoolBlockWithTransactions[] {
// update this thread's mempool with the results
for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
const block: string[] = blocks[blockIndex];
let txid: string;
let mempoolTx: TransactionExtended;
let totalSize = 0;
let totalVsize = 0;
let totalWeight = 0;
let totalFees = 0;
const transactions: TransactionExtended[] = [];
for (let txIndex = 0; txIndex < block.length; txIndex++) {
txid = block[txIndex];
if (txid) {
mempoolTx = mempool[txid];
blocks.forEach((block, blockIndex) => {
let runningVsize = 0;
block.forEach(tx => {
if (tx.txid && tx.txid in mempool) {
// save position in projected blocks
mempoolTx.position = {
mempool[tx.txid].position = {
block: blockIndex,
vsize: totalVsize + (mempoolTx.vsize / 2),
vsize: runningVsize + (mempool[tx.txid].vsize / 2),
};
mempoolTx.cpfpChecked = true;
runningVsize += mempool[tx.txid].vsize;
totalSize += mempoolTx.size;
totalVsize += mempoolTx.vsize;
totalWeight += mempoolTx.weight;
totalFees += mempoolTx.fee;
if (totalVsize <= sizeLimit) {
transactions.push(mempoolTx);
if (tx.effectiveFeePerVsize != null) {
mempool[tx.txid].effectiveFeePerVsize = tx.effectiveFeePerVsize;
}
}
}
readyBlocks.push({
transactionIds: block,
transactions,
totalSize,
totalWeight,
totalFees
});
}
for (const cluster of Object.values(clusters)) {
for (const memberTxid of cluster) {
if (memberTxid in mempool) {
const mempoolTx = mempool[memberTxid];
const ancestors: Ancestor[] = [];
const descendants: Ancestor[] = [];
let matched = false;
cluster.forEach(txid => {
if (txid === memberTxid) {
matched = true;
} else {
const relative = {
txid: txid,
fee: mempool[txid].fee,
weight: mempool[txid].weight,
};
if (matched) {
descendants.push(relative);
} else {
ancestors.push(relative);
if (tx.cpfpRoot && tx.cpfpRoot in clusters) {
const ancestors: Ancestor[] = [];
const descendants: Ancestor[] = [];
const cluster = clusters[tx.cpfpRoot];
let matched = false;
cluster.forEach(txid => {
if (!txid || !mempool[txid]) {
logger.warn('projected transaction ancestor missing from mempool cache');
return;
}
}
});
mempoolTx.ancestors = ancestors;
mempoolTx.descendants = descendants;
mempoolTx.bestDescendant = null;
if (txid === tx.txid) {
matched = true;
} else {
const relative = {
txid: txid,
fee: mempool[txid].fee,
weight: mempool[txid].weight,
};
if (matched) {
descendants.push(relative);
} else {
ancestors.push(relative);
}
}
});
mempool[tx.txid].ancestors = ancestors;
mempool[tx.txid].descendants = descendants;
mempool[tx.txid].bestDescendant = null;
}
mempool[tx.txid].cpfpChecked = tx.cpfpChecked;
} else {
logger.warn('projected transaction missing from mempool cache');
}
}
});
});
if (blocks.length) {
const sortedFiltered = Array.from(this.filteredTxs.values()).sort((a, b) => b.feePerVsize - a.feePerVsize);
blocks[blocks.length - 1] = blocks[blocks.length - 1].concat(sortedFiltered);
}
const mempoolBlocks = readyBlocks.map(b => this.dataToMempoolBlocks(b.transactionIds, b.transactions, b.totalSize, b.totalWeight, b.totalFees));
// unpack the condensed blocks into proper mempool blocks
const mempoolBlocks = blocks.map((transactions) => {
return this.dataToMempoolBlocks(transactions.map(tx => {
return mempool[tx.txid] || null;
}).filter(tx => !!tx));
});
if (saveResults) {
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks);
this.mempoolBlocks = mempoolBlocks;
this.mempoolBlockDeltas = deltas;
this.updateMinFee(this.mempoolBlocks);
}
return mempoolBlocks;
}
private dataToMempoolBlocks(transactionIds: string[], transactions: TransactionExtended[], totalSize: number, totalWeight: number, totalFees: number): MempoolBlockWithTransactions {
private dataToMempoolBlocks(transactions: TransactionExtended[]): MempoolBlockWithTransactions {
let totalSize = 0;
let totalWeight = 0;
const fitTransactions: TransactionExtended[] = [];
transactions.forEach(tx => {
totalSize += tx.size;
totalWeight += tx.weight;
if ((totalWeight + tx.weight) <= config.MEMPOOL.BLOCK_WEIGHT_UNITS * 1.2) {
fitTransactions.push(tx);
}
});
const feeStats = Common.calcEffectiveFeeStatistics(transactions);
return {
blockSize: totalSize,
blockVSize: (totalWeight / 4), // fractional vsize to avoid rounding errors
nTx: transactionIds.length,
totalFees: totalFees,
blockVSize: totalWeight / 4,
nTx: transactions.length,
totalFees: transactions.reduce((acc, cur) => acc + cur.fee, 0),
medianFee: feeStats.medianFee, // Common.percentile(transactions.map((tx) => tx.effectiveFeePerVsize), config.MEMPOOL.RECOMMENDED_FEE_PERCENTILE),
feeRange: feeStats.feeRange, //Common.getFeesInRange(transactions, rangeLength),
transactionIds: transactionIds,
transactions: transactions.map((tx) => Common.stripTransaction(tx)),
transactionIds: transactions.map((tx) => tx.txid),
transactions: fitTransactions.map((tx) => Common.stripTransaction(tx)),
};
}
private resetUids(): void {
this.uidMap.clear();
this.nextUid = 1;
}
private setUid(tx: TransactionExtended): number {
const uid = this.nextUid;
this.nextUid++;
this.uidMap.set(uid, tx.txid);
tx.uid = uid;
return uid;
}
private getUid(tx: TransactionExtended): number | void {
if (tx?.uid != null && this.uidMap.has(tx.uid)) {
return tx.uid;
}
}
private removeUids(uids: number[]): void {
for (const uid of uids) {
this.uidMap.delete(uid);
}
}
private convertResultTxids({ blocks, rates, clusters }: { blocks: number[][], rates: Map<number, number>, clusters: Map<number, number[]>})
: { blocks: string[][], rates: { [root: string]: number }, clusters: { [root: string]: string[] }} {
const convertedBlocks: string[][] = blocks.map(block => block.map(uid => {
return this.uidMap.get(uid) || '';
}));
const convertedRates = {};
for (const rateUid of rates.keys()) {
const rateTxid = this.uidMap.get(rateUid);
if (rateTxid) {
convertedRates[rateTxid] = rates.get(rateUid);
}
}
const convertedClusters = {};
for (const rootUid of clusters.keys()) {
const rootTxid = this.uidMap.get(rootUid);
if (rootTxid) {
const members = clusters.get(rootUid)?.map(uid => {
return this.uidMap.get(uid);
// Mark all transactions with in-mempool relatives
private markRelatives(mempool: { [txid: string]: TransactionExtended }): void {
for (const tx of Object.values(mempool)) {
if (!tx.hasRelatives) {
let hasRelatives = false;
tx.vin.forEach(parent => {
if (mempool[parent.txid] != null) {
hasRelatives = true;
mempool[parent.txid].hasRelatives = true;
}
});
convertedClusters[rootTxid] = members;
tx.hasRelatives = hasRelatives;
}
}
return { blocks: convertedBlocks, rates: convertedRates, clusters: convertedClusters } as { blocks: string[][], rates: { [root: string]: number }, clusters: { [root: string]: string[] }};
}
private updateMarkedRelatives(mempool: { [txid: string]: TransactionExtended }, added: TransactionExtended[]): void {
const newFiltered: TransactionExtended[] = [];
for (const tx of added) {
if (!tx.hasRelatives) {
let hasRelatives = false;
tx.vin.forEach(parent => {
if (mempool[parent.txid] != null) {
hasRelatives = true;
mempool[parent.txid].hasRelatives = true;
}
});
tx.hasRelatives = hasRelatives;
}
}
}
private updateFilteredTxs(mempool: { [txid: string]: TransactionExtended }, filterFee: number): { filtered: string[], included: TransactionExtended[] } {
const filtered: string[] = [];
const included: TransactionExtended[] = [];
for (const tx of Object.values(mempool)) {
if (!tx.hasRelatives) {
if (tx.feePerVsize < filterFee) {
// filter out tx
if (!this.filteredTxs.has(tx.txid)) {
this.filteredTxs.set(tx.txid, tx);
filtered.push(tx.txid);
}
} else {
// include tx
if (this.filteredTxs.has(tx.txid)) {
this.filteredTxs.delete(tx.txid);
included.push(tx);
}
}
}
}
return { filtered, included };
}
private updateMinFee(mempoolBlocks: MempoolBlockWithTransactions[]): void {
let totalFeeRate = 0;
let totalSize = 0;
let count = 0;
if (mempoolBlocks.length === 8) {
const lastBlock = mempoolBlocks[mempoolBlocks.length - 1];
for (let i = 0; i < lastBlock.transactions.length && totalSize < 16_000_000; i++) {
totalFeeRate += lastBlock.transactions[i].rate || (lastBlock.transactions[i].fee / lastBlock.transactions[i].vsize);
totalSize += lastBlock.transactions[i].vsize;
count++;
}
this.minFee = count ? (totalFeeRate / count) : 1;
} else {
this.minFee = 1;
}
}
private getFilterFee(): number {
const purgingBelow = Mempool.getMempoolInfo().mempoolminfee * 100000;
const filterFee = Math.max(purgingBelow, this.minFee);
return filterFee;
}
}

View File

@@ -11,6 +11,7 @@ import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import rbfCache from './rbf-cache';
class Mempool {
private static WEBSOCKET_REFRESH_RATE_MS = 10000;
private inSync: boolean = false;
private mempoolCacheDelta: number = -1;
private mempoolCache: { [txId: string]: TransactionExtended } = {};
@@ -33,6 +34,7 @@ class Mempool {
private SAMPLE_TIME = 10000; // In ms
private timer = new Date().getTime();
private missingTxCount = 0;
private mainLoopTimeout: number = 120000;
constructor() {
@@ -132,7 +134,7 @@ class Mempool {
this.mempoolCacheDelta = Math.abs(diff);
if (!this.inSync) {
loadingIndicators.setProgress('mempool', currentMempoolSize / transactions.length * 100);
loadingIndicators.setProgress('mempool', Object.keys(this.mempoolCache).length / transactions.length * 100);
}
// https://github.com/mempool/mempool/issues/3283
@@ -145,7 +147,6 @@ class Mempool {
}
};
let loggerTimer = new Date().getTime() / 1000;
for (const txid of transactions) {
if (!this.mempoolCache[txid]) {
try {
@@ -168,12 +169,9 @@ class Mempool {
logger.debug(`Error finding transaction '${txid}' in the mempool: ` + (e instanceof Error ? e.message : e));
}
}
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer);
if (elapsedSeconds > 4) {
const progress = (currentMempoolSize + newTransactions.length) / transactions.length * 100;
logger.debug(`Mempool is synchronizing. Processed ${newTransactions.length}/${diff} txs (${Math.round(progress)}%)`);
loadingIndicators.setProgress('mempool', progress);
loggerTimer = new Date().getTime() / 1000;
if ((new Date().getTime()) - start > Mempool.WEBSOCKET_REFRESH_RATE_MS) {
break;
}
}

View File

@@ -1,10 +1,10 @@
import config from '../config';
import logger from '../logger';
import { CompactThreadTransaction, AuditTransaction } from '../mempool.interfaces';
import { ThreadTransaction, MempoolBlockWithTransactions, AuditTransaction } from '../mempool.interfaces';
import { PairingHeap } from '../utils/pairing-heap';
import { parentPort } from 'worker_threads';
let mempool: Map<number, CompactThreadTransaction> = new Map();
let mempool: { [txid: string]: ThreadTransaction } = {};
if (parentPort) {
parentPort.on('message', (params) => {
@@ -12,18 +12,18 @@ if (parentPort) {
mempool = params.mempool;
} else if (params.type === 'update') {
params.added.forEach(tx => {
mempool.set(tx.uid, tx);
mempool[tx.txid] = tx;
});
params.removed.forEach(uid => {
mempool.delete(uid);
params.removed.forEach(txid => {
delete mempool[txid];
});
}
const { blocks, rates, clusters } = makeBlockTemplates(mempool);
const { blocks, clusters } = makeBlockTemplates(mempool);
// return the result to main thread.
if (parentPort) {
parentPort.postMessage({ blocks, rates, clusters });
parentPort.postMessage({ blocks, clusters });
}
});
}
@@ -32,25 +32,26 @@ if (parentPort) {
* Build projected mempool blocks using an approximation of the transaction selection algorithm from Bitcoin Core
* (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp)
*/
function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
: { blocks: number[][], rates: Map<number, number>, clusters: Map<number, number[]> } {
function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction })
: { blocks: ThreadTransaction[][], clusters: { [root: string]: string[] } } {
const start = Date.now();
const auditPool: Map<number, AuditTransaction> = new Map();
const auditPool: { [txid: string]: AuditTransaction } = {};
const mempoolArray: AuditTransaction[] = [];
const cpfpClusters: Map<number, number[]> = new Map();
const restOfArray: ThreadTransaction[] = [];
const cpfpClusters: { [root: string]: string[] } = {};
mempool.forEach(tx => {
tx.dirty = false;
// grab the top feerate txs up to maxWeight
Object.values(mempool).sort((a, b) => b.feePerVsize - a.feePerVsize).forEach(tx => {
// initializing everything up front helps V8 optimize property access later
auditPool.set(tx.uid, {
uid: tx.uid,
auditPool[tx.txid] = {
txid: tx.txid,
fee: tx.fee,
weight: tx.weight,
feePerVsize: tx.feePerVsize,
effectiveFeePerVsize: tx.feePerVsize,
inputs: tx.inputs || [],
vin: tx.vin,
relativesSet: false,
ancestorMap: new Map<number, AuditTransaction>(),
ancestorMap: new Map<string, AuditTransaction>(),
children: new Set<AuditTransaction>(),
ancestorFee: 0,
ancestorWeight: 0,
@@ -58,8 +59,8 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
used: false,
modified: false,
modifiedNode: null,
});
mempoolArray.push(auditPool.get(tx.uid) as AuditTransaction);
};
mempoolArray.push(auditPool[tx.txid]);
});
// Build relatives graph & calculate ancestor scores
@@ -72,8 +73,8 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
// Sort by descending ancestor score
mempoolArray.sort((a, b) => {
if (b.score === a.score) {
// tie-break by uid for stability
return a.uid < b.uid ? -1 : 1;
// tie-break by lexicographic txid order for stability
return a.txid < b.txid ? -1 : 1;
} else {
return (b.score || 0) - (a.score || 0);
}
@@ -81,13 +82,14 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
// Build blocks by greedily choosing the highest feerate package
// (i.e. the package rooted in the transaction with the best ancestor score)
const blocks: number[][] = [];
const blocks: ThreadTransaction[][] = [];
let blockWeight = 4000;
let blockSize = 0;
let transactions: AuditTransaction[] = [];
const modified: PairingHeap<AuditTransaction> = new PairingHeap((a, b): boolean => {
if (a.score === b.score) {
// tie-break by uid for stability
return a.uid > b.uid;
// tie-break by lexicographic txid order for stability
return a.txid > b.txid;
} else {
return (a.score || 0) > (b.score || 0);
}
@@ -124,30 +126,24 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx];
let isCluster = false;
if (sortedTxSet.length > 1) {
cpfpClusters.set(nextTx.uid, sortedTxSet.map(tx => tx.uid));
cpfpClusters[nextTx.txid] = sortedTxSet.map(tx => tx.txid);
isCluster = true;
}
const effectiveFeeRate = nextTx.ancestorFee / (nextTx.ancestorWeight / 4);
const used: AuditTransaction[] = [];
while (sortedTxSet.length) {
const ancestor = sortedTxSet.pop();
const mempoolTx = mempool.get(ancestor.uid);
if (!mempoolTx) {
continue;
}
const mempoolTx = mempool[ancestor.txid];
ancestor.used = true;
ancestor.usedBy = nextTx.uid;
ancestor.usedBy = nextTx.txid;
// update original copy of this tx with effective fee rate & relatives data
if (mempoolTx.effectiveFeePerVsize !== effectiveFeeRate) {
mempoolTx.effectiveFeePerVsize = effectiveFeeRate;
mempoolTx.dirty = true;
}
if (mempoolTx.cpfpRoot !== nextTx.uid) {
mempoolTx.cpfpRoot = isCluster ? nextTx.uid : null;
mempoolTx.dirty;
mempoolTx.effectiveFeePerVsize = effectiveFeeRate;
if (isCluster) {
mempoolTx.cpfpRoot = nextTx.txid;
}
mempoolTx.cpfpChecked = true;
transactions.push(ancestor);
blockSize += ancestor.size;
blockWeight += ancestor.weight;
used.push(ancestor);
}
@@ -173,10 +169,11 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
if ((exceededPackageTries || queueEmpty) && blocks.length < 7) {
// construct this block
if (transactions.length) {
blocks.push(transactions.map(t => t.uid));
blocks.push(transactions.map(t => mempool[t.txid]));
}
// reset for the next block
transactions = [];
blockSize = 0;
blockWeight = 4000;
// 'overflow' packages didn't fit in this block, but are valid candidates for the next
@@ -197,32 +194,24 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
}
// add the final unbounded block if it contains any transactions
if (transactions.length > 0) {
blocks.push(transactions.map(t => t.uid));
}
// get map of dirty transactions
const rates = new Map<number, number>();
for (const tx of mempool.values()) {
if (tx?.dirty) {
rates.set(tx.uid, tx.effectiveFeePerVsize || tx.feePerVsize);
}
blocks.push(transactions.map(t => mempool[t.txid]));
}
const end = Date.now();
const time = end - start;
logger.debug('Mempool templates calculated in ' + time / 1000 + ' seconds');
return { blocks, rates, clusters: cpfpClusters };
return { blocks, clusters: cpfpClusters };
}
// traverse in-mempool ancestors
// recursion unavoidable, but should be limited to depth < 25 by mempool policy
function setRelatives(
tx: AuditTransaction,
mempool: Map<number, AuditTransaction>,
mempool: { [txid: string]: AuditTransaction },
): void {
for (const parent of tx.inputs) {
const parentTx = mempool.get(parent);
for (const parent of tx.vin) {
const parentTx = mempool[parent];
if (parentTx && !tx.ancestorMap?.has(parent)) {
tx.ancestorMap.set(parent, parentTx);
parentTx.children.add(tx);
@@ -231,7 +220,7 @@ function setRelatives(
setRelatives(parentTx, mempool);
}
parentTx.ancestorMap.forEach((ancestor) => {
tx.ancestorMap.set(ancestor.uid, ancestor);
tx.ancestorMap.set(ancestor.txid, ancestor);
});
}
};
@@ -249,7 +238,7 @@ function setRelatives(
// avoids recursion to limit call stack depth
function updateDescendants(
rootTx: AuditTransaction,
mempool: Map<number, AuditTransaction>,
mempool: { [txid: string]: AuditTransaction },
modified: PairingHeap<AuditTransaction>,
): void {
const descendantSet: Set<AuditTransaction> = new Set();
@@ -265,9 +254,9 @@ function updateDescendants(
});
while (descendants.length) {
descendantTx = descendants.pop();
if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.uid)) {
if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.txid)) {
// remove tx as ancestor
descendantTx.ancestorMap.delete(rootTx.uid);
descendantTx.ancestorMap.delete(rootTx.txid);
descendantTx.ancestorFee -= rootTx.fee;
descendantTx.ancestorWeight -= rootTx.weight;
tmpScore = descendantTx.score;

View File

@@ -282,7 +282,7 @@ class WebsocketHandler {
this.printLogs();
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
await mempoolBlocks.$updateBlockTemplates(newMempool, newTransactions, deletedTransactions, true);
await mempoolBlocks.$updateBlockTemplates(newMempool, newTransactions, deletedTransactions.map(tx => tx.txid), true);
} else {
mempoolBlocks.updateMempoolBlocks(newMempool, true);
}

View File

@@ -80,22 +80,22 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
descendants?: Ancestor[];
bestDescendant?: BestDescendant | null;
cpfpChecked?: boolean;
hasRelatives?: boolean;
position?: {
block: number,
vsize: number,
};
uid?: number;
}
export interface AuditTransaction {
uid: number;
txid: string;
fee: number;
weight: number;
feePerVsize: number;
effectiveFeePerVsize: number;
inputs: number[];
vin: string[];
relativesSet: boolean;
ancestorMap: Map<number, AuditTransaction>;
ancestorMap: Map<string, AuditTransaction>;
children: Set<AuditTransaction>;
ancestorFee: number;
ancestorWeight: number;
@@ -105,25 +105,13 @@ export interface AuditTransaction {
modifiedNode: HeapNode<AuditTransaction>;
}
export interface CompactThreadTransaction {
uid: number;
fee: number;
weight: number;
feePerVsize: number;
effectiveFeePerVsize?: number;
inputs: number[];
cpfpRoot?: string;
cpfpChecked?: boolean;
dirty?: boolean;
}
export interface ThreadTransaction {
txid: string;
fee: number;
weight: number;
feePerVsize: number;
effectiveFeePerVsize?: number;
inputs: number[];
vin: string[];
cpfpRoot?: string;
cpfpChecked?: boolean;
}

View File

@@ -4,8 +4,6 @@ import { AppPreloadingStrategy } from './app.preloading-strategy'
import { StartComponent } from './components/start/start.component';
import { TransactionComponent } from './components/transaction/transaction.component';
import { BlockComponent } from './components/block/block.component';
import { ClockMinedComponent as ClockMinedComponent } from './components/clock/clock-mined.component';
import { ClockMempoolComponent as ClockMempoolComponent } from './components/clock/clock-mempool.component';
import { AddressComponent } from './components/address/address.component';
import { MasterPageComponent } from './components/master-page/master-page.component';
import { AboutComponent } from './components/about/about.component';
@@ -357,14 +355,6 @@ let routes: Routes = [
},
],
},
{
path: 'clock-mined',
component: ClockMinedComponent,
},
{
path: 'clock-mempool',
component: ClockMempoolComponent,
},
{
path: 'status',
data: { networks: ['bitcoin', 'liquid'] },

View File

@@ -29,14 +29,6 @@ export const mempoolFeeColors = [
'ba3243',
'b92b48',
'b9254b',
'b8214d',
'b71d4f',
'b61951',
'b41453',
'b30e55',
'b10857',
'b00259',
'ae005b',
];
export const chartColors = [
@@ -77,7 +69,6 @@ export const chartColors = [
"#3E2723",
"#212121",
"#263238",
"#801313",
];
export const poolsColor = {

View File

@@ -23,7 +23,6 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
@Input() unavailable: boolean = false;
@Input() auditHighlighting: boolean = false;
@Input() blockConversion: Price;
@Input() pixelAlign: boolean = false;
@Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>();
@Output() txHoverEvent = new EventEmitter<string>();
@Output() readyEvent = new EventEmitter();
@@ -202,8 +201,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
this.start();
} else {
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray,
highlighting: this.auditHighlighting, pixelAlign: this.pixelAlign });
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray, highlighting: this.auditHighlighting });
this.start();
}
}

View File

@@ -15,7 +15,6 @@ export default class BlockScene {
gridWidth: number;
gridHeight: number;
gridSize: number;
pixelAlign: boolean;
vbytesPerUnit: number;
unitPadding: number;
unitWidth: number;
@@ -24,24 +23,19 @@ export default class BlockScene {
animateUntil = 0;
dirty: boolean;
constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting, pixelAlign }:
constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
{ width: number, height: number, resolution: number, blockLimit: number,
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean, pixelAlign: boolean }
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
) {
this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting, pixelAlign });
this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting });
}
resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void {
this.width = width;
this.height = height;
this.gridSize = this.width / this.gridWidth;
if (this.pixelAlign) {
this.unitPadding = Math.max(1, Math.floor(this.gridSize / 2.5));
this.unitWidth = this.gridSize - (this.unitPadding);
} else {
this.unitPadding = width / 500;
this.unitWidth = this.gridSize - (this.unitPadding * 2);
}
this.unitPadding = width / 500;
this.unitWidth = this.gridSize - (this.unitPadding * 2);
this.dirty = true;
if (this.initialised && this.scene) {
@@ -215,15 +209,14 @@ export default class BlockScene {
this.animateUntil = Math.max(this.animateUntil, tx.setHover(value));
}
private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting, pixelAlign }:
private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
{ width: number, height: number, resolution: number, blockLimit: number,
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean, pixelAlign: boolean }
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
): void {
this.orientation = orientation;
this.flip = flip;
this.vertexArray = vertexArray;
this.highlightingEnabled = highlighting;
this.pixelAlign = pixelAlign;
this.scene = {
count: 0,
@@ -349,12 +342,7 @@ export default class BlockScene {
private gridToScreen(position: Square | void): Square {
if (position) {
const slotSize = (position.s * this.gridSize);
let squareSize;
if (this.pixelAlign) {
squareSize = slotSize - (this.unitPadding);
} else {
squareSize = slotSize - (this.unitPadding * 2);
}
const squareSize = slotSize - (this.unitPadding * 2);
// The grid is laid out notionally left-to-right, bottom-to-top,
// so we rotate and/or flip the y axis to match the target configuration.

View File

@@ -1,61 +1,53 @@
<div class="blocks-container blockchain-blocks-container" [class.time-ltr]="timeLtr" [class.minimal]="minimal"
[style.left]="static ? (offset || 0) + 'px' : null" [style.--block-size]="blockWidth+'px'"
<div class="blocks-container blockchain-blocks-container" [class.time-ltr]="timeLtr"
[style.left]="static ? (offset || 0) + 'px' : null"
*ngIf="static || (loadingBlocks$ | async) === false; else loadingBlocksTemplate">
<div *ngFor="let block of blocks; let i = index; trackBy: trackByBlocksFn">
<ng-container *ngIf="connected && block && !block.loading && !block.placeholder; else placeholderBlock">
<div
*ngIf="minimal && spotlight < 0 && chainTip + spotlight + 1 === block.height"
class="spotlight-bottom"
[style.left]="blockStyles[i].left"
></div>
<div [attr.data-cy]="'bitcoin-block-offset-' + offset + '-index-' + i"
class="text-center bitcoin-block mined-block blockchain-blocks-offset-{{ offset }}-index-{{ i }}"
[class.offscreen]="!static && count && i >= count"
id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]"
[class.blink-bg]="isSpecial(block.height)">
<a draggable="false" [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }"
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}">&nbsp;</a>
<div *ngIf="!minimal" [attr.data-cy]="'bitcoin-block-' + i + '-height'" class="block-height">
<div [attr.data-cy]="'bitcoin-block-' + i + '-height'" class="block-height">
<a [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height
}}</a>
</div>
<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">
~{{ block?.extras?.medianFee | number:feeRounding }} <ng-container
i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
<div *ngIf="block?.extras; else emptyfees" [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fees">
~{{ block?.extras?.medianFee | number:feeRounding }} <ng-container
i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
</div>
<ng-template #emptyfees>
<div [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fees">
&nbsp;
</div>
<ng-template #emptyfees>
<div [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fees">
&nbsp;
</div>
</ng-template>
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-fee-span'" class="fee-span"
*ngIf="block?.extras?.feeRange; else emptyfeespan">
{{ block?.extras?.feeRange?.[0] | number:feeRounding }} - {{
block?.extras?.feeRange[block?.extras?.feeRange?.length - 1] | number:feeRounding }} <ng-container
i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
</ng-template>
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-fee-span'" class="fee-span"
*ngIf="block?.extras?.feeRange; else emptyfeespan">
{{ block?.extras?.feeRange?.[0] | number:feeRounding }} - {{
block?.extras?.feeRange[block?.extras?.feeRange?.length - 1] | number:feeRounding }} <ng-container
i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
</div>
<ng-template #emptyfeespan>
<div [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fee-span">
&nbsp;
</div>
<ng-template #emptyfeespan>
<div [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fee-span">
&nbsp;
</div>
</ng-template>
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-total-fees'" *ngIf="showMiningInfo"
class="block-size">
<app-amount [satoshis]="block.extras?.totalFees ?? 0" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
</div>
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + 'block-size'" *ngIf="!showMiningInfo"
class="block-size" [innerHTML]="'&lrm;' + (block.size | bytes: 2)"></div>
<div [attr.data-cy]="'bitcoin-block-' + i + '-transactions'" class="transaction-count">
<ng-container
*ngTemplateOutlet="block.tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.tx_count | number}"></ng-container>
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
</div>
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-time'" class="time-difference">
<app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time></div>
</ng-container>
</ng-template>
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-total-fees'" *ngIf="showMiningInfo"
class="block-size">
<app-amount [satoshis]="block.extras?.totalFees ?? 0" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
</div>
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + 'block-size'" *ngIf="!showMiningInfo"
class="block-size" [innerHTML]="'&lrm;' + (block.size | bytes: 2)"></div>
<div [attr.data-cy]="'bitcoin-block-' + i + '-transactions'" class="transaction-count">
<ng-container
*ngTemplateOutlet="block.tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.tx_count | number}"></ng-container>
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
</div>
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-time'" class="time-difference">
<app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time></div>
</div>
<div class="animated" [class]="showMiningInfo ? 'show' : 'hide'" *ngIf="block.extras?.pool != undefined">
<a [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-pool'" class="badge badge-primary"
@@ -87,11 +79,11 @@
</div>
<ng-template #loadingBlocksTemplate>
<div class="blocks-container" [class.time-ltr]="timeLtr" [style.--block-size]="blockWidth+'px'">
<div class="blocks-container" [class.time-ltr]="timeLtr">
<div class="flashing">
<div *ngFor="let block of emptyBlocks; let i = index; trackBy: trackByBlocksFn">
<div class="text-center bitcoin-block mined-block" id="bitcoin-block-{{ block.height }}"
[ngStyle]="emptyBlockStyles[i]" [class.offscreen]="!static && count && i >= count"></div>
[ngStyle]="emptyBlockStyles[i]"></div>
</div>
</div>
</div>

View File

@@ -1,6 +1,6 @@
.bitcoin-block {
width: var(--block-size);
height: var(--block-size);
width: 125px;
height: 125px;
}
.blockLink {
@@ -22,11 +22,7 @@
.mined-block {
position: absolute;
top: 0px;
transition: background 2s, left 2s, transform 1s, opacity 1s;
}
.mined-block.offscreen {
opacity: 0;
transition: background 2s, left 2s, transform 1s;
}
.mined-block.placeholder-block {
@@ -39,11 +35,9 @@
}
.blocks-container {
--block-size: 125px;
--block-offset: calc(0.32 * var(--block-size));
position: absolute;
top: 0px;
left: var(--block-offset);
left: 40px;
}
.block-body {
@@ -83,11 +77,11 @@
.bitcoin-block::after {
content: '';
width: var(--block-size);
height: calc(0.192 * var(--block-size));
width: 125px;
height: 24px;
position:absolute;
top: calc(-0.192 * var(--block-size));
left: calc(-0.16 * var(--block-size));
top: -24px;
left: -20px;
background-color: #232838;
transform:skew(40deg);
transform-origin:top;
@@ -95,11 +89,11 @@
.bitcoin-block::before {
content: '';
width: calc(0.16 * var(--block-size));
height: var(--block-size);
width: 20px;
height: 125px;
position: absolute;
top: calc(-0.096 * var(--block-size));
left: calc(-0.16 * var(--block-size));
top: -12px;
left: -20px;
background-color: #191c27;
transform: skewY(50deg);
@@ -174,16 +168,4 @@
.bitcoin-block {
transform: scaleX(-1);
}
}
.spotlight-bottom {
position: absolute;
width: calc(0.6 * var(--block-size));
height: calc(0.25 * var(--block-size));
border-left: solid calc(0.3 * var(--block-size)) transparent;
border-bottom: solid calc(0.3 * var(--block-size)) white;
border-right: solid calc(0.3 * var(--block-size)) transparent;
transform: translate(calc(0.2 * var(--block-size)), calc(1.1 * var(--block-size)));
border-radius: 2px;
z-index: -1;
}

View File

@@ -24,9 +24,6 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
@Input() count: number = 8; // number of blocks in this chunk (dynamic blocks only)
@Input() loadingTip: boolean = false;
@Input() connected: boolean = true;
@Input() minimal: boolean = false;
@Input() blockWidth: number = 125;
@Input() spotlight: number = 0;
specialBlocks = specialBlocks;
network = '';
@@ -54,10 +51,6 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
timeLtrSubscription: Subscription;
timeLtr: boolean;
blockOffset: number = 155;
dividerBlockOffset: number = 205;
blockPadding: number = 30;
gradientColors = {
'': ['#9339f4', '#105fb0'],
bisq: ['#9339f4', '#105fb0'],
@@ -125,7 +118,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.blockStyles = [];
if (this.blocksFilled && block.height > this.chainTip) {
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, i ? -this.blockOffset : -this.dividerBlockOffset)));
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, i ? -155 : -205)));
setTimeout(() => {
this.blockStyles = [];
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i)));
@@ -166,13 +159,6 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.blockWidth && this.blockWidth) {
this.blockPadding = 0.24 * this.blockWidth;
this.blockOffset = this.blockWidth + this.blockPadding;
this.dividerBlockOffset = this.blockOffset + (0.4 * this.blockWidth);
this.blockStyles = [];
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i)));
}
if (this.static) {
const animateSlide = changes.height && (changes.height.currentValue === changes.height.previousValue + 1);
this.updateStaticBlocks(animateSlide);
@@ -205,14 +191,14 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
}
this.arrowVisible = true;
if (newBlockFromLeft) {
this.arrowLeftPx = blockindex * this.blockOffset + this.blockPadding - this.dividerBlockOffset;
this.arrowLeftPx = blockindex * 155 + 30 - 205;
setTimeout(() => {
this.arrowTransition = '2s';
this.arrowLeftPx = blockindex * this.blockOffset + this.blockPadding;
this.arrowLeftPx = blockindex * 155 + 30;
this.cd.markForCheck();
}, 50);
} else {
this.arrowLeftPx = blockindex * this.blockOffset + this.blockPadding;
this.arrowLeftPx = blockindex * 155 + 30;
if (!animate) {
setTimeout(() => {
this.arrowTransition = '2s';
@@ -259,7 +245,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
}
this.blocks = this.blocks.slice(0, this.count);
this.blockStyles = [];
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, animateSlide ? -this.blockOffset : 0)));
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, animateSlide ? -155 : 0)));
this.cd.markForCheck();
if (animateSlide) {
// animate blocks slide right
@@ -301,7 +287,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
}
return {
left: addLeft + this.blockOffset * index + 'px',
left: addLeft + 155 * index + 'px',
background: `repeating-linear-gradient(
#2d3348,
#2d3348 ${greenBackgroundHeight}%,
@@ -323,7 +309,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
const addLeft = animateEnterFrom || 0;
return {
left: addLeft + (this.blockOffset * index) + 'px',
left: addLeft + (155 * index) + 'px',
background: "#2d3348",
};
}
@@ -331,7 +317,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
getStyleForPlaceholderBlock(index: number, animateEnterFrom: number = 0) {
const addLeft = animateEnterFrom || 0;
return {
left: addLeft + (this.blockOffset * index) + 'px',
left: addLeft + (155 * index) + 'px',
};
}
@@ -339,7 +325,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
const addLeft = animateEnterFrom || 0;
return {
left: addLeft + this.blockOffset * this.emptyBlocks.indexOf(block) + 'px',
left: addLeft + 155 * this.emptyBlocks.indexOf(block) + 'px',
background: "#2d3348",
};
}

View File

@@ -1,42 +0,0 @@
<div class="clock-face" [style]="faceStyle">
<ng-content></ng-content>
<svg
class="cut-out"
width="384"
height="384"
viewBox="0 0 384 384"
>
<g>
<path
class="face"
d="M 0,0 V 384 H 384 V 0 Z M 192,15 A 177,177 0 0 1 369,192 177,177 0 0 1 192,369 177,177 0 0 1 15,192 177,177 0 0 1 192,15 Z"
/>
</g>
</svg>
<svg
class="demo-dial"
width="384"
height="384"
viewBox="0 0 384 384"
>
<defs>
<pattern id="dial-gradient" patternUnits="userSpaceOnUse" width="384" height="384">
<image class="dial-gradient-img" href="/resources/clock/gradient.png" x="0" y="0" width="384" height="384" [style.transform]="'rotate(' + (minutes * 6) + 'deg)'" />
</pattern>
</defs>
<path *ngFor="let angle of minorTicks" class="tick minor" d="M 192,27 v 10" [style.transform]="'rotate(' + angle + 'deg)'"/>
<path *ngFor="let angle of majorTicks" class="tick major" d="M 192,27 v 18" [style.transform]="'rotate(' + angle + 'deg)'"/>
<ng-container *ngFor="let segment of segments; trackBy: trackBySegment">
<path class="block-segment" [attr.d]="segment.path" />
<!-- <circle class="segment-mark start" [attr.cx]="segment.start.x" [attr.cy]="segment.start.y" r="2" style="fill:green;stroke:white;stroke-width:1px;" />
<circle class="segment-mark end" [attr.cx]="segment.end.x" [attr.cy]="segment.end.y" r="2" style="fill:red;stroke:white;stroke-width:1px;" /> -->
</ng-container>
<!-- <polyline points="468.750,82.031 468.750,35 " id="polyline322" style="fill:none;stroke:#ffffff;stroke-width:4.84839;stroke-dasharray:none;stroke-opacity:1" transform="matrix(0.41250847,0,0,0.93092534,-1.3627708,-32.692008)" /> -->
<path class="tick very major" d="M 192,0 v 45" />
<path id="hour" class="gnomon hour" d="M 178,3 206,3 192,40 Z" [style.transform]="'rotate(' + (hours * 30) + 'deg)'" />
<path id="minute" class="gnomon minute" d="M 180,4 204,4 192,38 Z" [style.transform]="'rotate(' + (minutes * 6) + 'deg)'" />
</svg>
</div>

View File

@@ -1,69 +0,0 @@
.clock-face {
position: relative;
height: 84.375%;
margin: auto;
overflow: hidden;
.cut-out, .demo-dial {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
width: 100%;
height: 100%;
.face {
fill: #11131f;
}
}
.gnomon {
transform-origin: center;
stroke-linejoin: round;
&.minute {
fill:#80C2E1;
stroke:#80C2E1;
stroke-width: 2px;
}
&.hour {
fill: #105fb0;
stroke: #105fb0;
stroke-width: 6px;
}
}
.tick {
transform-origin: center;
fill: none;
stroke: white;
stroke-width: 2px;
stroke-linecap: butt;
&.minor {
stroke-opacity: 0.5;
}
&.very.major {
stroke-width: 4px;
}
}
.block-segment {
fill: none;
stroke: url(#dial-gradient);
stroke-width: 18px;
}
.dial-segment {
fill: none;
stroke: white;
stroke-width: 2px;
}
.dial-gradient-img {
transform-origin: center;
}
}

View File

@@ -1,148 +0,0 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core';
import { Subscription, tap, timer } from 'rxjs';
import { WebsocketService } from '../../services/websocket.service';
import { StateService } from '../../services/state.service';
@Component({
selector: 'app-clock-face',
templateUrl: './clock-face.component.html',
styleUrls: ['./clock-face.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ClockFaceComponent implements OnInit, OnChanges, OnDestroy {
@Input() size: number = 300;
blocksSubscription: Subscription;
timeSubscription: Subscription;
faceStyle;
dialPath;
blockTimes = [];
segments = [];
hours: number = 0;
minutes: number = 0;
minorTicks: number[] = [];
majorTicks: number[] = [];
constructor(
public stateService: StateService,
private websocketService: WebsocketService,
private cd: ChangeDetectorRef
) {
this.updateTime();
this.makeTicks();
}
ngOnInit(): void {
this.timeSubscription = timer(0, 250).pipe(
tap(() => {
this.updateTime();
})
).subscribe();
this.blocksSubscription = this.stateService.blocks$
.subscribe(([block]) => {
if (block) {
this.blockTimes.push([block.height, new Date(block.timestamp * 1000)]);
// using block-reported times, so ensure they are sorted chronologically
this.blockTimes = this.blockTimes.sort((a, b) => a[1].getTime() - b[1].getTime());
this.updateSegments();
}
});
}
ngOnChanges(): void {
this.faceStyle = {
width: `${this.size}px`,
height: `${this.size}px`,
};
}
ngOnDestroy(): void {
this.timeSubscription.unsubscribe();
}
updateTime(): void {
const now = new Date();
const seconds = now.getSeconds() + (now.getMilliseconds() / 1000);
this.minutes = (now.getMinutes() + (seconds / 60)) % 60;
this.hours = now.getHours() + (this.minutes / 60);
this.updateSegments();
}
updateSegments(): void {
const now = new Date();
this.blockTimes = this.blockTimes.filter(time => (now.getTime() - time[1].getTime()) <= 3600000);
const tail = new Date(now.getTime() - 3600000);
const hourStart = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours());
const times = [
['start', tail],
...this.blockTimes,
['end', now],
];
const minuteTimes = times.map(time => {
return [time[0], (time[1].getTime() - hourStart.getTime()) / 60000];
});
this.segments = [];
const r = 174;
const cx = 192;
const cy = cx;
for (let i = 1; i < minuteTimes.length; i++) {
const arc = this.getArc(minuteTimes[i-1][1], minuteTimes[i][1], r, cx, cy);
if (arc) {
arc.id = minuteTimes[i][0];
this.segments.push(arc);
}
}
const arc = this.getArc(minuteTimes[0][1], minuteTimes[1][1], r, cx, cy);
if (arc) {
this.dialPath = arc.path;
}
this.cd.markForCheck();
}
getArc(startTime, endTime, r, cx, cy): any {
const startDegrees = (startTime + 0.2) * 6;
const endDegrees = (endTime - 0.2) * 6;
const start = this.getPointOnCircle(startDegrees, r, cx, cy);
const end = this.getPointOnCircle(endDegrees, r, cx, cy);
const arcLength = endDegrees - startDegrees;
// merge gaps and omit lines shorter than 1 degree
if (arcLength >= 1) {
const path = `M ${start.x} ${start.y} A ${r} ${r} 0 ${arcLength > 180 ? 1 : 0} 1 ${end.x} ${end.y}`;
return {
path,
start,
end
};
} else {
return null;
}
}
getPointOnCircle(deg, r, cx, cy) {
const modDeg = ((deg % 360) + 360) % 360;
const rad = (modDeg * Math.PI) / 180;
return {
x: cx + (r * Math.sin(rad)),
y: cy - (r * Math.cos(rad)),
};
}
makeTicks() {
this.minorTicks = [];
this.majorTicks = [];
for (let i = 1; i < 60; i++) {
if (i % 5 === 0) {
this.majorTicks.push(i * 6);
} else {
this.minorTicks.push(i * 6);
}
}
}
trackBySegment(index: number, segment) {
return segment.id;
}
}

View File

@@ -1 +0,0 @@
<app-clock mode="mempool"></app-clock>

View File

@@ -1,7 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-clock-mempool',
templateUrl: './clock-mempool.component.html',
})
export class ClockMempoolComponent {}

View File

@@ -1 +0,0 @@
<app-clock mode="block"></app-clock>

View File

@@ -1,7 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-clock-mined',
templateUrl: './clock-mined.component.html',
})
export class ClockMinedComponent {}

View File

@@ -1,67 +0,0 @@
<div class="clock-wrapper" [style]="wrapperStyle">
<div class="clockchain-bar" [style.height]="chainHeight + 'px'">
<div class="clockchain">
<app-clockchain [width]="chainWidth" [height]="chainHeight" [mode]="mode"></app-clockchain>
</div>
</div>
<div class="clock-face">
<app-clock-face [size]="clockSize">
<div class="block-wrapper">
<ng-container *ngIf="block && block.height >= 0">
<ng-container *ngIf="mode === 'block'; else mempoolMode;">
<div class="block-cube">
<div class="side top"></div>
<div class="side bottom"></div>
<div class="side right" [style]="blockStyle"></div>
<div class="side left" [style]="blockStyle"></div>
<div class="side front" [style]="blockStyle"></div>
<div class="side back" [style]="blockStyle"></div>
</div>
</ng-container>
<ng-template #mempoolMode>
<div class="block-sizer" [style]="blockSizerStyle">
<app-mempool-block-overview [index]="0" [pixelAlign]="true"></app-mempool-block-overview>
</div>
</ng-template>
<div class="fader"></div>
<div class="title-wrapper">
<h1 class="block-height">{{ block.height }}</h1>
</div>
</ng-container>
</div>
</app-clock-face>
</div>
<ng-container *ngIf="!hideStats">
<div class="stats top left">
<p class="label" i18n="clock.fiat-price">fiat price</p>
<p>
<app-fiat [value]="100000000" digitsInfo="1.2-2" colorClass="white-color"></app-fiat>
</p>
</div>
<div class="stats top right">
<p class="label" i18n="clock.priority-rate|priority fee rate">priority rate</p>
<p *ngIf="recommendedFees$ | async as recommendedFees;" i18n="shared.sat-vbyte|sat/vB">{{ recommendedFees.fastestFee + 300 }} sat/vB</p>
</div>
<div *ngIf="mode !== 'mempool' && block" class="stats bottom left">
<p [innerHTML]="block.size | bytes: 2"></p>
<p class="label" i18n="clock.block-size">block size</p>
</div>
<div *ngIf="mode !== 'mempool' && block" class="stats bottom right">
<p class="force-wrap">
<ng-container *ngTemplateOutlet="block.tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.tx_count | number}"></ng-container>
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} <span class="label">transaction</span></ng-template>
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} <span class="label">transactions</span></ng-template>
</p>
</div>
<ng-container *ngIf="mempoolInfo$ | async as mempoolInfo;">
<div *ngIf="mode === 'mempool'" class="stats bottom left">
<p [innerHTML]="mempoolInfo.usage | bytes: 0"></p>
<p class="label" i18n="dashboard.memory-usage|Memory usage">memory usage</p>
</div>
<div *ngIf="mode === 'mempool'" class="stats bottom right">
<p>{{ mempoolInfo.size | number }}</p>
<p class="label" i18n="dashboard.unconfirmed|Unconfirmed count">unconfirmed</p>
</div>
</ng-container>
</ng-container>
</div>

View File

@@ -1,190 +0,0 @@
.clock-wrapper {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
--chain-height: 60px;
--clock-width: 300px;
.clockchain-bar, .clock-face {
flex-shrink: 0;
flex-grow: 0;
}
.clockchain-bar {
position: relative;
width: 100%;
height: 15.625%;
z-index: 2;
// overflow: hidden;
// background: #1d1f31;
// box-shadow: 0 0 15px #000;
}
.clock-face {
position: relative;
height: 84.375%;
margin: auto;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
z-index: 1;
}
.stats {
position: absolute;
z-index: 3;
p {
margin: 0;
font-size: calc(0.055 * var(--clock-width));
line-height: calc(0.05 * var(--clock-width));
opacity: 0.8;
&.force-wrap {
word-spacing: 10000px;
}
::ng-deep .symbol {
font-size: inherit;
color: white;
}
}
.label {
font-size: calc(0.04 * var(--clock-width));
line-height: calc(0.05 * var(--clock-width));
}
&.top {
top: calc(var(--chain-height) + 2%);
}
&.bottom {
bottom: 2%;
}
&.left {
left: 5%;
}
&.right {
right: 5%;
text-align: end;
text-align: right;
}
}
}
.title-wrapper {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
.block-height {
font-size: calc(0.2 * var(--clock-width));
padding: 0;
margin: 0;
background: radial-gradient(rgba(0,0,0,0.5), transparent 67%);
padding: calc(0.05 * var(--clock-width)) calc(0.15 * var(--clock-width));
}
}
.block-wrapper {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
width: 100%;
height: 100%;
.block-sizer {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
.fader {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: radial-gradient(transparent 0%, transparent 44%, #11131f 58%, #11131f 100%);
}
.block-cube {
--side-width: calc(0.4 * var(--clock-width));
--half-side: calc(0.2 * var(--clock-width));
--neg-half-side: calc(-0.2 * var(--clock-width));
transform-style: preserve-3d;
animation: block-spin 60s infinite linear;
position: absolute;
z-index: -1;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: var(--side-width);
height: var(--side-width);
.side {
width: var(--side-width);
height: var(--side-width);
line-height: 100px;
text-align: center;
background: #232838;
display: block;
position: absolute;
}
.side.top {
transform: rotateX(90deg);
margin-top: var(--neg-half-side);
}
.side.bottom {
background: #105fb0;
transform: rotateX(-90deg);
margin-top: var(--half-side);
}
.side.right {
transform: rotateY(90deg);
margin-left: var(--half-side);
}
.side.left {
transform: rotateY(-90deg);
margin-left: var(--neg-half-side);
}
.side.front {
transform: translateZ(var(--half-side));
}
.side.back {
transform: translateZ(var(--neg-half-side));
}
}
}
@keyframes block-spin {
0% {transform: translate(-50%, -50%) rotateX(-20deg) rotateY(0deg);}
100% {transform: translate(-50%, -50%) rotateX(-20deg) rotateY(-360deg);}
}

View File

@@ -1,105 +0,0 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Input, OnInit } from '@angular/core';
import { Observable, Subscription } from 'rxjs';
import { StateService } from '../../services/state.service';
import { BlockExtended } from '../../interfaces/node-api.interface';
import { WebsocketService } from '../../services/websocket.service';
import { MempoolInfo, Recommendedfees } from '../../interfaces/websocket.interface';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-clock',
templateUrl: './clock.component.html',
styleUrls: ['./clock.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ClockComponent implements OnInit {
@Input() mode: 'block' | 'mempool' = 'block';
hideStats: boolean = false;
blocksSubscription: Subscription;
recommendedFees$: Observable<Recommendedfees>;
mempoolInfo$: Observable<MempoolInfo>;
block: BlockExtended;
clockSize: number = 300;
chainWidth: number = 384;
chainHeight: number = 60;
blockStyle;
blockSizerStyle;
wrapperStyle;
limitWidth: number;
limitHeight: number;
gradientColors = {
'': ['#9339f4', '#105fb0'],
bisq: ['#9339f4', '#105fb0'],
liquid: ['#116761', '#183550'],
'liquidtestnet': ['#494a4a', '#272e46'],
testnet: ['#1d486f', '#183550'],
signet: ['#6f1d5d', '#471850'],
};
constructor(
public stateService: StateService,
private websocketService: WebsocketService,
private route: ActivatedRoute,
private cd: ChangeDetectorRef,
) {
this.route.queryParams.subscribe((params) => {
this.hideStats = params && params.stats === 'false';
this.limitWidth = Number.parseInt(params.width) || null;
this.limitHeight = Number.parseInt(params.height) || null;
});
}
ngOnInit(): void {
this.resizeCanvas();
this.websocketService.want(['blocks']);
this.blocksSubscription = this.stateService.blocks$
.subscribe(([block]) => {
if (block) {
this.block = block;
this.blockStyle = this.getStyleForBlock(this.block);
this.cd.markForCheck();
}
});
this.recommendedFees$ = this.stateService.recommendedFees$;
this.mempoolInfo$ = this.stateService.mempoolInfo$;
}
getStyleForBlock(block: BlockExtended) {
const greenBackgroundHeight = 100 - (block.weight / this.stateService.env.BLOCK_WEIGHT_UNITS) * 100;
return {
background: `repeating-linear-gradient(
#2d3348,
#2d3348 ${greenBackgroundHeight}%,
${this.gradientColors[''][0]} ${Math.max(greenBackgroundHeight, 0)}%,
${this.gradientColors[''][1]} 100%
)`,
};
}
@HostListener('window:resize', ['$event'])
resizeCanvas(): void {
const windowWidth = this.limitWidth || window.innerWidth;
const windowHeight = this.limitHeight || window.innerHeight;
this.chainWidth = windowWidth;
this.chainHeight = Math.max(60, windowHeight / 8);
this.clockSize = Math.min(800, windowWidth, windowHeight - (1.4 * this.chainHeight));
const size = Math.ceil(this.clockSize / 75) * 75;
const margin = (this.clockSize - size) / 2;
this.blockSizerStyle = {
transform: `translate(${margin}px, ${margin}px)`,
width: `${size}px`,
height: `${size}px`,
};
this.wrapperStyle = {
'--clock-width': `${this.clockSize}px`,
'--chain-height': `${this.chainHeight}px`,
'width': this.limitWidth ? `${this.limitWidth}px` : undefined,
'height': this.limitHeight ? `${this.limitHeight}px` : undefined,
};
this.cd.markForCheck();
}
}

View File

@@ -1,28 +0,0 @@
<div
class="text-center" class="blockchain-wrapper" [class.time-ltr]="timeLtr" #container
[class.ltr-transition]="ltrTransitionEnabled" [style.width]="width + 'px'" [style.height]="height + 'px'"
>
<div class="position-container" [ngClass]="network ? network : ''" [style.top]="(height / 3) + 'px'">
<span>
<div class="blocks-wrapper">
<app-mempool-blocks [minimal]="true" [count]="mempoolBlocks" [blockWidth]="blockWidth" [spotlight]="mode === 'mempool' ? 1 : 0"></app-mempool-blocks>
<app-blockchain-blocks [minimal]="true" [count]="blockchainBlocks" [blockWidth]="blockWidth" [spotlight]="mode === 'block' ? -1 : 0"></app-blockchain-blocks>
</div>
<div class="divider" [style.top]="-(height / 6) + 'px'">
<svg
viewBox="0 0 2 175"
[style.width]="'2px'"
[style.height]="(5 * height / 6) + 'px'"
>
<line
class="divider-line"
x0="0"
x1="0"
y0="0"
y1="175px"
></line>
</svg>
</div>
</span>
</div>
</div>

View File

@@ -1,94 +0,0 @@
.divider {
position: absolute;
left: -0.5px;
top: 0;
.divider-line {
stroke: white;
stroke-width: 4px;
stroke-linecap: butt;
stroke-dasharray: 25px 25px;
}
}
.blockchain-wrapper {
height: 100%;
-webkit-user-select: none; /* Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE10+/Edge */
user-select: none; /* Standard */
}
.position-container {
position: absolute;
left: 50%;
top: 0;
}
.black-background {
background-color: #11131f;
z-index: 100;
position: relative;
}
.scroll-spacer {
position: absolute;
top: 0;
left: 0;
width: 1px;
height: 1px;
pointer-events: none;
}
.loading-block {
position: absolute;
text-align: center;
margin: auto;
width: 300px;
left: -150px;
top: 0px;
}
.time-toggle {
color: white;
font-size: 0.8rem;
position: absolute;
bottom: -1.8em;
left: 1px;
transform: translateX(-50%);
background: none;
border: none;
outline: none;
margin: 0;
padding: 0;
}
.blockchain-wrapper.ltr-transition .blocks-wrapper,
.blockchain-wrapper.ltr-transition .position-container,
.blockchain-wrapper.ltr-transition .time-toggle {
transition: transform 1s;
}
.blockchain-wrapper.time-ltr {
.blocks-wrapper {
transform: scaleX(-1);
}
.time-toggle {
transform: translateX(-50%) scaleX(-1);
}
}
:host-context(.ltr-layout) {
.blockchain-wrapper.time-ltr .blocks-wrapper,
.blockchain-wrapper .blocks-wrapper {
direction: ltr;
}
}
:host-context(.rtl-layout) {
.blockchain-wrapper.time-ltr .blocks-wrapper,
.blockchain-wrapper .blocks-wrapper {
direction: rtl;
}
}

View File

@@ -1,73 +0,0 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, OnChanges, ChangeDetectorRef } from '@angular/core';
import { firstValueFrom, Subscription } from 'rxjs';
import { StateService } from '../../services/state.service';
@Component({
selector: 'app-clockchain',
templateUrl: './clockchain.component.html',
styleUrls: ['./clockchain.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ClockchainComponent implements OnInit, OnChanges, OnDestroy {
@Input() width: number = 300;
@Input() height: number = 60;
@Input() mode: 'mempool' | 'block';
mempoolBlocks: number = 3;
blockchainBlocks: number = 6;
blockWidth: number = 50;
dividerStyle;
network: string;
timeLtrSubscription: Subscription;
timeLtr: boolean = this.stateService.timeLtr.value;
ltrTransitionEnabled = false;
connectionStateSubscription: Subscription;
loadingTip: boolean = true;
connected: boolean = true;
constructor(
public stateService: StateService,
private cd: ChangeDetectorRef,
) {}
ngOnInit() {
this.ngOnChanges();
this.network = this.stateService.network;
this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {
this.timeLtr = !!ltr;
});
this.connectionStateSubscription = this.stateService.connectionState$.subscribe(state => {
this.connected = (state === 2);
});
firstValueFrom(this.stateService.chainTip$).then(() => {
this.loadingTip = false;
});
}
ngOnChanges() {
this.blockWidth = Math.floor(7 * this.height / 12);
this.mempoolBlocks = Math.floor(((this.width / 2) - (this.blockWidth * 0.32)) / (1.24 * this.blockWidth));
this.blockchainBlocks = this.mempoolBlocks;
this.dividerStyle = {
width: '2px',
height: `${this.height}px`,
};
this.cd.markForCheck();
}
ngOnDestroy() {
this.timeLtrSubscription.unsubscribe();
this.connectionStateSubscription.unsubscribe();
}
trackByPageFn(index: number, item: { index: number }) {
return item.index;
}
toggleTimeDirection() {
this.ltrTransitionEnabled = true;
this.stateService.timeLtr.next(!this.timeLtr);
}
}

View File

@@ -26,23 +26,15 @@ export class FeesBoxComponent implements OnInit {
this.recommendedFees$ = this.stateService.recommendedFees$
.pipe(
tap((fees) => {
let feeLevelIndex = feeLevels.slice().reverse().findIndex((feeLvl) => fees.economyFee >= feeLvl);
let feeLevelIndex = feeLevels.slice().reverse().findIndex((feeLvl) => fees.minimumFee >= feeLvl);
feeLevelIndex = feeLevelIndex >= 0 ? feeLevels.length - feeLevelIndex : feeLevelIndex;
const startColor = '#' + (mempoolFeeColors[feeLevelIndex - 1] || mempoolFeeColors[mempoolFeeColors.length - 1]);
feeLevelIndex = feeLevels.slice().reverse().findIndex((feeLvl) => fees.hourFee >= feeLvl);
feeLevelIndex = feeLevelIndex >= 0 ? feeLevels.length - feeLevelIndex : feeLevelIndex;
const lowColor = '#' + (mempoolFeeColors[feeLevelIndex - 1] || mempoolFeeColors[mempoolFeeColors.length - 1]);
feeLevelIndex = feeLevels.slice().reverse().findIndex((feeLvl) => fees.halfHourFee >= feeLvl);
feeLevelIndex = feeLevelIndex >= 0 ? feeLevels.length - feeLevelIndex : feeLevelIndex;
const medColor = '#' + (mempoolFeeColors[feeLevelIndex - 1] || mempoolFeeColors[mempoolFeeColors.length - 1]);
feeLevelIndex = feeLevels.slice().reverse().findIndex((feeLvl) => fees.fastestFee >= feeLvl);
feeLevelIndex = feeLevelIndex >= 0 ? feeLevels.length - feeLevelIndex : feeLevelIndex;
const highColor = '#' + (mempoolFeeColors[feeLevelIndex - 1] || mempoolFeeColors[mempoolFeeColors.length - 1]);
const endColor = '#' + (mempoolFeeColors[feeLevelIndex - 1] || mempoolFeeColors[mempoolFeeColors.length - 1]);
this.gradient = `linear-gradient(to right, ${lowColor} 0%, ${medColor} 50%, ${highColor} 100%)`;
this.gradient = `linear-gradient(to right, ${startColor}, ${endColor})`;
this.noPriority = startColor;
}
)

View File

@@ -5,6 +5,5 @@
[blockLimit]="stateService.blockVSize"
[orientation]="timeLtr ? 'right' : 'left'"
[flip]="true"
[pixelAlign]="pixelAlign"
(txClickEvent)="onTxClick($event)"
></app-block-overview-graph>

View File

@@ -16,7 +16,6 @@ import { Router } from '@angular/router';
})
export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit {
@Input() index: number;
@Input() pixelAlign: boolean = false;
@Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>();
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;

View File

@@ -1,47 +1,40 @@
<ng-container *ngIf="(loadingBlocks$ | async) === false; else loadingBlocks" [class.minimal]="minimal">
<div class="mempool-blocks-container" [class.time-ltr]="timeLtr" [style.--block-size]="blockWidth+'px'" *ngIf="(difficultyAdjustments$ | async) as da;">
<ng-container *ngIf="(loadingBlocks$ | async) === false; else loadingBlocks">
<div class="mempool-blocks-container" [class.time-ltr]="timeLtr" *ngIf="(difficultyAdjustments$ | async) as da;">
<div class="flashing">
<ng-template ngFor let-projectedBlock [ngForOf]="mempoolBlocks$ | async" let-i="index" [ngForTrackBy]="trackByFn">
<div
*ngIf="minimal && spotlight > 0 && spotlight === i + 1"
class="spotlight-bottom"
[style.right]="mempoolBlockStyles[i].right"
></div>
<div @blockEntryTrigger [@.disabled]="i > 0 || !animateEntry" [attr.data-cy]="'mempool-block-' + i" class="bitcoin-block text-center mempool-block" [class.hide-block]="count && i >= count" id="mempool-block-{{ i }}" [ngStyle]="mempoolBlockStyles[i]" [class.blink-bg]="projectedBlock.blink">
<div @blockEntryTrigger [@.disabled]="i > 0 || !animateEntry" [attr.data-cy]="'mempool-block-' + i" class="bitcoin-block text-center mempool-block" id="mempool-block-{{ i }}" [ngStyle]="mempoolBlockStyles[i]" [class.blink-bg]="projectedBlock.blink">
<a draggable="false" [routerLink]="['/mempool-block/' | relativeUrl, i]"
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}">&nbsp;</a>
<div class="block-body">
<ng-container *ngIf="!minimal">
<div [attr.data-cy]="'mempool-block-' + i + '-fees'" class="fees">
~{{ projectedBlock.medianFee | number:feeRounding }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
</div>
<div [attr.data-cy]="'mempool-block-' + i + '-fee-span'" class="fee-span">
{{ projectedBlock.feeRange[0] | number:feeRounding }} - {{ projectedBlock.feeRange[projectedBlock.feeRange.length - 1] | number:feeRounding }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
</div>
<div *ngIf="showMiningInfo" class="block-size">
<app-amount [attr.data-cy]="'mempool-block-' + i + '-total-fees'" [satoshis]="projectedBlock.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
</div>
<div *ngIf="!showMiningInfo" class="block-size" [innerHTML]="'&lrm;' + (projectedBlock.blockSize | bytes: 2)"></div>
<div [attr.data-cy]="'mempool-block-' + i + '-transaction-count'" class="transaction-count">
<ng-container *ngTemplateOutlet="projectedBlock.nTx === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: projectedBlock.nTx | number}"></ng-container>
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
</div>
<div [attr.data-cy]="'mempool-block-' + i + '-time'" class="time-difference" *ngIf="projectedBlock.blockVSize <= stateService.blockVSize; else mergedBlock">
<ng-template [ngIf]="network === 'liquid' || network === 'liquidtestnet'" [ngIfElse]="timeDiffMainnet">
<app-time kind="until" [time]="(1 * i) + now + 61000" [fastRender]="false" [fixedRender]="true"></app-time>
</ng-template>
<ng-template #timeDiffMainnet>
<app-time kind="until" [time]="da.timeAvg * (i + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true"></app-time>
</ng-template>
</div>
<ng-template #mergedBlock>
<div [attr.data-cy]="'mempool-block-' + i + '-blocks'" class="time-difference">
<b>(<ng-container *ngTemplateOutlet="blocksPlural; context: {$implicit: projectedBlock.blockVSize / stateService.blockVSize | ceil }"></ng-container>)</b>
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
</div>
<div [attr.data-cy]="'mempool-block-' + i + '-fees'" class="fees">
~{{ projectedBlock.medianFee | number:feeRounding }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
</div>
<div [attr.data-cy]="'mempool-block-' + i + '-fee-span'" class="fee-span">
{{ projectedBlock.feeRange[0] | number:feeRounding }} - {{ projectedBlock.feeRange[projectedBlock.feeRange.length - 1] | number:feeRounding }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
</div>
<div *ngIf="showMiningInfo" class="block-size">
<app-amount [attr.data-cy]="'mempool-block-' + i + '-total-fees'" [satoshis]="projectedBlock.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
</div>
<div *ngIf="!showMiningInfo" class="block-size" [innerHTML]="'&lrm;' + (projectedBlock.blockSize | bytes: 2)"></div>
<div [attr.data-cy]="'mempool-block-' + i + '-transaction-count'" class="transaction-count">
<ng-container *ngTemplateOutlet="projectedBlock.nTx === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: projectedBlock.nTx | number}"></ng-container>
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
</div>
<div [attr.data-cy]="'mempool-block-' + i + '-time'" class="time-difference" *ngIf="projectedBlock.blockVSize <= stateService.blockVSize; else mergedBlock">
<ng-template [ngIf]="network === 'liquid' || network === 'liquidtestnet'" [ngIfElse]="timeDiffMainnet">
<app-time kind="until" [time]="(1 * i) + now + 61000" [fastRender]="false" [fixedRender]="true"></app-time>
</ng-template>
</ng-container>
<ng-template #timeDiffMainnet>
<app-time kind="until" [time]="da.timeAvg * (i + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true"></app-time>
</ng-template>
</div>
<ng-template #mergedBlock>
<div [attr.data-cy]="'mempool-block-' + i + '-blocks'" class="time-difference">
<b>(<ng-container *ngTemplateOutlet="blocksPlural; context: {$implicit: projectedBlock.blockVSize / stateService.blockVSize | ceil }"></ng-container>)</b>
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
</div>
</ng-template>
</div>
<span class="animated-border"></span>
</div>
@@ -52,10 +45,10 @@
</ng-container>
<ng-template #loadingBlocks>
<div class="mempool-blocks-container" [class.time-ltr]="timeLtr" [style.--block-size]="blockWidth+'px'">
<div class="mempool-blocks-container" [class.time-ltr]="timeLtr">
<div class="flashing">
<ng-template ngFor let-projectedBlock [ngForOf]="mempoolEmptyBlocks" let-i="index" [ngForTrackBy]="trackByFn">
<div class="bitcoin-block text-center mempool-block" [class.hide-block]="count && i >= count" id="mempool-block-{{ i }}" [ngStyle]="mempoolEmptyBlockStyles[i]"></div>
<div class="bitcoin-block text-center mempool-block" id="mempool-block-{{ i }}" [ngStyle]="mempoolEmptyBlockStyles[i]"></div>
</ng-template>
</div>
</div>

View File

@@ -1,7 +1,7 @@
.bitcoin-block {
width: var(--block-size);
height: var(--block-size);
transition: background 2s, right 2s, transform 1s, opacity 1s;
width: 125px;
height: 125px;
transition: background 2s, right 2s, transform 1s;
}
.block-size {
@@ -14,7 +14,6 @@
top: 0px;
right: 0px;
left: 0px;
--block-size: 125px;
}
.flashing {
@@ -67,11 +66,11 @@
.bitcoin-block::after {
content: '';
width: var(--block-size);
height: calc(0.192 * var(--block-size));
width: 125px;
height: 24px;
position:absolute;
top: calc(-0.192 * var(--block-size));
left: calc(-0.16 * var(--block-size));
top: -24px;
left: -20px;
background-color: #232838;
transform:skew(40deg);
transform-origin:top;
@@ -80,11 +79,11 @@
.bitcoin-block::before {
content: '';
width: calc(0.16 * var(--block-size));
height: var(--block-size);
width: 20px;
height: 125px;
position: absolute;
top: calc(-0.096 * var(--block-size));
left: calc(-0.16 * var(--block-size));
top: -12px;
left: -20px;
background-color: #191c27;
z-index: -1;
@@ -101,10 +100,6 @@
background-color: #2d2825;
}
.mempool-block.hide-block {
opacity: 0;
}
.black-background {
background-color: #11131f;
z-index: 100;
@@ -146,7 +141,7 @@
.bitcoin-block::before {
transform: skewY(-50deg);
left: var(--block-size);
left: 125px;
}
.block-body {
transform: scaleX(-1);
@@ -157,16 +152,4 @@
#arrow-up {
transform: translateX(70px);
}
}
.spotlight-bottom {
position: absolute;
width: calc(0.6 * var(--block-size));
height: calc(0.25 * var(--block-size));
border-left: solid calc(0.3 * var(--block-size)) transparent;
border-bottom: solid calc(0.3 * var(--block-size)) white;
border-right: solid calc(0.3 * var(--block-size)) transparent;
transform: translate(calc(-0.2 * var(--block-size)), calc(1.1 * var(--block-size)));
border-radius: 2px;
z-index: -1;
}

View File

@@ -1,4 +1,4 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener } from '@angular/core';
import { Subscription, Observable, fromEvent, merge, of, combineLatest } from 'rxjs';
import { MempoolBlock } from '../../interfaces/websocket.interface';
import { StateService } from '../../services/state.service';
@@ -23,12 +23,7 @@ import { animate, style, transition, trigger } from '@angular/animations';
])],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
@Input() minimal: boolean = false;
@Input() blockWidth: number = 125;
@Input() count: number = null;
@Input() spotlight: number = 0;
export class MempoolBlocksComponent implements OnInit, OnDestroy {
specialBlocks = specialBlocks;
mempoolBlocks: MempoolBlock[] = [];
mempoolEmptyBlocks: MempoolBlock[] = this.mountEmptyBlocks();
@@ -53,9 +48,8 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
timeLtr: boolean;
animateEntry: boolean = false;
blockOffset: number = 155;
blockPadding: number = 30;
containerOffset: number = 40;
blockWidth = 125;
blockPadding = 30;
arrowVisible = false;
tabHidden = false;
feeRounding = '1.0-0';
@@ -224,14 +218,6 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
});
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.blockWidth && this.blockWidth) {
this.blockPadding = 0.24 * this.blockWidth;
this.containerOffset = 0.32 * this.blockWidth;
this.blockOffset = this.blockWidth + this.blockPadding;
}
}
ngOnDestroy() {
this.markBlocksSubscription.unsubscribe();
this.blockSubscription.unsubscribe();
@@ -252,24 +238,17 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
reduceMempoolBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] {
const innerWidth = this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2;
let blocksAmount;
if (this.count) {
blocksAmount = 8;
} else {
blocksAmount = Math.min(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT, Math.floor(innerWidth / (this.blockWidth + this.blockPadding)));
}
const blocksAmount = Math.min(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT, Math.floor(innerWidth / (this.blockWidth + this.blockPadding)));
while (blocks.length > blocksAmount) {
const block = blocks.pop();
if (!this.count) {
const lastBlock = blocks[blocks.length - 1];
lastBlock.blockSize += block.blockSize;
lastBlock.blockVSize += block.blockVSize;
lastBlock.nTx += block.nTx;
lastBlock.feeRange = lastBlock.feeRange.concat(block.feeRange);
lastBlock.feeRange.sort((a, b) => a - b);
lastBlock.medianFee = this.median(lastBlock.feeRange);
lastBlock.totalFees += block.totalFees;
}
const lastBlock = blocks[blocks.length - 1];
lastBlock.blockSize += block.blockSize;
lastBlock.blockVSize += block.blockVSize;
lastBlock.nTx += block.nTx;
lastBlock.feeRange = lastBlock.feeRange.concat(block.feeRange);
lastBlock.feeRange.sort((a, b) => a - b);
lastBlock.medianFee = this.median(lastBlock.feeRange);
lastBlock.totalFees += block.totalFees;
}
if (blocks.length) {
blocks[blocks.length - 1].isStack = blocks[blocks.length - 1].blockVSize > this.stateService.blockVSize;
@@ -315,14 +294,14 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
});
return {
'right': this.containerOffset + index * this.blockOffset + 'px',
'right': 40 + index * 155 + 'px',
'background': backgroundGradients.join(',') + ')'
};
}
getStyleForMempoolEmptyBlock(index: number) {
return {
'right': this.containerOffset + index * this.blockOffset + 'px',
'right': 40 + index * 155 + 'px',
'background': '#554b45',
};
}

View File

@@ -23,7 +23,8 @@ import { download, formatterXAxis, formatterXAxisLabel } from '../../shared/grap
})
export class MempoolGraphComponent implements OnInit, OnChanges {
@Input() data: any[];
@Input() filterSize = 100000;
@Input() limitFee = 350;
@Input() limitFilterFee = 1;
@Input() height: number | string = 200;
@Input() top: number | string = 20;
@Input() right: number | string = 10;
@@ -98,20 +99,16 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
}
generateArray(mempoolStats: OptimizedMempoolStats[]) {
let finalArray: number[][][] = [];
const finalArray: number[][][] = [];
let feesArray: number[][] = [];
let maxTier = 0;
for (let index = 37; index > -1; index--) {
const limitFeesTemplate = this.template === 'advanced' ? 26 : 20;
for (let index = limitFeesTemplate; index > -1; index--) {
feesArray = [];
mempoolStats.forEach((stats) => {
if (stats.vsizes[index] >= this.filterSize) {
maxTier = Math.max(maxTier, index);
}
feesArray.push([stats.added * 1000, stats.vsizes[index] ? stats.vsizes[index] : 0]);
});
finalArray.push(feesArray);
}
this.feeLimitIndex = maxTier;
finalArray.reverse();
return finalArray;
}
@@ -124,7 +121,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
const newColors = [];
for (let index = 0; index < series.length; index++) {
const value = series[index];
if (index < this.feeLimitIndex) {
if (index >= this.feeLimitIndex) {
newColors.push(this.chartColorsOrdered[index]);
seriesGraph.push({
zlevel: 0,
@@ -374,21 +371,17 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
orderLevels() {
this.feeLevelsOrdered = [];
let maxIndex = Math.min(feeLevels.length, this.feeLimitIndex);
for (let i = 0; i < maxIndex; i++) {
for (let i = 0; i < feeLevels.length; i++) {
if (feeLevels[i] === this.limitFilterFee) {
this.feeLimitIndex = i;
}
if (feeLevels[i] <= this.limitFee) {
if (this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') {
if (i === maxIndex - 1) {
this.feeLevelsOrdered.push(`${(feeLevels[i] / 10).toFixed(1)}+`);
} else {
this.feeLevelsOrdered.push(`${(feeLevels[i] / 10).toFixed(1)} - ${(feeLevels[i + 1] / 10).toFixed(1)}`);
}
this.feeLevelsOrdered.push(`${(feeLevels[i] / 10).toFixed(1)} - ${(feeLevels[i + 1] / 10).toFixed(1)}`);
} else {
if (i === maxIndex - 1) {
this.feeLevelsOrdered.push(`${feeLevels[i]}+`);
} else {
this.feeLevelsOrdered.push(`${feeLevels[i]} - ${feeLevels[i + 1]}`);
}
this.feeLevelsOrdered.push(`${feeLevels[i]} - ${feeLevels[i + 1]}`);
}
}
}
this.chartColorsOrdered = chartColors.slice(0, this.feeLevelsOrdered.length);
}

View File

@@ -84,7 +84,8 @@
</div>
<div class="card-body">
<div class="incoming-transactions-graph">
<app-mempool-graph #mempoolgraph dir="ltr" [template]="'advanced'" [height]="500" [left]="65" [right]="10"
<app-mempool-graph #mempoolgraph dir="ltr" [template]="'advanced'" [limitFee]="500"
[limitFilterFee]="filterFeeIndex" [height]="500" [left]="65" [right]="10"
[data]="mempoolStats && mempoolStats.length ? mempoolStats : null"></app-mempool-graph>
</div>
</div>

View File

@@ -3,6 +3,7 @@
<div class="chart-holder">
<app-mempool-graph
[template]="'advanced'"
[limitFee]="500"
[height]="600"
[left]="60"
[right]="10"

View File

@@ -85,7 +85,7 @@
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<ng-template [ngIf]="transactionTime !== 0">
<ng-template [ngIf]="transactionTime !== 0 && !replaced">
<tr *ngIf="transactionTime === -1; else firstSeenTmpl">
<td><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>

View File

@@ -216,7 +216,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.error = undefined;
this.waitingForTransaction = false;
this.graphExpanded = false;
this.transactionTime = tx.firstSeen || 0;
this.transactionTime = 0;
this.setupGraph();
this.fetchRbfHistory$.next(this.tx.txid);
@@ -338,10 +338,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
if (tx.firstSeen) {
this.transactionTime = tx.firstSeen;
} else {
this.getTransactionTime();
this.transactionTime = 0;
}
} else {
this.transactionTime = 0;
this.getTransactionTime();
}
if (this.tx?.status?.confirmed) {
@@ -471,9 +471,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.apiService
.getTransactionTimes$([this.tx.txid])
.subscribe((transactionTimes) => {
if (transactionTimes?.length) {
this.transactionTime = transactionTimes[0];
}
this.transactionTime = transactionTimes[0];
});
}

View File

@@ -26,7 +26,8 @@
<div class="mempool-graph">
<app-mempool-graph
[template]="'widget'"
[filterSize]="1000000"
[limitFee]="150"
[limitFilterFee]="1"
[data]="mempoolStats.value?.mempool"
[windowPreferenceOverride]="'2h'"
></app-mempool-graph>

View File

@@ -14,6 +14,7 @@ import { LbtcPegsGraphComponent } from '../components/lbtc-pegs-graph/lbtc-pegs-
import { GraphsComponent } from '../components/graphs/graphs.component';
import { StatisticsComponent } from '../components/statistics/statistics.component';
import { MempoolBlockComponent } from '../components/mempool-block/mempool-block.component';
import { MempoolBlockOverviewComponent } from '../components/mempool-block-overview/mempool-block-overview.component';
import { PoolRankingComponent } from '../components/pool-ranking/pool-ranking.component';
import { PoolComponent } from '../components/pool/pool.component';
import { TelevisionComponent } from '../components/television/television.component';
@@ -41,6 +42,7 @@ import { CommonModule } from '@angular/common';
BlockFeeRatesGraphComponent,
BlockSizesWeightsGraphComponent,
FeeDistributionGraphComponent,
MempoolBlockOverviewComponent,
IncomingTransactionsGraphComponent,
MempoolGraphComponent,
LbtcPegsGraphComponent,

View File

@@ -90,13 +90,6 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati
import { TestnetAlertComponent } from './components/testnet-alert/testnet-alert.component';
import { GlobalFooterComponent } from './components/global-footer/global-footer.component';
import { MempoolBlockOverviewComponent } from '../components/mempool-block-overview/mempool-block-overview.component';
import { ClockchainComponent } from '../components/clockchain/clockchain.component';
import { ClockFaceComponent } from '../components/clock-face/clock-face.component';
import { ClockComponent } from '../components/clock/clock.component';
import { ClockMinedComponent } from '../components/clock/clock-mined.component';
import { ClockMempoolComponent } from '../components/clock/clock-mempool.component';
@NgModule({
declarations: [
ClipboardComponent,
@@ -179,13 +172,6 @@ import { ClockMempoolComponent } from '../components/clock/clock-mempool.compone
GeolocationComponent,
TestnetAlertComponent,
GlobalFooterComponent,
MempoolBlockOverviewComponent,
ClockchainComponent,
ClockComponent,
ClockMinedComponent,
ClockMempoolComponent,
ClockFaceComponent,
],
imports: [
CommonModule,
@@ -293,13 +279,6 @@ import { ClockMempoolComponent } from '../components/clock/clock-mempool.compone
GeolocationComponent,
PreviewTitleComponent,
GlobalFooterComponent,
MempoolBlockOverviewComponent,
ClockchainComponent,
ClockComponent,
ClockMinedComponent,
ClockMempoolComponent,
ClockFaceComponent,
]
})
export class SharedModule {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -285,10 +285,6 @@ body {
color: #fff;
}
.white-color {
color: white;
}
.green-color {
color: #3bcc49;
}