Compare commits
68 Commits
mononaut/a
...
hunicus/ro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a633892dc | ||
|
|
e7a1817475 | ||
|
|
e5efc2957a | ||
|
|
20d7e56de2 | ||
|
|
0586e04d67 | ||
|
|
39bde61538 | ||
|
|
0fb92e6ebb | ||
|
|
c8d3653ef3 | ||
|
|
a5575c0876 | ||
|
|
1872e5d12f | ||
|
|
176f5e1377 | ||
|
|
c0e235c01a | ||
|
|
2faeb1071e | ||
|
|
618ba56c42 | ||
|
|
d955dbff55 | ||
|
|
ee39283241 | ||
|
|
c42670259e | ||
|
|
bb61ff97fa | ||
|
|
c630d705df | ||
|
|
c5bf167e36 | ||
|
|
e8420853e2 | ||
|
|
a8a5b733e5 | ||
|
|
30f8d5cf96 | ||
|
|
f09939a201 | ||
|
|
a99515e94a | ||
|
|
c4f7b99978 | ||
|
|
dcf73ec3f3 | ||
|
|
bbe0579cdd | ||
|
|
4390ffe3b6 | ||
|
|
6b93e61b56 | ||
|
|
c79d031c86 | ||
|
|
ae5a0312be | ||
|
|
0b2ffb3e91 | ||
|
|
d009edbbf3 | ||
|
|
1fbdf97639 | ||
|
|
a36303e1fb | ||
|
|
27a3a1575d | ||
|
|
93d24d1cf7 | ||
|
|
bfb842d7ea | ||
|
|
5b62966863 | ||
|
|
3013386ca5 | ||
|
|
aedaf53137 | ||
|
|
7157efcf79 | ||
|
|
57ac1486a0 | ||
|
|
3c022ad755 | ||
|
|
ca9b48283d | ||
|
|
c8fc416c88 | ||
|
|
35d80eec1c | ||
|
|
74b2014dff | ||
|
|
9883e59f12 | ||
|
|
30396c5dca | ||
|
|
ec0d5e0c23 | ||
|
|
3c0bb11208 | ||
|
|
0b74cf1d89 | ||
|
|
c558c85f36 | ||
|
|
ea51ab8d0b | ||
|
|
62169cee3f | ||
|
|
e7e7b30807 | ||
|
|
107bdbc209 | ||
|
|
0b4615cbf0 | ||
|
|
aa9fd845ef | ||
|
|
e41ce16bbb | ||
|
|
f12403747d | ||
|
|
da3c3e8f5c | ||
|
|
a447887901 | ||
|
|
d3bd434255 | ||
|
|
49e057e726 | ||
|
|
7f3e4eb534 |
@@ -122,9 +122,5 @@
|
||||
"LIQUID_ONION": "http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1",
|
||||
"BISQ_URL": "https://bisq.markets/api",
|
||||
"BISQ_ONION": "http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api"
|
||||
},
|
||||
"MEMPOOL_SERVICES": {
|
||||
"API": "https://mempool.space/api",
|
||||
"ACCELERATIONS": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,9 +118,5 @@
|
||||
},
|
||||
"CLIGHTNING": {
|
||||
"SOCKET": "__CLIGHTNING_SOCKET__"
|
||||
},
|
||||
"MEMPOOl_SERVICES": {
|
||||
"API": "__MEMPOOL_SERVICES_API__",
|
||||
"ACCELERATIONS": "__MEMPOOL_SERVICES_ACCELERATIONS__"
|
||||
}
|
||||
}
|
||||
@@ -117,11 +117,6 @@ describe('Mempool Backend Config', () => {
|
||||
GEOLITE2_ASN: '/usr/local/share/GeoIP/GeoLite2-ASN.mmdb',
|
||||
GEOIP2_ISP: '/usr/local/share/GeoIP/GeoIP2-ISP.mmdb'
|
||||
});
|
||||
|
||||
expect(config.MEMPOOL_SERVICES).toStrictEqual({
|
||||
API: "",
|
||||
ACCELERATIONS: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -155,8 +150,6 @@ describe('Mempool Backend Config', () => {
|
||||
expect(config.PRICE_DATA_SERVER).toStrictEqual(fixture.PRICE_DATA_SERVER);
|
||||
|
||||
expect(config.EXTERNAL_DATA_SERVER).toStrictEqual(fixture.EXTERNAL_DATA_SERVER);
|
||||
|
||||
expect(config.MEMPOOL_SERVICES).toStrictEqual(fixture.MEMPOOL_SERVICES);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -5,16 +5,15 @@ import { TransactionExtended, MempoolBlockWithTransactions } from '../mempool.in
|
||||
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
|
||||
|
||||
class Audit {
|
||||
auditBlock(transactions: TransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: TransactionExtended }, useAccelerations: boolean = false)
|
||||
: { censored: string[], added: string[], fresh: string[], sigop: string[], accelerated: string[], score: number, similarity: number } {
|
||||
auditBlock(transactions: TransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: TransactionExtended })
|
||||
: { censored: string[], added: string[], fresh: string[], sigop: string[], score: number, similarity: number } {
|
||||
if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
|
||||
return { censored: [], added: [], fresh: [], sigop: [], accelerated: [], score: 0, similarity: 1 };
|
||||
return { censored: [], added: [], fresh: [], sigop: [], score: 0, similarity: 1 };
|
||||
}
|
||||
|
||||
const matches: string[] = []; // present in both mined block and template
|
||||
const added: string[] = []; // present in mined block, not in template
|
||||
const fresh: string[] = []; // missing, but firstSeen within PROPAGATION_MARGIN
|
||||
const accelerated: string[] = []; // prioritized by the mempool accelerator
|
||||
const isCensored = {}; // missing, without excuse
|
||||
const isDisplaced = {};
|
||||
let displacedWeight = 0;
|
||||
@@ -27,9 +26,6 @@ class Audit {
|
||||
const now = Math.round((Date.now() / 1000));
|
||||
for (const tx of transactions) {
|
||||
inBlock[tx.txid] = tx;
|
||||
if (mempool[tx.txid] && mempool[tx.txid].acceleration) {
|
||||
accelerated.push(tx.txid);
|
||||
}
|
||||
}
|
||||
// coinbase is always expected
|
||||
if (transactions[0]) {
|
||||
@@ -142,7 +138,6 @@ class Audit {
|
||||
added,
|
||||
fresh,
|
||||
sigop: [],
|
||||
accelerated,
|
||||
score,
|
||||
similarity,
|
||||
};
|
||||
|
||||
@@ -213,7 +213,6 @@ class BitcoinRoutes {
|
||||
effectiveFeePerVsize: tx.effectiveFeePerVsize || null,
|
||||
sigops: tx.sigops,
|
||||
adjustedVsize: tx.adjustedVsize,
|
||||
acceleration: tx.acceleration
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -127,14 +127,6 @@ class Blocks {
|
||||
}
|
||||
}
|
||||
|
||||
if (addMempoolData) {
|
||||
transactions.forEach((tx) => {
|
||||
if (!tx.cpfpChecked) {
|
||||
Common.setRelativesAndGetCpfpInfo(tx as MempoolTransactionExtended, mempool); // Child Pay For Parent
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!quiet) {
|
||||
logger.debug(`${transactionsFound} of ${txIds.length} found in mempool. ${transactionsFetched} fetched through backend service.`);
|
||||
}
|
||||
@@ -282,10 +274,14 @@ class Blocks {
|
||||
}
|
||||
|
||||
extras.matchRate = null;
|
||||
extras.expectedFees = null;
|
||||
extras.expectedWeight = null;
|
||||
if (config.MEMPOOL.AUDIT) {
|
||||
const auditScore = await BlocksAuditsRepository.$getBlockAuditScore(block.id);
|
||||
if (auditScore != null) {
|
||||
extras.matchRate = auditScore.matchRate;
|
||||
extras.expectedFees = auditScore.expectedFees;
|
||||
extras.expectedWeight = auditScore.expectedWeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -455,6 +451,46 @@ class Blocks {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [INDEXING] Index expected fees & weight for all audited blocks
|
||||
*/
|
||||
public async $generateAuditStats(): Promise<void> {
|
||||
const blockIds = await BlocksAuditsRepository.$getBlocksWithoutSummaries();
|
||||
if (!blockIds?.length) {
|
||||
return;
|
||||
}
|
||||
let timer = Date.now();
|
||||
let indexedThisRun = 0;
|
||||
let indexedTotal = 0;
|
||||
logger.debug(`Indexing ${blockIds.length} block audit details`);
|
||||
for (const hash of blockIds) {
|
||||
const summary = await BlocksSummariesRepository.$getTemplate(hash);
|
||||
let totalFees = 0;
|
||||
let totalWeight = 0;
|
||||
for (const tx of summary?.transactions || []) {
|
||||
totalFees += tx.fee;
|
||||
totalWeight += (tx.vsize * 4);
|
||||
}
|
||||
await BlocksAuditsRepository.$setSummary(hash, totalFees, totalWeight);
|
||||
const cachedBlock = this.blocks.find(block => block.id === hash);
|
||||
if (cachedBlock) {
|
||||
cachedBlock.extras.expectedFees = totalFees;
|
||||
cachedBlock.extras.expectedWeight = totalWeight;
|
||||
}
|
||||
|
||||
indexedThisRun++;
|
||||
indexedTotal++;
|
||||
const elapsedSeconds = (Date.now() - timer) / 1000;
|
||||
if (elapsedSeconds > 5) {
|
||||
const blockPerSeconds = indexedThisRun / elapsedSeconds;
|
||||
logger.debug(`Indexed ${indexedTotal} / ${blockIds.length} block audit details (${blockPerSeconds.toFixed(1)}/s)`);
|
||||
timer = Date.now();
|
||||
indexedThisRun = 0;
|
||||
}
|
||||
}
|
||||
logger.debug(`Indexing block audit details completed`);
|
||||
}
|
||||
|
||||
/**
|
||||
* [INDEXING] Index all blocks metadata for the mining dashboard
|
||||
*/
|
||||
@@ -645,9 +681,12 @@ class Blocks {
|
||||
logger.info(`Re-indexed 10 blocks and summaries. Also re-indexed the last difficulty adjustments. Will re-index latest hashrates in a few seconds.`, logger.tags.mining);
|
||||
indexer.reindex();
|
||||
}
|
||||
await blocksRepository.$saveBlockInDatabase(blockExtended);
|
||||
this.updateTimerProgress(timer, `saved ${this.currentBlockHeight} to database`);
|
||||
}
|
||||
|
||||
await blocksRepository.$saveBlockInDatabase(blockExtended);
|
||||
this.updateTimerProgress(timer, `saved ${this.currentBlockHeight} to database`);
|
||||
|
||||
if (!fastForwarded) {
|
||||
const lastestPriceId = await PricesRepository.$getLatestPriceId();
|
||||
this.updateTimerProgress(timer, `got latest price id ${this.currentBlockHeight}`);
|
||||
if (priceUpdater.historyInserted === true && lastestPriceId !== null) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Ancestor, CpfpInfo, CpfpSummary, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats } from '../mempool.interfaces';
|
||||
import { Ancestor, CpfpInfo, CpfpSummary, CpfpCluster, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats } from '../mempool.interfaces';
|
||||
import config from '../config';
|
||||
import { NodeSocket } from '../repositories/NodesSocketsRepository';
|
||||
import { isIP } from 'net';
|
||||
@@ -91,6 +91,14 @@ export class Common {
|
||||
if (replaced.size) {
|
||||
matches[tx.txid] = { replaced: Array.from(replaced), replacedBy: tx };
|
||||
}
|
||||
// remove this tx from the spendMap
|
||||
// prevents the same tx being replaced more than once
|
||||
for (const vin of tx.vin) {
|
||||
const key = `${vin.txid}:${vin.vout}`;
|
||||
if (spendMap.get(key)?.txid === tx.txid) {
|
||||
spendMap.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
@@ -101,7 +109,6 @@ export class Common {
|
||||
fee: tx.fee,
|
||||
vsize: tx.weight / 4,
|
||||
value: tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0),
|
||||
acc: tx.acceleration || undefined,
|
||||
rate: tx.effectiveFeePerVsize,
|
||||
};
|
||||
}
|
||||
@@ -367,40 +374,80 @@ export class Common {
|
||||
}
|
||||
|
||||
static calculateCpfp(height: number, transactions: TransactionExtended[]): CpfpSummary {
|
||||
const clusters: { root: string, height: number, txs: Ancestor[], effectiveFeePerVsize: number }[] = [];
|
||||
let cluster: TransactionExtended[] = [];
|
||||
let ancestors: { [txid: string]: boolean } = {};
|
||||
const clusters: CpfpCluster[] = []; // list of all cpfp clusters in this block
|
||||
const clusterMap: { [txid: string]: CpfpCluster } = {}; // map transactions to their cpfp cluster
|
||||
let clusterTxs: TransactionExtended[] = []; // working list of elements of the current cluster
|
||||
let ancestors: { [txid: string]: boolean } = {}; // working set of ancestors of the current cluster root
|
||||
const txMap = {};
|
||||
// initialize the txMap
|
||||
for (const tx of transactions) {
|
||||
txMap[tx.txid] = tx;
|
||||
}
|
||||
// reverse pass to identify CPFP clusters
|
||||
for (let i = transactions.length - 1; i >= 0; i--) {
|
||||
const tx = transactions[i];
|
||||
txMap[tx.txid] = tx;
|
||||
if (!ancestors[tx.txid]) {
|
||||
let totalFee = 0;
|
||||
let totalVSize = 0;
|
||||
cluster.forEach(tx => {
|
||||
clusterTxs.forEach(tx => {
|
||||
totalFee += tx?.fee || 0;
|
||||
totalVSize += (tx.weight / 4);
|
||||
});
|
||||
const effectiveFeePerVsize = totalFee / totalVSize;
|
||||
if (cluster.length > 1) {
|
||||
clusters.push({
|
||||
root: cluster[0].txid,
|
||||
let cluster: CpfpCluster;
|
||||
if (clusterTxs.length > 1) {
|
||||
cluster = {
|
||||
root: clusterTxs[0].txid,
|
||||
height,
|
||||
txs: cluster.map(tx => { return { txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }; }),
|
||||
txs: clusterTxs.map(tx => { return { txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }; }),
|
||||
effectiveFeePerVsize,
|
||||
});
|
||||
};
|
||||
clusters.push(cluster);
|
||||
}
|
||||
cluster.forEach(tx => {
|
||||
clusterTxs.forEach(tx => {
|
||||
txMap[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize;
|
||||
if (cluster) {
|
||||
clusterMap[tx.txid] = cluster;
|
||||
}
|
||||
});
|
||||
cluster = [];
|
||||
// reset working vars
|
||||
clusterTxs = [];
|
||||
ancestors = {};
|
||||
}
|
||||
cluster.push(tx);
|
||||
clusterTxs.push(tx);
|
||||
tx.vin.forEach(vin => {
|
||||
ancestors[vin.txid] = true;
|
||||
});
|
||||
}
|
||||
// forward pass to enforce ancestor rate caps
|
||||
for (const tx of transactions) {
|
||||
let minAncestorRate = tx.effectiveFeePerVsize;
|
||||
for (const vin of tx.vin) {
|
||||
if (txMap[vin.txid]?.effectiveFeePerVsize) {
|
||||
minAncestorRate = Math.min(minAncestorRate, txMap[vin.txid].effectiveFeePerVsize);
|
||||
}
|
||||
}
|
||||
// check rounded values to skip cases with almost identical fees
|
||||
const roundedMinAncestorRate = Math.ceil(minAncestorRate);
|
||||
const roundedEffectiveFeeRate = Math.floor(tx.effectiveFeePerVsize);
|
||||
if (roundedMinAncestorRate < roundedEffectiveFeeRate) {
|
||||
tx.effectiveFeePerVsize = minAncestorRate;
|
||||
if (!clusterMap[tx.txid]) {
|
||||
// add a single-tx cluster to record the dependent rate
|
||||
const cluster = {
|
||||
root: tx.txid,
|
||||
height,
|
||||
txs: [{ txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }],
|
||||
effectiveFeePerVsize: minAncestorRate,
|
||||
};
|
||||
clusterMap[tx.txid] = cluster;
|
||||
clusters.push(cluster);
|
||||
} else {
|
||||
// update the existing cluster with the dependent rate
|
||||
clusterMap[tx.txid].effectiveFeePerVsize = minAncestorRate;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
transactions,
|
||||
clusters,
|
||||
|
||||
@@ -534,9 +534,11 @@ class DatabaseMigration {
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 62 && isBitcoin === true) {
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD accelerated_txs JSON DEFAULT "[]"');
|
||||
await this.updateToSchemaVersion(61);
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD expected_fees BIGINT UNSIGNED DEFAULT NULL');
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD expected_weight BIGINT UNSIGNED DEFAULT NULL');
|
||||
await this.updateToSchemaVersion(62);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1055,7 +1057,7 @@ class DatabaseMigration {
|
||||
}
|
||||
|
||||
public async $blocksReindexingTruncate(): Promise<void> {
|
||||
logger.warn(`Truncating pools, blocks and hashrates for re-indexing (using '--reindex-blocks'). You can cancel this command within 5 seconds`);
|
||||
logger.warn(`Truncating pools, blocks, hashrates and difficulty_adjustments tables for re-indexing (using '--reindex-blocks'). You can cancel this command within 5 seconds`);
|
||||
await Common.sleep$(5000);
|
||||
|
||||
await this.$executeQuery(`TRUNCATE blocks`);
|
||||
|
||||
@@ -373,7 +373,7 @@ class NodesApi {
|
||||
|
||||
public async $searchNodeByPublicKeyOrAlias(search: string) {
|
||||
try {
|
||||
const publicKeySearch = search.replace('%', '') + '%';
|
||||
const publicKeySearch = search.replace(/[^a-zA-Z0-9]/g, '') + '%';
|
||||
const aliasSearch = search
|
||||
.replace(/[-_.]/g, ' ') // Replace all -_. characters with empty space. Eg: "ln.nicehash" becomes "ln nicehash".
|
||||
.replace(/[^a-zA-Z0-9 ]/g, '') // Remove all special characters and keep just A to Z, 0 to 9.
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Common, OnlineFeeStatsCalculator } from './common';
|
||||
import config from '../config';
|
||||
import { Worker } from 'worker_threads';
|
||||
import path from 'path';
|
||||
import mempool from './mempool';
|
||||
|
||||
class MempoolBlocks {
|
||||
private mempoolBlocks: MempoolBlockWithTransactions[] = [];
|
||||
@@ -170,7 +169,7 @@ class MempoolBlocks {
|
||||
for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) {
|
||||
let added: TransactionStripped[] = [];
|
||||
let removed: string[] = [];
|
||||
const changed: { txid: string, rate: number | undefined, acc: number | undefined }[] = [];
|
||||
const changed: { txid: string, rate: number | undefined }[] = [];
|
||||
if (mempoolBlocks[i] && !prevBlocks[i]) {
|
||||
added = mempoolBlocks[i].transactions;
|
||||
} else if (!mempoolBlocks[i] && prevBlocks[i]) {
|
||||
@@ -192,8 +191,8 @@ class MempoolBlocks {
|
||||
mempoolBlocks[i].transactions.forEach(tx => {
|
||||
if (!prevIds[tx.txid]) {
|
||||
added.push(tx);
|
||||
} else if (tx.rate !== prevIds[tx.txid].rate || tx.acc !== prevIds[tx.txid].acc) {
|
||||
changed.push({ txid: tx.txid, rate: tx.rate, acc: tx.acc });
|
||||
} else if (tx.rate !== prevIds[tx.txid].rate) {
|
||||
changed.push({ txid: tx.txid, rate: tx.rate });
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -206,17 +205,15 @@ class MempoolBlocks {
|
||||
return mempoolBlockDeltas;
|
||||
}
|
||||
|
||||
public async $makeBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, saveResults: boolean = false, useAccelerations: boolean = false): Promise<MempoolBlockWithTransactions[]> {
|
||||
public async $makeBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, 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, true);
|
||||
this.setUid(tx);
|
||||
}
|
||||
|
||||
const accelerations = useAccelerations ? mempool.getAccelerations() : {};
|
||||
|
||||
// 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();
|
||||
@@ -224,7 +221,7 @@ class MempoolBlocks {
|
||||
if (entry.uid != null) {
|
||||
strippedMempool.set(entry.uid, {
|
||||
uid: entry.uid,
|
||||
fee: entry.fee + (useAccelerations ? (accelerations[entry.txid] || 0) : 0),
|
||||
fee: entry.fee,
|
||||
weight: (entry.adjustedVsize * 4),
|
||||
sigops: entry.sigops,
|
||||
feePerVsize: entry.adjustedFeePerVsize || entry.feePerVsize,
|
||||
@@ -263,7 +260,7 @@ class MempoolBlocks {
|
||||
// clean up thread error listener
|
||||
this.txSelectionWorker?.removeListener('error', threadErrorListener);
|
||||
|
||||
const processed = this.processBlockTemplates(newMempool, blocks, rates, clusters, accelerations, saveResults);
|
||||
const processed = this.processBlockTemplates(newMempool, blocks, rates, clusters, saveResults);
|
||||
logger.debug(`makeBlockTemplates completed in ${(Date.now() - start)/1000} seconds`);
|
||||
return processed;
|
||||
} catch (e) {
|
||||
@@ -272,29 +269,25 @@ class MempoolBlocks {
|
||||
return this.mempoolBlocks;
|
||||
}
|
||||
|
||||
public async $updateBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, added: MempoolTransactionExtended[], removed: MempoolTransactionExtended[], accelerationDelta: string[] = [], saveResults: boolean = false, useAccelerations: boolean = false): Promise<void> {
|
||||
public async $updateBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, added: MempoolTransactionExtended[], removed: MempoolTransactionExtended[], saveResults: boolean = false): Promise<void> {
|
||||
if (!this.txSelectionWorker) {
|
||||
// need to reset the worker
|
||||
await this.$makeBlockTemplates(newMempool, saveResults, useAccelerations);
|
||||
await this.$makeBlockTemplates(newMempool, saveResults);
|
||||
return;
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
const accelerations = useAccelerations ? mempool.getAccelerations() : {};
|
||||
const addedAndChanged: MempoolTransactionExtended[] = useAccelerations ? accelerationDelta.map(txid => newMempool[txid]).filter(tx => tx != null).concat(added) : added;
|
||||
|
||||
for (const tx of addedAndChanged) {
|
||||
for (const tx of Object.values(added)) {
|
||||
this.setUid(tx);
|
||||
}
|
||||
const removedUids = removed.map(tx => this.getUid(tx)).filter(uid => uid != null) as number[];
|
||||
|
||||
// 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[] = addedAndChanged.filter(entry => entry.uid != null).map(entry => {
|
||||
const addedStripped: CompactThreadTransaction[] = added.filter(entry => entry.uid != null).map(entry => {
|
||||
return {
|
||||
uid: entry.uid || 0,
|
||||
fee: entry.fee + (useAccelerations ? (accelerations[entry.txid] || 0) : 0),
|
||||
fee: entry.fee,
|
||||
weight: (entry.adjustedVsize * 4),
|
||||
sigops: entry.sigops,
|
||||
feePerVsize: entry.adjustedFeePerVsize || entry.feePerVsize,
|
||||
@@ -321,14 +314,14 @@ class MempoolBlocks {
|
||||
// clean up thread error listener
|
||||
this.txSelectionWorker?.removeListener('error', threadErrorListener);
|
||||
|
||||
this.processBlockTemplates(newMempool, blocks, rates, clusters, accelerations, saveResults);
|
||||
this.processBlockTemplates(newMempool, blocks, rates, clusters, saveResults);
|
||||
logger.debug(`updateBlockTemplates completed in ${(Date.now() - start) / 1000} seconds`);
|
||||
} 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[] }, accelerations, saveResults): MempoolBlockWithTransactions[] {
|
||||
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];
|
||||
@@ -365,10 +358,11 @@ class MempoolBlocks {
|
||||
block: blockIndex,
|
||||
vsize: totalVsize + (mempoolTx.vsize / 2),
|
||||
};
|
||||
mempoolTx.ancestors = [];
|
||||
mempoolTx.descendants = [];
|
||||
mempoolTx.bestDescendant = null;
|
||||
mempoolTx.cpfpChecked = true;
|
||||
|
||||
mempoolTx.acceleration = accelerations[txid];
|
||||
|
||||
// online calculation of stack-of-blocks fee stats
|
||||
if (hasBlockStack && blockIndex === blocks.length - 1 && feeStatsCalculator) {
|
||||
feeStatsCalculator.processNext(mempoolTx);
|
||||
@@ -458,18 +452,12 @@ class MempoolBlocks {
|
||||
this.nextUid = 1;
|
||||
}
|
||||
|
||||
// use reset=true to overwrite existing uids held by tx objects (required after resetUids)
|
||||
private setUid(tx: MempoolTransactionExtended, reset = false): number {
|
||||
let uid = reset ? null : this.getUid(tx);
|
||||
if (uid == null) {
|
||||
uid = this.nextUid;
|
||||
this.nextUid++;
|
||||
this.uidMap.set(uid, tx.txid);
|
||||
tx.uid = uid;
|
||||
return uid;
|
||||
} else {
|
||||
return uid;
|
||||
}
|
||||
private setUid(tx: MempoolTransactionExtended): number {
|
||||
const uid = this.nextUid;
|
||||
this.nextUid++;
|
||||
this.uidMap.set(uid, tx.txid);
|
||||
tx.uid = uid;
|
||||
return uid;
|
||||
}
|
||||
|
||||
private getUid(tx: MempoolTransactionExtended): number | void {
|
||||
|
||||
@@ -9,7 +9,6 @@ import loadingIndicators from './loading-indicators';
|
||||
import bitcoinClient from './bitcoin/bitcoin-client';
|
||||
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
||||
import rbfCache from './rbf-cache';
|
||||
import accelerationApi, { Acceleration } from './services/acceleration';
|
||||
|
||||
class Mempool {
|
||||
private inSync: boolean = false;
|
||||
@@ -19,11 +18,9 @@ class Mempool {
|
||||
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
|
||||
maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 };
|
||||
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
|
||||
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void) | undefined;
|
||||
deletedTransactions: MempoolTransactionExtended[]) => void) | undefined;
|
||||
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
|
||||
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => Promise<void>) | undefined;
|
||||
|
||||
private accelerations: { [txId: string]: number } = {};
|
||||
deletedTransactions: MempoolTransactionExtended[]) => Promise<void>) | undefined;
|
||||
|
||||
private txPerSecondArray: number[] = [];
|
||||
private txPerSecond: number = 0;
|
||||
@@ -68,12 +65,12 @@ class Mempool {
|
||||
}
|
||||
|
||||
public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
|
||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void): void {
|
||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]) => void): void {
|
||||
this.mempoolChangedCallback = fn;
|
||||
}
|
||||
|
||||
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
|
||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => Promise<void>): void {
|
||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]) => Promise<void>): void {
|
||||
this.$asyncMempoolChangedCallback = fn;
|
||||
}
|
||||
|
||||
@@ -93,10 +90,10 @@ class Mempool {
|
||||
}
|
||||
}
|
||||
if (this.mempoolChangedCallback) {
|
||||
this.mempoolChangedCallback(this.mempoolCache, [], [], []);
|
||||
this.mempoolChangedCallback(this.mempoolCache, [], []);
|
||||
}
|
||||
if (this.$asyncMempoolChangedCallback) {
|
||||
await this.$asyncMempoolChangedCallback(this.mempoolCache, [], [], []);
|
||||
await this.$asyncMempoolChangedCallback(this.mempoolCache, [], []);
|
||||
}
|
||||
this.addToSpendMap(Object.values(this.mempoolCache));
|
||||
}
|
||||
@@ -189,6 +186,12 @@ class Mempool {
|
||||
loadingIndicators.setProgress('mempool', progress);
|
||||
loggerTimer = new Date().getTime() / 1000;
|
||||
}
|
||||
// Break and restart mempool loop if we spend too much time processing
|
||||
// new transactions that may lead to falling behind on block height
|
||||
if (this.inSync && (new Date().getTime()) - start > 10_000) {
|
||||
logger.debug('Breaking mempool loop because the 10s time limit exceeded.');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset esplora 404 counter and log a warning if needed
|
||||
@@ -232,11 +235,6 @@ class Mempool {
|
||||
}
|
||||
}
|
||||
|
||||
const accelerationDelta = await this.$updateAccelerations();
|
||||
if (accelerationDelta.length) {
|
||||
hasChange = true;
|
||||
}
|
||||
|
||||
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
|
||||
this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6);
|
||||
|
||||
@@ -249,11 +247,11 @@ class Mempool {
|
||||
this.mempoolCacheDelta = Math.abs(transactions.length - Object.keys(this.mempoolCache).length);
|
||||
|
||||
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
||||
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta);
|
||||
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
|
||||
}
|
||||
if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
||||
this.updateTimerProgress(timer, 'running async mempool callback');
|
||||
await this.$asyncMempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta);
|
||||
await this.$asyncMempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
|
||||
this.updateTimerProgress(timer, 'completed async mempool callback');
|
||||
}
|
||||
|
||||
@@ -264,48 +262,6 @@ class Mempool {
|
||||
this.clearTimer(timer);
|
||||
}
|
||||
|
||||
public getAccelerations(): { [txid: string]: number } {
|
||||
return this.accelerations;
|
||||
}
|
||||
|
||||
public async $updateAccelerations(): Promise<string[]> {
|
||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const newAccelerations = await accelerationApi.$fetchAccelerations();
|
||||
|
||||
const changed: string[] = [];
|
||||
|
||||
const newAccelerationMap: { [txid: string]: number } = {};
|
||||
for (const acceleration of newAccelerations) {
|
||||
newAccelerationMap[acceleration.txid] = acceleration.feeDelta;
|
||||
if (this.accelerations[acceleration.txid] == null) {
|
||||
// new acceleration
|
||||
changed.push(acceleration.txid);
|
||||
} else if (this.accelerations[acceleration.txid] !== acceleration.feeDelta) {
|
||||
// feeDelta changed
|
||||
changed.push(acceleration.txid);
|
||||
}
|
||||
}
|
||||
|
||||
for (const oldTxid of Object.keys(this.accelerations)) {
|
||||
if (!newAccelerationMap[oldTxid]) {
|
||||
// removed
|
||||
changed.push(oldTxid);
|
||||
}
|
||||
}
|
||||
|
||||
this.accelerations = newAccelerationMap;
|
||||
|
||||
return changed;
|
||||
} catch (e: any) {
|
||||
logger.debug(`Failed to update accelerations: ` + (e instanceof Error ? e.message : e));
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private startTimer() {
|
||||
const state: any = {
|
||||
start: Date.now(),
|
||||
|
||||
@@ -13,11 +13,15 @@ import BlocksAuditsRepository from '../../repositories/BlocksAuditsRepository';
|
||||
import PricesRepository from '../../repositories/PricesRepository';
|
||||
import { bitcoinCoreApi } from '../bitcoin/bitcoin-api-factory';
|
||||
import { IEsploraApi } from '../bitcoin/esplora-api.interface';
|
||||
import database from '../../database';
|
||||
|
||||
class Mining {
|
||||
private blocksPriceIndexingRunning = false;
|
||||
public lastHashrateIndexingDate: number | null = null;
|
||||
public lastWeeklyHashrateIndexingDate: number | null = null;
|
||||
|
||||
public reindexHashrateRequested = false;
|
||||
public reindexDifficultyAdjustmentRequested = false;
|
||||
|
||||
/**
|
||||
* Get historical blocks health
|
||||
@@ -289,6 +293,14 @@ class Mining {
|
||||
* Generate daily hashrate data
|
||||
*/
|
||||
public async $generateNetworkHashrateHistory(): Promise<void> {
|
||||
// If a re-index was requested, truncate first
|
||||
if (this.reindexHashrateRequested === true) {
|
||||
logger.notice(`hashrates will now be re-indexed`);
|
||||
await database.query(`TRUNCATE hashrates`);
|
||||
this.lastHashrateIndexingDate = 0;
|
||||
this.reindexHashrateRequested = false;
|
||||
}
|
||||
|
||||
// We only run this once a day around midnight
|
||||
const today = new Date().getUTCDate();
|
||||
if (today === this.lastHashrateIndexingDate) {
|
||||
@@ -394,6 +406,13 @@ class Mining {
|
||||
* Index difficulty adjustments
|
||||
*/
|
||||
public async $indexDifficultyAdjustments(): Promise<void> {
|
||||
// If a re-index was requested, truncate first
|
||||
if (this.reindexDifficultyAdjustmentRequested === true) {
|
||||
logger.notice(`difficulty_adjustments will now be re-indexed`);
|
||||
await database.query(`TRUNCATE difficulty_adjustments`);
|
||||
this.reindexDifficultyAdjustmentRequested = false;
|
||||
}
|
||||
|
||||
const indexedHeightsArray = await DifficultyAdjustmentsRepository.$getAdjustmentsHeights();
|
||||
const indexedHeights = {};
|
||||
for (const height of indexedHeightsArray) {
|
||||
@@ -472,11 +491,11 @@ class Mining {
|
||||
}
|
||||
this.blocksPriceIndexingRunning = true;
|
||||
|
||||
let totalInserted = 0;
|
||||
try {
|
||||
const prices: any[] = await PricesRepository.$getPricesTimesAndId();
|
||||
const blocksWithoutPrices: any[] = await BlocksRepository.$getBlocksWithoutPrice();
|
||||
|
||||
let totalInserted = 0;
|
||||
const blocksPrices: BlockPrice[] = [];
|
||||
|
||||
for (const block of blocksWithoutPrices) {
|
||||
@@ -521,7 +540,13 @@ class Mining {
|
||||
}
|
||||
} catch (e) {
|
||||
this.blocksPriceIndexingRunning = false;
|
||||
throw e;
|
||||
logger.err(`Cannot index block prices. ${e}`);
|
||||
}
|
||||
|
||||
if (totalInserted > 0) {
|
||||
logger.info(`Indexing blocks prices completed. Indexed ${totalInserted}`, logger.tags.mining);
|
||||
} else {
|
||||
logger.debug(`Indexing blocks prices completed. Indexed 0.`, logger.tags.mining);
|
||||
}
|
||||
|
||||
this.blocksPriceIndexingRunning = false;
|
||||
|
||||
@@ -4,6 +4,7 @@ import config from '../config';
|
||||
import PoolsRepository from '../repositories/PoolsRepository';
|
||||
import { PoolTag } from '../mempool.interfaces';
|
||||
import diskCache from './disk-cache';
|
||||
import mining from './mining/mining';
|
||||
|
||||
class PoolsParser {
|
||||
miningPools: any[] = [];
|
||||
@@ -73,14 +74,12 @@ class PoolsParser {
|
||||
if (JSON.stringify(pool.addresses) !== poolDB.addresses ||
|
||||
JSON.stringify(pool.regexes) !== poolDB.regexes) {
|
||||
// Pool addresses changed or coinbase tags changed
|
||||
logger.notice(`Updating addresses and/or coinbase tags for ${pool.name} mining pool. If 'AUTOMATIC_BLOCK_REINDEXING' is enabled, we will re-index its blocks and 'unknown' blocks`);
|
||||
logger.notice(`Updating addresses and/or coinbase tags for ${pool.name} mining pool.`);
|
||||
await PoolsRepository.$updateMiningPoolTags(poolDB.id, pool.addresses, pool.regexes);
|
||||
await this.$deleteBlocksForPool(poolDB);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Mining pools-v2.json import completed');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,7 +127,15 @@ class PoolsParser {
|
||||
LIMIT 1`,
|
||||
[pool.id]
|
||||
);
|
||||
const oldestBlockHeight = oldestPoolBlock.length ?? 0 > 0 ? oldestPoolBlock[0].height : 130635;
|
||||
|
||||
let firstKnownBlockPool = 130635; // https://mempool.space/block/0000000000000a067d94ff753eec72830f1205ad3a4c216a08a80c832e551a52
|
||||
if (config.MEMPOOL.NETWORK === 'testnet') {
|
||||
firstKnownBlockPool = 21106; // https://mempool.space/testnet/block/0000000070b701a5b6a1b965f6a38e0472e70b2bb31b973e4638dec400877581
|
||||
} else if (config.MEMPOOL.NETWORK === 'signet') {
|
||||
firstKnownBlockPool = 0;
|
||||
}
|
||||
|
||||
const oldestBlockHeight = oldestPoolBlock.length ?? 0 > 0 ? oldestPoolBlock[0].height : firstKnownBlockPool;
|
||||
const [unknownPool] = await DB.query(`SELECT id from pools where slug = "unknown"`);
|
||||
this.uniqueLog(logger.notice, `Deleting blocks with unknown mining pool from height ${oldestBlockHeight} for re-indexing`);
|
||||
await DB.query(`
|
||||
@@ -142,16 +149,31 @@ class PoolsParser {
|
||||
WHERE pool_id = ?`,
|
||||
[pool.id]
|
||||
);
|
||||
|
||||
// Re-index hashrates and difficulty adjustments later
|
||||
mining.reindexHashrateRequested = true;
|
||||
mining.reindexDifficultyAdjustmentRequested = true;
|
||||
}
|
||||
|
||||
private async $deleteUnknownBlocks(): Promise<void> {
|
||||
let firstKnownBlockPool = 130635; // https://mempool.space/block/0000000000000a067d94ff753eec72830f1205ad3a4c216a08a80c832e551a52
|
||||
if (config.MEMPOOL.NETWORK === 'testnet') {
|
||||
firstKnownBlockPool = 21106; // https://mempool.space/testnet/block/0000000070b701a5b6a1b965f6a38e0472e70b2bb31b973e4638dec400877581
|
||||
} else if (config.MEMPOOL.NETWORK === 'signet') {
|
||||
firstKnownBlockPool = 0;
|
||||
}
|
||||
|
||||
const [unknownPool] = await DB.query(`SELECT id from pools where slug = "unknown"`);
|
||||
this.uniqueLog(logger.notice, `Deleting blocks with unknown mining pool from height 130635 for re-indexing`);
|
||||
this.uniqueLog(logger.notice, `Deleting blocks with unknown mining pool from height ${firstKnownBlockPool} for re-indexing`);
|
||||
await DB.query(`
|
||||
DELETE FROM blocks
|
||||
WHERE pool_id = ? AND height >= 130635`,
|
||||
WHERE pool_id = ? AND height >= ${firstKnownBlockPool}`,
|
||||
[unknownPool[0].id]
|
||||
);
|
||||
|
||||
// Re-index hashrates and difficulty adjustments later
|
||||
mining.reindexHashrateRequested = true;
|
||||
mining.reindexDifficultyAdjustmentRequested = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { query } from '../../utils/axios-query';
|
||||
import config from '../../config';
|
||||
import { BlockExtended, PoolTag } from '../../mempool.interfaces';
|
||||
|
||||
export interface Acceleration {
|
||||
txid: string,
|
||||
feeDelta: number,
|
||||
}
|
||||
|
||||
class AccelerationApi {
|
||||
public async $fetchAccelerations(): Promise<Acceleration[]> {
|
||||
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
|
||||
const response = await query(`${config.MEMPOOL_SERVICES.API}/accelerations`);
|
||||
return (response as Acceleration[]) || [];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async $fetchPools(): Promise<PoolTag[]> {
|
||||
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
|
||||
const response = await query(`${config.MEMPOOL_SERVICES.API}/partners`);
|
||||
return (response as PoolTag[]) || [];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async $isAcceleratedBlock(block: BlockExtended): Promise<boolean> {
|
||||
const pools = await this.$fetchPools();
|
||||
if (block?.extras?.pool?.id == null) {
|
||||
return false;
|
||||
}
|
||||
return pools.reduce((match, tag) => match || tag.uniqueId === block.extras.pool.id, false);
|
||||
}
|
||||
}
|
||||
|
||||
export default new AccelerationApi();
|
||||
@@ -21,7 +21,6 @@ import Audit from './audit';
|
||||
import { deepClone } from '../utils/clone';
|
||||
import priceUpdater from '../tasks/price-updater';
|
||||
import { ApiPrice } from '../repositories/PricesRepository';
|
||||
import accelerationApi from './services/acceleration';
|
||||
|
||||
class WebsocketHandler {
|
||||
private wss: WebSocket.Server | undefined;
|
||||
@@ -145,7 +144,6 @@ class WebsocketHandler {
|
||||
response['txPosition'] = {
|
||||
txid: trackTxid,
|
||||
position: tx.position,
|
||||
accelerated: tx.acceleration || undefined,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
@@ -304,7 +302,7 @@ class WebsocketHandler {
|
||||
}
|
||||
|
||||
async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended },
|
||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]): Promise<void> {
|
||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]): Promise<void> {
|
||||
if (!this.wss) {
|
||||
throw new Error('WebSocket.Server is not set');
|
||||
}
|
||||
@@ -312,7 +310,7 @@ class WebsocketHandler {
|
||||
this.printLogs();
|
||||
|
||||
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
|
||||
await mempoolBlocks.$updateBlockTemplates(newMempool, newTransactions, deletedTransactions, accelerationDelta, true, config.MEMPOOL_SERVICES.ACCELERATIONS);
|
||||
await mempoolBlocks.$updateBlockTemplates(newMempool, newTransactions, deletedTransactions, true);
|
||||
} else {
|
||||
mempoolBlocks.updateMempoolBlocks(newMempool, true);
|
||||
}
|
||||
@@ -546,27 +544,22 @@ class WebsocketHandler {
|
||||
if (config.MEMPOOL.AUDIT) {
|
||||
let projectedBlocks;
|
||||
let auditMempool = _memPool;
|
||||
const isAccelerated = config.MEMPOOL_SERVICES.ACCELERATIONS && await accelerationApi.$isAcceleratedBlock(block);
|
||||
// template calculation functions have mempool side effects, so calculate audits using
|
||||
// a cloned copy of the mempool if we're running a different algorithm for mempool updates
|
||||
const separateAudit = config.MEMPOOL.ADVANCED_GBT_AUDIT !== config.MEMPOOL.ADVANCED_GBT_MEMPOOL;
|
||||
if (separateAudit) {
|
||||
auditMempool = deepClone(_memPool);
|
||||
if (config.MEMPOOL.ADVANCED_GBT_AUDIT) {
|
||||
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false, isAccelerated);
|
||||
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false);
|
||||
} else {
|
||||
projectedBlocks = mempoolBlocks.updateMempoolBlocks(auditMempool, false);
|
||||
}
|
||||
} else {
|
||||
if ((config.MEMPOOL_SERVICES.ACCELERATIONS && !isAccelerated)) {
|
||||
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false, isAccelerated);
|
||||
} else {
|
||||
projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
|
||||
}
|
||||
projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
|
||||
}
|
||||
|
||||
if (Common.indexingEnabled() && memPool.isInSync()) {
|
||||
const { censored, added, fresh, sigop, accelerated, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
|
||||
const { censored, added, fresh, sigop, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
|
||||
const matchRate = Math.round(score * 100 * 100) / 100;
|
||||
|
||||
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions.map((tx) => {
|
||||
@@ -578,11 +571,18 @@ class WebsocketHandler {
|
||||
};
|
||||
}) : [];
|
||||
|
||||
let totalFees = 0;
|
||||
let totalWeight = 0;
|
||||
for (const tx of stripped) {
|
||||
totalFees += tx.fee;
|
||||
totalWeight += (tx.vsize * 4);
|
||||
}
|
||||
|
||||
BlocksSummariesRepository.$saveTemplate({
|
||||
height: block.height,
|
||||
template: {
|
||||
id: block.id,
|
||||
transactions: stripped
|
||||
transactions: stripped,
|
||||
}
|
||||
});
|
||||
|
||||
@@ -594,12 +594,15 @@ class WebsocketHandler {
|
||||
missingTxs: censored,
|
||||
freshTxs: fresh,
|
||||
sigopTxs: sigop,
|
||||
acceleratedTxs: accelerated,
|
||||
matchRate: matchRate,
|
||||
expectedFees: totalFees,
|
||||
expectedWeight: totalWeight,
|
||||
});
|
||||
|
||||
if (block.extras) {
|
||||
block.extras.matchRate = matchRate;
|
||||
block.extras.expectedFees = totalFees;
|
||||
block.extras.expectedWeight = totalWeight;
|
||||
block.extras.similarity = similarity;
|
||||
}
|
||||
}
|
||||
@@ -621,7 +624,7 @@ class WebsocketHandler {
|
||||
}
|
||||
|
||||
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
|
||||
await mempoolBlocks.$makeBlockTemplates(_memPool, true, config.MEMPOOL_SERVICES.ACCELERATIONS);
|
||||
await mempoolBlocks.$makeBlockTemplates(_memPool, true);
|
||||
} else {
|
||||
mempoolBlocks.updateMempoolBlocks(_memPool, true);
|
||||
}
|
||||
|
||||
@@ -129,10 +129,6 @@ interface IConfig {
|
||||
GEOLITE2_ASN: string;
|
||||
GEOIP2_ISP: string;
|
||||
},
|
||||
MEMPOOL_SERVICES: {
|
||||
API: string;
|
||||
ACCELERATIONS: boolean;
|
||||
},
|
||||
}
|
||||
|
||||
const defaults: IConfig = {
|
||||
@@ -262,10 +258,6 @@ const defaults: IConfig = {
|
||||
'GEOLITE2_ASN': '/usr/local/share/GeoIP/GeoLite2-ASN.mmdb',
|
||||
'GEOIP2_ISP': '/usr/local/share/GeoIP/GeoIP2-ISP.mmdb'
|
||||
},
|
||||
'MEMPOOL_SERVICES': {
|
||||
'API': '',
|
||||
'ACCELERATIONS': false,
|
||||
}
|
||||
};
|
||||
|
||||
class Config implements IConfig {
|
||||
@@ -285,7 +277,6 @@ class Config implements IConfig {
|
||||
PRICE_DATA_SERVER: IConfig['PRICE_DATA_SERVER'];
|
||||
EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER'];
|
||||
MAXMIND: IConfig['MAXMIND'];
|
||||
MEMPOOL_SERVICES: IConfig['MEMPOOL_SERVICES'];
|
||||
|
||||
constructor() {
|
||||
const configs = this.merge(configFromFile, defaults);
|
||||
@@ -305,7 +296,6 @@ class Config implements IConfig {
|
||||
this.PRICE_DATA_SERVER = configs.PRICE_DATA_SERVER;
|
||||
this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER;
|
||||
this.MAXMIND = configs.MAXMIND;
|
||||
this.MEMPOOL_SERVICES = configs.MEMPOOL_SERVICES;
|
||||
}
|
||||
|
||||
merge = (...objects: object[]): IConfig => {
|
||||
|
||||
@@ -134,6 +134,7 @@ class Indexer {
|
||||
await mining.$generatePoolHashrateHistory();
|
||||
await blocks.$generateBlocksSummariesDatabase();
|
||||
await blocks.$generateCPFPDatabase();
|
||||
await blocks.$generateAuditStats();
|
||||
} catch (e) {
|
||||
this.indexerRunning = false;
|
||||
logger.err(`Indexer failed, trying again in 10 seconds. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
|
||||
@@ -34,13 +34,16 @@ export interface BlockAudit {
|
||||
freshTxs: string[],
|
||||
sigopTxs: string[],
|
||||
addedTxs: string[],
|
||||
acceleratedTxs: string[],
|
||||
matchRate: number,
|
||||
expectedFees?: number,
|
||||
expectedWeight?: number,
|
||||
}
|
||||
|
||||
export interface AuditScore {
|
||||
hash: string,
|
||||
matchRate?: number,
|
||||
expectedFees?: number
|
||||
expectedWeight?: number
|
||||
}
|
||||
|
||||
export interface MempoolBlock {
|
||||
@@ -86,7 +89,6 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
|
||||
block: number,
|
||||
vsize: number,
|
||||
};
|
||||
acceleration?: number;
|
||||
uid?: number;
|
||||
}
|
||||
|
||||
@@ -175,7 +177,6 @@ export interface TransactionStripped {
|
||||
fee: number;
|
||||
vsize: number;
|
||||
value: number;
|
||||
acc?: number;
|
||||
rate?: number; // effective fee rate
|
||||
}
|
||||
|
||||
@@ -185,6 +186,8 @@ export interface BlockExtension {
|
||||
feeRange: number[]; // fee rate percentiles
|
||||
reward: number;
|
||||
matchRate: number | null;
|
||||
expectedFees: number | null;
|
||||
expectedWeight: number | null;
|
||||
similarity?: number;
|
||||
pool: {
|
||||
id: number; // Note - This is the `unique_id`, not to mix with the auto increment `id`
|
||||
@@ -256,9 +259,16 @@ export interface WorkingEffectiveFeeStats extends EffectiveFeeStats {
|
||||
maxFee: number;
|
||||
}
|
||||
|
||||
export interface CpfpCluster {
|
||||
root: string,
|
||||
height: number,
|
||||
txs: Ancestor[],
|
||||
effectiveFeePerVsize: number,
|
||||
}
|
||||
|
||||
export interface CpfpSummary {
|
||||
transactions: TransactionExtended[];
|
||||
clusters: { root: string, height: number, txs: Ancestor[], effectiveFeePerVsize: number }[];
|
||||
clusters: CpfpCluster[];
|
||||
}
|
||||
|
||||
export interface Statistic {
|
||||
|
||||
@@ -6,9 +6,9 @@ import { BlockAudit, AuditScore } from '../mempool.interfaces';
|
||||
class BlocksAuditRepositories {
|
||||
public async $saveAudit(audit: BlockAudit): Promise<void> {
|
||||
try {
|
||||
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, fresh_txs, sigop_txs, accelerated_txs, match_rate)
|
||||
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
|
||||
JSON.stringify(audit.addedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.acceleratedTxs), audit.matchRate]);
|
||||
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, fresh_txs, sigop_txs, match_rate, expected_fees, expected_weight)
|
||||
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
|
||||
JSON.stringify(audit.addedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), audit.matchRate, audit.expectedFees, audit.expectedWeight]);
|
||||
} catch (e: any) {
|
||||
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
|
||||
logger.debug(`Cannot save block audit for block ${audit.hash} because it has already been indexed, ignoring`);
|
||||
@@ -18,6 +18,19 @@ class BlocksAuditRepositories {
|
||||
}
|
||||
}
|
||||
|
||||
public async $setSummary(hash: string, expectedFees: number, expectedWeight: number) {
|
||||
try {
|
||||
await DB.query(`
|
||||
UPDATE blocks_audits SET
|
||||
expected_fees = ?,
|
||||
expected_weight = ?
|
||||
WHERE hash = ?
|
||||
`, [expectedFees, expectedWeight, hash]);
|
||||
} catch (e: any) {
|
||||
logger.err(`Cannot update block audit in db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
public async $getBlocksHealthHistory(div: number, interval: string | null): Promise<any> {
|
||||
try {
|
||||
let query = `SELECT UNIX_TIMESTAMP(time) as time, height, match_rate FROM blocks_audits`;
|
||||
@@ -51,7 +64,15 @@ class BlocksAuditRepositories {
|
||||
const [rows]: any[] = await DB.query(
|
||||
`SELECT blocks.height, blocks.hash as id, UNIX_TIMESTAMP(blocks.blockTimestamp) as timestamp, blocks.size,
|
||||
blocks.weight, blocks.tx_count,
|
||||
transactions, template, missing_txs as missingTxs, added_txs as addedTxs, fresh_txs as freshTxs, sigop_txs as sigopTxs, accelerated_txs as acceleratedTxs, match_rate as matchRate
|
||||
transactions,
|
||||
template,
|
||||
missing_txs as missingTxs,
|
||||
added_txs as addedTxs,
|
||||
fresh_txs as freshTxs,
|
||||
sigop_txs as sigopTxs,
|
||||
match_rate as matchRate,
|
||||
expected_fees as expectedFees,
|
||||
expected_weight as expectedWeight
|
||||
FROM blocks_audits
|
||||
JOIN blocks ON blocks.hash = blocks_audits.hash
|
||||
JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash
|
||||
@@ -64,7 +85,6 @@ class BlocksAuditRepositories {
|
||||
rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
|
||||
rows[0].freshTxs = JSON.parse(rows[0].freshTxs);
|
||||
rows[0].sigopTxs = JSON.parse(rows[0].sigopTxs);
|
||||
rows[0].acceleratedTxs = JSON.parse(rows[0].acceleratedTxs);
|
||||
rows[0].transactions = JSON.parse(rows[0].transactions);
|
||||
rows[0].template = JSON.parse(rows[0].template);
|
||||
|
||||
@@ -82,7 +102,7 @@ class BlocksAuditRepositories {
|
||||
public async $getBlockAuditScore(hash: string): Promise<AuditScore> {
|
||||
try {
|
||||
const [rows]: any[] = await DB.query(
|
||||
`SELECT hash, match_rate as matchRate
|
||||
`SELECT hash, match_rate as matchRate, expected_fees as expectedFees, expected_weight as expectedWeight
|
||||
FROM blocks_audits
|
||||
WHERE blocks_audits.hash = "${hash}"
|
||||
`);
|
||||
@@ -96,7 +116,7 @@ class BlocksAuditRepositories {
|
||||
public async $getBlockAuditScores(maxHeight: number, minHeight: number): Promise<AuditScore[]> {
|
||||
try {
|
||||
const [rows]: any[] = await DB.query(
|
||||
`SELECT hash, match_rate as matchRate
|
||||
`SELECT hash, match_rate as matchRate, expected_fees as expectedFees, expected_weight as expectedWeight
|
||||
FROM blocks_audits
|
||||
WHERE blocks_audits.height BETWEEN ? AND ?
|
||||
`, [minHeight, maxHeight]);
|
||||
@@ -106,6 +126,32 @@ class BlocksAuditRepositories {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public async $getBlocksWithoutSummaries(): Promise<string[]> {
|
||||
try {
|
||||
const [fromRows]: any[] = await DB.query(`
|
||||
SELECT height
|
||||
FROM blocks_audits
|
||||
WHERE expected_fees IS NULL
|
||||
ORDER BY height DESC
|
||||
LIMIT 1
|
||||
`);
|
||||
if (!fromRows?.length) {
|
||||
return [];
|
||||
}
|
||||
const fromHeight = fromRows[0].height;
|
||||
const [idRows]: any[] = await DB.query(`
|
||||
SELECT hash
|
||||
FROM blocks_audits
|
||||
WHERE height <= ?
|
||||
ORDER BY height DESC
|
||||
`, [fromHeight]);
|
||||
return idRows.map(row => row.hash);
|
||||
} catch (e: any) {
|
||||
logger.err(`Cannot fetch block audit from db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new BlocksAuditRepositories();
|
||||
|
||||
@@ -577,19 +577,6 @@ class BlocksRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return blocks height
|
||||
*/
|
||||
public async $getBlocksHeightsAndTimestamp(): Promise<object[]> {
|
||||
try {
|
||||
const [rows]: any[] = await DB.query(`SELECT height, blockTimestamp as timestamp FROM blocks`);
|
||||
return rows;
|
||||
} catch (e) {
|
||||
logger.err('Cannot get blocks height and timestamp from the db. Reason: ' + (e instanceof Error ? e.message : e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get general block stats
|
||||
*/
|
||||
@@ -877,7 +864,7 @@ class BlocksRepository {
|
||||
/**
|
||||
* Get all blocks which have not be linked to a price yet
|
||||
*/
|
||||
public async $getBlocksWithoutPrice(): Promise<object[]> {
|
||||
public async $getBlocksWithoutPrice(): Promise<object[]> {
|
||||
try {
|
||||
const [rows]: any[] = await DB.query(`
|
||||
SELECT UNIX_TIMESTAMP(blocks.blockTimestamp) as timestamp, blocks.height
|
||||
@@ -889,7 +876,7 @@ class BlocksRepository {
|
||||
return rows;
|
||||
} catch (e) {
|
||||
logger.err('Cannot get blocks height and timestamp from the db. Reason: ' + (e instanceof Error ? e.message : e));
|
||||
throw e;
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -909,7 +896,6 @@ class BlocksRepository {
|
||||
logger.debug(`Cannot save blocks prices for blocks [${blockPrices[0].height} to ${blockPrices[blockPrices.length - 1].height}] because it has already been indexed, ignoring`);
|
||||
} else {
|
||||
logger.err(`Cannot save blocks prices for blocks [${blockPrices[0].height} to ${blockPrices[blockPrices.length - 1].height}] into db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -928,7 +914,7 @@ class BlocksRepository {
|
||||
return blocks;
|
||||
} catch (e) {
|
||||
logger.err(`Cannot get blocks with missing coinstatsindex. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
throw e;
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1032,10 +1018,14 @@ class BlocksRepository {
|
||||
|
||||
// Match rate is not part of the blocks table, but it is part of APIs so we must include it
|
||||
extras.matchRate = null;
|
||||
extras.expectedFees = null;
|
||||
extras.expectedWeight = null;
|
||||
if (config.MEMPOOL.AUDIT) {
|
||||
const auditScore = await BlocksAuditsRepository.$getBlockAuditScore(dbBlk.id);
|
||||
if (auditScore != null) {
|
||||
extras.matchRate = auditScore.matchRate;
|
||||
extras.expectedFees = auditScore.expectedFees;
|
||||
extras.expectedWeight = auditScore.expectedWeight;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,21 @@ class BlocksSummariesRepository {
|
||||
}
|
||||
}
|
||||
|
||||
public async $getTemplate(id: string): Promise<BlockSummary | undefined> {
|
||||
try {
|
||||
const [templates]: any[] = await DB.query(`SELECT * from blocks_templates WHERE id = ?`, [id]);
|
||||
if (templates.length > 0) {
|
||||
return {
|
||||
id: templates[0].id,
|
||||
transactions: JSON.parse(templates[0].template),
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
logger.err(`Cannot get block template for block id ${id}. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public async $getIndexedSummariesId(): Promise<string[]> {
|
||||
try {
|
||||
const [rows]: any[] = await DB.query(`SELECT id from blocks_summaries`);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import cluster, { Cluster } from 'cluster';
|
||||
import { RowDataPacket } from 'mysql2';
|
||||
import DB from '../database';
|
||||
import logger from '../logger';
|
||||
import { Ancestor } from '../mempool.interfaces';
|
||||
import { Ancestor, CpfpCluster } from '../mempool.interfaces';
|
||||
import transactionRepository from '../repositories/TransactionRepository';
|
||||
|
||||
class CpfpRepository {
|
||||
@@ -12,7 +11,7 @@ class CpfpRepository {
|
||||
}
|
||||
// skip clusters of transactions with the same fees
|
||||
const roundedEffectiveFee = Math.round(effectiveFeePerVsize * 100) / 100;
|
||||
const equalFee = txs.reduce((acc, tx) => {
|
||||
const equalFee = txs.length > 1 && txs.reduce((acc, tx) => {
|
||||
return (acc && Math.round(((tx.fee || 0) / (tx.weight / 4)) * 100) / 100 === roundedEffectiveFee);
|
||||
}, true);
|
||||
if (equalFee) {
|
||||
@@ -54,9 +53,9 @@ class CpfpRepository {
|
||||
const txs: any[] = [];
|
||||
|
||||
for (const cluster of clusters) {
|
||||
if (cluster.txs?.length > 1) {
|
||||
if (cluster.txs?.length) {
|
||||
const roundedEffectiveFee = Math.round(cluster.effectiveFeePerVsize * 100) / 100;
|
||||
const equalFee = cluster.txs.reduce((acc, tx) => {
|
||||
const equalFee = cluster.txs.length > 1 && cluster.txs.reduce((acc, tx) => {
|
||||
return (acc && Math.round(((tx.fee || 0) / (tx.weight / 4)) * 100) / 100 === roundedEffectiveFee);
|
||||
}, true);
|
||||
if (!equalFee) {
|
||||
@@ -111,7 +110,7 @@ class CpfpRepository {
|
||||
}
|
||||
}
|
||||
|
||||
public async $getCluster(clusterRoot: string): Promise<Cluster | void> {
|
||||
public async $getCluster(clusterRoot: string): Promise<CpfpCluster | void> {
|
||||
const [clusterRows]: any = await DB.query(
|
||||
`
|
||||
SELECT *
|
||||
@@ -121,6 +120,7 @@ class CpfpRepository {
|
||||
[clusterRoot]
|
||||
);
|
||||
const cluster = clusterRows[0];
|
||||
cluster.effectiveFeePerVsize = cluster.fee_rate;
|
||||
if (cluster?.txs) {
|
||||
cluster.txs = this.unpack(cluster.txs);
|
||||
return cluster;
|
||||
|
||||
@@ -105,6 +105,7 @@ class TransactionRepository {
|
||||
return {
|
||||
descendants,
|
||||
ancestors,
|
||||
effectiveFeePerVsize: cluster.effectiveFeePerVsize,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ class PoolsUpdater {
|
||||
poolsParser.setMiningPools(poolsJson);
|
||||
|
||||
if (config.DATABASE.ENABLED === false) { // Don't run db operations
|
||||
logger.info('Mining pools-v2.json import completed (no database)');
|
||||
logger.info(`Mining pools-v2.json (${githubSha}) import completed (no database)`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ class PoolsUpdater {
|
||||
logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, logger.tags.mining);
|
||||
await DB.query('ROLLBACK;');
|
||||
}
|
||||
logger.info('PoolsUpdater completed');
|
||||
logger.info(`Mining pools-v2.json (${githubSha}) import completed`);
|
||||
|
||||
} catch (e) {
|
||||
this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week
|
||||
|
||||
3
contributors/joostjager.txt
Normal file
3
contributors/joostjager.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of January 25, 2022.
|
||||
|
||||
Signed: joostjager
|
||||
@@ -124,9 +124,5 @@
|
||||
"GEOLITE2_CITY": "__MAXMIND_GEOLITE2_CITY__",
|
||||
"GEOLITE2_ASN": "__MAXMIND_GEOLITE2_ASN__",
|
||||
"GEOIP2_ISP": "__MAXMIND_GEOIP2_ISP__"
|
||||
},
|
||||
"MEMPOOL_SERVICES": {
|
||||
"API": "__MEMPOOL_SERVICES_API__",
|
||||
"ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__
|
||||
}
|
||||
}
|
||||
@@ -126,10 +126,6 @@ __MAXMIND_GEOLITE2_CITY__=${MAXMIND_GEOLITE2_CITY:="/backend/GeoIP/GeoLite2-City
|
||||
__MAXMIND_GEOLITE2_ASN__=${MAXMIND_GEOLITE2_ASN:="/backend/GeoIP/GeoLite2-ASN.mmdb"}
|
||||
__MAXMIND_GEOIP2_ISP__=${MAXMIND_GEOIP2_ISP:=""}
|
||||
|
||||
# MEMPOOL_SERVICES
|
||||
__MEMPOOL_SERVICES_API__==${MEMPOOL_SERVICES_API:=""}
|
||||
__MEMPOOL_SERVICES_ACCELERATIONS__==${MEMPOOL_SERVICES_ACCELERATIONS:=false}
|
||||
|
||||
|
||||
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
|
||||
|
||||
@@ -247,9 +243,5 @@ sed -i "s!__MAXMIND_GEOLITE2_CITY__!${__MAXMIND_GEOLITE2_CITY__}!g" mempool-conf
|
||||
sed -i "s!__MAXMIND_GEOLITE2_ASN__!${__MAXMIND_GEOLITE2_ASN__}!g" mempool-config.json
|
||||
sed -i "s!__MAXMIND_GEOIP2_ISP__!${__MAXMIND_GEOIP2_ISP__}!g" mempool-config.json
|
||||
|
||||
# MEMPOOL_SERVICES
|
||||
sed -i "s!__MEMPOOL_SERVICES_API__!${__MEMPOOL_SERVICES_API__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS__}!g" mempool-config.json
|
||||
|
||||
|
||||
node /backend/package/index.js
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
describe('Liquid', () => {
|
||||
describe.skip('Liquid', () => {
|
||||
const baseModule = Cypress.env('BASE_MODULE');
|
||||
const basePath = '';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
describe('Liquid Testnet', () => {
|
||||
describe.skip('Liquid Testnet', () => {
|
||||
const baseModule = Cypress.env('BASE_MODULE');
|
||||
const basePath = '/testnet';
|
||||
|
||||
|
||||
@@ -537,7 +537,7 @@ describe('Mainnet', () => {
|
||||
cy.get('.container-xl > :nth-child(3)').invoke('css', 'width').should('equal', alertWidth);
|
||||
});
|
||||
|
||||
cy.get('.btn-danger').then(getRectangle).then((rectA) => {
|
||||
cy.get('.btn-warning').then(getRectangle).then((rectA) => {
|
||||
cy.get('.alert').then(getRectangle).then((rectB) => {
|
||||
expect(areOverlapping(rectA, rectB), 'Confirmations box and RBF alert are overlapping').to.be.false;
|
||||
});
|
||||
@@ -582,7 +582,7 @@ describe('Mainnet', () => {
|
||||
cy.get(alertLocator).invoke('css', 'width').should('equal', firstWidth);
|
||||
});
|
||||
|
||||
cy.get('.btn-danger').then(getRectangle).then((rectA) => {
|
||||
cy.get('.btn-warning').then(getRectangle).then((rectA) => {
|
||||
cy.get('.alert').then(getRectangle).then((rectB) => {
|
||||
expect(areOverlapping(rectA, rectB), 'Confirmations box and RBF alert are overlapping').to.be.false;
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"start:local-staging": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-staging",
|
||||
"start:mixed": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c mixed",
|
||||
"build": "npm run generate-config && npm run ng -- build --configuration production --localize && npm run sync-assets && npm run build-mempool.js",
|
||||
"sync-assets": "rsync -av ./src/resources ./dist/mempool/browser && node sync-assets.js 'dist/mempool/browser/resources/'",
|
||||
"sync-assets": "rsync -av ./src/resources ./dist/mempool/browser && node sync-assets.js 'dist/mempool/browser/resources'",
|
||||
"sync-assets-dev": "node sync-assets.js 'src/resources/'",
|
||||
"generate-config": "node generate-config.js",
|
||||
"build-mempool.js": "npm run build-mempool-js && npm run build-mempool-liquid-js && npm run build-mempool-bisq-js",
|
||||
|
||||
@@ -4,8 +4,7 @@ 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 { ClockComponent } from './components/clock/clock.component';
|
||||
import { AddressComponent } from './components/address/address.component';
|
||||
import { MasterPageComponent } from './components/master-page/master-page.component';
|
||||
import { AboutComponent } from './components/about/about.component';
|
||||
@@ -358,12 +357,16 @@ let routes: Routes = [
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'clock-mined',
|
||||
component: ClockMinedComponent,
|
||||
path: 'clock',
|
||||
redirectTo: 'clock/mempool/0'
|
||||
},
|
||||
{
|
||||
path: 'clock-mempool',
|
||||
component: ClockMempoolComponent,
|
||||
path: 'clock/:mode',
|
||||
redirectTo: 'clock/:mode/0'
|
||||
},
|
||||
{
|
||||
path: 'clock/:mode/:index',
|
||||
component: ClockComponent,
|
||||
},
|
||||
{
|
||||
path: 'status',
|
||||
|
||||
@@ -173,6 +173,21 @@
|
||||
</svg>
|
||||
<span>Exodus</span>
|
||||
</a>
|
||||
<a href="https://www.luminex.io" target="_blank" title="Luminex">
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="66.95" height="80" viewBox="0 0 300.43 385" style="padding-top: 10px;">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #f2ea25;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="cls-1" d="m309.02,90.04c0,49.65-38.73,90.04-95.34,90.04s-95.34-40.39-95.34-90.04S153.77,0,213.69,0c56.28,0,95.34,40.39,95.34,90.04Zm-63.56,0c0-20.52-14.23-37.07-31.78-37.07s-31.78,16.55-31.78,37.07,14.23,37.07,31.78,37.07,31.78-16.55,31.78-37.07Z"/>
|
||||
<path class="cls-1" d="m311.87,372.67h-66.34l-31.84-47.76-31.84,47.76h-66.34l58.38-90.22-53.07-79.61h66.34l26.54,42.46,26.53-42.46h66.34l-53.07,79.61,58.38,90.22Z"/>
|
||||
<rect class="cls-1" width="60.69" height="372.67"/>
|
||||
</svg>
|
||||
<span>Luminex</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -205,7 +220,7 @@
|
||||
<img class="image" src="/resources/profile/mynodebtc.png" />
|
||||
<span>myNode</span>
|
||||
</a>
|
||||
<a href="https://github.com/RoninDojo/RoninDojo" target="_blank" title="RoninDojo">
|
||||
<a href="https://code.samourai.io/ronindojo/RoninDojo" target="_blank" title="RoninDojo">
|
||||
<img class="image" src="/resources/profile/ronindojo.png" />
|
||||
<span>RoninDojo</span>
|
||||
</a>
|
||||
|
||||
@@ -133,7 +133,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
}
|
||||
}
|
||||
|
||||
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: number | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
|
||||
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
|
||||
if (this.scene) {
|
||||
this.scene.update(add, remove, change, direction, resetLayout);
|
||||
this.start();
|
||||
|
||||
@@ -156,7 +156,7 @@ export default class BlockScene {
|
||||
this.updateAll(startTime, 200, direction);
|
||||
}
|
||||
|
||||
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: number | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
|
||||
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
|
||||
const startTime = performance.now();
|
||||
const removed = this.removeBatch(remove, startTime, direction);
|
||||
|
||||
@@ -181,7 +181,6 @@ export default class BlockScene {
|
||||
// update effective rates
|
||||
change.forEach(tx => {
|
||||
if (this.txs[tx.txid]) {
|
||||
this.txs[tx.txid].acc = tx.acc;
|
||||
this.txs[tx.txid].feerate = tx.rate || (this.txs[tx.txid].fee / this.txs[tx.txid].vsize);
|
||||
this.txs[tx.txid].rate = tx.rate;
|
||||
this.txs[tx.txid].dirty = true;
|
||||
|
||||
@@ -16,7 +16,6 @@ const auditColors = {
|
||||
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
|
||||
added: hexToColor('0099ff'),
|
||||
selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
|
||||
accelerated: hexToColor('8F5FF6'),
|
||||
};
|
||||
|
||||
// convert from this class's update format to TxSprite's update format
|
||||
@@ -37,9 +36,8 @@ export default class TxView implements TransactionStripped {
|
||||
vsize: number;
|
||||
value: number;
|
||||
feerate: number;
|
||||
acc?: number;
|
||||
rate?: number;
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected' | 'accelerated';
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected';
|
||||
context?: 'projected' | 'actual';
|
||||
scene?: BlockScene;
|
||||
|
||||
@@ -62,7 +60,6 @@ export default class TxView implements TransactionStripped {
|
||||
this.vsize = tx.vsize;
|
||||
this.value = tx.value;
|
||||
this.feerate = tx.rate || (tx.fee / tx.vsize); // sort by effective fee rate where available
|
||||
this.acc = tx.acc;
|
||||
this.rate = tx.rate;
|
||||
this.status = tx.status;
|
||||
this.initialised = false;
|
||||
@@ -167,11 +164,6 @@ export default class TxView implements TransactionStripped {
|
||||
const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1];
|
||||
// Normal mode
|
||||
if (!this.scene?.highlightingEnabled) {
|
||||
if (this.acc) {
|
||||
return auditColors.accelerated;
|
||||
} else {
|
||||
return feeLevelColor;
|
||||
}
|
||||
return feeLevelColor;
|
||||
}
|
||||
// Block audit
|
||||
@@ -187,8 +179,6 @@ export default class TxView implements TransactionStripped {
|
||||
return auditColors.added;
|
||||
case 'selected':
|
||||
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
|
||||
case 'accelerated':
|
||||
return auditColors.accelerated;
|
||||
case 'found':
|
||||
if (this.context === 'projected') {
|
||||
return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];
|
||||
@@ -196,11 +186,7 @@ export default class TxView implements TransactionStripped {
|
||||
return feeLevelColor;
|
||||
}
|
||||
default:
|
||||
if (this.acc) {
|
||||
return auditColors.accelerated;
|
||||
} else {
|
||||
return feeLevelColor;
|
||||
}
|
||||
return feeLevelColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,8 +29,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="effectiveRate && effectiveRate !== feeRate">
|
||||
<td *ngIf="!this.acceleration" class="td-width" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
|
||||
<td *ngIf="this.acceleration" class="td-width" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Accelerated fee rate</td>
|
||||
<td class="td-width" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
|
||||
<td>
|
||||
{{ effectiveRate | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
||||
</td>
|
||||
@@ -49,7 +48,6 @@
|
||||
<td *ngSwitchCase="'fresh'"><span class="badge badge-warning" i18n="transaction.audit.recently-broadcasted">Recently broadcasted</span></td>
|
||||
<td *ngSwitchCase="'added'"><span class="badge badge-warning" i18n="transaction.audit.added">Added</span></td>
|
||||
<td *ngSwitchCase="'selected'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
|
||||
<td *ngSwitchCase="'accelerated'"><span class="badge badge-success" i18n="transaction.audit.accelerated">Accelerated</span></td>
|
||||
</ng-container>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -21,7 +21,6 @@ export class BlockOverviewTooltipComponent implements OnChanges {
|
||||
vsize = 1;
|
||||
feeRate = 0;
|
||||
effectiveRate;
|
||||
acceleration;
|
||||
|
||||
tooltipPosition: Position = { x: 0, y: 0 };
|
||||
|
||||
@@ -54,7 +53,6 @@ export class BlockOverviewTooltipComponent implements OnChanges {
|
||||
this.vsize = tx.vsize || 1;
|
||||
this.feeRate = this.fee / this.vsize;
|
||||
this.effectiveRate = tx.rate;
|
||||
this.acceleration = tx.acc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
<ng-container *ngIf="!isLoadingBlock; else loadingRest">
|
||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||
<td i18n="mempool-block.fee-span">Fee span</td>
|
||||
<td><span>{{ block.extras.feeRange[1] | number:'1.0-0' }} - {{ block.extras.feeRange[block.extras.feeRange.length - 1] | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></span></td>
|
||||
<td><span>{{ block?.extras?.minFee | number:'1.0-0' }} - {{ block?.extras?.maxFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></span></td>
|
||||
</tr>
|
||||
<tr *ngIf="block?.extras?.medianFee != undefined">
|
||||
<td class="td-width" i18n="block.median-fee">Median fee</td>
|
||||
@@ -226,6 +226,9 @@
|
||||
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !showAudit"></app-block-overview-graph>
|
||||
<ng-container *ngIf="!isMobile || mode !== 'actual'; else emptyBlockInfo"></ng-container>
|
||||
</div>
|
||||
<ng-container *ngIf="network !== 'liquid'">
|
||||
<ng-container *ngTemplateOutlet="isMobile && mode === 'actual' ? actualDetails : expectedDetails"></ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="col-sm" *ngIf="!isMobile">
|
||||
<h3 class="block-subtitle actual" *ngIf="!isMobile"><ng-container i18n="block.actual-block">Actual Block</ng-container> <a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="how-do-block-audits-work"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></h3>
|
||||
@@ -235,6 +238,9 @@
|
||||
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !showAudit"></app-block-overview-graph>
|
||||
<ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container>
|
||||
</div>
|
||||
<ng-container *ngIf="network !== 'liquid'">
|
||||
<ng-container *ngTemplateOutlet="actualDetails"></ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -385,5 +391,60 @@
|
||||
</a>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #expectedDetails>
|
||||
<table *ngIf="block && blockAudit && blockAudit.expectedFees != null" class="table table-borderless table-striped audit-details-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td i18n="block.total-fees|Total fees in a block">Total fees</td>
|
||||
<td>
|
||||
<app-amount [satoshis]="blockAudit.expectedFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="block.weight">Weight</td>
|
||||
<td [innerHTML]="'‎' + (blockAudit.expectedWeight | wuBytes: 2)"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="mempool-block.transactions">Transactions</td>
|
||||
<td>{{ blockAudit.template?.length || 0 }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #actualDetails>
|
||||
<table *ngIf="block && blockAudit && blockAudit.expectedFees != null" class="table table-borderless table-striped audit-details-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td i18n="block.total-fees|Total fees in a block">Total fees</td>
|
||||
<td>
|
||||
<app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
||||
<span *ngIf="blockAudit.feeDelta" class="difference" [class.positive]="blockAudit.feeDelta <= 0" [class.negative]="blockAudit.feeDelta > 0">
|
||||
{{ blockAudit.feeDelta < 0 ? '+' : '' }}{{ (-blockAudit.feeDelta * 100) | amountShortener: 2 }}%
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="block.weight">Weight</td>
|
||||
<td [innerHTML]>
|
||||
<span [innerHTML]="'‎' + (block.weight | wuBytes: 2)"></span>
|
||||
<span *ngIf="blockAudit.weightDelta" class="difference" [class.positive]="blockAudit.weightDelta <= 0" [class.negative]="blockAudit.weightDelta > 0">
|
||||
{{ blockAudit.weightDelta < 0 ? '+' : '' }}{{ (-blockAudit.weightDelta * 100) | amountShortener: 2 }}%
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="mempool-block.transactions">Transactions</td>
|
||||
<td>
|
||||
{{ block.tx_count }}
|
||||
<span *ngIf="blockAudit.txDelta" class="difference" [class.positive]="blockAudit.txDelta <= 0" [class.negative]="blockAudit.txDelta > 0">
|
||||
{{ blockAudit.txDelta < 0 ? '+' : '' }}{{ (-blockAudit.txDelta * 100) | amountShortener: 2 }}%
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-template>
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
@@ -38,6 +38,17 @@
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.difference {
|
||||
margin-left: 0.5em;
|
||||
|
||||
&.positive {
|
||||
color: rgb(66, 183, 71);
|
||||
}
|
||||
&.negative {
|
||||
color: rgb(183, 66, 66);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,3 +263,10 @@ h1 {
|
||||
top: 11px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.audit-details-table {
|
||||
margin-top: 1.25rem;
|
||||
@media (max-width: 767.98px) {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +138,8 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
|
||||
if (block.id === this.blockHash) {
|
||||
this.block = block;
|
||||
block.extras.minFee = this.getMinBlockFee(block);
|
||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||
if (block?.extras?.reward != undefined) {
|
||||
this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
|
||||
}
|
||||
@@ -234,6 +236,8 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
this.updateAuditAvailableFromBlockHeight(block.height);
|
||||
this.block = block;
|
||||
block.extras.minFee = this.getMinBlockFee(block);
|
||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||
this.blockHeight = block.height;
|
||||
this.lastBlockHeight = this.blockHeight;
|
||||
this.nextBlockHeight = block.height + 1;
|
||||
@@ -336,7 +340,6 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
const isSelected = {};
|
||||
const isFresh = {};
|
||||
const isSigop = {};
|
||||
const isAccelerated = {};
|
||||
this.numMissing = 0;
|
||||
this.numUnexpected = 0;
|
||||
|
||||
@@ -359,9 +362,6 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
for (const txid of blockAudit.sigopTxs || []) {
|
||||
isSigop[txid] = true;
|
||||
}
|
||||
for (const txid of blockAudit.acceleratedTxs || []) {
|
||||
isAccelerated[txid] = true;
|
||||
}
|
||||
// set transaction statuses
|
||||
for (const tx of blockAudit.template) {
|
||||
tx.context = 'projected';
|
||||
@@ -374,9 +374,6 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
isMissing[tx.txid] = true;
|
||||
this.numMissing++;
|
||||
}
|
||||
if (isAccelerated[tx.txid]) {
|
||||
tx.status = 'accelerated';
|
||||
}
|
||||
}
|
||||
for (const [index, tx] of blockAudit.transactions.entries()) {
|
||||
tx.context = 'actual';
|
||||
@@ -391,13 +388,15 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
isSelected[tx.txid] = true;
|
||||
this.numUnexpected++;
|
||||
}
|
||||
if (isAccelerated[tx.txid]) {
|
||||
tx.status = 'accelerated';
|
||||
}
|
||||
}
|
||||
for (const tx of blockAudit.transactions) {
|
||||
inBlock[tx.txid] = true;
|
||||
}
|
||||
|
||||
blockAudit.feeDelta = blockAudit.expectedFees > 0 ? (blockAudit.expectedFees - this.block.extras.totalFees) / blockAudit.expectedFees : 0;
|
||||
blockAudit.weightDelta = blockAudit.expectedWeight > 0 ? (blockAudit.expectedWeight - this.block.weight) / blockAudit.expectedWeight : 0;
|
||||
blockAudit.txDelta = blockAudit.template.length > 0 ? (blockAudit.template.length - this.block.tx_count) / blockAudit.template.length : 0;
|
||||
|
||||
this.setAuditAvailable(true);
|
||||
} else {
|
||||
this.setAuditAvailable(false);
|
||||
@@ -673,4 +672,23 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getMinBlockFee(block: BlockExtended): number {
|
||||
if (block?.extras?.feeRange) {
|
||||
// heuristic to check if feeRange is adjusted for effective rates
|
||||
if (block.extras.medianFee === block.extras.feeRange[3]) {
|
||||
return block.extras.feeRange[1];
|
||||
} else {
|
||||
return block.extras.feeRange[0];
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
getMaxBlockFee(block: BlockExtended): number {
|
||||
if (block?.extras?.feeRange) {
|
||||
return block.extras.feeRange[block.extras.feeRange.length - 1];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,10 @@
|
||||
[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 } }"
|
||||
<a draggable="false" [routerLink]="[getHref(i, block) | relativeUrl]" [state]="{ data: { block: block } }"
|
||||
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a>
|
||||
<div *ngIf="!minimal" [attr.data-cy]="'bitcoin-block-' + i + '-height'" class="block-height">
|
||||
<a [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height
|
||||
<a [routerLink]="[getHref(i, block) | relativeUrl]" [state]="{ data: { block: block } }">{{ block.height
|
||||
}}</a>
|
||||
</div>
|
||||
<div class="block-body">
|
||||
@@ -31,9 +31,8 @@
|
||||
</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
|
||||
*ngIf="block?.extras?.minFee != null && block?.extras?.maxFee != null; else emptyfeespan">
|
||||
{{ block.extras.minFee | number:feeRounding }} - {{ block.extras.maxFee | number:feeRounding }} <ng-container
|
||||
i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
|
||||
</div>
|
||||
<ng-template #emptyfeespan>
|
||||
|
||||
@@ -27,6 +27,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() minimal: boolean = false;
|
||||
@Input() blockWidth: number = 125;
|
||||
@Input() spotlight: number = 0;
|
||||
@Input() getHref?: (index, block) => string = (index, block) => `/block/${block.id}`;
|
||||
|
||||
specialBlocks = specialBlocks;
|
||||
network = '';
|
||||
@@ -113,6 +114,9 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.blocksFilled = false;
|
||||
}
|
||||
|
||||
block.extras.minFee = this.getMinBlockFee(block);
|
||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||
|
||||
this.blocks.unshift(block);
|
||||
this.blocks = this.blocks.slice(0, this.dynamicBlocksAmount);
|
||||
|
||||
@@ -239,6 +243,10 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
if (height >= 0) {
|
||||
this.cacheService.loadBlock(height);
|
||||
block = this.cacheService.getCachedBlock(height) || null;
|
||||
if (block) {
|
||||
block.extras.minFee = this.getMinBlockFee(block);
|
||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||
}
|
||||
}
|
||||
this.blocks.push(block || {
|
||||
placeholder: height < 0,
|
||||
@@ -277,6 +285,8 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
onBlockLoaded(block: BlockExtended) {
|
||||
const blockIndex = this.height - block.height;
|
||||
if (blockIndex >= 0 && blockIndex < this.blocks.length) {
|
||||
block.extras.minFee = this.getMinBlockFee(block);
|
||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||
this.blocks[blockIndex] = block;
|
||||
this.blockStyles[blockIndex] = this.getStyleForBlock(block, blockIndex);
|
||||
}
|
||||
@@ -365,4 +375,23 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
return emptyBlocks;
|
||||
}
|
||||
|
||||
getMinBlockFee(block: BlockExtended): number {
|
||||
if (block?.extras?.feeRange) {
|
||||
// heuristic to check if feeRange is adjusted for effective rates
|
||||
if (block.extras.medianFee === block.extras.feeRange[3]) {
|
||||
return block.extras.feeRange[1];
|
||||
} else {
|
||||
return block.extras.feeRange[0];
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
getMaxBlockFee(block: BlockExtended): number {
|
||||
if (block?.extras?.feeRange) {
|
||||
return block.extras.feeRange[block.extras.feeRange.length - 1];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
<th *ngIf="indexingAvailable" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}" i18n="mining.pool-name"
|
||||
i18n-ngbTooltip="mining.pool-name" ngbTooltip="Pool" placement="bottom" #miningpool [disableTooltip]="!isEllipsisActive(miningpool)">Pool</th>
|
||||
<th class="timestamp" i18n="latest-blocks.timestamp" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">Timestamp</th>
|
||||
<th class="mined" i18n="latest-blocks.mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">Mined</th>
|
||||
<th *ngIf="auditAvailable" class="health text-right" i18n="latest-blocks.health" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"
|
||||
i18n-ngbTooltip="latest-blocks.health" ngbTooltip="Health" placement="bottom" #health [disableTooltip]="!isEllipsisActive(health)">Health</th>
|
||||
<th *ngIf="indexingAvailable" class="reward text-right" i18n="latest-blocks.reward" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"
|
||||
i18n-ngbTooltip="latest-blocks.reward" ngbTooltip="Reward" placement="bottom" #reward [disableTooltip]="!isEllipsisActive(reward)">Reward</th>
|
||||
<th *ngIf="indexingAvailable && !widget" class="fees text-right" i18n="latest-blocks.fees" [class]="indexingAvailable ? '' : 'legacy'">Fees</th>
|
||||
<th *ngIf="auditAvailable && !widget" class="fee-delta" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"></th>
|
||||
<th *ngIf="indexingAvailable" class="txs text-right" i18n="dashboard.txs" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"
|
||||
i18n-ngbTooltip="dashboard.txs" ngbTooltip="TXs" placement="bottom" #txs [disableTooltip]="!isEllipsisActive(txs)">TXs</th>
|
||||
<th *ngIf="!indexingAvailable" class="txs text-right" i18n="dashboard.txs" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">Transactions</th>
|
||||
@@ -42,26 +42,18 @@
|
||||
<td class="timestamp" *ngIf="!widget" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
||||
</td>
|
||||
<td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
|
||||
<app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time>
|
||||
</td>
|
||||
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<a
|
||||
*ngIf="block?.extras?.matchRate != null; else nullHealth"
|
||||
class="health-badge badge"
|
||||
[class.badge-success]="auditScores[block.id] >= 99"
|
||||
[class.badge-warning]="auditScores[block.id] >= 75 && auditScores[block.id] < 99"
|
||||
[class.badge-danger]="auditScores[block.id] < 75"
|
||||
[routerLink]="auditScores[block.id] != null ? ['/block/' | relativeUrl, block.id] : null"
|
||||
[class.badge-success]="block.extras.matchRate >= 99"
|
||||
[class.badge-warning]="block.extras.matchRate >= 75 && block.extras.matchRate < 99"
|
||||
[class.badge-danger]="block.extras.matchRate < 75"
|
||||
[routerLink]="block.extras.matchRate != null ? ['/block/' | relativeUrl, block.id] : null"
|
||||
[state]="{ data: { block: block } }"
|
||||
*ngIf="auditScores[block.id] != null; else nullHealth"
|
||||
>{{ auditScores[block.id] }}%</a>
|
||||
>{{ block.extras.matchRate }}%</a>
|
||||
<ng-template #nullHealth>
|
||||
<ng-container *ngIf="!loadingScores; else loadingHealth">
|
||||
<span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
<ng-template #loadingHealth>
|
||||
<span class="skeleton-loader" style="max-width: 60px"></span>
|
||||
<span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span>
|
||||
</ng-template>
|
||||
</td>
|
||||
<td *ngIf="indexingAvailable" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
@@ -70,6 +62,11 @@
|
||||
<td *ngIf="indexingAvailable && !widget" class="fees text-right" [class]="indexingAvailable ? '' : 'legacy'">
|
||||
<app-amount [satoshis]="block.extras.totalFees" [noFiat]="true" digitsInfo="1.2-2"></app-amount>
|
||||
</td>
|
||||
<td *ngIf="auditAvailable" class="fee-delta" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<span *ngIf="block.extras.feeDelta" class="difference" [class.positive]="block.extras.feeDelta >= 0" [class.negative]="block.extras.feeDelta < 0">
|
||||
{{ block.extras.feeDelta > 0 ? '+' : '' }}{{ (block.extras.feeDelta * 100) | amountShortener: 2 }}%
|
||||
</span>
|
||||
</td>
|
||||
<td class="txs text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
{{ block.tx_count | number }}
|
||||
</td>
|
||||
@@ -106,6 +103,9 @@
|
||||
<td *ngIf="indexingAvailable && !widget" class="fees text-right" [class]="indexingAvailable ? '' : 'legacy'">
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
<td *ngIf="auditAvailable && !widget" class="fee-delta" [class]="indexingAvailable ? '' : 'legacy'">
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
<td class="txs text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
|
||||
@@ -23,6 +23,17 @@ tr, td, th {
|
||||
border: 0px;
|
||||
padding-top: 0.65rem !important;
|
||||
padding-bottom: 0.7rem !important;
|
||||
|
||||
.difference {
|
||||
margin-left: 0.5em;
|
||||
|
||||
&.positive {
|
||||
color: rgb(66, 183, 71);
|
||||
}
|
||||
&.negative {
|
||||
color: rgb(183, 66, 66);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.clear-link {
|
||||
@@ -90,7 +101,7 @@ tr, td, th {
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
width: 18%;
|
||||
width: 10%;
|
||||
@media (max-width: 1100px) {
|
||||
display: none;
|
||||
}
|
||||
@@ -123,8 +134,8 @@ tr, td, th {
|
||||
}
|
||||
|
||||
.txs {
|
||||
padding-right: 40px;
|
||||
width: 8%;
|
||||
padding-right: 20px;
|
||||
width: 6%;
|
||||
@media (max-width: 1100px) {
|
||||
padding-right: 10px;
|
||||
}
|
||||
@@ -160,6 +171,16 @@ tr, td, th {
|
||||
.fees.widget {
|
||||
width: 20%;
|
||||
}
|
||||
.fee-delta {
|
||||
width: 6%;
|
||||
padding-left: 0;
|
||||
@media (max-width: 991px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.fee-delta.widget {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.reward {
|
||||
width: 8%;
|
||||
@@ -214,7 +235,7 @@ tr, td, th {
|
||||
|
||||
.health {
|
||||
width: 10%;
|
||||
@media (max-width: 1105px) {
|
||||
@media (max-width: 1100px) {
|
||||
width: 13%;
|
||||
}
|
||||
@media (max-width: 560px) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input } from '@angular/core';
|
||||
import { BehaviorSubject, combineLatest, concat, Observable, timer, EMPTY, Subscription, of } from 'rxjs';
|
||||
import { catchError, delayWhen, map, retryWhen, scan, skip, switchMap, tap } from 'rxjs/operators';
|
||||
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef } from '@angular/core';
|
||||
import { BehaviorSubject, combineLatest, Observable, timer, of } from 'rxjs';
|
||||
import { delayWhen, map, retryWhen, scan, switchMap, tap } from 'rxjs/operators';
|
||||
import { BlockExtended } from '../../interfaces/node-api.interface';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
@@ -12,19 +12,14 @@ import { WebsocketService } from '../../services/websocket.service';
|
||||
styleUrls: ['./blocks-list.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BlocksList implements OnInit, OnDestroy {
|
||||
export class BlocksList implements OnInit {
|
||||
@Input() widget: boolean = false;
|
||||
|
||||
blocks$: Observable<BlockExtended[]> = undefined;
|
||||
auditScores: { [hash: string]: number | void } = {};
|
||||
|
||||
auditScoreSubscription: Subscription;
|
||||
latestScoreSubscription: Subscription;
|
||||
|
||||
indexingAvailable = false;
|
||||
auditAvailable = false;
|
||||
isLoading = true;
|
||||
loadingScores = true;
|
||||
fromBlockHeight = undefined;
|
||||
paginationMaxSize: number;
|
||||
page = 1;
|
||||
@@ -39,6 +34,7 @@ export class BlocksList implements OnInit, OnDestroy {
|
||||
private apiService: ApiService,
|
||||
private websocketService: WebsocketService,
|
||||
public stateService: StateService,
|
||||
private cd: ChangeDetectorRef,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -65,7 +61,7 @@ export class BlocksList implements OnInit, OnDestroy {
|
||||
this.blocksCount = blocks[0].height + 1;
|
||||
}
|
||||
this.isLoading = false;
|
||||
this.lastBlockHeight = Math.max(...blocks.map(o => o.height))
|
||||
this.lastBlockHeight = Math.max(...blocks.map(o => o.height));
|
||||
}),
|
||||
map(blocks => {
|
||||
if (this.indexingAvailable) {
|
||||
@@ -81,7 +77,7 @@ export class BlocksList implements OnInit, OnDestroy {
|
||||
return blocks;
|
||||
}),
|
||||
retryWhen(errors => errors.pipe(delayWhen(() => timer(10000))))
|
||||
)
|
||||
);
|
||||
})
|
||||
),
|
||||
this.stateService.blocks$
|
||||
@@ -112,68 +108,25 @@ export class BlocksList implements OnInit, OnDestroy {
|
||||
acc = acc.slice(0, this.widget ? 6 : 15);
|
||||
}
|
||||
return acc;
|
||||
}, [])
|
||||
);
|
||||
|
||||
if (this.indexingAvailable && this.auditAvailable) {
|
||||
this.auditScoreSubscription = this.fromHeightSubject.pipe(
|
||||
switchMap((fromBlockHeight) => {
|
||||
this.loadingScores = true;
|
||||
return this.apiService.getBlockAuditScores$(this.page === 1 ? undefined : fromBlockHeight)
|
||||
.pipe(
|
||||
catchError(() => {
|
||||
return EMPTY;
|
||||
})
|
||||
);
|
||||
}, []),
|
||||
switchMap((blocks) => {
|
||||
blocks.forEach(block => {
|
||||
block.extras.feeDelta = block.extras.expectedFees ? (block.extras.totalFees - block.extras.expectedFees) / block.extras.expectedFees : 0;
|
||||
});
|
||||
return of(blocks);
|
||||
})
|
||||
).subscribe((scores) => {
|
||||
Object.values(scores).forEach(score => {
|
||||
this.auditScores[score.hash] = score?.matchRate != null ? score.matchRate : null;
|
||||
});
|
||||
this.loadingScores = false;
|
||||
});
|
||||
|
||||
this.latestScoreSubscription = this.stateService.blocks$.pipe(
|
||||
switchMap((block) => {
|
||||
if (block[0]?.extras?.matchRate != null) {
|
||||
return of({
|
||||
hash: block[0].id,
|
||||
matchRate: block[0]?.extras?.matchRate,
|
||||
});
|
||||
}
|
||||
else if (block[0]?.id && this.auditScores[block[0].id] === undefined) {
|
||||
return this.apiService.getBlockAuditScore$(block[0].id)
|
||||
.pipe(
|
||||
catchError(() => {
|
||||
return EMPTY;
|
||||
})
|
||||
);
|
||||
} else {
|
||||
return EMPTY;
|
||||
}
|
||||
}),
|
||||
).subscribe((score) => {
|
||||
if (score && score.hash) {
|
||||
this.auditScores[score.hash] = score?.matchRate != null ? score.matchRate : null;
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.auditScoreSubscription?.unsubscribe();
|
||||
this.latestScoreSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
pageChange(page: number) {
|
||||
pageChange(page: number): void {
|
||||
this.fromHeightSubject.next((this.blocksCount - 1) - (page - 1) * 15);
|
||||
}
|
||||
|
||||
trackByBlock(index: number, block: BlockExtended) {
|
||||
trackByBlock(index: number, block: BlockExtended): number {
|
||||
return block.height;
|
||||
}
|
||||
|
||||
isEllipsisActive(e) {
|
||||
isEllipsisActive(e): boolean {
|
||||
return (e.offsetWidth < e.scrollWidth);
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<app-clock mode="mempool"></app-clock>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-clock-mempool',
|
||||
templateUrl: './clock-mempool.component.html',
|
||||
})
|
||||
export class ClockMempoolComponent {}
|
||||
@@ -1 +0,0 @@
|
||||
<app-clock mode="block"></app-clock>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-clock-mined',
|
||||
templateUrl: './clock-mined.component.html',
|
||||
})
|
||||
export class ClockMinedComponent {}
|
||||
@@ -1,14 +1,19 @@
|
||||
<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>
|
||||
<app-clockchain
|
||||
[width]="chainWidth"
|
||||
[height]="chainHeight"
|
||||
[mode]="mode"
|
||||
[index]="blockIndex"
|
||||
></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;">
|
||||
<ng-container *ngIf="blocks && blocks.length">
|
||||
<ng-container *ngIf="mode === 'mined'; else mempoolMode;">
|
||||
<div class="block-cube">
|
||||
<div class="side top"></div>
|
||||
<div class="side bottom"></div>
|
||||
@@ -20,12 +25,12 @@
|
||||
</ng-container>
|
||||
<ng-template #mempoolMode>
|
||||
<div class="block-sizer" [style]="blockSizerStyle">
|
||||
<app-mempool-block-overview [index]="0" [pixelAlign]="true"></app-mempool-block-overview>
|
||||
<app-mempool-block-overview [index]="blockIndex" [pixelAlign]="true"></app-mempool-block-overview>
|
||||
</div>
|
||||
</ng-template>
|
||||
<div class="fader"></div>
|
||||
<div class="title-wrapper">
|
||||
<h1 class="block-height">{{ block.height }}</h1>
|
||||
<h1 class="block-height">{{ blocks[mode === 'mempool' ? 0 : blockIndex].height }}</h1>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
@@ -42,13 +47,13 @@
|
||||
<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 }} sat/vB</p>
|
||||
</div>
|
||||
<div *ngIf="mode !== 'mempool' && block" class="stats bottom left">
|
||||
<p [innerHTML]="block.size | bytes: 2"></p>
|
||||
<div *ngIf="mode !== 'mempool' && blocks?.length" class="stats bottom left">
|
||||
<p [innerHTML]="blocks[blockIndex].size | bytes: 2"></p>
|
||||
<p class="label" i18n="clock.block-size">block size</p>
|
||||
</div>
|
||||
<div *ngIf="mode !== 'mempool' && block" class="stats bottom right">
|
||||
<div *ngIf="mode !== 'mempool' && blocks?.length" 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-container *ngTemplateOutlet="blocks[blockIndex].tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: blocks[blockIndex].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>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Input, OnInit } from '@angular/core';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { Observable, Subscription, of, switchMap, tap } 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';
|
||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-clock',
|
||||
@@ -13,12 +14,14 @@ import { ActivatedRoute } from '@angular/router';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ClockComponent implements OnInit {
|
||||
@Input() mode: 'block' | 'mempool' = 'block';
|
||||
hideStats: boolean = false;
|
||||
mode: 'mempool' | 'mined' = 'mined';
|
||||
blockIndex: number;
|
||||
pageSubscription: Subscription;
|
||||
blocksSubscription: Subscription;
|
||||
recommendedFees$: Observable<Recommendedfees>;
|
||||
mempoolInfo$: Observable<MempoolInfo>;
|
||||
block: BlockExtended;
|
||||
blocks: BlockExtended[] = [];
|
||||
clockSize: number = 300;
|
||||
chainWidth: number = 384;
|
||||
chainHeight: number = 60;
|
||||
@@ -41,6 +44,8 @@ export class ClockComponent implements OnInit {
|
||||
public stateService: StateService,
|
||||
private websocketService: WebsocketService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private relativeUrlPipe: RelativeUrlPipe,
|
||||
private cd: ChangeDetectorRef,
|
||||
) {
|
||||
this.route.queryParams.subscribe((params) => {
|
||||
@@ -57,14 +62,40 @@ export class ClockComponent implements OnInit {
|
||||
this.blocksSubscription = this.stateService.blocks$
|
||||
.subscribe(([block]) => {
|
||||
if (block) {
|
||||
this.block = block;
|
||||
this.blockStyle = this.getStyleForBlock(this.block);
|
||||
this.cd.markForCheck();
|
||||
this.blocks.unshift(block);
|
||||
this.blocks = this.blocks.slice(0, 16);
|
||||
if (this.blocks[this.blockIndex]) {
|
||||
this.blockStyle = this.getStyleForBlock(this.blocks[this.blockIndex]);
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.recommendedFees$ = this.stateService.recommendedFees$;
|
||||
this.mempoolInfo$ = this.stateService.mempoolInfo$;
|
||||
|
||||
this.pageSubscription = this.route.paramMap.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
const rawMode: string = params.get('mode');
|
||||
const mode = rawMode === 'mempool' ? 'mempool' : 'mined';
|
||||
const index: number = Number.parseInt(params.get('index'));
|
||||
if (mode !== rawMode || index < 0 || isNaN(index)) {
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/clock'), mode, index || 0]);
|
||||
}
|
||||
return of({
|
||||
mode,
|
||||
index,
|
||||
});
|
||||
}),
|
||||
tap((page: { mode: 'mempool' | 'mined', index: number }) => {
|
||||
this.mode = page.mode;
|
||||
this.blockIndex = page.index || 0;
|
||||
if (this.blocks[this.blockIndex]) {
|
||||
this.blockStyle = this.getStyleForBlock(this.blocks[this.blockIndex]);
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
})
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
getStyleForBlock(block: BlockExtended) {
|
||||
|
||||
@@ -5,8 +5,20 @@
|
||||
<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>
|
||||
<app-mempool-blocks
|
||||
[minimal]="true"
|
||||
[count]="mempoolBlocks"
|
||||
[blockWidth]="blockWidth"
|
||||
[spotlight]="mode === 'mempool' ? index + 1 : 0"
|
||||
[getHref]="getMempoolUrl"
|
||||
></app-mempool-blocks>
|
||||
<app-blockchain-blocks
|
||||
[minimal]="true"
|
||||
[count]="blockchainBlocks"
|
||||
[blockWidth]="blockWidth"
|
||||
[spotlight]="mode === 'mined' ? -index - 1 : 0"
|
||||
[getHref]="getMinedUrl"
|
||||
></app-blockchain-blocks>
|
||||
</div>
|
||||
<div class="divider" [style.top]="-(height / 6) + 'px'">
|
||||
<svg
|
||||
|
||||
@@ -11,7 +11,8 @@ import { StateService } from '../../services/state.service';
|
||||
export class ClockchainComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() width: number = 300;
|
||||
@Input() height: number = 60;
|
||||
@Input() mode: 'mempool' | 'block';
|
||||
@Input() mode: 'mempool' | 'mined';
|
||||
@Input() index: number = 0;
|
||||
|
||||
mempoolBlocks: number = 3;
|
||||
blockchainBlocks: number = 6;
|
||||
@@ -70,4 +71,12 @@ export class ClockchainComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.ltrTransitionEnabled = true;
|
||||
this.stateService.timeLtr.next(!this.timeLtr);
|
||||
}
|
||||
|
||||
getMempoolUrl(index): string {
|
||||
return `/clock/mempool/${index}`;
|
||||
}
|
||||
|
||||
getMinedUrl(index): string {
|
||||
return `/clock/block/${index}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<ng-container *ngIf="{ val: network$ | async } as network">
|
||||
<header>
|
||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
|
||||
<a class="navbar-brand" [ngClass]="{'dual-logos': subdomain}" [routerLink]="['/' | relativeUrl]">
|
||||
<a class="navbar-brand" [ngClass]="{'dual-logos': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)">
|
||||
<ng-template [ngIf]="subdomain">
|
||||
<div class="subdomain_container">
|
||||
<img [src]="'/api/v1/enterprise/images/' + subdomain + '/logo'" class="subdomain_logo">
|
||||
|
||||
@@ -53,4 +53,8 @@ export class MasterPageComponent implements OnInit {
|
||||
onResize(): void {
|
||||
this.isMobile = window.innerWidth <= 767.98;
|
||||
}
|
||||
|
||||
brandClick(e): void {
|
||||
this.stateService.resetScroll$.next(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
|
||||
|
||||
updateBlock(delta: MempoolBlockDelta): void {
|
||||
const blockMined = (this.stateService.latestBlockHeight > this.lastBlockHeight);
|
||||
|
||||
if (this.blockIndex !== this.index) {
|
||||
const direction = (this.blockIndex == null || this.index < this.blockIndex) ? this.poolDirection : this.chainDirection;
|
||||
this.blockGraph.replace(delta.added, direction);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
[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">
|
||||
<a draggable="false" [routerLink]="['/mempool-block/' | relativeUrl, i]"
|
||||
<a draggable="false" [routerLink]="[getHref(i) | relativeUrl]"
|
||||
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a>
|
||||
<div class="block-body">
|
||||
<ng-container *ngIf="!minimal">
|
||||
@@ -47,7 +47,7 @@
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div *ngIf="arrowVisible" id="arrow-up" [ngStyle]="{'right': rightPosition + 75 + 'px', transition: transition }" [class.blink]="txPosition?.accelerated"></div>
|
||||
<div *ngIf="arrowVisible" id="arrow-up" [ngStyle]="{'right': rightPosition + 75 + 'px', transition: transition }"></div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
|
||||
@@ -169,34 +169,4 @@
|
||||
transform: translate(calc(-0.2 * var(--block-size)), calc(1.1 * var(--block-size)));
|
||||
border-radius: 2px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.blink{
|
||||
width:400px;
|
||||
height:400px;
|
||||
border-bottom: 35px solid #FFF;
|
||||
animation: blink 0.2s infinite;
|
||||
}
|
||||
@keyframes blink{
|
||||
0% {
|
||||
border-bottom: 35px solid green;
|
||||
}
|
||||
50% {
|
||||
border-bottom: 35px solid yellow;
|
||||
}
|
||||
100% {
|
||||
border-bottom: 35px solid orange;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes blink{
|
||||
0% {
|
||||
border-bottom: 35px solid green;
|
||||
}
|
||||
50% {
|
||||
border-bottom: 35px solid yellow;
|
||||
}
|
||||
100% {
|
||||
border-bottom: 35px solid orange;
|
||||
}
|
||||
}
|
||||
@@ -26,9 +26,9 @@ import { animate, style, transition, trigger } from '@angular/animations';
|
||||
export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() minimal: boolean = false;
|
||||
@Input() blockWidth: number = 125;
|
||||
@Input() containerWidth: number = null;
|
||||
@Input() count: number = null;
|
||||
@Input() spotlight: number = 0;
|
||||
@Input() getHref?: (index) => string = (index) => `/mempool-block/${index}`;
|
||||
|
||||
specialBlocks = specialBlocks;
|
||||
mempoolBlocks: MempoolBlock[] = [];
|
||||
@@ -253,7 +253,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
reduceEmptyBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] {
|
||||
const innerWidth = this.containerWidth || (this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2);
|
||||
const innerWidth = this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2;
|
||||
const blocksAmount = Math.min(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT, Math.floor(innerWidth / (this.blockWidth + this.blockPadding)));
|
||||
while (blocks.length < blocksAmount) {
|
||||
blocks.push({
|
||||
@@ -273,7 +273,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
reduceMempoolBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] {
|
||||
const innerWidth = this.containerWidth || (this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2);
|
||||
const innerWidth = this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2;
|
||||
let blocksAmount;
|
||||
if (this.count) {
|
||||
blocksAmount = 8;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<app-indexing-progress *ngIf="showLoadingIndicator"></app-indexing-progress>
|
||||
|
||||
<ng-container *ngIf="specialEvent">
|
||||
<div class="pyro">
|
||||
<div class="before"></div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, ElementRef, HostListener, OnInit, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { Component, ElementRef, HostListener, OnInit, OnDestroy, ViewChild, Input } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { specialBlocks } from '../../app.constants';
|
||||
@@ -9,6 +9,8 @@ import { specialBlocks } from '../../app.constants';
|
||||
styleUrls: ['./start.component.scss'],
|
||||
})
|
||||
export class StartComponent implements OnInit, OnDestroy {
|
||||
@Input() showLoadingIndicator = false;
|
||||
|
||||
interval = 60;
|
||||
colors = ['#5E35B1', '#ffffff'];
|
||||
|
||||
@@ -25,6 +27,7 @@ export class StartComponent implements OnInit, OnDestroy {
|
||||
markBlockSubscription: Subscription;
|
||||
blockCounterSubscription: Subscription;
|
||||
@ViewChild('blockchainContainer') blockchainContainer: ElementRef;
|
||||
resetScrollSubscription: Subscription;
|
||||
|
||||
isMobile: boolean = false;
|
||||
isiOS: boolean = false;
|
||||
@@ -106,6 +109,12 @@ export class StartComponent implements OnInit, OnDestroy {
|
||||
}, 60 * 60 * 1000);
|
||||
}
|
||||
});
|
||||
this.resetScrollSubscription = this.stateService.resetScroll$.subscribe(reset => {
|
||||
if (reset) {
|
||||
this.resetScroll();
|
||||
this.stateService.resetScroll$.next(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
@@ -385,5 +394,6 @@ export class StartComponent implements OnInit, OnDestroy {
|
||||
this.chainTipSubscription.unsubscribe();
|
||||
this.markBlockSubscription.unsubscribe();
|
||||
this.blockCounterSubscription.unsubscribe();
|
||||
this.resetScrollSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,13 @@
|
||||
|
||||
<form [formGroup]="radioGroupForm" class="formRadioGroup"
|
||||
[class]="(stateService.env.MINING_DASHBOARD || stateService.env.LIGHTNING) ? 'mining' : 'no-menu'" (click)="saveGraphPreference()">
|
||||
<div *ngIf="!isMobile()" class="btn-group btn-group-toggle">
|
||||
<label class="btn btn-primary btn-sm mr-2">
|
||||
<a [routerLink]="['/tv' | relativeUrl]" style="color: white" id="btn-tv">
|
||||
<fa-icon [icon]="['fas', 'tv']" [fixedWidth]="true" i18n-title="master-page.tvview" title="TV view"></fa-icon>
|
||||
</a>
|
||||
</label>
|
||||
<div class="small-buttons">
|
||||
<a class="btn btn-primary btn-sm mb-0" [routerLink]="['/clock/mempool/0' | relativeUrl]" style="color: white" id="btn-clock">
|
||||
<fa-icon [icon]="['fas', 'clock']" [fixedWidth]="true" i18n-title="master-page.clockview" title="Clock view"></fa-icon>
|
||||
</a>
|
||||
<a *ngIf="!isMobile()" class="btn btn-primary btn-sm mb-0" [routerLink]="['/tv' | relativeUrl]" style="color: white" id="btn-tv">
|
||||
<fa-icon [icon]="['fas', 'tv']" [fixedWidth]="true" i18n-title="master-page.tvview" title="TV view"></fa-icon>
|
||||
</a>
|
||||
</div>
|
||||
<div class="btn-group btn-group-toggle" name="radioBasic">
|
||||
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '2h'">
|
||||
|
||||
@@ -150,17 +150,30 @@
|
||||
margin: 0px 0px;
|
||||
}
|
||||
.btn {
|
||||
width: 49.25%;
|
||||
width: 50%;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
margin: 1px 0;
|
||||
margin-right: 0.5rem;
|
||||
@media (min-width: 830px) {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-right: 0;
|
||||
@media (min-width: 830px) {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
.dropdown {
|
||||
width: 49.25%;
|
||||
display: flex;
|
||||
width: 50%;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
@media (min-width: 830px) {
|
||||
width: auto;
|
||||
margin: 0px 5px;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
#dropdownFees {
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
<app-start></app-start>
|
||||
<app-start [showLoadingIndicator]="true"></app-start>
|
||||
<app-footer></app-footer>
|
||||
|
||||
@@ -18,7 +18,12 @@
|
||||
</span>
|
||||
|
||||
<div class="container-buttons">
|
||||
<app-confirmations [chainTip]="latestBlock?.height" [height]="tx?.status?.block_height" [replaced]="replaced"></app-confirmations>
|
||||
<app-confirmations
|
||||
[chainTip]="latestBlock?.height"
|
||||
[height]="tx?.status?.block_height"
|
||||
[replaced]="replaced"
|
||||
[removed]="this.rbfInfo?.mined && !this.tx?.status?.confirmed"
|
||||
></app-confirmations>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
@@ -478,8 +483,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="cpfpInfo && hasEffectiveFeeRate">
|
||||
<td *ngIf="cpfpInfo.acceleration" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Accelerated fee rate</td>
|
||||
<td *ngIf="!cpfpInfo.acceleration" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
|
||||
<td i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
|
||||
<td>
|
||||
<div class="effective-fee-container">
|
||||
{{ tx.effectiveFeePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
||||
|
||||
@@ -178,9 +178,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
} else {
|
||||
this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize;
|
||||
}
|
||||
if (cpfpInfo.acceleration) {
|
||||
this.tx.acceleration = cpfpInfo.acceleration;
|
||||
}
|
||||
|
||||
this.cpfpInfo = cpfpInfo;
|
||||
this.hasEffectiveFeeRate = hasRelatives || (this.tx.effectiveFeePerVsize && (Math.abs(this.tx.effectiveFeePerVsize - this.tx.feePerVsize) > 0.01));
|
||||
|
||||
@@ -19,7 +19,6 @@ export interface Transaction {
|
||||
ancestors?: Ancestor[];
|
||||
bestDescendant?: BestDescendant | null;
|
||||
cpfpChecked?: boolean;
|
||||
acceleration?: number;
|
||||
deleteAfter?: number;
|
||||
_unblinded?: any;
|
||||
_deduced?: boolean;
|
||||
|
||||
@@ -27,7 +27,6 @@ export interface CpfpInfo {
|
||||
effectiveFeePerVsize?: number;
|
||||
sigops?: number;
|
||||
adjustedVsize?: number;
|
||||
acceleration?: number;
|
||||
}
|
||||
|
||||
export interface RbfInfo {
|
||||
@@ -130,10 +129,15 @@ export interface PoolStat {
|
||||
export interface BlockExtension {
|
||||
totalFees?: number;
|
||||
medianFee?: number;
|
||||
minFee?: number;
|
||||
maxFee?: number;
|
||||
feeRange?: number[];
|
||||
reward?: number;
|
||||
coinbaseRaw?: string;
|
||||
matchRate?: number;
|
||||
expectedFees?: number;
|
||||
expectedWeight?: number;
|
||||
feeDelta?: number;
|
||||
similarity?: number;
|
||||
pool?: {
|
||||
id: number;
|
||||
@@ -150,6 +154,11 @@ export interface BlockAudit extends BlockExtended {
|
||||
missingTxs: string[],
|
||||
addedTxs: string[],
|
||||
matchRate: number,
|
||||
expectedFees: number,
|
||||
expectedWeight: number,
|
||||
feeDelta?: number,
|
||||
weightDelta?: number,
|
||||
txDelta?: number,
|
||||
template: TransactionStripped[],
|
||||
transactions: TransactionStripped[],
|
||||
}
|
||||
@@ -159,7 +168,7 @@ export interface TransactionStripped {
|
||||
fee: number;
|
||||
vsize: number;
|
||||
value: number;
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected' | 'accelerated';
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected';
|
||||
}
|
||||
|
||||
interface RbfTransaction extends TransactionStripped {
|
||||
@@ -169,7 +178,6 @@ interface RbfTransaction extends TransactionStripped {
|
||||
export interface MempoolPosition {
|
||||
block: number,
|
||||
vsize: number,
|
||||
accelerated?: boolean
|
||||
}
|
||||
|
||||
export interface RewardStats {
|
||||
|
||||
@@ -57,7 +57,7 @@ export interface MempoolBlockWithTransactions extends MempoolBlock {
|
||||
export interface MempoolBlockDelta {
|
||||
added: TransactionStripped[],
|
||||
removed: string[],
|
||||
changed?: { txid: string, rate: number | undefined, acc: number | undefined }[];
|
||||
changed?: { txid: string, rate: number | undefined }[];
|
||||
}
|
||||
|
||||
export interface MempoolInfo {
|
||||
@@ -75,9 +75,8 @@ export interface TransactionStripped {
|
||||
fee: number;
|
||||
vsize: number;
|
||||
value: number;
|
||||
acc?: number; // acceleration delta
|
||||
rate?: number; // effective fee rate
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected' | 'accelerated';
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected';
|
||||
context?: 'projected' | 'actual';
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +126,7 @@ export class StateService {
|
||||
keyNavigation$ = new Subject<KeyboardEvent>();
|
||||
|
||||
blockScrolling$: Subject<boolean> = new Subject<boolean>();
|
||||
resetScroll$: Subject<boolean> = new Subject<boolean>();
|
||||
timeLtr: BehaviorSubject<boolean>;
|
||||
hideFlow: BehaviorSubject<boolean>;
|
||||
hideAudit: BehaviorSubject<boolean>;
|
||||
|
||||
@@ -6,8 +6,11 @@
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && replaced">
|
||||
<button type="button" class="btn btn-sm btn-danger {{buttonClass}}" i18n="transaction.unconfirmed|Transaction unconfirmed state">Replaced</button>
|
||||
<button type="button" class="btn btn-sm btn-warning {{buttonClass}}" i18n="transaction.replaced|Transaction replaced state">Replaced</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && !replaced">
|
||||
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && !replaced && removed">
|
||||
<button type="button" class="btn btn-sm btn-warning {{buttonClass}}" i18n="transaction.audit.removed|Transaction removed state">Removed</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && !replaced && !removed">
|
||||
<button type="button" class="btn btn-sm btn-danger {{buttonClass}}" i18n="transaction.unconfirmed|Transaction unconfirmed state">Unconfirmed</button>
|
||||
</ng-template>
|
||||
@@ -11,6 +11,7 @@ export class ConfirmationsComponent implements OnChanges {
|
||||
@Input() chainTip: number;
|
||||
@Input() height: number;
|
||||
@Input() replaced: boolean = false;
|
||||
@Input() removed: boolean = false;
|
||||
@Input() hideUnconfirmed: boolean = false;
|
||||
@Input() buttonClass: string = '';
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common';
|
||||
import { NgbCollapseModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
|
||||
import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle,
|
||||
faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown,
|
||||
faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown,
|
||||
faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft } from '@fortawesome/free-solid-svg-icons';
|
||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
||||
import { MasterPageComponent } from '../components/master-page/master-page.component';
|
||||
@@ -95,8 +95,6 @@ import { MempoolBlockOverviewComponent } from '../components/mempool-block-overv
|
||||
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: [
|
||||
@@ -185,8 +183,6 @@ import { ClockMempoolComponent } from '../components/clock/clock-mempool.compone
|
||||
MempoolBlockOverviewComponent,
|
||||
ClockchainComponent,
|
||||
ClockComponent,
|
||||
ClockMinedComponent,
|
||||
ClockMempoolComponent,
|
||||
ClockFaceComponent,
|
||||
],
|
||||
imports: [
|
||||
@@ -300,8 +296,6 @@ import { ClockMempoolComponent } from '../components/clock/clock-mempool.compone
|
||||
MempoolBlockOverviewComponent,
|
||||
ClockchainComponent,
|
||||
ClockComponent,
|
||||
ClockMinedComponent,
|
||||
ClockMempoolComponent,
|
||||
ClockFaceComponent,
|
||||
]
|
||||
})
|
||||
@@ -310,6 +304,7 @@ export class SharedModule {
|
||||
library.addIcons(faInfoCircle);
|
||||
library.addIcons(faChartArea);
|
||||
library.addIcons(faTv);
|
||||
library.addIcons(faClock);
|
||||
library.addIcons(faTachometerAlt);
|
||||
library.addIcons(faCubes);
|
||||
library.addIcons(faHammer);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
var https = require('https');
|
||||
var fs = require('fs');
|
||||
var crypto = require('crypto');
|
||||
|
||||
const CONFIG_FILE_NAME = 'mempool-frontend-config.json';
|
||||
let configContent = {};
|
||||
@@ -37,39 +38,180 @@ function download(filename, url) {
|
||||
});
|
||||
}
|
||||
|
||||
function downloadMiningPoolLogos() {
|
||||
const options = {
|
||||
host: 'api.github.com',
|
||||
path: '/repos/mempool/mining-pool-logos/contents/',
|
||||
method: 'GET',
|
||||
headers: {'user-agent': 'node.js'}
|
||||
};
|
||||
|
||||
https.get(options, (response) => {
|
||||
let chunks_of_data = [];
|
||||
|
||||
response.on('data', (fragments) => {
|
||||
chunks_of_data.push(fragments);
|
||||
});
|
||||
|
||||
response.on('end', () => {
|
||||
let response_body = Buffer.concat(chunks_of_data);
|
||||
try {
|
||||
const poolLogos = JSON.parse(response_body.toString());
|
||||
for (const poolLogo of poolLogos) {
|
||||
download(`${PATH}/mining-pools/${poolLogo.name}`, poolLogo.download_url);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Unable to download mining pool logos. Trying again at next restart. Reason: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
});
|
||||
|
||||
response.on('error', (error) => {
|
||||
throw new Error(error);
|
||||
});
|
||||
})
|
||||
function getLocalHash(filePath) {
|
||||
const size = fs.statSync(filePath);
|
||||
const buffer = fs.readFileSync(filePath);
|
||||
const bufferWithHeader = Buffer.concat([Buffer.from('blob '), Buffer.from(`${size.size}`), Buffer.from('\0'), buffer]);
|
||||
return crypto.createHash('sha1').update(bufferWithHeader).digest('hex');
|
||||
}
|
||||
|
||||
function downloadMiningPoolLogos$() {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log('Checking if mining pool logos needs downloading or updating...');
|
||||
const options = {
|
||||
host: 'api.github.com',
|
||||
path: '/repos/mempool/mining-pool-logos/contents/',
|
||||
method: 'GET',
|
||||
headers: {'user-agent': 'node.js'}
|
||||
};
|
||||
|
||||
https.get(options, (response) => {
|
||||
const chunks_of_data = [];
|
||||
|
||||
response.on('data', (fragments) => {
|
||||
chunks_of_data.push(fragments);
|
||||
});
|
||||
|
||||
response.on('end', () => {
|
||||
const response_body = Buffer.concat(chunks_of_data);
|
||||
try {
|
||||
const poolLogos = JSON.parse(response_body.toString());
|
||||
if (poolLogos.message) {
|
||||
reject(poolLogos.message);
|
||||
}
|
||||
let downloadedCount = 0;
|
||||
for (const poolLogo of poolLogos) {
|
||||
const filePath = `${PATH}/mining-pools/${poolLogo.name}`;
|
||||
if (fs.existsSync(filePath)) {
|
||||
const localHash = getLocalHash(filePath);
|
||||
if (localHash !== poolLogo.sha) {
|
||||
console.log(`${poolLogo.name} is different on the remote, downloading...`);
|
||||
download(filePath, poolLogo.download_url);
|
||||
downloadedCount++;
|
||||
}
|
||||
} else {
|
||||
console.log(`${poolLogo.name} is missing, downloading...`);
|
||||
download(filePath, poolLogo.download_url);
|
||||
downloadedCount++;
|
||||
}
|
||||
}
|
||||
console.log(`Downloaded ${downloadedCount} and skipped ${poolLogos.length - downloadedCount} existing mining pool logos`);
|
||||
resolve();
|
||||
} catch (e) {
|
||||
reject(`Unable to download mining pool logos. Trying again at next restart. Reason: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
});
|
||||
|
||||
response.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function downloadPromoVideoSubtiles$() {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log('Checking if promo video subtitles needs downloading or updating...');
|
||||
const options = {
|
||||
host: 'api.github.com',
|
||||
path: '/repos/mempool/mempool-promo/contents/subtitles',
|
||||
method: 'GET',
|
||||
headers: {'user-agent': 'node.js'}
|
||||
};
|
||||
|
||||
https.get(options, (response) => {
|
||||
const chunks_of_data = [];
|
||||
|
||||
response.on('data', (fragments) => {
|
||||
chunks_of_data.push(fragments);
|
||||
});
|
||||
|
||||
response.on('end', () => {
|
||||
const response_body = Buffer.concat(chunks_of_data);
|
||||
try {
|
||||
const videoLanguages = JSON.parse(response_body.toString());
|
||||
if (videoLanguages.message) {
|
||||
reject(videoLanguages.message);
|
||||
}
|
||||
let downloadedCount = 0;
|
||||
for (const language of videoLanguages) {
|
||||
const filePath = `${PATH}/promo-video/${language.name}`;
|
||||
if (fs.existsSync(filePath)) {
|
||||
const localHash = getLocalHash(filePath);
|
||||
if (localHash !== language.sha) {
|
||||
console.log(`${language.name} is different on the remote, updating`);
|
||||
download(filePath, language.download_url);
|
||||
downloadedCount++;
|
||||
}
|
||||
} else {
|
||||
console.log(`${language.name} is missing, downloading`);
|
||||
download(filePath, language.download_url);
|
||||
downloadedCount++;
|
||||
}
|
||||
}
|
||||
console.log(`Downloaded ${downloadedCount} and skipped ${videoLanguages.length - downloadedCount} existing video subtitles`);
|
||||
resolve();
|
||||
} catch (e) {
|
||||
reject(`Unable to download video subtitles. Trying again at next restart. Reason: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
});
|
||||
|
||||
response.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function downloadPromoVideo$() {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log('Checking if promo video needs downloading or updating...');
|
||||
const options = {
|
||||
host: 'api.github.com',
|
||||
path: '/repos/mempool/mempool-promo/contents',
|
||||
method: 'GET',
|
||||
headers: {'user-agent': 'node.js'}
|
||||
};
|
||||
|
||||
https.get(options, (response) => {
|
||||
const chunks_of_data = [];
|
||||
|
||||
response.on('data', (fragments) => {
|
||||
chunks_of_data.push(fragments);
|
||||
});
|
||||
|
||||
response.on('end', () => {
|
||||
const response_body = Buffer.concat(chunks_of_data);
|
||||
try {
|
||||
const contents = JSON.parse(response_body.toString());
|
||||
if (contents.message) {
|
||||
reject(contents.message);
|
||||
}
|
||||
for (const item of contents) {
|
||||
if (item.name !== 'promo.mp4') {
|
||||
continue;
|
||||
}
|
||||
const filePath = `${PATH}/promo-video/mempool-promo.mp4`;
|
||||
if (fs.existsSync(filePath)) {
|
||||
const localHash = getLocalHash(filePath);
|
||||
if (localHash !== item.sha) {
|
||||
console.log(`mempool-promo.mp4 is different on the remote, updating`);
|
||||
download(filePath, item.download_url);
|
||||
console.log('mempool-promo.mp4 downloaded.');
|
||||
} else {
|
||||
console.log(`mempool-promo.mp4 is already up to date. Skipping.`);
|
||||
}
|
||||
} else {
|
||||
console.log(`mempool-promo.mp4 is missing, downloading`);
|
||||
download(filePath, item.download_url);
|
||||
}
|
||||
}
|
||||
resolve();
|
||||
} catch (e) {
|
||||
reject(`Unable to download video. Trying again at next restart. Reason: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
});
|
||||
|
||||
response.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
let assetsJsonUrl = 'https://raw.githubusercontent.com/mempool/asset_registry_db/master/index.json';
|
||||
let assetsMinimalJsonUrl = 'https://raw.githubusercontent.com/mempool/asset_registry_db/master/index.minimal.json';
|
||||
|
||||
@@ -81,11 +223,6 @@ if (configContent.BASE_MODULE && configContent.BASE_MODULE === 'liquid') {
|
||||
const testnetAssetsJsonUrl = 'https://raw.githubusercontent.com/Blockstream/asset_registry_testnet_db/master/index.json';
|
||||
const testnetAssetsMinimalJsonUrl = 'https://raw.githubusercontent.com/Blockstream/asset_registry_testnet_db/master/index.minimal.json';
|
||||
|
||||
const promoPrefix = PATH + 'promo-video/';
|
||||
const promoVideoFile = promoPrefix + 'mempool-promo.mp4';
|
||||
const promoVideoUrl = 'https://raw.githubusercontent.com/mempool/mempool-promo/master/promo.mp4';
|
||||
const promoVideoLanguages = ['en','sv','ja','zh','cs','fi','fr','de','it','lt','nb','fa','pl','ro','pt'];
|
||||
|
||||
console.log('Downloading assets');
|
||||
download(PATH + 'assets.json', assetsJsonUrl);
|
||||
console.log('Downloading assets minimal');
|
||||
@@ -94,13 +231,10 @@ console.log('Downloading testnet assets');
|
||||
download(PATH + 'assets-testnet.json', testnetAssetsJsonUrl);
|
||||
console.log('Downloading testnet assets minimal');
|
||||
download(PATH + 'assets-testnet.minimal.json', testnetAssetsMinimalJsonUrl);
|
||||
if (!fs.existsSync(promoVideoFile)) {
|
||||
console.log('Downloading promo video');
|
||||
download(promoVideoFile, promoVideoUrl);
|
||||
}
|
||||
console.log('Downloading promo video subtitles');
|
||||
for( const l of promoVideoLanguages ) {
|
||||
download(promoPrefix + l + ".vtt", "https://raw.githubusercontent.com/mempool/mempool-promo/master/subtitles/" + l + ".vtt");
|
||||
}
|
||||
console.log('Downloading mining pool logos');
|
||||
downloadMiningPoolLogos();
|
||||
|
||||
downloadMiningPoolLogos$()
|
||||
.then(() => downloadPromoVideoSubtiles$())
|
||||
.then(() => downloadPromoVideo$())
|
||||
.catch((error) => {
|
||||
throw new Error(error);
|
||||
});
|
||||
|
||||
@@ -1239,7 +1239,7 @@ if [ "${BITCOIN_ELECTRS_INSTALL}" = ON ];then
|
||||
case $OS in
|
||||
FreeBSD)
|
||||
echo "[*] Patching Bitcoin Electrs code for FreeBSD"
|
||||
osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_HOME}/.cargo/registry/src/github.com-1ecc6299db9ec823/sysconf-0.3.4\" && patch -p1 < \"${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/freebsd/sysconf.patch\""
|
||||
osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_HOME}/.cargo/registry/src/index.crates.io-6f17d22bba15001f/sysconf-0.3.4\" && patch -p1 < \"${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/freebsd/sysconf.patch\""
|
||||
osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_ELECTRS_HOME}/src/new_index/\" && sed -i.bak -e s/Snappy/None/ db.rs && rm db.rs.bak"
|
||||
osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_ELECTRS_HOME}/src/bin/\" && sed -i.bak -e 's/from_secs(5)/from_secs(1)/' electrs.rs && rm electrs.rs.bak"
|
||||
;;
|
||||
@@ -1289,7 +1289,7 @@ if [ "${ELEMENTS_ELECTRS_INSTALL}" = ON ];then
|
||||
case $OS in
|
||||
FreeBSD)
|
||||
echo "[*] Patching Liquid Electrs code for FreeBSD"
|
||||
osSudo "${ELEMENTS_USER}" sh -c "cd \"${ELEMENTS_HOME}/.cargo/registry/src/github.com-1ecc6299db9ec823/sysconf-0.3.4\" && patch -p1 < \"${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/freebsd/sysconf.patch\""
|
||||
osSudo "${ELEMENTS_USER}" sh -c "cd \"${ELEMENTS_HOME}/.cargo/registry/src/index.crates.io-6f17d22bba15001f/sysconf-0.3.4\" && patch -p1 < \"${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/freebsd/sysconf.patch\""
|
||||
;;
|
||||
Debian)
|
||||
;;
|
||||
|
||||
@@ -97,10 +97,10 @@ location @mempool-api-v1-cache-normal {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_cache api;
|
||||
proxy_cache_valid 200 10s;
|
||||
proxy_cache_valid 200 2s;
|
||||
proxy_redirect off;
|
||||
|
||||
expires 10s;
|
||||
expires 2s;
|
||||
}
|
||||
|
||||
location @mempool-api-v1-cache-disabled {
|
||||
|
||||
Reference in New Issue
Block a user