Compare commits

..

1 Commits

Author SHA1 Message Date
transifex-integration[bot]
8ee7ec2886 Translate frontend/src/locale/messages.xlf in fa
100% reviewed for the source file 'frontend/src/locale/messages.xlf'
on the 'fa' language.
2023-03-09 23:51:31 +00:00
174 changed files with 10818 additions and 20387 deletions

View File

@@ -1,26 +0,0 @@
name: 'Print images digest'
on:
workflow_dispatch:
inputs:
version:
description: 'Image Version'
required: false
default: 'latest'
type: string
jobs:
print-images-sha:
runs-on: 'ubuntu-latest'
name: Print digest for images
steps:
- name: Checkout
uses: actions/checkout@v3
with:
path: digest
- name: Run script
working-directory: digest
run: |
sh ./docker/scripts/get_image_digest.sh $VERSION
env:
VERSION: ${{ github.event.inputs.version }}

View File

@@ -1,13 +1,13 @@
# The Mempool Open Source Project™ [![mempool](https://img.shields.io/endpoint?url=https://dashboard.cypress.io/badge/simple/ry4br7/master&style=flat-square)](https://dashboard.cypress.io/projects/ry4br7/runs)
https://user-images.githubusercontent.com/93150691/226236121-375ea64f-b4a1-4cc0-8fad-a6fb33226840.mp4
<br>
https://user-images.githubusercontent.com/232186/222445818-234aa6c9-c233-4c52-b3f0-e32b8232893b.mp4
Mempool is the fully-featured mempool visualizer, explorer, and API service running at [mempool.space](https://mempool.space/).
It is an open-source project developed and operated for the benefit of the Bitcoin community, with a focus on the emerging transaction fee market that is evolving Bitcoin into a multi-layer ecosystem.
![mempool](https://mempool.space/resources/screenshots/v2.4.0-dashboard.png)
# Installation Methods
Mempool can be self-hosted on a wide variety of your own hardware, ranging from a simple one-click installation on a Raspberry Pi full-node distro all the way to a robust production instance on a powerful FreeBSD server.

View File

@@ -171,58 +171,52 @@ Helpful link: https://gist.github.com/System-Glitch/cb4e87bf1ae3fec9925725bb3ebe
Run bitcoind on regtest:
```
bitcoind -regtest
bitcoind -regtest -rpcport=8332
```
Create a new wallet, if needed:
```
bitcoin-cli -regtest createwallet test
bitcoin-cli -regtest -rpcport=8332 createwallet test
```
Load wallet (this command may take a while if you have lot of UTXOs):
```
bitcoin-cli -regtest loadwallet test
bitcoin-cli -regtest -rpcport=8332 loadwallet test
```
Get a new address:
```
address=$(bitcoin-cli -regtest getnewaddress)
address=$(./src/bitcoin-cli -regtest -rpcport=8332 getnewaddress)
```
Mine blocks to the previously generated address. You need at least 101 blocks before you can spend. This will take some time to execute (~1 min):
```
bitcoin-cli -regtest generatetoaddress 101 $address
bitcoin-cli -regtest -rpcport=8332 generatetoaddress 101 $address
```
Send 0.1 BTC at 5 sat/vB to another address:
```
bitcoin-cli -named -regtest sendtoaddress address=$(bitcoin-cli -regtest getnewaddress) amount=0.1 fee_rate=5
./src/bitcoin-cli -named -regtest -rpcport=8332 sendtoaddress address=$(./src/bitcoin-cli -regtest -rpcport=8332 getnewaddress) amount=0.1 fee_rate=5
```
See more example of `sendtoaddress`:
```
bitcoin-cli sendtoaddress # will print the help
./src/bitcoin-cli sendtoaddress # will print the help
```
Mini script to generate random network activity (random TX count with random tx fee-rate). It's slow so don't expect to use this to test mempool spam, except if you let it run for a long time, or maybe with multiple regtest nodes connected to each other.
Mini script to generate transactions with random TX fee-rate (between 1 to 100 sat/vB). It's slow so don't expect to use this to test mempool spam, except if you let it run for a long time, or maybe with multiple regtest nodes connected to each other.
```
#!/bin/bash
address=$(bitcoin-cli -regtest getnewaddress)
bitcoin-cli -regtest generatetoaddress 101 $address
address=$(./src/bitcoin-cli -regtest -rpcport=8332 getnewaddress)
for i in {1..1000000}
do
for y in $(seq 1 "$(jot -r 1 1 1000)")
do
bitcoin-cli -regtest -named sendtoaddress address=$address amount=0.01 fee_rate=$(jot -r 1 1 100)
done
bitcoin-cli -regtest generatetoaddress 1 $address
sleep 5
./src/bitcoin-cli -regtest -rpcport=8332 -named sendtoaddress address=$address amount=0.01 fee_rate=$(jot -r 1 1 100)
done
```
Generate block at regular interval (every 10 seconds in this example):
```
watch -n 10 "bitcoin-cli -regtest generatetoaddress 1 $address"
watch -n 10 "./src/bitcoin-cli -regtest -rpcport=8332 generatetoaddress 1 $address"
```
### Mining pools update

View File

@@ -27,15 +27,13 @@
"AUDIT": false,
"ADVANCED_GBT_AUDIT": false,
"ADVANCED_GBT_MEMPOOL": false,
"CPFP_INDEXING": false,
"DISK_CACHE_BLOCK_INTERVAL": 6
"CPFP_INDEXING": false
},
"CORE_RPC": {
"HOST": "127.0.0.1",
"PORT": 8332,
"USERNAME": "mempool",
"PASSWORD": "mempool",
"TIMEOUT": 60000
"PASSWORD": "mempool"
},
"ELECTRUM": {
"HOST": "127.0.0.1",
@@ -49,8 +47,7 @@
"HOST": "127.0.0.1",
"PORT": 8332,
"USERNAME": "mempool",
"PASSWORD": "mempool",
"TIMEOUT": 60000
"PASSWORD": "mempool"
},
"DATABASE": {
"ENABLED": true,
@@ -94,8 +91,7 @@
"LND": {
"TLS_CERT_PATH": "tls.cert",
"MACAROON_PATH": "readonly.macaroon",
"REST_API_URL": "https://localhost:8080",
"TIMEOUT": 10000
"REST_API_URL": "https://localhost:8080"
},
"CLIGHTNING": {
"SOCKET": "lightning-rpc"

View File

@@ -1,16 +1,16 @@
{
"name": "mempool-backend",
"version": "2.5.0",
"version": "2.5.0-dev",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "mempool-backend",
"version": "2.5.0",
"version": "2.5.0-dev",
"license": "GNU Affero General Public License v3.0",
"dependencies": {
"@babel/core": "^7.20.12",
"@mempool/electrum-client": "1.1.9",
"@mempool/electrum-client": "^1.1.7",
"@types/node": "^16.18.11",
"axios": "~0.27.2",
"bitcoinjs-lib": "~6.1.0",
@@ -1272,9 +1272,9 @@
}
},
"node_modules/@mempool/electrum-client": {
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/@mempool/electrum-client/-/electrum-client-1.1.9.tgz",
"integrity": "sha512-mlvPiCzUlaETpYW3i6V87A24jjMYgsebaXtUo3WQyyLnYUuxs0KiXQ2mnKh3h15j8Xg/hfxeGIi+5OC9u0nftQ==",
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/@mempool/electrum-client/-/electrum-client-1.1.8.tgz",
"integrity": "sha512-6YP6UJstlk2GgC++NwPJthMPvLozyEMlqPq7RjvIWSwrL0smvM0Q0PAOohwZJtJFDWspuEUtNRF7aHQT2ztnYg==",
"engines": {
"node": ">=6"
}
@@ -7248,9 +7248,9 @@
}
},
"@mempool/electrum-client": {
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/@mempool/electrum-client/-/electrum-client-1.1.9.tgz",
"integrity": "sha512-mlvPiCzUlaETpYW3i6V87A24jjMYgsebaXtUo3WQyyLnYUuxs0KiXQ2mnKh3h15j8Xg/hfxeGIi+5OC9u0nftQ=="
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/@mempool/electrum-client/-/electrum-client-1.1.8.tgz",
"integrity": "sha512-6YP6UJstlk2GgC++NwPJthMPvLozyEMlqPq7RjvIWSwrL0smvM0Q0PAOohwZJtJFDWspuEUtNRF7aHQT2ztnYg=="
},
"@nodelib/fs.scandir": {
"version": "2.1.5",

View File

@@ -1,6 +1,6 @@
{
"name": "mempool-backend",
"version": "2.5.0",
"version": "2.5.0-dev",
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
"license": "GNU Affero General Public License v3.0",
"homepage": "https://mempool.space",
@@ -35,7 +35,7 @@
},
"dependencies": {
"@babel/core": "^7.20.12",
"@mempool/electrum-client": "1.1.9",
"@mempool/electrum-client": "^1.1.7",
"@types/node": "^16.18.11",
"axios": "~0.27.2",
"bitcoinjs-lib": "~6.1.0",

View File

@@ -28,15 +28,13 @@
"ADVANCED_GBT_AUDIT": "__MEMPOOL_ADVANCED_GBT_AUDIT__",
"ADVANCED_GBT_MEMPOOL": "__MEMPOOL_ADVANCED_GBT_MEMPOOL__",
"CPFP_INDEXING": "__MEMPOOL_CPFP_INDEXING__",
"MAX_BLOCKS_BULK_QUERY": "__MEMPOOL_MAX_BLOCKS_BULK_QUERY__",
"DISK_CACHE_BLOCK_INTERVAL": "__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__"
"MAX_BLOCKS_BULK_QUERY": "__MEMPOOL_MAX_BLOCKS_BULK_QUERY__"
},
"CORE_RPC": {
"HOST": "__CORE_RPC_HOST__",
"PORT": 15,
"USERNAME": "__CORE_RPC_USERNAME__",
"PASSWORD": "__CORE_RPC_PASSWORD__",
"TIMEOUT": "__CORE_RPC_TIMEOUT__"
"PASSWORD": "__CORE_RPC_PASSWORD__"
},
"ELECTRUM": {
"HOST": "__ELECTRUM_HOST__",
@@ -50,8 +48,7 @@
"HOST": "__SECOND_CORE_RPC_HOST__",
"PORT": 17,
"USERNAME": "__SECOND_CORE_RPC_USERNAME__",
"PASSWORD": "__SECOND_CORE_RPC_PASSWORD__",
"TIMEOUT": "__SECOND_CORE_RPC_TIMEOUT__"
"PASSWORD": "__SECOND_CORE_RPC_PASSWORD__"
},
"DATABASE": {
"ENABLED": false,
@@ -110,8 +107,7 @@
"LND": {
"TLS_CERT_PATH": "",
"MACAROON_PATH": "",
"REST_API_URL": "https://localhost:8080",
"TIMEOUT": 10000
"REST_API_URL": "https://localhost:8080"
},
"CLIGHTNING": {
"SOCKET": "__CLIGHTNING_SOCKET__"

View File

@@ -14,20 +14,18 @@ describe('Mempool Difficulty Adjustment', () => {
750134, // Current block height
0.6280047707459726, // Previous retarget % (Passed through)
'mainnet', // Network (if testnet, next value is non-zero)
0, // Latest block timestamp in seconds (only used if difficulty already locked in)
0, // If not testnet, not used
],
{ // Expected Result
progressPercent: 9.027777777777777,
difficultyChange: 13.180707740199772,
difficultyChange: 12.562233927411782,
estimatedRetargetDate: 1661895424692,
remainingBlocks: 1834,
remainingTime: 977591692,
previousRetarget: 0.6280047707459726,
previousTime: 1660820820,
nextRetargetHeight: 751968,
timeAvg: 533038,
timeOffset: 0,
expectedBlocks: 161.68833333333333,
},
],
[ // Vector 2 (testnet)
@@ -41,40 +39,15 @@ describe('Mempool Difficulty Adjustment', () => {
],
{ // Expected Result is same other than timeOffset
progressPercent: 9.027777777777777,
difficultyChange: 13.180707740199772,
difficultyChange: 12.562233927411782,
estimatedRetargetDate: 1661895424692,
remainingBlocks: 1834,
remainingTime: 977591692,
previousTime: 1660820820,
previousRetarget: 0.6280047707459726,
nextRetargetHeight: 751968,
timeAvg: 533038,
timeOffset: -667000, // 11 min 7 seconds since last block (testnet only)
// If we add time avg to abs(timeOffset) it makes exactly 1200000 ms, or 20 minutes
expectedBlocks: 161.68833333333333,
},
],
[ // Vector 3 (mainnet lock-in (epoch ending 788255))
[ // Inputs
dt('2023-04-20T09:57:33.000Z'), // Last DA time (in seconds)
dt('2023-05-04T14:54:09.000Z'), // Current time (now) (in seconds)
788255, // Current block height
1.7220298879531821, // Previous retarget % (Passed through)
'mainnet', // Network (if testnet, next value is non-zero)
dt('2023-05-04T14:54:26.000Z'), // Latest block timestamp in seconds
],
{ // Expected Result
progressPercent: 99.95039682539682,
difficultyChange: -1.4512637555574193,
estimatedRetargetDate: 1683212658129,
remainingBlocks: 1,
remainingTime: 609129,
previousRetarget: 1.7220298879531821,
previousTime: 1681984653,
nextRetargetHeight: 788256,
timeAvg: 609129,
timeOffset: 0,
expectedBlocks: 2045.66,
},
],
] as [[number, number, number, number, string, number], DifficultyAdjustment][];

View File

@@ -42,7 +42,6 @@ describe('Mempool Backend Config', () => {
ADVANCED_GBT_MEMPOOL: false,
CPFP_INDEXING: false,
MAX_BLOCKS_BULK_QUERY: 0,
DISK_CACHE_BLOCK_INTERVAL: 6,
});
expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true });
@@ -53,16 +52,14 @@ describe('Mempool Backend Config', () => {
HOST: '127.0.0.1',
PORT: 8332,
USERNAME: 'mempool',
PASSWORD: 'mempool',
TIMEOUT: 60000
PASSWORD: 'mempool'
});
expect(config.SECOND_CORE_RPC).toStrictEqual({
HOST: '127.0.0.1',
PORT: 8332,
USERNAME: 'mempool',
PASSWORD: 'mempool',
TIMEOUT: 60000
PASSWORD: 'mempool'
});
expect(config.DATABASE).toStrictEqual({
@@ -109,13 +106,6 @@ describe('Mempool Backend Config', () => {
BISQ_URL: 'https://bisq.markets/api',
BISQ_ONION: 'http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api'
});
expect(config.MAXMIND).toStrictEqual({
ENABLED: false,
GEOLITE2_CITY: '/usr/local/share/GeoIP/GeoLite2-City.mmdb',
GEOLITE2_ASN: '/usr/local/share/GeoIP/GeoLite2-ASN.mmdb',
GEOIP2_ISP: '/usr/local/share/GeoIP/GeoIP2-ISP.mmdb'
});
});
});

View File

@@ -1,14 +1,13 @@
import config from '../config';
import logger from '../logger';
import { TransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
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 })
: { censored: string[], added: string[], fresh: string[], score: number, similarity: number } {
: { censored: string[], added: string[], fresh: string[], score: number } {
if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
return { censored: [], added: [], fresh: [], score: 0, similarity: 1 };
return { censored: [], added: [], fresh: [], score: 0 };
}
const matches: string[] = []; // present in both mined block and template
@@ -17,8 +16,6 @@ class Audit {
const isCensored = {}; // missing, without excuse
const isDisplaced = {};
let displacedWeight = 0;
let matchedWeight = 0;
let projectedWeight = 0;
const inBlock = {};
const inTemplate = {};
@@ -40,19 +37,12 @@ class Audit {
} else {
isCensored[txid] = true;
}
displacedWeight += mempool[txid]?.weight || 0;
} else {
matchedWeight += mempool[txid]?.weight || 0;
displacedWeight += mempool[txid].weight;
}
projectedWeight += mempool[txid]?.weight || 0;
inTemplate[txid] = true;
}
if (transactions[0]) {
displacedWeight += (4000 - transactions[0].weight);
projectedWeight += transactions[0].weight;
matchedWeight += transactions[0].weight;
}
displacedWeight += (4000 - transactions[0].weight);
// we can expect an honest miner to include 'displaced' transactions in place of recent arrivals and censored txs
// these displaced transactions should occupy the first N weight units of the next projected block
@@ -62,24 +52,19 @@ class Audit {
let failures = 0;
while (projectedBlocks[1] && index < projectedBlocks[1].transactionIds.length && failures < 500) {
const txid = projectedBlocks[1].transactionIds[index];
const tx = mempool[txid];
if (tx) {
const fits = (tx.weight - displacedWeightRemaining) < 4000;
const feeMatches = tx.effectiveFeePerVsize >= lastFeeRate;
if (fits || feeMatches) {
isDisplaced[txid] = true;
if (fits) {
lastFeeRate = Math.min(lastFeeRate, tx.effectiveFeePerVsize);
}
if (tx.firstSeen == null || (now - (tx?.firstSeen || 0)) > PROPAGATION_MARGIN) {
displacedWeightRemaining -= tx.weight;
}
failures = 0;
} else {
failures++;
const fits = (mempool[txid].weight - displacedWeightRemaining) < 4000;
const feeMatches = mempool[txid].effectiveFeePerVsize >= lastFeeRate;
if (fits || feeMatches) {
isDisplaced[txid] = true;
if (fits) {
lastFeeRate = Math.min(lastFeeRate, mempool[txid].effectiveFeePerVsize);
}
if (mempool[txid].firstSeen == null || (now - (mempool[txid]?.firstSeen || 0)) > PROPAGATION_MARGIN) {
displacedWeightRemaining -= mempool[txid].weight;
}
failures = 0;
} else {
logger.warn('projected transaction missing from mempool cache');
failures++;
}
index++;
}
@@ -93,7 +78,17 @@ class Audit {
} else {
if (!isDisplaced[tx.txid]) {
added.push(tx.txid);
} else {
}
let blockIndex = -1;
let index = -1;
projectedBlocks.forEach((block, bi) => {
const i = block.transactionIds.indexOf(tx.txid);
if (i >= 0) {
blockIndex = bi;
index = i;
}
});
overflowWeight += tx.weight;
}
totalWeight += tx.weight;
@@ -106,39 +101,32 @@ class Audit {
index = projectedBlocks[0].transactionIds.length - 1;
while (index >= 0) {
const txid = projectedBlocks[0].transactionIds[index];
const tx = mempool[txid];
if (tx) {
if (overflowWeightRemaining > 0) {
if (isCensored[txid]) {
delete isCensored[txid];
}
if (tx.effectiveFeePerVsize > maxOverflowRate) {
maxOverflowRate = tx.effectiveFeePerVsize;
rateThreshold = (Math.ceil(maxOverflowRate * 100) / 100) + 0.005;
}
} else if (tx.effectiveFeePerVsize <= rateThreshold) { // tolerance of 0.01 sat/vb + rounding
if (isCensored[txid]) {
delete isCensored[txid];
}
if (overflowWeightRemaining > 0) {
if (isCensored[txid]) {
delete isCensored[txid];
}
if (mempool[txid].effectiveFeePerVsize > maxOverflowRate) {
maxOverflowRate = mempool[txid].effectiveFeePerVsize;
rateThreshold = (Math.ceil(maxOverflowRate * 100) / 100) + 0.005;
}
} else if (mempool[txid].effectiveFeePerVsize <= rateThreshold) { // tolerance of 0.01 sat/vb + rounding
if (isCensored[txid]) {
delete isCensored[txid];
}
overflowWeightRemaining -= (mempool[txid]?.weight || 0);
} else {
logger.warn('projected transaction missing from mempool cache');
}
overflowWeightRemaining -= (mempool[txid]?.weight || 0);
index--;
}
const numCensored = Object.keys(isCensored).length;
const numMatches = matches.length - 1; // adjust for coinbase tx
const score = numMatches > 0 ? (numMatches / (numMatches + numCensored)) : 0;
const similarity = projectedWeight ? matchedWeight / projectedWeight : 1;
return {
censored: Object.keys(isCensored),
added,
fresh,
score,
similarity,
score
};
}
}

View File

@@ -7,7 +7,7 @@ const nodeRpcCredentials: BitcoinRpcCredentials = {
port: config.CORE_RPC.PORT,
user: config.CORE_RPC.USERNAME,
pass: config.CORE_RPC.PASSWORD,
timeout: config.CORE_RPC.TIMEOUT,
timeout: 60000,
};
export default new bitcoin.Client(nodeRpcCredentials);

View File

@@ -7,7 +7,7 @@ const nodeRpcCredentials: BitcoinRpcCredentials = {
port: config.SECOND_CORE_RPC.PORT,
user: config.SECOND_CORE_RPC.USERNAME,
pass: config.SECOND_CORE_RPC.PASSWORD,
timeout: config.SECOND_CORE_RPC.TIMEOUT,
timeout: 60000,
};
export default new bitcoin.Client(nodeRpcCredentials);

View File

@@ -16,7 +16,7 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
super(bitcoinClient);
const electrumConfig = { client: 'mempool-v2', version: '1.4' };
const electrumPersistencePolicy = { retryPeriod: 1000, maxRetry: Number.MAX_SAFE_INTEGER, callback: null };
const electrumPersistencePolicy = { retryPeriod: 10000, maxRetry: 1000, callback: null };
const electrumCallbacks = {
onConnect: (client, versionInfo) => { logger.info(`Connected to Electrum Server at ${config.ELECTRUM.HOST}:${config.ELECTRUM.PORT} (${JSON.stringify(versionInfo)})`); },

View File

@@ -143,10 +143,7 @@ class Blocks {
* @returns BlockSummary
*/
public summarizeBlock(block: IBitcoinApi.VerboseBlock): BlockSummary {
if (Common.isLiquid()) {
block = this.convertLiquidFees(block);
}
const stripped = block.tx.map((tx: IBitcoinApi.VerboseTransaction) => {
const stripped = block.tx.map((tx) => {
return {
txid: tx.txid,
vsize: tx.weight / 4,
@@ -161,13 +158,6 @@ class Blocks {
};
}
private convertLiquidFees(block: IBitcoinApi.VerboseBlock): IBitcoinApi.VerboseBlock {
block.tx.forEach(tx => {
tx.fee = Object.values(tx.fee || {}).reduce((total, output) => total + output, 0);
});
return block;
}
/**
* Return a block with additional data (reward, coinbase, fees...)
* @param block
@@ -520,8 +510,6 @@ class Blocks {
}
public async $updateBlocks() {
diskCache.lock();
let fastForwarded = false;
const blockHeightTip = await bitcoinApi.$getBlockHeightTip();
@@ -560,7 +548,7 @@ class Blocks {
}
while (this.currentBlockHeight < blockHeightTip) {
if (this.currentBlockHeight === 0) {
if (this.currentBlockHeight < blockHeightTip - config.MEMPOOL.INITIAL_BLOCKS_AMOUNT) {
this.currentBlockHeight = blockHeightTip;
} else {
this.currentBlockHeight++;
@@ -583,10 +571,11 @@ class Blocks {
if (!fastForwarded) {
const lastBlock = await blocksRepository.$getBlockByHeight(blockExtended.height - 1);
if (lastBlock !== null && blockExtended.previousblockhash !== lastBlock.id) {
logger.warn(`Chain divergence detected at block ${lastBlock.height}, re-indexing most recent data`, logger.tags.mining);
logger.warn(`Chain divergence detected at block ${lastBlock.height}, re-indexing most recent data`);
// We assume there won't be a reorg with more than 10 block depth
await BlocksRepository.$deleteBlocksFrom(lastBlock.height - 10);
await HashratesRepository.$deleteLastEntries();
await BlocksSummariesRepository.$deleteBlocksFrom(lastBlock.height - 10);
await cpfpRepository.$deleteClustersFrom(lastBlock.height - 10);
for (let i = 10; i >= 0; --i) {
const newBlock = await this.$indexBlock(lastBlock.height - i);
@@ -597,7 +586,7 @@ class Blocks {
}
await mining.$indexDifficultyAdjustments();
await DifficultyAdjustmentsRepository.$deleteLastAdjustment();
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);
logger.info(`Re-indexed 10 blocks and summaries. Also re-indexed the last difficulty adjustments. Will re-index latest hashrates in a few seconds.`);
indexer.reindex();
}
await blocksRepository.$saveBlockInDatabase(blockExtended);
@@ -652,15 +641,13 @@ class Blocks {
if (this.newBlockCallbacks.length) {
this.newBlockCallbacks.forEach((cb) => cb(blockExtended, txIds, transactions));
}
if (!memPool.hasPriority() && (block.height % config.MEMPOOL.DISK_CACHE_BLOCK_INTERVAL === 0)) {
if (!memPool.hasPriority()) {
diskCache.$saveCacheToDisk();
}
// wait for pending async callbacks to finish
await Promise.all(callbackPromises);
}
diskCache.unlock();
}
/**
@@ -731,7 +718,7 @@ class Blocks {
// Index the response if needed
if (Common.blocksSummariesIndexingEnabled() === true) {
await BlocksSummariesRepository.$saveTransactions(block.height, block.hash, summary.transactions);
await BlocksSummariesRepository.$saveSummary({height: block.height, mined: summary});
}
return summary.transactions;
@@ -847,7 +834,7 @@ class Blocks {
if (cleanBlock.fee_amt_percentiles === null) {
const block = await bitcoinClient.getBlock(cleanBlock.hash, 2);
const summary = this.summarizeBlock(block);
await BlocksSummariesRepository.$saveTransactions(cleanBlock.height, cleanBlock.hash, summary.transactions);
await BlocksSummariesRepository.$saveSummary({ height: block.height, mined: summary });
cleanBlock.fee_amt_percentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(cleanBlock.hash);
}
if (cleanBlock.fee_amt_percentiles !== null) {

View File

@@ -1,4 +1,4 @@
import { CpfpInfo, MempoolBlockWithTransactions, TransactionExtended, TransactionStripped } from '../mempool.interfaces';
import { CpfpInfo, TransactionExtended, TransactionStripped } from '../mempool.interfaces';
import config from '../config';
import { NodeSocket } from '../repositories/NodesSocketsRepository';
import { isIP } from 'net';
@@ -164,30 +164,6 @@ export class Common {
return parents;
}
// calculates the ratio of matched transactions to projected transactions by weight
static getSimilarity(projectedBlock: MempoolBlockWithTransactions, transactions: TransactionExtended[]): number {
let matchedWeight = 0;
let projectedWeight = 0;
const inBlock = {};
for (const tx of transactions) {
inBlock[tx.txid] = tx;
}
// look for transactions that were expected in the template, but missing from the mined block
for (const tx of projectedBlock.transactions) {
if (inBlock[tx.txid]) {
matchedWeight += tx.vsize * 4;
}
projectedWeight += tx.vsize * 4;
}
projectedWeight += transactions[0].weight;
matchedWeight += transactions[0].weight;
return projectedWeight ? matchedWeight / projectedWeight : 1;
}
static getSqlInterval(interval: string | null): string | null {
switch (interval) {
case '24h': return '1 DAY';

View File

@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2';
class DatabaseMigration {
private static currentVersion = 59;
private static currentVersion = 58;
private queryTimeout = 3600_000;
private statisticsAddedIndexed = false;
private uniqueLogs: string[] = [];
@@ -497,7 +497,6 @@ class DatabaseMigration {
this.uniqueLog(logger.notice, this.blocksTruncatedMessage);
await this.$executeQuery('DELETE FROM `pools`');
await this.$executeQuery('ALTER TABLE pools AUTO_INCREMENT = 1');
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
this.uniqueLog(logger.notice, '`pools` table has been truncated`');
await this.updateToSchemaVersion(56);
}
@@ -511,11 +510,6 @@ class DatabaseMigration {
// We only run some migration queries for this version
await this.updateToSchemaVersion(58);
}
if (databaseSchemaVersion < 59 && (config.MEMPOOL.NETWORK === 'signet' || config.MEMPOOL.NETWORK === 'testnet')) {
// https://github.com/mempool/mempool/issues/3360
await this.$executeQuery(`TRUNCATE prices`);
}
}
/**

View File

@@ -9,11 +9,9 @@ export interface DifficultyAdjustment {
remainingBlocks: number; // Block count
remainingTime: number; // Duration of time in ms
previousRetarget: number; // Percent: -75 to 300
previousTime: number; // Unix time in ms
nextRetargetHeight: number; // Block Height
timeAvg: number; // Duration of time in ms
timeOffset: number; // (Testnet) Time since last block (cap @ 20min) in ms
expectedBlocks: number; // Block count
}
export function calcDifficultyAdjustment(
@@ -24,29 +22,31 @@ export function calcDifficultyAdjustment(
network: string,
latestBlockTimestamp: number,
): DifficultyAdjustment {
const ESTIMATE_LAG_BLOCKS = 146; // For first 7.2% of epoch, don't estimate.
const EPOCH_BLOCK_LENGTH = 2016; // Bitcoin mainnet
const BLOCK_SECONDS_TARGET = 600; // Bitcoin mainnet
const TESTNET_MAX_BLOCK_SECONDS = 1200; // Bitcoin testnet
const diffSeconds = Math.max(0, nowSeconds - DATime);
const diffSeconds = nowSeconds - DATime;
const blocksInEpoch = (blockHeight >= 0) ? blockHeight % EPOCH_BLOCK_LENGTH : 0;
const progressPercent = (blockHeight >= 0) ? blocksInEpoch / EPOCH_BLOCK_LENGTH * 100 : 100;
const remainingBlocks = EPOCH_BLOCK_LENGTH - blocksInEpoch;
const nextRetargetHeight = (blockHeight >= 0) ? blockHeight + remainingBlocks : 0;
const expectedBlocks = diffSeconds / BLOCK_SECONDS_TARGET;
const actualTimespan = (blocksInEpoch === 2015 ? latestBlockTimestamp : nowSeconds) - DATime;
let difficultyChange = 0;
let timeAvgSecs = blocksInEpoch ? diffSeconds / blocksInEpoch : BLOCK_SECONDS_TARGET;
difficultyChange = (BLOCK_SECONDS_TARGET / (actualTimespan / (blocksInEpoch + 1)) - 1) * 100;
// Max increase is x4 (+300%)
if (difficultyChange > 300) {
difficultyChange = 300;
}
// Max decrease is /4 (-75%)
if (difficultyChange < -75) {
difficultyChange = -75;
let timeAvgSecs = BLOCK_SECONDS_TARGET;
// Only calculate the estimate once we have 7.2% of blocks in current epoch
if (blocksInEpoch >= ESTIMATE_LAG_BLOCKS) {
timeAvgSecs = diffSeconds / blocksInEpoch;
difficultyChange = (BLOCK_SECONDS_TARGET / timeAvgSecs - 1) * 100;
// Max increase is x4 (+300%)
if (difficultyChange > 300) {
difficultyChange = 300;
}
// Max decrease is /4 (-75%)
if (difficultyChange < -75) {
difficultyChange = -75;
}
}
// Testnet difficulty is set to 1 after 20 minutes of no blocks,
@@ -74,11 +74,9 @@ export function calcDifficultyAdjustment(
remainingBlocks,
remainingTime,
previousRetarget,
previousTime: DATime,
nextRetargetHeight,
timeAvg,
timeOffset,
expectedBlocks,
};
}

View File

@@ -11,96 +11,48 @@ import { Common } from './common';
class DiskCache {
private cacheSchemaVersion = 3;
private static TMP_FILE_NAME = config.MEMPOOL.CACHE_DIR + '/tmp-cache.json';
private static TMP_FILE_NAMES = config.MEMPOOL.CACHE_DIR + '/tmp-cache{number}.json';
private static FILE_NAME = config.MEMPOOL.CACHE_DIR + '/cache.json';
private static FILE_NAMES = config.MEMPOOL.CACHE_DIR + '/cache{number}.json';
private static CHUNK_FILES = 25;
private isWritingCache = false;
private semaphore: { resume: (() => void)[], locks: number } = {
resume: [],
locks: 0,
};
constructor() { }
constructor() {
if (!cluster.isPrimary) {
return;
}
process.on('SIGINT', (e) => {
this.$saveCacheToDisk(true);
process.exit(0);
});
}
async $saveCacheToDisk(sync: boolean = false): Promise<void> {
async $saveCacheToDisk(): Promise<void> {
if (!cluster.isPrimary) {
return;
}
if (this.isWritingCache) {
logger.debug('Saving cache already in progress. Skipping.');
logger.debug('Saving cache already in progress. Skipping.')
return;
}
try {
logger.debug(`Writing mempool and blocks data to disk cache (${ sync ? 'sync' : 'async' })...`);
logger.debug('Writing mempool and blocks data to disk cache (async)...');
this.isWritingCache = true;
const mempool = memPool.getMempool();
const mempoolArray: TransactionExtended[] = [];
for (const tx in mempool) {
if (mempool[tx] && !mempool[tx].deleteAfter) {
mempoolArray.push(mempool[tx]);
}
mempoolArray.push(mempool[tx]);
}
Common.shuffleArray(mempoolArray);
const chunkSize = Math.floor(mempoolArray.length / DiskCache.CHUNK_FILES);
if (sync) {
fs.writeFileSync(DiskCache.TMP_FILE_NAME, JSON.stringify({
network: config.MEMPOOL.NETWORK,
cacheSchemaVersion: this.cacheSchemaVersion,
blocks: blocks.getBlocks(),
blockSummaries: blocks.getBlockSummaries(),
await fsPromises.writeFile(DiskCache.FILE_NAME, JSON.stringify({
cacheSchemaVersion: this.cacheSchemaVersion,
blocks: blocks.getBlocks(),
blockSummaries: blocks.getBlockSummaries(),
mempool: {},
mempoolArray: mempoolArray.splice(0, chunkSize),
}), { flag: 'w' });
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
await fsPromises.writeFile(DiskCache.FILE_NAMES.replace('{number}', i.toString()), JSON.stringify({
mempool: {},
mempoolArray: mempoolArray.splice(0, chunkSize),
}), { flag: 'w' });
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
fs.writeFileSync(DiskCache.TMP_FILE_NAMES.replace('{number}', i.toString()), JSON.stringify({
mempool: {},
mempoolArray: mempoolArray.splice(0, chunkSize),
}), { flag: 'w' });
}
fs.renameSync(DiskCache.TMP_FILE_NAME, DiskCache.FILE_NAME);
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
fs.renameSync(DiskCache.TMP_FILE_NAMES.replace('{number}', i.toString()), DiskCache.FILE_NAMES.replace('{number}', i.toString()));
}
} else {
await this.$yield();
await fsPromises.writeFile(DiskCache.TMP_FILE_NAME, JSON.stringify({
network: config.MEMPOOL.NETWORK,
cacheSchemaVersion: this.cacheSchemaVersion,
blocks: blocks.getBlocks(),
blockSummaries: blocks.getBlockSummaries(),
mempool: {},
mempoolArray: mempoolArray.splice(0, chunkSize),
}), { flag: 'w' });
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
await this.$yield();
await fsPromises.writeFile(DiskCache.TMP_FILE_NAMES.replace('{number}', i.toString()), JSON.stringify({
mempool: {},
mempoolArray: mempoolArray.splice(0, chunkSize),
}), { flag: 'w' });
}
await fsPromises.rename(DiskCache.TMP_FILE_NAME, DiskCache.FILE_NAME);
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
await fsPromises.rename(DiskCache.TMP_FILE_NAMES.replace('{number}', i.toString()), DiskCache.FILE_NAMES.replace('{number}', i.toString()));
}
}
logger.debug('Mempool and blocks data saved to disk cache');
this.isWritingCache = false;
} catch (e) {
@@ -109,7 +61,7 @@ class DiskCache {
}
}
wipeCache(): void {
wipeCache() {
logger.notice(`Wiping nodejs backend cache/cache*.json files`);
try {
fs.unlinkSync(DiskCache.FILE_NAME);
@@ -131,7 +83,7 @@ class DiskCache {
}
}
async $loadMempoolCache(): Promise<void> {
loadMempoolCache() {
if (!fs.existsSync(DiskCache.FILE_NAME)) {
return;
}
@@ -145,10 +97,6 @@ class DiskCache {
logger.notice('Disk cache contains an outdated schema version. Clearing it and skipping the cache loading.');
return this.wipeCache();
}
if (data.network && data.network !== config.MEMPOOL.NETWORK) {
logger.notice('Disk cache contains data from a different network. Clearing it and skipping the cache loading.');
return this.wipeCache();
}
if (data.mempoolArray) {
for (const tx of data.mempoolArray) {
@@ -175,39 +123,13 @@ class DiskCache {
}
}
await memPool.$setMempool(data.mempool);
memPool.setMempool(data.mempool);
blocks.setBlocks(data.blocks);
blocks.setBlockSummaries(data.blockSummaries || []);
} catch (e) {
logger.warn('Failed to parse mempoool and blocks cache. Skipping. Reason: ' + (e instanceof Error ? e.message : e));
}
}
private $yield(): Promise<void> {
if (this.semaphore.locks) {
logger.debug('Pause writing mempool and blocks data to disk cache (async)');
return new Promise((resolve) => {
this.semaphore.resume.push(resolve);
});
} else {
return Promise.resolve();
}
}
public lock(): void {
this.semaphore.locks++;
}
public unlock(): void {
this.semaphore.locks = Math.max(0, this.semaphore.locks - 1);
if (!this.semaphore.locks && this.semaphore.resume.length) {
const nextResume = this.semaphore.resume.shift();
if (nextResume) {
logger.debug('Resume writing mempool and blocks data to disk cache (async)');
nextResume();
}
}
}
}
export default new DiskCache();

View File

@@ -417,24 +417,24 @@ class NodesApi {
if (!ispList[isp1]) {
ispList[isp1] = {
ids: [channel.isp1ID],
id: channel.isp1ID.toString(),
capacity: 0,
channels: 0,
nodes: {},
};
} else if (ispList[isp1].ids.includes(channel.isp1ID) === false) {
ispList[isp1].ids.push(channel.isp1ID);
} else if (ispList[isp1].id.indexOf(channel.isp1ID) === -1) {
ispList[isp1].id += ',' + channel.isp1ID.toString();
}
if (!ispList[isp2]) {
ispList[isp2] = {
ids: [channel.isp2ID],
id: channel.isp2ID.toString(),
capacity: 0,
channels: 0,
nodes: {},
};
} else if (ispList[isp2].ids.includes(channel.isp2ID) === false) {
ispList[isp2].ids.push(channel.isp2ID);
} else if (ispList[isp2].id.indexOf(channel.isp2ID) === -1) {
ispList[isp2].id += ',' + channel.isp2ID.toString();
}
ispList[isp1].capacity += channel.capacity;
@@ -444,11 +444,11 @@ class NodesApi {
ispList[isp2].channels += 1;
ispList[isp2].nodes[channel.node2PublicKey] = true;
}
const ispRanking: any[] = [];
for (const isp of Object.keys(ispList)) {
ispRanking.push([
ispList[isp].ids.sort((a, b) => a - b).join(','),
ispList[isp].id,
isp,
ispList[isp].capacity,
ispList[isp].channels,

View File

@@ -4,29 +4,21 @@ import * as fs from 'fs';
import { AbstractLightningApi } from '../lightning-api-abstract-factory';
import { ILightningApi } from '../lightning-api.interface';
import config from '../../../config';
import logger from '../../../logger';
class LndApi implements AbstractLightningApi {
axiosConfig: AxiosRequestConfig = {};
constructor() {
if (!config.LIGHTNING.ENABLED) {
return;
}
try {
if (config.LIGHTNING.ENABLED) {
this.axiosConfig = {
headers: {
'Grpc-Metadata-macaroon': fs.readFileSync(config.LND.MACAROON_PATH).toString('hex'),
'Grpc-Metadata-macaroon': fs.readFileSync(config.LND.MACAROON_PATH).toString('hex')
},
httpsAgent: new Agent({
ca: fs.readFileSync(config.LND.TLS_CERT_PATH)
}),
timeout: config.LND.TIMEOUT
timeout: 10000
};
} catch (e) {
config.LIGHTNING.ENABLED = false;
logger.updateNetwork();
logger.err(`Could not initialize LND Macaroon/TLS Cert. Disabling LIGHTNING. ` + (e instanceof Error ? e.message : e));
}
}

View File

@@ -1,5 +1,5 @@
import logger from '../logger';
import { MempoolBlock, TransactionExtended, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction } from '../mempool.interfaces';
import { MempoolBlock, TransactionExtended, ThreadTransaction, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor } from '../mempool.interfaces';
import { Common } from './common';
import config from '../config';
import { Worker } from 'worker_threads';
@@ -10,9 +10,6 @@ class MempoolBlocks {
private mempoolBlockDeltas: MempoolBlockDelta[] = [];
private txSelectionWorker: Worker | null = null;
private nextUid: number = 1;
private uidMap: Map<number, string> = new Map(); // map short numerical uids to full txids
constructor() {}
public getMempoolBlocks(): MempoolBlock[] {
@@ -90,34 +87,19 @@ class MempoolBlocks {
private calculateMempoolBlocks(transactionsSorted: TransactionExtended[], prevBlocks: MempoolBlockWithTransactions[]): MempoolBlockWithTransactions[] {
const mempoolBlocks: MempoolBlockWithTransactions[] = [];
let blockSize = 0;
let blockWeight = 0;
let blockVsize = 0;
let blockFees = 0;
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
let transactionIds: string[] = [];
let blockSize = 0;
let transactions: TransactionExtended[] = [];
transactionsSorted.forEach((tx) => {
if (blockWeight + tx.weight <= config.MEMPOOL.BLOCK_WEIGHT_UNITS
|| mempoolBlocks.length === config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT - 1) {
blockWeight += tx.weight;
blockVsize += tx.vsize;
blockSize += tx.size;
blockFees += tx.fee;
transactions.push(tx);
transactionIds.push(tx.txid);
} else {
mempoolBlocks.push(this.dataToMempoolBlocks(transactions, mempoolBlocks.length));
blockVsize = 0;
tx.position = {
block: mempoolBlocks.length,
vsize: blockVsize + (tx.vsize / 2),
};
blockVsize += tx.vsize;
blockWeight = tx.weight;
blockSize = tx.size;
blockFees = tx.fee;
transactionIds = [tx.txid];
transactions = [tx];
}
});
@@ -165,29 +147,19 @@ class MempoolBlocks {
return mempoolBlockDeltas;
}
public async $makeBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, saveResults: boolean = false): Promise<MempoolBlockWithTransactions[]> {
const start = Date.now();
// reset mempool short ids
this.resetUids();
for (const tx of Object.values(newMempool)) {
this.setUid(tx);
}
public async makeBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, saveResults: boolean = false): Promise<MempoolBlockWithTransactions[]> {
// prepare a stripped down version of the mempool with only the minimum necessary data
// to reduce the overhead of passing this data to the worker thread
const strippedMempool: Map<number, CompactThreadTransaction> = new Map();
const strippedMempool: { [txid: string]: ThreadTransaction } = {};
Object.values(newMempool).forEach(entry => {
if (entry.uid != null) {
strippedMempool.set(entry.uid, {
uid: entry.uid,
fee: entry.fee,
weight: entry.weight,
feePerVsize: entry.fee / (entry.weight / 4),
effectiveFeePerVsize: entry.effectiveFeePerVsize || (entry.fee / (entry.weight / 4)),
inputs: entry.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => uid != null) as number[],
});
}
strippedMempool[entry.txid] = {
txid: entry.txid,
fee: entry.fee,
weight: entry.weight,
feePerVsize: entry.fee / (entry.weight / 4),
effectiveFeePerVsize: entry.fee / (entry.weight / 4),
vin: entry.vin.map(v => v.txid),
};
});
// (re)initialize tx selection worker thread
@@ -206,7 +178,7 @@ class MempoolBlocks {
// run the block construction algorithm in a separate thread, and wait for a result
let threadErrorListener;
try {
const workerResultPromise = new Promise<{ blocks: number[][], rates: Map<number, number>, clusters: Map<number, number[]> }>((resolve, reject) => {
const workerResultPromise = new Promise<{ blocks: ThreadTransaction[][], clusters: { [root: string]: string[] } }>((resolve, reject) => {
threadErrorListener = reject;
this.txSelectionWorker?.once('message', (result): void => {
resolve(result);
@@ -214,149 +186,103 @@ class MempoolBlocks {
this.txSelectionWorker?.once('error', reject);
});
this.txSelectionWorker.postMessage({ type: 'set', mempool: strippedMempool });
const { blocks, rates, clusters } = this.convertResultTxids(await workerResultPromise);
const { blocks, clusters } = await workerResultPromise;
// clean up thread error listener
this.txSelectionWorker?.removeListener('error', threadErrorListener);
const processed = this.processBlockTemplates(newMempool, blocks, rates, clusters, saveResults);
logger.debug(`makeBlockTemplates completed in ${(Date.now() - start)/1000} seconds`);
return processed;
return this.processBlockTemplates(newMempool, blocks, clusters, saveResults);
} catch (e) {
logger.err('makeBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
}
return this.mempoolBlocks;
}
public async $updateBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, added: TransactionExtended[], removed: TransactionExtended[], saveResults: boolean = false): Promise<void> {
public async updateBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, added: TransactionExtended[], removed: string[], saveResults: boolean = false): Promise<void> {
if (!this.txSelectionWorker) {
// need to reset the worker
await this.$makeBlockTemplates(newMempool, saveResults);
this.makeBlockTemplates(newMempool, saveResults);
return;
}
const start = Date.now();
for (const tx of Object.values(added)) {
this.setUid(tx);
}
const removedUids = removed.map(tx => this.getUid(tx)).filter(uid => uid != null) as number[];
// prepare a stripped down version of the mempool with only the minimum necessary data
// to reduce the overhead of passing this data to the worker thread
const addedStripped: CompactThreadTransaction[] = added.filter(entry => entry.uid != null).map(entry => {
const addedStripped: ThreadTransaction[] = added.map(entry => {
return {
uid: entry.uid || 0,
txid: entry.txid,
fee: entry.fee,
weight: entry.weight,
feePerVsize: entry.fee / (entry.weight / 4),
effectiveFeePerVsize: entry.effectiveFeePerVsize || (entry.fee / (entry.weight / 4)),
inputs: entry.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => uid != null) as number[],
effectiveFeePerVsize: entry.fee / (entry.weight / 4),
vin: entry.vin.map(v => v.txid),
};
});
// run the block construction algorithm in a separate thread, and wait for a result
let threadErrorListener;
try {
const workerResultPromise = new Promise<{ blocks: number[][], rates: Map<number, number>, clusters: Map<number, number[]> }>((resolve, reject) => {
const workerResultPromise = new Promise<{ blocks: ThreadTransaction[][], clusters: { [root: string]: string[] } }>((resolve, reject) => {
threadErrorListener = reject;
this.txSelectionWorker?.once('message', (result): void => {
resolve(result);
});
this.txSelectionWorker?.once('error', reject);
});
this.txSelectionWorker.postMessage({ type: 'update', added: addedStripped, removed: removedUids });
const { blocks, rates, clusters } = this.convertResultTxids(await workerResultPromise);
this.removeUids(removedUids);
this.txSelectionWorker.postMessage({ type: 'update', added: addedStripped, removed });
const { blocks, clusters } = await workerResultPromise;
// clean up thread error listener
this.txSelectionWorker?.removeListener('error', threadErrorListener);
this.processBlockTemplates(newMempool, blocks, rates, clusters, saveResults);
logger.debug(`updateBlockTemplates completed in ${(Date.now() - start) / 1000} seconds`);
this.processBlockTemplates(newMempool, blocks, clusters, saveResults);
} catch (e) {
logger.err('updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
}
}
private processBlockTemplates(mempool, blocks: string[][], rates: { [root: string]: number }, clusters: { [root: string]: string[] }, saveResults): MempoolBlockWithTransactions[] {
for (const txid of Object.keys(rates)) {
if (txid in mempool) {
mempool[txid].effectiveFeePerVsize = rates[txid];
}
}
const readyBlocks: { transactionIds, transactions, totalSize, totalWeight, totalFees }[] = [];
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
private processBlockTemplates(mempool, blocks, clusters, saveResults): MempoolBlockWithTransactions[] {
// update this thread's mempool with the results
for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
const block: string[] = blocks[blockIndex];
let txid: string;
let mempoolTx: TransactionExtended;
let totalSize = 0;
let totalVsize = 0;
let totalWeight = 0;
let totalFees = 0;
const transactions: TransactionExtended[] = [];
for (let txIndex = 0; txIndex < block.length; txIndex++) {
txid = block[txIndex];
if (txid) {
mempoolTx = mempool[txid];
// save position in projected blocks
mempoolTx.position = {
block: blockIndex,
vsize: totalVsize + (mempoolTx.vsize / 2),
};
mempoolTx.cpfpChecked = true;
totalSize += mempoolTx.size;
totalVsize += mempoolTx.vsize;
totalWeight += mempoolTx.weight;
totalFees += mempoolTx.fee;
transactions.push(mempoolTx);
}
}
readyBlocks.push({
transactionIds: block,
transactions,
totalSize,
totalWeight,
totalFees
});
}
for (const cluster of Object.values(clusters)) {
for (const memberTxid of cluster) {
if (memberTxid in mempool) {
const mempoolTx = mempool[memberTxid];
const ancestors: Ancestor[] = [];
const descendants: Ancestor[] = [];
let matched = false;
cluster.forEach(txid => {
if (txid === memberTxid) {
matched = true;
} else {
const relative = {
txid: txid,
fee: mempool[txid].fee,
weight: mempool[txid].weight,
};
if (matched) {
descendants.push(relative);
blocks.forEach(block => {
block.forEach(tx => {
if (tx.txid in mempool) {
if (tx.effectiveFeePerVsize != null) {
mempool[tx.txid].effectiveFeePerVsize = tx.effectiveFeePerVsize;
}
if (tx.cpfpRoot && tx.cpfpRoot in clusters) {
const ancestors: Ancestor[] = [];
const descendants: Ancestor[] = [];
const cluster = clusters[tx.cpfpRoot];
let matched = false;
cluster.forEach(txid => {
if (txid === tx.txid) {
matched = true;
} else {
ancestors.push(relative);
const relative = {
txid: txid,
fee: mempool[txid].fee,
weight: mempool[txid].weight,
};
if (matched) {
descendants.push(relative);
} else {
ancestors.push(relative);
}
}
}
});
mempoolTx.ancestors = ancestors;
mempoolTx.descendants = descendants;
mempoolTx.bestDescendant = null;
});
mempool[tx.txid].ancestors = ancestors;
mempool[tx.txid].descendants = descendants;
mempool[tx.txid].bestDescendant = null;
}
mempool[tx.txid].cpfpChecked = tx.cpfpChecked;
}
}
}
});
});
const mempoolBlocks = readyBlocks.map((b, i) => this.dataToMempoolBlocks(b.transactions, i));
// unpack the condensed blocks into proper mempool blocks
const mempoolBlocks = blocks.map((transactions, blockIndex) => {
return this.dataToMempoolBlocks(transactions.map(tx => {
return mempool[tx.txid] || null;
}).filter(tx => !!tx), blockIndex);
});
if (saveResults) {
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks);
@@ -398,56 +324,6 @@ class MempoolBlocks {
transactions: fitTransactions.map((tx) => Common.stripTransaction(tx)),
};
}
private resetUids(): void {
this.uidMap.clear();
this.nextUid = 1;
}
private setUid(tx: TransactionExtended): number {
const uid = this.nextUid;
this.nextUid++;
this.uidMap.set(uid, tx.txid);
tx.uid = uid;
return uid;
}
private getUid(tx: TransactionExtended): number | void {
if (tx?.uid != null && this.uidMap.has(tx.uid)) {
return tx.uid;
}
}
private removeUids(uids: number[]): void {
for (const uid of uids) {
this.uidMap.delete(uid);
}
}
private convertResultTxids({ blocks, rates, clusters }: { blocks: number[][], rates: Map<number, number>, clusters: Map<number, number[]>})
: { blocks: string[][], rates: { [root: string]: number }, clusters: { [root: string]: string[] }} {
const convertedBlocks: string[][] = blocks.map(block => block.map(uid => {
return this.uidMap.get(uid) || '';
}));
const convertedRates = {};
for (const rateUid of rates.keys()) {
const rateTxid = this.uidMap.get(rateUid);
if (rateTxid) {
convertedRates[rateTxid] = rates.get(rateUid);
}
}
const convertedClusters = {};
for (const rootUid of clusters.keys()) {
const rootTxid = this.uidMap.get(rootUid);
if (rootTxid) {
const members = clusters.get(rootUid)?.map(uid => {
return this.uidMap.get(uid);
});
convertedClusters[rootTxid] = members;
}
}
return { blocks: convertedBlocks, rates: convertedRates, clusters: convertedClusters } as { blocks: string[][], rates: { [root: string]: number }, clusters: { [root: string]: string[] }};
}
}
export default new MempoolBlocks();

View File

@@ -20,7 +20,7 @@ class Mempool {
maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 };
private mempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
deletedTransactions: TransactionExtended[]) => void) | undefined;
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
private asyncMempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
deletedTransactions: TransactionExtended[]) => Promise<void>) | undefined;
private txPerSecondArray: number[] = [];
@@ -31,13 +31,9 @@ class Mempool {
private mempoolProtection = 0;
private latestTransactions: any[] = [];
private ESPLORA_MISSING_TX_WARNING_THRESHOLD = 100;
private SAMPLE_TIME = 10000; // In ms
private timer = new Date().getTime();
private missingTxCount = 0;
constructor() {
setInterval(this.updateTxPerSecond.bind(this), 1000);
setInterval(this.deleteExpiredTransactions.bind(this), 20000);
}
/**
@@ -71,20 +67,20 @@ class Mempool {
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: TransactionExtended; },
newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]) => Promise<void>) {
this.$asyncMempoolChangedCallback = fn;
this.asyncMempoolChangedCallback = fn;
}
public getMempool(): { [txid: string]: TransactionExtended } {
return this.mempoolCache;
}
public async $setMempool(mempoolData: { [txId: string]: TransactionExtended }) {
public setMempool(mempoolData: { [txId: string]: TransactionExtended }) {
this.mempoolCache = mempoolData;
if (this.mempoolChangedCallback) {
this.mempoolChangedCallback(this.mempoolCache, [], []);
}
if (this.$asyncMempoolChangedCallback) {
await this.$asyncMempoolChangedCallback(this.mempoolCache, [], []);
if (this.asyncMempoolChangedCallback) {
this.asyncMempoolChangedCallback(this.mempoolCache, [], []);
}
}
@@ -132,16 +128,6 @@ class Mempool {
loadingIndicators.setProgress('mempool', Object.keys(this.mempoolCache).length / transactions.length * 100);
}
// https://github.com/mempool/mempool/issues/3283
const logEsplora404 = (missingTxCount, threshold, time) => {
const log = `In the past ${time / 1000} seconds, esplora tx API replied ${missingTxCount} times with a 404 error code while updating nodejs backend mempool`;
if (missingTxCount >= threshold) {
logger.warn(log);
} else if (missingTxCount > 0) {
logger.debug(log);
}
};
for (const txid of transactions) {
if (!this.mempoolCache[txid]) {
try {
@@ -156,10 +142,7 @@ class Mempool {
}
hasChange = true;
newTransactions.push(transaction);
} catch (e: any) {
if (config.MEMPOOL.BACKEND === 'esplora' && e.response?.status === 404) {
this.missingTxCount++;
}
} catch (e) {
logger.debug(`Error finding transaction '${txid}' in the mempool: ` + (e instanceof Error ? e.message : e));
}
}
@@ -169,14 +152,6 @@ class Mempool {
}
}
// Reset esplora 404 counter and log a warning if needed
const elapsedTime = new Date().getTime() - this.timer;
if (elapsedTime > this.SAMPLE_TIME) {
logEsplora404(this.missingTxCount, this.ESPLORA_MISSING_TX_WARNING_THRESHOLD, elapsedTime);
this.timer = new Date().getTime();
this.missingTxCount = 0;
}
// Prevent mempool from clear on bitcoind restart by delaying the deletion
if (this.mempoolProtection === 0
&& currentMempoolSize > 20000
@@ -222,8 +197,8 @@ class Mempool {
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
}
if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length)) {
await this.$asyncMempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
if (this.asyncMempoolChangedCallback && (hasChange || deletedTransactions.length)) {
await this.asyncMempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
}
const end = new Date().getTime();
@@ -255,7 +230,7 @@ class Mempool {
}
}
public deleteExpiredTransactions() {
private deleteExpiredTransactions() {
const now = new Date().getTime();
for (const tx in this.mempoolCache) {
const lazyDeleteAt = this.mempoolCache[tx].deleteAfter;

View File

@@ -13,7 +13,6 @@ 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;
@@ -142,9 +141,6 @@ class Mining {
const blockCount1w: number = await BlocksRepository.$blockCount(pool.id, '1w');
const totalBlock1w: number = await BlocksRepository.$blockCount(null, '1w');
const avgHealth = await BlocksRepository.$getAvgBlockHealthPerPoolId(pool.id);
const totalReward = await BlocksRepository.$getTotalRewardForPoolId(pool.id);
let currentEstimatedHashrate = 0;
try {
currentEstimatedHashrate = await bitcoinClient.getNetworkHashPs(totalBlock24h);
@@ -166,8 +162,6 @@ class Mining {
},
estimatedHashrate: currentEstimatedHashrate * (blockCount24h / totalBlock24h),
reportedHashrate: null,
avgBlockHealth: avgHealth,
totalReward: totalReward,
};
}

View File

@@ -1,11 +1,11 @@
import config from '../config';
import logger from '../logger';
import { CompactThreadTransaction, AuditTransaction } from '../mempool.interfaces';
import { ThreadTransaction, MempoolBlockWithTransactions, AuditTransaction } from '../mempool.interfaces';
import { PairingHeap } from '../utils/pairing-heap';
import { Common } from './common';
import { parentPort } from 'worker_threads';
let mempool: Map<number, CompactThreadTransaction> = new Map();
let mempool: { [txid: string]: ThreadTransaction } = {};
if (parentPort) {
parentPort.on('message', (params) => {
@@ -13,18 +13,18 @@ if (parentPort) {
mempool = params.mempool;
} else if (params.type === 'update') {
params.added.forEach(tx => {
mempool.set(tx.uid, tx);
mempool[tx.txid] = tx;
});
params.removed.forEach(uid => {
mempool.delete(uid);
params.removed.forEach(txid => {
delete mempool[txid];
});
}
const { blocks, rates, clusters } = makeBlockTemplates(mempool);
const { blocks, clusters } = makeBlockTemplates(mempool);
// return the result to main thread.
if (parentPort) {
parentPort.postMessage({ blocks, rates, clusters });
parentPort.postMessage({ blocks, clusters });
}
});
}
@@ -33,25 +33,26 @@ if (parentPort) {
* Build projected mempool blocks using an approximation of the transaction selection algorithm from Bitcoin Core
* (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp)
*/
function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
: { blocks: number[][], rates: Map<number, number>, clusters: Map<number, number[]> } {
function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction })
: { blocks: ThreadTransaction[][], clusters: { [root: string]: string[] } } {
const start = Date.now();
const auditPool: Map<number, AuditTransaction> = new Map();
const auditPool: { [txid: string]: AuditTransaction } = {};
const mempoolArray: AuditTransaction[] = [];
const cpfpClusters: Map<number, number[]> = new Map();
const restOfArray: ThreadTransaction[] = [];
const cpfpClusters: { [root: string]: string[] } = {};
mempool.forEach(tx => {
tx.dirty = false;
// grab the top feerate txs up to maxWeight
Object.values(mempool).sort((a, b) => b.feePerVsize - a.feePerVsize).forEach(tx => {
// initializing everything up front helps V8 optimize property access later
auditPool.set(tx.uid, {
uid: tx.uid,
auditPool[tx.txid] = {
txid: tx.txid,
fee: tx.fee,
weight: tx.weight,
feePerVsize: tx.feePerVsize,
effectiveFeePerVsize: tx.feePerVsize,
inputs: tx.inputs || [],
vin: tx.vin,
relativesSet: false,
ancestorMap: new Map<number, AuditTransaction>(),
ancestorMap: new Map<string, AuditTransaction>(),
children: new Set<AuditTransaction>(),
ancestorFee: 0,
ancestorWeight: 0,
@@ -59,8 +60,8 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
used: false,
modified: false,
modifiedNode: null,
});
mempoolArray.push(auditPool.get(tx.uid) as AuditTransaction);
};
mempoolArray.push(auditPool[tx.txid]);
});
// Build relatives graph & calculate ancestor scores
@@ -71,28 +72,15 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
}
// Sort by descending ancestor score
mempoolArray.sort((a, b) => {
if (b.score === a.score) {
// tie-break by uid for stability
return a.uid < b.uid ? -1 : 1;
} else {
return (b.score || 0) - (a.score || 0);
}
});
mempoolArray.sort((a, b) => (b.score || 0) - (a.score || 0));
// Build blocks by greedily choosing the highest feerate package
// (i.e. the package rooted in the transaction with the best ancestor score)
const blocks: number[][] = [];
const blocks: ThreadTransaction[][] = [];
let blockWeight = 4000;
let blockSize = 0;
let transactions: AuditTransaction[] = [];
const modified: PairingHeap<AuditTransaction> = new PairingHeap((a, b): boolean => {
if (a.score === b.score) {
// tie-break by uid for stability
return a.uid > b.uid;
} else {
return (a.score || 0) > (b.score || 0);
}
});
const modified: PairingHeap<AuditTransaction> = new PairingHeap((a, b): boolean => (a.score || 0) > (b.score || 0));
let overflow: AuditTransaction[] = [];
let failures = 0;
let top = 0;
@@ -119,36 +107,30 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
if (nextTx && !nextTx?.used) {
// Check if the package fits into this block
if (blocks.length >= 7 || (blockWeight + nextTx.ancestorWeight < config.MEMPOOL.BLOCK_WEIGHT_UNITS)) {
if (blockWeight + nextTx.ancestorWeight < config.MEMPOOL.BLOCK_WEIGHT_UNITS) {
const ancestors: AuditTransaction[] = Array.from(nextTx.ancestorMap.values());
// sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx];
let isCluster = false;
if (sortedTxSet.length > 1) {
cpfpClusters.set(nextTx.uid, sortedTxSet.map(tx => tx.uid));
cpfpClusters[nextTx.txid] = sortedTxSet.map(tx => tx.txid);
isCluster = true;
}
const effectiveFeeRate = nextTx.ancestorFee / (nextTx.ancestorWeight / 4);
const used: AuditTransaction[] = [];
while (sortedTxSet.length) {
const ancestor = sortedTxSet.pop();
const mempoolTx = mempool.get(ancestor.uid);
if (!mempoolTx) {
continue;
}
const mempoolTx = mempool[ancestor.txid];
ancestor.used = true;
ancestor.usedBy = nextTx.uid;
ancestor.usedBy = nextTx.txid;
// update original copy of this tx with effective fee rate & relatives data
if (mempoolTx.effectiveFeePerVsize !== effectiveFeeRate) {
mempoolTx.effectiveFeePerVsize = effectiveFeeRate;
mempoolTx.dirty = true;
}
if (mempoolTx.cpfpRoot !== nextTx.uid) {
mempoolTx.cpfpRoot = isCluster ? nextTx.uid : null;
mempoolTx.dirty;
mempoolTx.effectiveFeePerVsize = effectiveFeeRate;
if (isCluster) {
mempoolTx.cpfpRoot = nextTx.txid;
}
mempoolTx.cpfpChecked = true;
transactions.push(ancestor);
blockSize += ancestor.size;
blockWeight += ancestor.weight;
used.push(ancestor);
}
@@ -174,10 +156,11 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
if ((exceededPackageTries || queueEmpty) && blocks.length < 7) {
// construct this block
if (transactions.length) {
blocks.push(transactions.map(t => t.uid));
blocks.push(transactions.map(t => mempool[t.txid]));
}
// reset for the next block
transactions = [];
blockSize = 0;
blockWeight = 4000;
// 'overflow' packages didn't fit in this block, but are valid candidates for the next
@@ -192,38 +175,50 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
overflow = [];
}
}
if (overflow.length > 0) {
logger.warn('GBT overflow list unexpectedly non-empty after final block constructed');
}
// add the final unbounded block if it contains any transactions
if (transactions.length > 0) {
blocks.push(transactions.map(t => t.uid));
}
// get map of dirty transactions
const rates = new Map<number, number>();
for (const tx of mempool.values()) {
if (tx?.dirty) {
rates.set(tx.uid, tx.effectiveFeePerVsize || tx.feePerVsize);
// pack any leftover transactions into the last block
for (const tx of overflow) {
if (!tx || tx?.used) {
continue;
}
blockWeight += tx.weight;
const mempoolTx = mempool[tx.txid];
// update original copy of this tx with effective fee rate & relatives data
mempoolTx.effectiveFeePerVsize = tx.score;
if (tx.ancestorMap.size > 0) {
cpfpClusters[tx.txid] = Array.from(tx.ancestorMap?.values()).map(a => a.txid);
mempoolTx.cpfpRoot = tx.txid;
}
mempoolTx.cpfpChecked = true;
transactions.push(tx);
tx.used = true;
}
const blockTransactions = transactions.map(t => mempool[t.txid]);
restOfArray.forEach(tx => {
blockWeight += tx.weight;
tx.effectiveFeePerVsize = tx.feePerVsize;
tx.cpfpChecked = false;
blockTransactions.push(tx);
});
if (blockTransactions.length) {
blocks.push(blockTransactions);
}
transactions = [];
const end = Date.now();
const time = end - start;
logger.debug('Mempool templates calculated in ' + time / 1000 + ' seconds');
return { blocks, rates, clusters: cpfpClusters };
return { blocks, clusters: cpfpClusters };
}
// traverse in-mempool ancestors
// recursion unavoidable, but should be limited to depth < 25 by mempool policy
function setRelatives(
tx: AuditTransaction,
mempool: Map<number, AuditTransaction>,
mempool: { [txid: string]: AuditTransaction },
): void {
for (const parent of tx.inputs) {
const parentTx = mempool.get(parent);
for (const parent of tx.vin) {
const parentTx = mempool[parent];
if (parentTx && !tx.ancestorMap?.has(parent)) {
tx.ancestorMap.set(parent, parentTx);
parentTx.children.add(tx);
@@ -232,7 +227,7 @@ function setRelatives(
setRelatives(parentTx, mempool);
}
parentTx.ancestorMap.forEach((ancestor) => {
tx.ancestorMap.set(ancestor.uid, ancestor);
tx.ancestorMap.set(ancestor.txid, ancestor);
});
}
};
@@ -250,7 +245,7 @@ function setRelatives(
// avoids recursion to limit call stack depth
function updateDescendants(
rootTx: AuditTransaction,
mempool: Map<number, AuditTransaction>,
mempool: { [txid: string]: AuditTransaction },
modified: PairingHeap<AuditTransaction>,
): void {
const descendantSet: Set<AuditTransaction> = new Set();
@@ -266,9 +261,9 @@ function updateDescendants(
});
while (descendants.length) {
descendantTx = descendants.pop();
if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.uid)) {
if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.txid)) {
// remove tx as ancestor
descendantTx.ancestorMap.delete(rootTx.uid);
descendantTx.ancestorMap.delete(rootTx.txid);
descendantTx.ancestorFee -= rootTx.fee;
descendantTx.ancestorWeight -= rootTx.weight;
tmpScore = descendantTx.score;

View File

@@ -211,7 +211,6 @@ class WebsocketHandler {
if (!_blocks) {
_blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT);
}
const da = difficultyAdjustment.getDifficultyAdjustment();
return {
'mempoolInfo': memPool.getMempoolInfo(),
'vBytesPerSecond': memPool.getVBytesPerSecond(),
@@ -221,7 +220,7 @@ class WebsocketHandler {
'transactions': memPool.getLatestTransactions(),
'backendInfo': backendInfo.getBackendInfo(),
'loadingIndicators': loadingIndicators.getLoadingIndicators(),
'da': da?.previousTime ? da : undefined,
'da': difficultyAdjustment.getDifficultyAdjustment(),
'fees': feeApi.getRecommendedFee(),
...this.extraInitProperties
};
@@ -247,14 +246,14 @@ class WebsocketHandler {
});
}
async $handleMempoolChange(newMempool: { [txid: string]: TransactionExtended },
async handleMempoolChange(newMempool: { [txid: string]: TransactionExtended },
newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]): Promise<void> {
if (!this.wss) {
throw new Error('WebSocket.Server is not set');
}
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
await mempoolBlocks.$updateBlockTemplates(newMempool, newTransactions, deletedTransactions, true);
await mempoolBlocks.updateBlockTemplates(newMempool, newTransactions, deletedTransactions.map(tx => tx.txid), true);
} else {
mempoolBlocks.updateMempoolBlocks(newMempool, true);
}
@@ -279,9 +278,7 @@ class WebsocketHandler {
response['mempoolInfo'] = mempoolInfo;
response['vBytesPerSecond'] = vBytesPerSecond;
response['transactions'] = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx));
if (da?.previousTime) {
response['da'] = da;
}
response['da'] = da;
response['fees'] = recommendedFees;
}
@@ -425,23 +422,17 @@ class WebsocketHandler {
if (config.MEMPOOL.AUDIT) {
let projectedBlocks;
let auditMempool = _memPool;
// 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);
} else {
projectedBlocks = mempoolBlocks.updateMempoolBlocks(auditMempool, false);
}
const auditMempool = (config.MEMPOOL.ADVANCED_GBT_AUDIT === config.MEMPOOL.ADVANCED_GBT_MEMPOOL) ? _memPool : deepClone(_memPool);
if (config.MEMPOOL.ADVANCED_GBT_AUDIT) {
projectedBlocks = await mempoolBlocks.makeBlockTemplates(auditMempool, false);
} else {
projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
projectedBlocks = mempoolBlocks.updateMempoolBlocks(auditMempool, false);
}
if (Common.indexingEnabled() && memPool.isInSync()) {
const { censored, added, fresh, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
const { censored, added, fresh, score } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
const matchRate = Math.round(score * 100 * 100) / 100;
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions.map((tx) => {
@@ -473,24 +464,20 @@ class WebsocketHandler {
if (block.extras) {
block.extras.matchRate = matchRate;
block.extras.similarity = similarity;
}
}
} else if (block.extras) {
const mBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
if (mBlocks?.length && mBlocks[0].transactions) {
block.extras.similarity = Common.getSimilarity(mBlocks[0], transactions);
}
}
const removed: string[] = [];
// Update mempool to remove transactions included in the new block
for (const txId of txIds) {
delete _memPool[txId];
removed.push(txId);
rbfCache.evict(txId);
}
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
await mempoolBlocks.$makeBlockTemplates(_memPool, true);
await mempoolBlocks.updateBlockTemplates(_memPool, [], removed, true);
} else {
mempoolBlocks.updateMempoolBlocks(_memPool, true);
}
@@ -512,7 +499,7 @@ class WebsocketHandler {
const response = {
'block': block,
'mempoolInfo': memPool.getMempoolInfo(),
'da': da?.previousTime ? da : undefined,
'da': da,
'fees': fees,
};

View File

@@ -33,7 +33,6 @@ interface IConfig {
ADVANCED_GBT_MEMPOOL: boolean;
CPFP_INDEXING: boolean;
MAX_BLOCKS_BULK_QUERY: number;
DISK_CACHE_BLOCK_INTERVAL: number;
};
ESPLORA: {
REST_API_URL: string;
@@ -52,7 +51,6 @@ interface IConfig {
TLS_CERT_PATH: string;
MACAROON_PATH: string;
REST_API_URL: string;
TIMEOUT: number;
};
CLIGHTNING: {
SOCKET: string;
@@ -67,14 +65,12 @@ interface IConfig {
PORT: number;
USERNAME: string;
PASSWORD: string;
TIMEOUT: number;
};
SECOND_CORE_RPC: {
HOST: string;
PORT: number;
USERNAME: string;
PASSWORD: string;
TIMEOUT: number;
};
DATABASE: {
ENABLED: boolean;
@@ -159,7 +155,6 @@ const defaults: IConfig = {
'ADVANCED_GBT_MEMPOOL': false,
'CPFP_INDEXING': false,
'MAX_BLOCKS_BULK_QUERY': 0,
'DISK_CACHE_BLOCK_INTERVAL': 6,
},
'ESPLORA': {
'REST_API_URL': 'http://127.0.0.1:3000',
@@ -173,15 +168,13 @@ const defaults: IConfig = {
'HOST': '127.0.0.1',
'PORT': 8332,
'USERNAME': 'mempool',
'PASSWORD': 'mempool',
'TIMEOUT': 60000,
'PASSWORD': 'mempool'
},
'SECOND_CORE_RPC': {
'HOST': '127.0.0.1',
'PORT': 8332,
'USERNAME': 'mempool',
'PASSWORD': 'mempool',
'TIMEOUT': 60000,
'PASSWORD': 'mempool'
},
'DATABASE': {
'ENABLED': true,
@@ -221,7 +214,6 @@ const defaults: IConfig = {
'TLS_CERT_PATH': '',
'MACAROON_PATH': '',
'REST_API_URL': 'https://localhost:8080',
'TIMEOUT': 10000,
},
'CLIGHTNING': {
'SOCKET': '',

View File

@@ -120,7 +120,7 @@ class Server {
await poolsUpdater.updatePoolsJson(); // Needs to be done before loading the disk cache because we sometimes wipe it
await syncAssets.syncAssets$();
if (config.MEMPOOL.ENABLED) {
await diskCache.$loadMempoolCache();
diskCache.loadMempoolCache();
}
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && cluster.isPrimary) {
@@ -179,7 +179,6 @@ class Server {
}
}
await blocks.$updateBlocks();
memPool.deleteExpiredTransactions();
await memPool.$updateMempool();
indexer.$run();
@@ -206,8 +205,6 @@ class Server {
setTimeout(this.runMainUpdateLoop.bind(this), 1000 * this.currentBackendRetryInterval);
this.currentBackendRetryInterval *= 2;
this.currentBackendRetryInterval = Math.min(this.currentBackendRetryInterval, 60);
} finally {
diskCache.unlock();
}
}
@@ -218,11 +215,11 @@ class Server {
await lightningStatsUpdater.$startService();
await forensicsService.$startService();
} catch(e) {
logger.err(`Exception in $runLightningBackend. Restarting in 1 minute. Reason: ${(e instanceof Error ? e.message : e)}`);
logger.err(`Nodejs lightning backend crashed. Restarting in 1 minute. Reason: ${(e instanceof Error ? e.message : e)}`);
await Common.sleep$(1000 * 60);
this.$runLightningBackend();
};
}
}
setUpWebsocketHandling(): void {
if (this.wss) {
@@ -240,7 +237,7 @@ class Server {
websocketHandler.setupConnectionHandling();
if (config.MEMPOOL.ENABLED) {
statistics.setNewStatisticsEntryCallback(websocketHandler.handleNewStatistic.bind(websocketHandler));
memPool.setAsyncMempoolChangedCallback(websocketHandler.$handleMempoolChange.bind(websocketHandler));
memPool.setAsyncMempoolChangedCallback(websocketHandler.handleMempoolChange.bind(websocketHandler));
blocks.setNewAsyncBlockCallback(websocketHandler.handleNewBlock.bind(websocketHandler));
}
priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));

View File

@@ -69,10 +69,6 @@ class Logger {
this.network = this.getNetwork();
}
public updateNetwork(): void {
this.network = this.getNetwork();
}
private addprio(prio): void {
this[prio] = (function(_this) {
return function(msg, tag?: string) {

View File

@@ -80,22 +80,17 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
bestDescendant?: BestDescendant | null;
cpfpChecked?: boolean;
deleteAfter?: number;
position?: {
block: number,
vsize: number,
};
uid?: number;
}
export interface AuditTransaction {
uid: number;
txid: string;
fee: number;
weight: number;
feePerVsize: number;
effectiveFeePerVsize: number;
inputs: number[];
vin: string[];
relativesSet: boolean;
ancestorMap: Map<number, AuditTransaction>;
ancestorMap: Map<string, AuditTransaction>;
children: Set<AuditTransaction>;
ancestorFee: number;
ancestorWeight: number;
@@ -105,25 +100,13 @@ export interface AuditTransaction {
modifiedNode: HeapNode<AuditTransaction>;
}
export interface CompactThreadTransaction {
uid: number;
fee: number;
weight: number;
feePerVsize: number;
effectiveFeePerVsize?: number;
inputs: number[];
cpfpRoot?: string;
cpfpChecked?: boolean;
dirty?: boolean;
}
export interface ThreadTransaction {
txid: string;
fee: number;
weight: number;
feePerVsize: number;
effectiveFeePerVsize?: number;
inputs: number[];
vin: string[];
cpfpRoot?: string;
cpfpChecked?: boolean;
}
@@ -170,7 +153,6 @@ export interface BlockExtension {
feeRange: number[]; // fee rate percentiles
reward: number;
matchRate: number | null;
similarity?: number;
pool: {
id: number; // Note - This is the `unique_id`, not to mix with the auto increment `id`
name: string;
@@ -326,11 +308,9 @@ export interface IDifficultyAdjustment {
remainingBlocks: number;
remainingTime: number;
previousRetarget: number;
previousTime: number;
nextRetargetHeight: number;
timeAvg: number;
timeOffset: number;
expectedBlocks: number;
}
export interface IndexedDifficultyAdjustment {

View File

@@ -330,55 +330,6 @@ class BlocksRepository {
}
}
/**
* Get average block health for all blocks for a single pool
*/
public async $getAvgBlockHealthPerPoolId(poolId: number): Promise<number> {
const params: any[] = [];
const query = `
SELECT AVG(blocks_audits.match_rate) AS avg_match_rate
FROM blocks
JOIN blocks_audits ON blocks.height = blocks_audits.height
WHERE blocks.pool_id = ?
`;
params.push(poolId);
try {
const [rows] = await DB.query(query, params);
if (!rows[0] || !rows[0].avg_match_rate) {
return 0;
}
return Math.round(rows[0].avg_match_rate * 100) / 100;
} catch (e) {
logger.err(`Cannot get average block health for pool id ${poolId}. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Get average block health for all blocks for a single pool
*/
public async $getTotalRewardForPoolId(poolId: number): Promise<number> {
const params: any[] = [];
const query = `
SELECT sum(reward) as total_reward
FROM blocks
WHERE blocks.pool_id = ?
`;
params.push(poolId);
try {
const [rows] = await DB.query(query, params);
if (!rows[0] || !rows[0].total_reward) {
return 0;
}
return rows[0].total_reward;
} catch (e) {
logger.err(`Cannot get total reward for pool id ${poolId}. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Get the oldest indexed block
*/
@@ -466,6 +417,30 @@ class BlocksRepository {
}
}
/**
* Get one block by hash
*/
public async $getBlockByHash(hash: string): Promise<object | null> {
try {
const query = `
SELECT ${BLOCK_DB_FIELDS}
FROM blocks
JOIN pools ON blocks.pool_id = pools.id
WHERE hash = ?;
`;
const [rows]: any[] = await DB.query(query, [hash]);
if (rows.length <= 0) {
return null;
}
return await this.formatDbBlockIntoExtendedBlock(rows[0]);
} catch (e) {
logger.err(`Cannot get indexed block ${hash}. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Return blocks difficulty
*/
@@ -575,6 +550,7 @@ class BlocksRepository {
if (blocks[idx].previous_block_hash !== blocks[idx - 1].hash) {
logger.warn(`Chain divergence detected at block ${blocks[idx - 1].height}`);
await this.$deleteBlocksFrom(blocks[idx - 1].height);
await BlocksSummariesRepository.$deleteBlocksFrom(blocks[idx - 1].height);
await HashratesRepository.$deleteHashratesFromTimestamp(blocks[idx - 1].timestamp - 604800);
await DifficultyAdjustmentsRepository.$deleteAdjustementsFromHeight(blocks[idx - 1].height);
return false;
@@ -594,7 +570,7 @@ class BlocksRepository {
* Delete blocks from the database from blockHeight
*/
public async $deleteBlocksFrom(blockHeight: number) {
logger.info(`Delete newer blocks from height ${blockHeight} from the database`, logger.tags.mining);
logger.info(`Delete newer blocks from height ${blockHeight} from the database`);
try {
await DB.query(`DELETE FROM blocks where height >= ${blockHeight}`);
@@ -953,7 +929,6 @@ class BlocksRepository {
}
// If we're missing block summary related field, check if we can populate them on the fly now
// This is for example triggered upon re-org
if (Common.blocksSummariesIndexingEnabled() &&
(extras.medianFeeAmt === null || extras.feePercentiles === null))
{
@@ -961,7 +936,7 @@ class BlocksRepository {
if (extras.feePercentiles === null) {
const block = await bitcoinClient.getBlock(dbBlk.id, 2);
const summary = blocks.summarizeBlock(block);
await BlocksSummariesRepository.$saveTransactions(dbBlk.height, dbBlk.hash, summary.transactions);
await BlocksSummariesRepository.$saveSummary({ height: block.height, mined: summary });
extras.feePercentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(dbBlk.id);
}
if (extras.feePercentiles !== null) {

View File

@@ -1,6 +1,6 @@
import DB from '../database';
import logger from '../logger';
import { BlockSummary, TransactionStripped } from '../mempool.interfaces';
import { BlockSummary } from '../mempool.interfaces';
class BlocksSummariesRepository {
public async $getByBlockId(id: string): Promise<BlockSummary | undefined> {
@@ -17,7 +17,7 @@ class BlocksSummariesRepository {
return undefined;
}
public async $saveSummary(params: { height: number, mined?: BlockSummary}): Promise<void> {
public async $saveSummary(params: { height: number, mined?: BlockSummary}) {
const blockId = params.mined?.id;
try {
const transactions = JSON.stringify(params.mined?.transactions || []);
@@ -37,20 +37,6 @@ class BlocksSummariesRepository {
}
}
public async $saveTransactions(blockHeight: number, blockId: string, transactions: TransactionStripped[]): Promise<void> {
try {
const transactionsStr = JSON.stringify(transactions);
await DB.query(`
INSERT INTO blocks_summaries
SET height = ?, transactions = ?, id = ?
ON DUPLICATE KEY UPDATE transactions = ?`,
[blockHeight, transactionsStr, blockId, transactionsStr]);
} catch (e: any) {
logger.debug(`Cannot save block summary transactions for ${blockId}. Reason: ${e instanceof Error ? e.message : e}`);
throw e;
}
}
public async $saveTemplate(params: { height: number, template: BlockSummary}) {
const blockId = params.template?.id;
try {
@@ -82,6 +68,19 @@ class BlocksSummariesRepository {
return [];
}
/**
* Delete blocks from the database from blockHeight
*/
public async $deleteBlocksFrom(blockHeight: number) {
logger.info(`Delete newer blocks summary from height ${blockHeight} from the database`);
try {
await DB.query(`DELETE FROM blocks_summaries where height >= ${blockHeight}`);
} catch (e) {
logger.err('Cannot delete indexed blocks summaries. Reason: ' + (e instanceof Error ? e.message : e));
}
}
/**
* Get the fee percentiles if the block has already been indexed, [] otherwise
*

View File

@@ -220,7 +220,7 @@ class HashratesRepository {
* Delete hashrates from the database from timestamp
*/
public async $deleteHashratesFromTimestamp(timestamp: number) {
logger.info(`Delete newer hashrates from timestamp ${new Date(timestamp * 1000).toUTCString()} from the database`, logger.tags.mining);
logger.info(`Delete newer hashrates from timestamp ${new Date(timestamp * 1000).toUTCString()} from the database`);
try {
await DB.query(`DELETE FROM hashrates WHERE hashrate_timestamp >= FROM_UNIXTIME(?)`, [timestamp]);

View File

@@ -22,15 +22,12 @@ class LightningStatsUpdater {
* Update the latest entry for each node every config.LIGHTNING.STATS_REFRESH_INTERVAL seconds
*/
private async $logStatsDaily(): Promise<void> {
try {
const date = new Date();
Common.setDateMidnight(date);
const networkGraph = await lightningApi.$getNetworkGraph();
await LightningStatsImporter.computeNetworkStats(date.getTime() / 1000, networkGraph);
logger.debug(`Updated latest network stats`, logger.tags.ln);
} catch (e) {
logger.err(`Exception in $logStatsDaily. Reason: ${(e instanceof Error ? e.message : e)}`);
}
const date = new Date();
Common.setDateMidnight(date);
const networkGraph = await lightningApi.$getNetworkGraph();
await LightningStatsImporter.computeNetworkStats(date.getTime() / 1000, networkGraph);
logger.debug(`Updated latest network stats`, logger.tags.ln);
}
}

View File

@@ -17,9 +17,7 @@ class PoolsUpdater {
treeUrl: string = config.MEMPOOL.POOLS_JSON_TREE_URL;
public async updatePoolsJson(): Promise<void> {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false ||
config.MEMPOOL.ENABLED === false
) {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) {
return;
}

View File

@@ -73,11 +73,6 @@ class PriceUpdater {
}
public async $run(): Promise<void> {
if (config.MEMPOOL.NETWORK === 'signet' || config.MEMPOOL.NETWORK === 'testnet') {
// Coins have no value on testnet/signet, so we want to always show 0
return;
}
if (this.running === true) {
return;
}
@@ -93,7 +88,7 @@ class PriceUpdater {
if (this.historyInserted === false && config.DATABASE.ENABLED === true) {
await this.$insertHistoricalPrices();
}
} catch (e: any) {
} catch (e) {
logger.err(`Cannot save BTC prices in db. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining);
}

View File

@@ -34,7 +34,6 @@ If you want to use different credentials, specify them in the `docker-compose.ym
CORE_RPC_PORT: "8332"
CORE_RPC_USERNAME: "customuser"
CORE_RPC_PASSWORD: "custompassword"
CORE_RPC_TIMEOUT: "60000"
```
The IP address in the example above refers to Docker's default gateway IP address so that the container can hit the `bitcoind` instance running on the host machine. If your setup is different, update it accordingly.
@@ -113,7 +112,6 @@ Below we list all settings from `mempool-config.json` and the corresponding over
"ADVANCED_GBT_MEMPOOL": false,
"CPFP_INDEXING": false,
"MAX_BLOCKS_BULK_QUERY": 0,
"DISK_CACHE_BLOCK_INTERVAL": 6
},
```
@@ -145,7 +143,6 @@ Corresponding `docker-compose.yml` overrides:
MEMPOOL_ADVANCED_GBT_MEMPOOL: ""
MEMPOOL_CPFP_INDEXING: ""
MAX_BLOCKS_BULK_QUERY: ""
DISK_CACHE_BLOCK_INTERVAL: ""
...
```
@@ -161,8 +158,7 @@ Corresponding `docker-compose.yml` overrides:
"HOST": "127.0.0.1",
"PORT": 8332,
"USERNAME": "mempool",
"PASSWORD": "mempool",
"TIMEOUT": 60000
"PASSWORD": "mempool"
},
```
@@ -174,7 +170,6 @@ Corresponding `docker-compose.yml` overrides:
CORE_RPC_PORT: ""
CORE_RPC_USERNAME: ""
CORE_RPC_PASSWORD: ""
CORE_RPC_TIMEOUT: 60000
...
```
@@ -224,8 +219,7 @@ Corresponding `docker-compose.yml` overrides:
"HOST": "127.0.0.1",
"PORT": 8332,
"USERNAME": "mempool",
"PASSWORD": "mempool",
"TIMEOUT": 60000
"PASSWORD": "mempool"
},
```
@@ -237,7 +231,6 @@ Corresponding `docker-compose.yml` overrides:
SECOND_CORE_RPC_PORT: ""
SECOND_CORE_RPC_USERNAME: ""
SECOND_CORE_RPC_PASSWORD: ""
SECOND_CORE_RPC_TIMEOUT: ""
...
```
@@ -410,7 +403,6 @@ Corresponding `docker-compose.yml` overrides:
"TLS_CERT_PATH": ""
"MACAROON_PATH": ""
"REST_API_URL": "https://localhost:8080"
"TIMEOUT": 10000
}
```
@@ -421,7 +413,6 @@ Corresponding `docker-compose.yml` overrides:
LND_TLS_CERT_PATH: ""
LND_MACAROON_PATH: ""
LND_REST_API_URL: "https://localhost:8080"
LND_TIMEOUT: 10000
...
```
@@ -441,26 +432,3 @@ Corresponding `docker-compose.yml` overrides:
CLIGHTNING_SOCKET: ""
...
```
<br/>
`mempool-config.json`:
```json
"MAXMIND": {
"ENABLED": true,
"GEOLITE2_CITY": "/usr/local/share/GeoIP/GeoLite2-City.mmdb",
"GEOLITE2_ASN": "/usr/local/share/GeoIP/GeoLite2-ASN.mmdb",
"GEOIP2_ISP": "/usr/local/share/GeoIP/GeoIP2-ISP.mmdb"
}
```
Corresponding `docker-compose.yml` overrides:
```yaml
api:
environment:
MAXMIND_ENABLED: true,
MAXMIND_GEOLITE2_CITY: "/backend/GeoIP/GeoLite2-City.mmdb",
MAXMIND_GEOLITE2_ASN": "/backend/GeoIP/GeoLite2-ASN.mmdb",
MAXMIND_GEOIP2_ISP": "/backend/GeoIP/GeoIP2-ISP.mmdb"
...
```

View File

@@ -17,7 +17,6 @@ WORKDIR /backend
RUN chown 1000:1000 ./
COPY --from=builder --chown=1000:1000 /build/package ./package/
COPY --from=builder --chown=1000:1000 /build/GeoIP ./GeoIP/
COPY --from=builder --chown=1000:1000 /build/mempool-config.json /build/start.sh /build/wait-for-it.sh ./
USER 1000

View File

@@ -26,15 +26,13 @@
"ADVANCED_GBT_AUDIT": __MEMPOOL_ADVANCED_GBT_AUDIT__,
"ADVANCED_GBT_MEMPOOL": __MEMPOOL_ADVANCED_GBT_MEMPOOL__,
"CPFP_INDEXING": __MEMPOOL_CPFP_INDEXING__,
"MAX_BLOCKS_BULK_QUERY": __MEMPOOL_MAX_BLOCKS_BULK_QUERY__,
"DISK_CACHE_BLOCK_INTERVAL": __MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__
"MAX_BLOCKS_BULK_QUERY": __MEMPOOL_MAX_BLOCKS_BULK_QUERY__
},
"CORE_RPC": {
"HOST": "__CORE_RPC_HOST__",
"PORT": __CORE_RPC_PORT__,
"USERNAME": "__CORE_RPC_USERNAME__",
"PASSWORD": "__CORE_RPC_PASSWORD__",
"TIMEOUT": __CORE_RPC_TIMEOUT__
"PASSWORD": "__CORE_RPC_PASSWORD__"
},
"ELECTRUM": {
"HOST": "__ELECTRUM_HOST__",
@@ -48,8 +46,7 @@
"HOST": "__SECOND_CORE_RPC_HOST__",
"PORT": __SECOND_CORE_RPC_PORT__,
"USERNAME": "__SECOND_CORE_RPC_USERNAME__",
"PASSWORD": "__SECOND_CORE_RPC_PASSWORD__",
"TIMEOUT": __SECOND_CORE_RPC_TIMEOUT__
"PASSWORD": "__SECOND_CORE_RPC_PASSWORD__"
},
"DATABASE": {
"ENABLED": __DATABASE_ENABLED__,
@@ -86,8 +83,7 @@
"LND": {
"TLS_CERT_PATH": "__LND_TLS_CERT_PATH__",
"MACAROON_PATH": "__LND_MACAROON_PATH__",
"REST_API_URL": "__LND_REST_API_URL__",
"TIMEOUT": "__LND_TIMEOUT__"
"REST_API_URL": "__LND_REST_API_URL__"
},
"CLIGHTNING": {
"SOCKET": "__CLIGHTNING_SOCKET__"
@@ -111,11 +107,5 @@
"LIQUID_ONION": "__EXTERNAL_DATA_SERVER_LIQUID_ONION__",
"BISQ_URL": "__EXTERNAL_DATA_SERVER_BISQ_URL__",
"BISQ_ONION": "__EXTERNAL_DATA_SERVER_BISQ_ONION__"
},
"MAXMIND": {
"ENABLED": __MAXMIND_ENABLED__,
"GEOLITE2_CITY": "__MAXMIND_GEOLITE2_CITY__",
"GEOLITE2_ASN": "__MAXMIND_GEOLITE2_ASN__",
"GEOIP2_ISP": "__MAXMIND_GEOIP2_ISP__"
}
}
}

View File

@@ -22,6 +22,7 @@ __MEMPOOL_EXTERNAL_MAX_RETRY__=${MEMPOOL_EXTERNAL_MAX_RETRY:=1}
__MEMPOOL_EXTERNAL_RETRY_INTERVAL__=${MEMPOOL_EXTERNAL_RETRY_INTERVAL:=0}
__MEMPOOL_USER_AGENT__=${MEMPOOL_USER_AGENT:=mempool}
__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info}
__MEMPOOL_INDEXING_BLOCKS_AMOUNT__=${MEMPOOL_INDEXING_BLOCKS_AMOUNT:=false}
__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__=${MEMPOOL_AUTOMATIC_BLOCK_REINDEXING:=false}
__MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json}
__MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master}
@@ -30,14 +31,12 @@ __MEMPOOL_ADVANCED_GBT_AUDIT__=${MEMPOOL_ADVANCED_GBT_AUDIT:=false}
__MEMPOOL_ADVANCED_GBT_MEMPOOL__=${MEMPOOL_ADVANCED_GBT_MEMPOOL:=false}
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
__MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0}
__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__=${MEMPOOL_DISK_CACHE_BLOCK_INTERVAL:=6}
# CORE_RPC
__CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1}
__CORE_RPC_PORT__=${CORE_RPC_PORT:=8332}
__CORE_RPC_USERNAME__=${CORE_RPC_USERNAME:=mempool}
__CORE_RPC_PASSWORD__=${CORE_RPC_PASSWORD:=mempool}
__CORE_RPC_TIMEOUT__=${CORE_RPC_TIMEOUT:=60000}
# ELECTRUM
__ELECTRUM_HOST__=${ELECTRUM_HOST:=127.0.0.1}
@@ -52,7 +51,6 @@ __SECOND_CORE_RPC_HOST__=${SECOND_CORE_RPC_HOST:=127.0.0.1}
__SECOND_CORE_RPC_PORT__=${SECOND_CORE_RPC_PORT:=8332}
__SECOND_CORE_RPC_USERNAME__=${SECOND_CORE_RPC_USERNAME:=mempool}
__SECOND_CORE_RPC_PASSWORD__=${SECOND_CORE_RPC_PASSWORD:=mempool}
__SECOND_CORE_RPC_TIMEOUT__=${SECOND_CORE_RPC_TIMEOUT:=60000}
# DATABASE
__DATABASE_ENABLED__=${DATABASE_ENABLED:=true}
@@ -110,18 +108,10 @@ __LIGHTNING_LOGGER_UPDATE_INTERVAL__=${LIGHTNING_LOGGER_UPDATE_INTERVAL:=30}
__LND_TLS_CERT_PATH__=${LND_TLS_CERT_PATH:=""}
__LND_MACAROON_PATH__=${LND_MACAROON_PATH:=""}
__LND_REST_API_URL__=${LND_REST_API_URL:="https://localhost:8080"}
__LND_TIMEOUT__=${LND_TIMEOUT:=10000}
# CLN
__CLIGHTNING_SOCKET__=${CLIGHTNING_SOCKET:=""}
# MAXMIND
__MAXMIND_ENABLED__=${MAXMIND_ENABLED:=true}
__MAXMIND_GEOLITE2_CITY__=${MAXMIND_GEOLITE2_CITY:="/backend/GeoIP/GeoLite2-City.mmdb"}
__MAXMIND_GEOLITE2_ASN__=${MAXMIND_GEOLITE2_ASN:="/backend/GeoIP/GeoLite2-ASN.mmdb"}
__MAXMIND_GEOIP2_ISP__=${MAXMIND_GEOIP2_ISP:=""}
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
sed -i "s/__MEMPOOL_NETWORK__/${__MEMPOOL_NETWORK__}/g" mempool-config.json
@@ -145,6 +135,7 @@ sed -i "s!__MEMPOOL_EXTERNAL_MAX_RETRY__!${__MEMPOOL_EXTERNAL_MAX_RETRY__}!g" me
sed -i "s!__MEMPOOL_EXTERNAL_RETRY_INTERVAL__!${__MEMPOOL_EXTERNAL_RETRY_INTERVAL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_USER_AGENT__!${__MEMPOOL_USER_AGENT__}!g" mempool-config.json
sed -i "s/__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__/${__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__}/g" mempool-config.json
sed -i "s/__MEMPOOL_INDEXING_BLOCKS_AMOUNT__/${__MEMPOOL_INDEXING_BLOCKS_AMOUNT__}/g" mempool-config.json
sed -i "s/__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__/${__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__}/g" mempool-config.json
sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json
@@ -153,13 +144,11 @@ sed -i "s!__MEMPOOL_ADVANCED_GBT_MEMPOOL__!${__MEMPOOL_ADVANCED_GBT_MEMPOOL__}!g
sed -i "s!__MEMPOOL_ADVANCED_GBT_AUDIT__!${__MEMPOOL_ADVANCED_GBT_AUDIT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_CPFP_INDEXING__!${__MEMPOOL_CPFP_INDEXING__}!g" mempool-config.json
sed -i "s!__MEMPOOL_MAX_BLOCKS_BULK_QUERY__!${__MEMPOOL_MAX_BLOCKS_BULK_QUERY__}!g" mempool-config.json
sed -i "s!__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__!${__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__}!g" mempool-config.json
sed -i "s/__CORE_RPC_HOST__/${__CORE_RPC_HOST__}/g" mempool-config.json
sed -i "s/__CORE_RPC_PORT__/${__CORE_RPC_PORT__}/g" mempool-config.json
sed -i "s/__CORE_RPC_USERNAME__/${__CORE_RPC_USERNAME__}/g" mempool-config.json
sed -i "s/__CORE_RPC_PASSWORD__/${__CORE_RPC_PASSWORD__}/g" mempool-config.json
sed -i "s/__CORE_RPC_TIMEOUT__/${__CORE_RPC_TIMEOUT__}/g" mempool-config.json
sed -i "s/__ELECTRUM_HOST__/${__ELECTRUM_HOST__}/g" mempool-config.json
sed -i "s/__ELECTRUM_PORT__/${__ELECTRUM_PORT__}/g" mempool-config.json
@@ -171,7 +160,6 @@ sed -i "s/__SECOND_CORE_RPC_HOST__/${__SECOND_CORE_RPC_HOST__}/g" mempool-config
sed -i "s/__SECOND_CORE_RPC_PORT__/${__SECOND_CORE_RPC_PORT__}/g" mempool-config.json
sed -i "s/__SECOND_CORE_RPC_USERNAME__/${__SECOND_CORE_RPC_USERNAME__}/g" mempool-config.json
sed -i "s/__SECOND_CORE_RPC_PASSWORD__/${__SECOND_CORE_RPC_PASSWORD__}/g" mempool-config.json
sed -i "s/__SECOND_CORE_RPC_TIMEOUT__/${__SECOND_CORE_RPC_TIMEOUT__}/g" mempool-config.json
sed -i "s/__DATABASE_ENABLED__/${__DATABASE_ENABLED__}/g" mempool-config.json
sed -i "s/__DATABASE_HOST__/${__DATABASE_HOST__}/g" mempool-config.json
@@ -223,16 +211,8 @@ sed -i "s!__LIGHTNING_LOGGER_UPDATE_INTERVAL__!${__LIGHTNING_LOGGER_UPDATE_INTER
sed -i "s!__LND_TLS_CERT_PATH__!${__LND_TLS_CERT_PATH__}!g" mempool-config.json
sed -i "s!__LND_MACAROON_PATH__!${__LND_MACAROON_PATH__}!g" mempool-config.json
sed -i "s!__LND_REST_API_URL__!${__LND_REST_API_URL__}!g" mempool-config.json
sed -i "s!__LND_TIMEOUT__!${__LND_TIMEOUT__}!g" mempool-config.json
# CLN
sed -i "s!__CLIGHTNING_SOCKET__!${__CLIGHTNING_SOCKET__}!g" mempool-config.json
# MAXMIND
sed -i "s!__MAXMIND_ENABLED__!${__MAXMIND_ENABLED__}!g" mempool-config.json
sed -i "s!__MAXMIND_GEOLITE2_CITY__!${__MAXMIND_GEOLITE2_CITY__}!g" mempool-config.json
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
node /backend/package/index.js

View File

@@ -10,10 +10,6 @@ cp /etc/nginx/nginx.conf /patch/nginx.conf
sed -i "s/__MEMPOOL_FRONTEND_HTTP_PORT__/${__MEMPOOL_FRONTEND_HTTP_PORT__}/g" /patch/nginx.conf
cat /patch/nginx.conf > /etc/nginx/nginx.conf
if [ "${LIGHTNING_DETECTED_PORT}" != "" ];then
export LIGHTNING=true
fi
# Runtime overrides - read env vars defined in docker compose
__TESTNET_ENABLED__=${TESTNET_ENABLED:=false}

View File

@@ -3,11 +3,6 @@
#backend
cp ./docker/backend/* ./backend/
#geoip-data
mkdir -p ./backend/GeoIP/
wget -O ./backend/GeoIP/GeoLite2-City.mmdb https://raw.githubusercontent.com/mempool/geoip-data/master/GeoLite2-City.mmdb
wget -O ./backend/GeoIP/GeoLite2-ASN.mmdb https://raw.githubusercontent.com/mempool/geoip-data/master/GeoLite2-ASN.mmdb
#frontend
localhostIP="127.0.0.1"
cp ./docker/frontend/* ./frontend

3
frontend/.gitignore vendored
View File

@@ -54,8 +54,7 @@ src/resources/assets-testnet.json
src/resources/assets-testnet.minimal.json
src/resources/pools.json
src/resources/mining-pools/*
src/resources/**/*.mp4
src/resources/**/*.vtt
src/resources/*.mp4
# environment config
mempool-frontend-config.json

View File

@@ -106,15 +106,13 @@ https://www.transifex.com/mempool/mempool/dashboard/
* Arabic @baro0k
* Czech @pixelmade2
* Danish @pierrevendelboe
* German @Emzy
* English (default)
* Spanish @maxhodler @bisqes
* Persian @techmix
* French @Bayernatoor
* Korean @kcalvinalvinn @sogoagain
* Korean @kcalvinalvinn
* Italian @HodlBits
* Lithuanian @eimze21
* Hebrew @rapidlab309
* Georgian @wyd_idk
* Hungarian @btcdragonlord

View File

@@ -38,10 +38,6 @@
"translation": "src/locale/messages.de.xlf",
"baseHref": "/de/"
},
"da": {
"translation": "src/locale/messages.da.xlf",
"baseHref": "/da/"
},
"es": {
"translation": "src/locale/messages.es.xlf",
"baseHref": "/es/"

View File

@@ -158,10 +158,10 @@ describe('Liquid', () => {
it('show empty unblinded TX', () => {
cy.visit(`${basePath}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=`);
cy.waitForSkeletonGone();
cy.get('.table-tx-vin tr:nth-child(1)').should('have.class', 'ng-star-inserted');
cy.get('.table-tx-vin tr:nth-child(1)').should('have.class', '');
cy.get('.table-tx-vin tr:nth-child(1) .amount').should('contain.text', 'Confidential');
cy.get('.table-tx-vout tr:nth-child(1)').should('have.class', 'ng-star-inserted');
cy.get('.table-tx-vout tr:nth-child(2)').should('have.class', 'ng-star-inserted');
cy.get('.table-tx-vout tr:nth-child(1)').should('have.class', '');
cy.get('.table-tx-vout tr:nth-child(2)').should('have.class', '');
cy.get('.table-tx-vout tr:nth-child(1) .amount').should('contain.text', 'Confidential');
cy.get('.table-tx-vout tr:nth-child(2) .amount').should('contain.text', 'Confidential');
});
@@ -169,8 +169,8 @@ describe('Liquid', () => {
it('show invalid unblinded TX hex', () => {
cy.visit(`${basePath}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=123`);
cy.waitForSkeletonGone();
cy.get('.table-tx-vin tr').should('have.class', 'ng-star-inserted');
cy.get('.table-tx-vout tr').should('have.class', 'ng-star-inserted');
cy.get('.table-tx-vin tr').should('have.class', '');
cy.get('.table-tx-vout tr').should('have.class', '');
cy.get('.error-unblinded').contains('Error: Invalid blinding data (invalid hex)');
});

View File

@@ -109,10 +109,10 @@ describe('Liquid Testnet', () => {
it('show empty unblinded TX', () => {
cy.visit(`${basePath}/tx/c3d908ab77891e4c569b0df71aae90f4720b157019ebb20db176f4f9c4d626b8#blinded=`);
cy.waitForSkeletonGone();
cy.get('.table-tx-vin tr:nth-child(1)').should('have.class', 'ng-star-inserted');
cy.get('.table-tx-vin tr:nth-child(1)').should('have.class', '');
cy.get('.table-tx-vin tr:nth-child(1) .amount').should('contain.text', 'Confidential');
cy.get('.table-tx-vout tr:nth-child(1)').should('have.class', 'ng-star-inserted');
cy.get('.table-tx-vout tr:nth-child(2)').should('have.class', 'ng-star-inserted');
cy.get('.table-tx-vout tr:nth-child(1)').should('have.class', '');
cy.get('.table-tx-vout tr:nth-child(2)').should('have.class', '');
cy.get('.table-tx-vout tr:nth-child(1) .amount').should('contain.text', 'Confidential');
cy.get('.table-tx-vout tr:nth-child(2) .amount').should('contain.text', 'Confidential');
});
@@ -120,8 +120,8 @@ describe('Liquid Testnet', () => {
it('show invalid unblinded TX hex', () => {
cy.visit(`${basePath}/tx/2477f220eef1d03f8ffa4a2861c275d155c3562adf0d79523aeeb0c59ee611ba#blinded=5000`);
cy.waitForSkeletonGone();
cy.get('.table-tx-vin tr').should('have.class', 'ng-star-inserted');
cy.get('.table-tx-vout tr').should('have.class', 'ng-star-inserted');
cy.get('.table-tx-vin tr').should('have.class', '');
cy.get('.table-tx-vout tr').should('have.class', '');
cy.get('.error-unblinded').contains('Error: Invalid blinding data (invalid hex)');
});

View File

@@ -1,12 +1,12 @@
{
"name": "mempool-frontend",
"version": "2.5.0",
"version": "2.5.0-dev",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "mempool-frontend",
"version": "2.5.0",
"version": "2.5.0-dev",
"license": "GNU Affero General Public License v3.0",
"dependencies": {
"@angular-devkit/build-angular": "^14.2.10",

View File

@@ -1,6 +1,6 @@
{
"name": "mempool-frontend",
"version": "2.5.0",
"version": "2.5.0-dev",
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
"license": "GNU Affero General Public License v3.0",
"homepage": "https://mempool.space",

View File

@@ -139,87 +139,26 @@ export const specialBlocks = {
'0': {
labelEvent: 'Genesis',
labelEventCompleted: 'The Genesis of Bitcoin',
networks: ['mainnet', 'testnet'],
},
'210000': {
labelEvent: 'Bitcoin\'s 1st Halving',
labelEventCompleted: 'Block Subsidy has halved to 25 BTC per block',
networks: ['mainnet', 'testnet'],
},
'420000': {
labelEvent: 'Bitcoin\'s 2nd Halving',
labelEventCompleted: 'Block Subsidy has halved to 12.5 BTC per block',
networks: ['mainnet', 'testnet'],
},
'630000': {
labelEvent: 'Bitcoin\'s 3rd Halving',
labelEventCompleted: 'Block Subsidy has halved to 6.25 BTC per block',
networks: ['mainnet', 'testnet'],
},
'709632': {
labelEvent: 'Taproot 🌱 activation',
labelEventCompleted: 'Taproot 🌱 has been activated!',
networks: ['mainnet'],
},
'840000': {
labelEvent: 'Bitcoin\'s 4th Halving',
labelEventCompleted: 'Block Subsidy has halved to 3.125 BTC per block',
networks: ['mainnet', 'testnet'],
},
'1050000': {
labelEvent: 'Bitcoin\'s 5th Halving',
labelEventCompleted: 'Block Subsidy has halved to 1.5625 BTC per block',
networks: ['mainnet', 'testnet'],
},
'1260000': {
labelEvent: 'Bitcoin\'s 6th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.78125 BTC per block',
networks: ['mainnet', 'testnet'],
},
'1470000': {
labelEvent: 'Bitcoin\'s 7th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.390625 BTC per block',
networks: ['mainnet', 'testnet'],
},
'1680000': {
labelEvent: 'Bitcoin\'s 8th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.1953125 BTC per block',
networks: ['mainnet', 'testnet'],
},
'1890000': {
labelEvent: 'Bitcoin\'s 9th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.09765625 BTC per block',
networks: ['mainnet', 'testnet'],
},
'2100000': {
labelEvent: 'Bitcoin\'s 10th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.04882812 BTC per block',
networks: ['mainnet', 'testnet'],
},
'2310000': {
labelEvent: 'Bitcoin\'s 11th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.02441406 BTC per block',
networks: ['mainnet', 'testnet'],
},
'2520000': {
labelEvent: 'Bitcoin\'s 12th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.01220703 BTC per block',
networks: ['mainnet', 'testnet'],
},
'2730000': {
labelEvent: 'Bitcoin\'s 13th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.00610351 BTC per block',
networks: ['mainnet', 'testnet'],
},
'2940000': {
labelEvent: 'Bitcoin\'s 14th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.00305175 BTC per block',
networks: ['mainnet', 'testnet'],
},
'3150000': {
labelEvent: 'Bitcoin\'s 15th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.00152587 BTC per block',
networks: ['mainnet', 'testnet'],
}
};

View File

@@ -254,30 +254,3 @@ export function selectPowerOfTen(val: number): { divider: number, unit: string }
return selectedPowerOfTen;
}
const featureActivation = {
mainnet: {
rbf: 399701,
segwit: 477120,
taproot: 709632,
},
testnet: {
rbf: 720255,
segwit: 872730,
taproot: 2032291,
},
signet: {
rbf: 0,
segwit: 0,
taproot: 0,
},
};
export function isFeatureActive(network: string, height: number, feature: 'rbf' | 'segwit' | 'taproot'): boolean {
const activationHeight = featureActivation[network || 'mainnet']?.[feature];
if (activationHeight != null) {
return height >= activationHeight;
} else {
return false;
}
}

View File

@@ -13,23 +13,7 @@
<p i18n>Our mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, completely self-hosted without any trusted third-parties.</p>
</div>
<video src="/resources/promo-video/mempool-promo.mp4" poster="/resources/promo-video/mempool-promo.jpg" controls loop playsinline [autoplay]="true" [muted]="true">
<track label="English" kind="captions" srclang="en" src="/resources/promo-video/en.vtt" [attr.default]="showSubtitles('en') ? '' : null">
<track label="日本語" kind="captions" srclang="ja" src="/resources/promo-video/ja.vtt" [attr.default]="showSubtitles('ja') ? '' : null">
<track label="中文" kind="captions" srclang="zh" src="/resources/promo-video/zh.vtt" [attr.default]="showSubtitles('zh') ? '' : null">
<track label="Svenska" kind="captions" srclang="sv" src="/resources/promo-video/sv.vtt" [attr.default]="showSubtitles('sv') ? '' : null">
<track label="Čeština" kind="captions" srclang="cs" src="/resources/promo-video/cs.vtt" [attr.default]="showSubtitles('cs') ? '' : null">
<track label="Suomi" kind="captions" srclang="fi" src="/resources/promo-video/fi.vtt" [attr.default]="showSubtitles('fi') ? '' : null">
<track label="Français" kind="captions" srclang="fr" src="/resources/promo-video/fr.vtt" [attr.default]="showSubtitles('fr') ? '' : null">
<track label="Deutsch" kind="captions" srclang="de" src="/resources/promo-video/de.vtt" [attr.default]="showSubtitles('de') ? '' : null">
<track label="Italiano" kind="captions" srclang="it" src="/resources/promo-video/it.vtt" [attr.default]="showSubtitles('it') ? '' : null">
<track label="Lietuvių" kind="captions" srclang="lt" src="/resources/promo-video/lt.vtt" [attr.default]="showSubtitles('lt') ? '' : null">
<track label="Norsk" kind="captions" srclang="nb" src="/resources/promo-video/nb.vtt" [attr.default]="showSubtitles('nb') ? '' : null">
<track label="فارسی" kind="captions" srclang="fa" src="/resources/promo-video/fa.vtt" [attr.default]="showSubtitles('fa') ? '' : null">
<track label="Polski" kind="captions" srclang="pl" src="/resources/promo-video/pl.vtt" [attr.default]="showSubtitles('pl') ? '' : null">
<track label="Română" kind="captions" srclang="ro" src="/resources/promo-video/ro.vtt" [attr.default]="showSubtitles('ro') ? '' : null">
<track label="Português" kind="captions" srclang="pt" src="/resources/promo-video/pt.vtt" [attr.default]="showSubtitles('pt') ? '' : null">
</video>
<video src="/resources/mempool-promo.mp4" poster="/resources/mempool-promo.jpg" controls loop playsinline [autoplay]="true" [muted]="true"></video>
<div class="enterprise-sponsor" id="enterprise-sponsors">
<h3 i18n="about.sponsors.enterprise.withRocket">Enterprise Sponsors 🚀</h3>
@@ -201,7 +185,7 @@
<span>Umbrel</span>
</a>
<a href="https://github.com/rootzoll/raspiblitz" target="_blank" title="RaspiBlitz">
<img class="image" src="/resources/profile/raspiblitz.svg" />
<img class="image" src="/resources/profile/raspiblitz.jpg" />
<span>RaspiBlitz</span>
</a>
<a href="https://github.com/mynodebtc/mynode" target="_blank" title="MyNode">
@@ -225,7 +209,7 @@
<span>EmbassyOS</span>
</a>
<a href="https://github.com/btcpayserver/btcpayserver" target="_blank" title="BTCPay Server">
<img class="image not-rounded" src="/resources/profile/btcpayserver.svg" />
<img class="image" src="/resources/profile/btcpayserver.svg" />
<span>BTCPay</span>
</a>
<a href="https://github.com/bisq-network/bisq" target="_blank" title="Bisq">
@@ -284,26 +268,6 @@
<img class="image" src="/resources/profile/nunchuk.svg" />
<span>Nunchuk</span>
</a>
<a href="https://github.com/bitcoin-s/bitcoin-s" target="_blank" title="bitcoin-s">
<img class="image" src="/resources/profile/bitcoin-s.svg" />
<span>bitcoin-s</span>
</a>
<a href="https://github.com/EdgeApp" target="_blank" title="Edge">
<img class="image not-rounded" src="/resources/profile/edge.svg" />
<span>Edge</span>
</a>
<a href="https://github.com/GaloyMoney" target="_blank" title="Galoy">
<img class="image" src="/resources/profile/galoy.svg" />
<span>Galoy</span>
</a>
<a href="https://github.com/BoltzExchange" target="_blank" title="Boltz">
<img class="image" src="/resources/profile/boltz.svg" />
<span>Boltz</span>
</a>
<a href="https://github.com/MutinyWallet" target="_blank" title="Mutiny">
<img class="image not-rounded" src="/resources/profile/mutiny.svg" />
<span>Mutiny</span>
</a>
</div>
</div>
@@ -419,12 +383,6 @@
<a target="_blank" href="https://matrix.to/#/#mempool:bitcoin.kyoto">
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="matrix" class="svg-inline--fa fa-matrix fa-w-16 fa-2x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1536 1792"><path fill="currentColor" d="M40.467 163.152v1465.696H145.92V1664H0V128h145.92v35.152zm450.757 464.64v74.14h2.069c19.79-28.356 43.717-50.215 71.483-65.575 27.765-15.656 59.963-23.336 96-23.336 34.56 0 66.165 6.795 94.818 20.086 28.652 13.293 50.216 37.22 65.28 70.893 16.246-23.926 38.4-45.194 66.166-63.507 27.766-18.314 60.848-27.472 98.954-27.472 28.948 0 55.828 3.545 80.64 10.635 24.812 7.088 45.785 18.314 63.508 33.968 17.722 15.656 31.31 35.742 41.354 60.85 9.747 25.107 14.768 55.236 14.768 90.683v366.573h-150.35V865.28c0-18.314-.59-35.741-2.068-51.987-1.476-16.247-5.316-30.426-11.52-42.24-6.499-12.112-15.656-21.563-28.062-28.653-12.405-7.088-29.242-10.634-50.214-10.634-21.268 0-38.4 4.135-51.397 12.112-12.997 8.27-23.336 18.608-30.72 31.901-7.386 12.997-12.407 27.765-14.77 44.602-2.363 16.542-3.84 33.379-3.84 50.216v305.133H692.971v-307.2c0-16.247-.294-32.197-1.18-48.149-.591-15.95-3.84-30.424-9.157-44.011-5.317-13.293-14.178-24.223-26.585-32.197-12.406-7.976-30.425-12.112-54.646-12.112-7.088 0-16.542 1.478-28.062 4.726-11.52 3.25-23.04 9.157-33.968 18.02-10.93 8.86-20.383 21.563-28.063 38.103-7.68 16.543-11.52 38.4-11.52 65.28v317.834H349.44V627.792zm1004.309 1001.056V163.152H1390.08V128H1536v1536h-145.92v-35.152z"/></svg>
</a>
<a target="_blank" href="https://youtube.com/@mempool">
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="youtube" class="svg-inline--fa fa-youtube fa-w-16 fa-2x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M549.655 124.083c-6.281-23.65-24.787-42.276-48.284-48.597C458.781 64 288 64 288 64S117.22 64 74.629 75.486c-23.497 6.322-42.003 24.947-48.284 48.597-11.412 42.867-11.412 132.305-11.412 132.305s0 89.438 11.412 132.305c6.281 23.65 24.787 41.5 48.284 47.821C117.22 448 288 448 288 448s170.78 0 213.371-11.486c23.497-6.321 42.003-24.171 48.284-47.821 11.412-42.867 11.412-132.305 11.412-132.305s0-89.438-11.412-132.305zm-317.51 213.508V175.185l142.739 81.205-142.739 81.201z"/></svg>
</a>
<a target="_blank" href="https://bitcointv.com/c/mempool/videos" class="bitcointv">
<svg xmlns="http://www.w3.org/2000/svg" focusable="false" viewBox="0 0 440 440"><path d="M225.57,2.08l-.69-.45a4.22,4.22,0,0,0-5.72,1.23L182.33,46.09a4,4,0,0,0,.88,5.81l9.38,6.38L173.48,97.49a4.22,4.22,0,0,0,2.45,4.19s3.55.7,4.53-1l41.92-40.56a3.62,3.62,0,0,0-1.51-5.1l-10.55-6.12L227.44,6.79A4.26,4.26,0,0,0,225.57,2.08Z" fill="currentColor"></path><path d="M118.52,401.83c-62.51,0-113.37-51-113.37-113.67V214.68C5.15,152,56,101,118.52,101H342.08a24.82,24.82,0,0,1,24.76,24.83V377a24.81,24.81,0,0,1-24.76,24.82Z"></path><path d="M342.08,105.18a20.65,20.65,0,0,1,20.61,20.66V377a20.66,20.66,0,0,1-20.61,20.66H118.52C58.3,397.67,9.31,348.55,9.31,288.16V214.68c0-60.38,49-109.5,109.21-109.5H342.08m0-8.34H118.52C53.62,96.84,1,149.6,1,214.68v73.48C1,353.24,53.62,406,118.52,406H342.08A29,29,0,0,0,371,377V125.84a29,29,0,0,0-28.92-29Z" fill="currentColor"></path><path fill="currentColor" d="M344.69,346.23A25.84,25.84,0,1,0,335,369.87l-10.22-10.2a11.69,11.69,0,1,1,4.77-5.12l10.31,10.28A25.84,25.84,0,0,0,344.69,346.23Z"></path><path fill="currentColor" d="M315.82,257.61a25.67,25.67,0,0,0-12.53,5.22L315,274.49a9.58,9.58,0,0,1,2.11-.73A9.72,9.72,0,1,1,309.4,283a9.4,9.4,0,0,1,.75-3.41L298.4,267.84a25.77,25.77,0,1,0,17.42-10.23Z"></path><path fill="currentColor" d="M313,214a7.76,7.76,0,1,1,1.41,10.91,7.62,7.62,0,0,1-2.19-2.69l-18.67-.14a25.94,25.94,0,1,0,.05-7l18.64.14A7.4,7.4,0,0,1,313,214Z"></path><path fill="currentColor" d="M341.2,144.08h-6.32c-1.67,0-3.61,1.87-3.61,4.29s1.94,4.29,3.61,4.29h6.32c1.67,0,3.61-1.87,3.61-4.29S342.87,144.08,341.2,144.08Z"></path><path fill="currentColor" d="M301.75,144.08h-6.44c-1.67,0-3.61,1.87-3.61,4.29s1.94,4.29,3.61,4.29h6.44c1.67,0,3.61-1.87,3.61-4.29S303.42,144.08,301.75,144.08Z"></path><path fill="currentColor" d="M321.77,144.08h-7c-1.67,0-3.62,1.87-3.62,4.29s1.95,4.29,3.62,4.29h7c1.67,0,3.62-1.87,3.62-4.29S323.44,144.08,321.77,144.08Z"></path><ellipse fill="currentColor" cx="295.97" cy="127.61" rx="4.27" ry="4.29"></ellipse><path fill="currentColor" d="M340.54,131.9a4.29,4.29,0,1,0-4.27-4.29A4.28,4.28,0,0,0,340.54,131.9Z"></path><path fill="currentColor" d="M318.26,131.9a4.29,4.29,0,1,0-4.27-4.29A4.29,4.29,0,0,0,318.26,131.9Z"></path><ellipse fill="currentColor" cx="295.97" cy="169.13" rx="4.27" ry="4.29"></ellipse><path fill="currentColor" d="M340.54,164.84a4.3,4.3,0,1,0,4.27,4.29A4.29,4.29,0,0,0,340.54,164.84Z"></path><path fill="currentColor" d="M318.26,164.84a4.3,4.3,0,1,0,4.28,4.29A4.29,4.29,0,0,0,318.26,164.84Z"></path><path d="M108.62,256.87c8.36-1,7.68-7.76,3.14-17-3.64-7.4-9.74-16.39-15.75-25.36-14.23-21.23-27.69-42.23-5.35-41.07,19.55,1,42.9,18.63,68.22,36.74,31.1,22.24,65.16,45.21,98.81,39.11a151.19,151.19,0,0,1,20-2.37V221a92,92,0,0,0-91.91-92.16H124.33A92,92,0,0,0,32.42,221v17.59c17.71,3.81,31,9.94,43.8,14.15C86.6,256.16,96.69,258.31,108.62,256.87Z"></path><path d="M273.37,310.79c-35-15.26-76.67-32.1-104-23.59-3.15,1-5,2.3-6,3.85-3.35,5.31,4.67,13.57,14.89,22.17,7.17,6,15.36,12.21,21.44,17.64,11.47,10.26,15.35,17.84-9.89,16.62-29.75-1.44-49.18-13.75-71.18-24l-.29-.14a165.84,165.84,0,0,0-22.93-8.91c-15.74-4.67-34.22-6.79-58.51-3.28A91.93,91.93,0,0,0,124.33,375h61.45A92,92,0,0,0,273.37,310.79Z"></path><path fill="currentColor" d="M257.69,249.31C224,255.41,190,232.44,158.88,210.2c-25.32-18.11-48.67-35.72-68.22-36.74C68.32,172.3,81.78,193.3,96,214.53c6,9,12.11,18,15.75,25.36,4.54,9.22,5.22,16-3.14,17-11.93,1.44-22-.71-32.4-4.13-12.8-4.21-26.09-10.34-43.8-14.15v44.26c0,1.26.14,2.48.19,3.72a91.8,91.8,0,0,0,2.9,19.62c.43,1.67.84,3.34,1.37,5,24.29-3.51,42.77-1.39,58.51,3.28a165.84,165.84,0,0,1,22.93,8.91c.39-.12.76-.26,1.14-.39l-.85.53c22,10.25,41.43,22.56,71.18,24,25.24,1.22,21.36-6.36,9.89-16.62-6.08-5.43-14.27-11.61-21.44-17.64-10.22-8.6-18.24-16.86-14.89-22.17,1-1.55,2.87-2.87,6-3.85,27.33-8.51,69,8.33,104,23.59.32-1,.56-2.05.84-3.07a92.33,92.33,0,0,0,3.48-24.87V246.94A151.19,151.19,0,0,0,257.69,249.31Z"></path><path fill="currentColor" d="M192,137a78,78,0,0,1,77.78,78v73.91a78,78,0,0,1-77.78,78H118.51a78,78,0,0,1-77.78-78V215a78,78,0,0,1,77.78-78H192m0-8.33H118.51A86.21,86.21,0,0,0,32.42,215v73.91a86.21,86.21,0,0,0,86.09,86.33H192a86.21,86.21,0,0,0,86.09-86.33V215A86.21,86.21,0,0,0,192,128.64Z"></path></svg>
</a>
</div>
</div>

View File

@@ -11,12 +11,6 @@
line-height: 32px;
}
.image.not-rounded {
border-radius: 0;
width: 60px;
height: 60px;
}
.intro {
margin: 25px auto 30px;
width: 250px;
@@ -42,11 +36,9 @@
video {
width: 640px;
height: 360px;
max-width: 90%;
margin-top: 0;
@media (min-width: 768px) {
height: 360px;
}
}
.social-icons {
@@ -59,13 +51,9 @@
.enterprise-sponsor,
.community-integrations-sponsor,
.maintainers {
margin-top: 30px;
margin-top: 68px;
margin-bottom: 68px;
scroll-margin: 30px;
@media (min-width: 768px) {
margin-top: 68px;
}
}
.maintainers {
@@ -211,16 +199,6 @@
a {
margin: 45px 10px;
}
.bitcointv svg {
width: 36px;
height: auto;
vertical-align: bottom;
margin-bottom: 2px;
margin-left: 5px;
}
.bitcointv svg:hover {
opacity: 0.75;
}
}
}
@@ -234,11 +212,6 @@
}
.community-integrations-sponsor {
max-width: 1110px;
max-width: 965px;
margin: auto;
}
.community-integrations-sponsor img.image {
width: 64px;
height: 64px;
}

View File

@@ -68,7 +68,7 @@ export class AboutComponent implements OnInit {
tap(() => this.goToAnchor())
);
}
ngAfterViewInit() {
this.goToAnchor();
}
@@ -90,8 +90,4 @@ export class AboutComponent implements OnInit {
this.showNavigateToSponsor = true;
}
}
showSubtitles(language) {
return ( this.locale.startsWith( language ) && !this.locale.startsWith('en') );
}
}

View File

@@ -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.feeRange[0] | 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>
</tr>
<tr *ngIf="block?.extras?.medianFee != undefined">
<td class="td-width" i18n="block.median-fee">Median fee</td>

View File

@@ -6,7 +6,7 @@
<div [attr.data-cy]="'bitcoin-block-offset-' + offset + '-index-' + i"
class="text-center bitcoin-block mined-block blockchain-blocks-offset-{{ offset }}-index-{{ i }}"
id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]"
[class.blink-bg]="isSpecial(block.height)">
[class.blink-bg]="(specialBlocks[block.height] !== undefined)">
<a draggable="false" [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }"
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}">&nbsp;</a>
<div [attr.data-cy]="'bitcoin-block-' + i + '-height'" class="block-height">

View File

@@ -20,8 +20,8 @@ interface BlockchainBlock extends BlockExtended {
export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
@Input() static: boolean = false;
@Input() offset: number = 0;
@Input() height: number = 0; // max height of blocks in chunk (dynamic blocks only)
@Input() count: number = 8; // number of blocks in this chunk (dynamic blocks only)
@Input() height: number = 0;
@Input() count: number = 8;
@Input() loadingTip: boolean = false;
@Input() connected: boolean = true;
@@ -31,7 +31,6 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
dynamicBlocksAmount: number = 8;
emptyBlocks: BlockExtended[] = this.mountEmptyBlocks();
markHeight: number;
chainTip: number;
blocksSubscription: Subscription;
blockPageSubscription: Subscription;
networkSubscription: Subscription;
@@ -74,7 +73,6 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
}
ngOnInit() {
this.chainTip = this.stateService.latestBlockHeight;
this.dynamicBlocksAmount = Math.min(8, this.stateService.env.KEEP_BLOCKS_AMOUNT);
if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
@@ -109,7 +107,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.blocks.unshift(block);
this.blocks = this.blocks.slice(0, this.dynamicBlocksAmount);
if (txConfirmed && block.height > this.chainTip) {
if (txConfirmed) {
this.markHeight = block.height;
this.moveArrowToPosition(true, true);
} else {
@@ -117,7 +115,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
}
this.blockStyles = [];
if (this.blocksFilled && block.height > this.chainTip) {
if (this.blocksFilled) {
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, i ? -155 : -205)));
setTimeout(() => {
this.blockStyles = [];
@@ -131,8 +129,6 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
if (this.blocks.length === this.dynamicBlocksAmount) {
this.blocksFilled = true;
}
this.chainTip = Math.max(this.chainTip, block.height);
this.cd.markForCheck();
});
} else {
@@ -269,10 +265,6 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.cd.markForCheck();
}
isSpecial(height: number): boolean {
return this.specialBlocks[height]?.networks.includes(this.stateService.network || 'mainnet') ? true : false;
}
getStyleForBlock(block: BlockchainBlock, index: number, animateEnterFrom: number = 0) {
if (!block || block.placeholder) {
return this.getStyleForPlaceholderBlock(index, animateEnterFrom);

View File

@@ -26,7 +26,7 @@
</thead>
<tbody *ngIf="blocks$ | async as blocks; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<tr *ngFor="let block of blocks; let i= index; trackBy: trackByBlock">
<td class="height text-left" [class]="widget ? 'widget' : ''">
<td class="text-left" [class]="widget ? 'widget' : ''">
<a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a>
</td>
<td *ngIf="indexingAvailable" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
@@ -89,6 +89,7 @@
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td *ngIf="indexingAvailable" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
<img width="1" height="25" style="opacity: 0">
<span class="skeleton-loader" style="max-width: 125px"></span>
</td>
<td class="timestamp" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
@@ -97,7 +98,7 @@
<td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
<span class="skeleton-loader" style="max-width: 125px"></span>
</td>
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
<td *ngIf="auditAvailable" class="health text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td *ngIf="indexingAvailable" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">

View File

@@ -51,12 +51,7 @@ tr, td, th {
.pool.widget {
width: 40%;
padding-left: 24px;
@media (min-width: 768px) AND (max-width: 926px) {
padding-left: 0px;
width: 60%;
}
@media (max-width: 430px) {
padding-left: 0px;
@media (max-width: 376px) {
width: 60%;
}
}
@@ -64,10 +59,6 @@ tr, td, th {
display: inline-block;
vertical-align: text-top;
padding-left: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 160px;
}
.height {
@@ -78,12 +69,6 @@ tr, td, th {
@media (max-width: 576px) {
width: 10%;
}
@media (min-width: 768px) AND (max-width: 926px) {
width: 30%;
}
@media (max-width: 430px) {
width: 30%;
}
}
.height.legacy {
width: 15%;
@@ -107,7 +92,7 @@ tr, td, th {
.mined {
width: 13%;
@media (max-width: 730px) {
@media (max-width: 576px) {
display: none;
}
}
@@ -153,7 +138,7 @@ tr, td, th {
.fees {
width: 8%;
@media (max-width: 820px) {
@media (max-width: 650px) {
display: none;
}
}
@@ -178,16 +163,6 @@ tr, td, th {
width: 30%;
padding-right: 0;
}
@media (min-width: 768px) AND (max-width: 926px) {
overflow: hidden;
text-overflow: ellipsis;
max-width: 90px;
}
@media (max-width: 430px) {
overflow: hidden;
text-overflow: ellipsis;
max-width: 90px;
}
}
.size {
@@ -214,10 +189,10 @@ tr, td, th {
.health {
width: 10%;
@media (max-width: 1105px) {
@media (max-width: 1000px) {
width: 13%;
}
@media (max-width: 560px) {
@media (max-width: 950px) {
display: none;
}
@@ -227,7 +202,7 @@ tr, td, th {
}
.health.widget {
width: 25%;
@media (max-width: 1105px) {
@media (max-width: 1000px) {
display: none;
}
@media (max-width: 767px) {
@@ -267,4 +242,4 @@ tr, td, th {
vertical-align: middle;
max-width: 50vw;
text-align: left;
}
}

View File

@@ -87,8 +87,8 @@ export class BlocksList implements OnInit, OnDestroy {
this.stateService.blocks$
.pipe(
switchMap((block) => {
if (block[0].height <= this.lastBlockHeight) {
return [null]; // Return an empty stream so the last pipe is not executed
if (block[0].height < this.lastBlockHeight) {
return []; // Return an empty stream so the last pipe is not executed
}
this.lastBlockHeight = block[0].height;
return [block];
@@ -101,16 +101,14 @@ export class BlocksList implements OnInit, OnDestroy {
this.lastPage = this.page;
return blocks[0];
}
if (blocks[1]) {
this.blocksCount = Math.max(this.blocksCount, blocks[1][0].height) + 1;
if (this.stateService.env.MINING_DASHBOARD) {
// @ts-ignore: Need to add an extra field for the template
blocks[1][0].extras.pool.logo = `/resources/mining-pools/` +
blocks[1][0].extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
}
acc.unshift(blocks[1][0]);
acc = acc.slice(0, this.widget ? 6 : 15);
this.blocksCount = Math.max(this.blocksCount, blocks[1][0].height) + 1;
if (this.stateService.env.MINING_DASHBOARD) {
// @ts-ignore: Need to add an extra field for the template
blocks[1][0].extras.pool.logo = `/resources/mining-pools/` +
blocks[1][0].extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
}
acc.unshift(blocks[1][0]);
acc = acc.slice(0, this.widget ? 6 : 15);
return acc;
}, [])
);

View File

@@ -2,19 +2,18 @@
<table class="table latest-adjustments">
<thead>
<tr>
<th class="" i18n="block.height">Height</th>
<th class="date text-left" i18n="mining.adjusted">Adjusted</th>
<th class="text-right" i18n="mining.difficulty">Difficulty</th>
<th class="text-right" i18n="mining.change">Change</th>
<th class="d-none d-md-block" i18n="block.height">Height</th>
<th i18n="mining.adjusted" class="text-left">Adjusted</th>
<th i18n="mining.difficulty" class="text-right">Difficulty</th>
<th i18n="mining.change" class="text-right">Change</th>
</tr>
</thead>
<tbody *ngIf="(hashrateObservable$ | async) as data">
<tr *ngFor="let diffChange of data">
<td class="">
<a [routerLink]="['/block' | relativeUrl, diffChange.height]">{{ diffChange.height }}</a>
</td>
<td class="date text-left">
<app-time kind="since" [time]="diffChange.timestamp" [fastRender]="true" [precision]="1"></app-time>
<td class="d-none d-md-block"><a [routerLink]="['/block' | relativeUrl, diffChange.height]">{{ diffChange.height
}}</a></td>
<td class="text-left">
<app-time kind="since" [time]="diffChange.timestamp" [fastRender]="true"></app-time>
</td>
<td class="text-right">{{ diffChange.difficultyShorten }}</td>
<td class="text-right" [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'">
@@ -24,8 +23,8 @@
</tbody>
<tbody *ngIf="isLoading">
<tr *ngFor="let item of [1,2,3,4,5,6]">
<td class=""><span class="skeleton-loader"></span></td>
<td class="date text-left"><span class="skeleton-loader w-75"></span></td>
<td class="d-none d-md-block w-75"><span class="skeleton-loader"></span></td>
<td class="text-left"><span class="skeleton-loader w-75"></span></td>
<td class="text-right"><span class="skeleton-loader w-75"></span></td>
<td class="text-right"><span class="skeleton-loader w-75"></span></td>
</tr>

View File

@@ -17,12 +17,3 @@
}
}
}
.date {
@media (min-width: 767px) AND (max-width: 991px) {
display: none;
}
@media (max-width: 500px) {
display: none;
}
}

View File

@@ -1,88 +0,0 @@
<div *ngIf="showTitle" class="main-title" i18n="dashboard.difficulty-adjustment">Difficulty Adjustment</div>
<div class="card-wrapper">
<div class="card">
<div class="card-body more-padding">
<div class="difficulty-adjustment-container" *ngIf="(isLoadingWebSocket$ | async) === false && (difficultyEpoch$ | async) as epochData; else loadingDifficulty">
<div class="item">
<h5 class="card-title" i18n="difficulty-box.remaining">Remaining</h5>
<div class="card-text">
<ng-container *ngTemplateOutlet="epochData.remainingBlocks === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.remainingBlocks }"></ng-container>
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
</div>
<div class="symbol"><app-time kind="until" [time]="epochData.estimatedRetargetDate" [fastRender]="true" [precision]="1"></app-time></div>
</div>
<div class="item">
<h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5>
<div *ngIf="epochData.remainingBlocks < 1870; else recentlyAdjusted" class="card-text" [ngStyle]="{'color': epochData.colorAdjustments}">
<span *ngIf="epochData.change > 0; else arrowDownDifficulty" >
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
</span>
<ng-template #arrowDownDifficulty >
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
</ng-template>
{{ epochData.change | absolute | number: '1.2-2' }}
<span class="symbol">%</span>
</div>
<ng-template #recentlyAdjusted>
<div class="card-text">&#8212;</div>
</ng-template>
<div class="symbol">
<span i18n="difficulty-box.previous">Previous</span>:
<span [ngStyle]="{'color': epochData.colorPreviousAdjustments}">
<span *ngIf="epochData.previousRetarget > 0; else arrowDownPreviousDifficulty" >
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
</span>
<ng-template #arrowDownPreviousDifficulty >
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
</ng-template>
{{ epochData.previousRetarget | absolute | number: '1.2-2' }} </span> %
</div>
</div>
<div class="item" *ngIf="showProgress">
<h5 class="card-title" i18n="difficulty-box.current-period">Current Period</h5>
<div class="card-text">{{ epochData.progress | number: '1.2-2' }} <span class="symbol">%</span></div>
<div class="progress small-bar">
<div class="progress-bar" role="progressbar" style="width: 15%; background-color: #105fb0" [ngStyle]="{'width': epochData.base}">&nbsp;</div>
</div>
</div>
<div class="item" *ngIf="showHalving">
<h5 class="card-title" i18n="difficulty-box.next-halving" i18n-ngbTooltip="difficulty-box.next-halving"
ngbTooltip="Next Halving" placement="bottom" #averagefee [disableTooltip]="!isEllipsisActive(averagefee)">Next Halving</h5>
<div class="card-text">
<ng-container *ngTemplateOutlet="epochData.blocksUntilHalving === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.blocksUntilHalving }"></ng-container>
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
</div>
<div class="symbol"><app-time kind="until" [time]="epochData.timeUntilHalving" [fastRender]="true" [precision]="1"></app-time></div>
</div>
</div>
</div>
</div>
</div>
<ng-template #loadingDifficulty>
<div class="difficulty-skeleton loading-container">
<div class="item">
<h5 class="card-title" i18n="difficulty-box.remaining">Remaining</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="difficulty-box.next-halving">Next Halving</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
</div>
</ng-template>

View File

@@ -1,160 +0,0 @@
.difficulty-adjustment-container {
display: flex;
flex-direction: row;
justify-content: space-around;
height: 76px;
.shared-block {
color: #ffffff66;
font-size: 12px;
}
.item {
padding: 0 5px;
width: 100%;
max-width: 150px;
&:nth-child(1) {
display: none;
@media (min-width: 485px) {
display: table-cell;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 992px) {
display: table-cell;
}
}
}
.card-text {
font-size: 22px;
margin-top: -9px;
position: relative;
}
}
.difficulty-skeleton {
display: flex;
justify-content: space-between;
@media (min-width: 376px) {
flex-direction: row;
}
.item {
min-width: 120px;
max-width: 150px;
margin: 0;
width: -webkit-fill-available;
@media (min-width: 376px) {
margin: 0 auto 0px;
}
&:first-child{
display: none;
@media (min-width: 485px) {
display: block;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 992px) {
display: block;
}
}
&:last-child {
margin-bottom: 0;
}
}
.card-text {
.skeleton-loader {
width: 100%;
display: block;
&:first-child {
margin: 14px auto 0;
max-width: 80px;
}
&:last-child {
margin: 10px auto 0;
max-width: 120px;
}
}
}
}
.card {
background-color: #1d1f31;
height: 100%;
}
.card-title {
color: #4a68b9;
font-size: 1rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.progress {
display: inline-flex;
width: 100%;
background-color: #2d3348;
height: 1.1rem;
max-width: 180px;
}
.skeleton-loader {
max-width: 100%;
}
.more-padding {
padding: 18px;
}
.small-bar {
height: 8px;
top: -4px;
max-width: 120px;
}
.loading-container {
min-height: 76px;
}
.main-title {
position: relative;
color: #ffffff91;
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;
font-weight: 500;
text-align: center;
padding-bottom: 3px;
}
.card-wrapper {
.card {
height: auto !important;
}
.card-body {
display: flex;
flex: inherit;
text-align: center;
flex-direction: column;
justify-content: space-around;
padding: 24px 20px;
}
}
.retarget-sign {
margin-right: -3px;
font-size: 14px;
top: -2px;
position: relative;
}
.previous-retarget-sign {
margin-right: -2px;
font-size: 10px;
}
.symbol {
font-size: 13px;
white-space: nowrap;
}

View File

@@ -1,90 +0,0 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { combineLatest, Observable, timer } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { StateService } from '../../services/state.service';
interface EpochProgress {
base: string;
change: number;
progress: number;
remainingBlocks: number;
newDifficultyHeight: number;
colorAdjustments: string;
colorPreviousAdjustments: string;
estimatedRetargetDate: number;
previousRetarget: number;
blocksUntilHalving: number;
timeUntilHalving: number;
}
@Component({
selector: 'app-difficulty-mining',
templateUrl: './difficulty-mining.component.html',
styleUrls: ['./difficulty-mining.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DifficultyMiningComponent implements OnInit {
isLoadingWebSocket$: Observable<boolean>;
difficultyEpoch$: Observable<EpochProgress>;
@Input() showProgress = true;
@Input() showHalving = false;
@Input() showTitle = true;
constructor(
public stateService: StateService,
) { }
ngOnInit(): void {
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
this.difficultyEpoch$ = combineLatest([
this.stateService.blocks$.pipe(map(([block]) => block)),
this.stateService.difficultyAdjustment$,
])
.pipe(
map(([block, da]) => {
let colorAdjustments = '#ffffff66';
if (da.difficultyChange > 0) {
colorAdjustments = '#3bcc49';
}
if (da.difficultyChange < 0) {
colorAdjustments = '#dc3545';
}
let colorPreviousAdjustments = '#dc3545';
if (da.previousRetarget) {
if (da.previousRetarget >= 0) {
colorPreviousAdjustments = '#3bcc49';
}
if (da.previousRetarget === 0) {
colorPreviousAdjustments = '#ffffff66';
}
} else {
colorPreviousAdjustments = '#ffffff66';
}
const blocksUntilHalving = 210000 - (block.height % 210000);
const timeUntilHalving = new Date().getTime() + (blocksUntilHalving * 600000);
const data = {
base: `${da.progressPercent.toFixed(2)}%`,
change: da.difficultyChange,
progress: da.progressPercent,
remainingBlocks: da.remainingBlocks - 1,
colorAdjustments,
colorPreviousAdjustments,
newDifficultyHeight: da.nextRetargetHeight,
estimatedRetargetDate: da.estimatedRetargetDate,
previousRetarget: da.previousRetarget,
blocksUntilHalving,
timeUntilHalving,
};
return data;
})
);
}
isEllipsisActive(e): boolean {
return (e.offsetWidth < e.scrollWidth);
}
}

View File

@@ -1,41 +0,0 @@
<div
#tooltip
*ngIf="status"
class="difficulty-tooltip"
[style.visibility]="status ? 'visible' : 'hidden'"
[style.left]="tooltipPosition.x + 'px'"
[style.top]="tooltipPosition.y + 'px'"
>
<ng-container [ngSwitch]="status">
<ng-container *ngSwitchCase="'mined'">
<ng-container *ngIf="isAhead">
<ng-container *ngTemplateOutlet="expected === 1 ? blocksSingular : blocksPlural; context: {$implicit: expected }"></ng-container>
<ng-template #blocksPlural let-i i18n="difficulty-box.expected-blocks">{{ i }} blocks expected</ng-template>
<ng-template #blocksSingular let-i i18n="difficulty-box.expected-block">{{ i }} block expected</ng-template>
</ng-container>
<ng-container *ngIf="!isAhead">
<ng-container *ngTemplateOutlet="mined === 1 ? blocksSingular : blocksPlural; context: {$implicit: mined }"></ng-container>
<ng-template #blocksPlural let-i i18n="difficulty-box.mined-blocks">{{ i }} blocks mined</ng-template>
<ng-template #blocksSingular let-i i18n="difficulty-box.mined-block">{{ i }} block mined</ng-template>
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'remaining'">
<ng-container *ngTemplateOutlet="remaining === 1 ? blocksSingular : blocksPlural; context: {$implicit: remaining }"></ng-container>
<ng-template #blocksPlural let-i i18n="difficulty-box.remaining-blocks">{{ i }} blocks remaining</ng-template>
<ng-template #blocksSingular let-i i18n="difficulty-box.remaining-block">{{ i }} block remaining</ng-template>
</ng-container>
<ng-container *ngSwitchCase="'ahead'">
<ng-container *ngTemplateOutlet="ahead === 1 ? blocksSingular : blocksPlural; context: {$implicit: ahead }"></ng-container>
<ng-template #blocksPlural let-i i18n="difficulty-box.blocks-ahead">{{ i }} blocks ahead</ng-template>
<ng-template #blocksSingular let-i i18n="difficulty-box.block-ahead">{{ i }} block ahead</ng-template>
</ng-container>
<ng-container *ngSwitchCase="'behind'">
<ng-container *ngTemplateOutlet="behind === 1 ? blocksSingular : blocksPlural; context: {$implicit: behind }"></ng-container>
<ng-template #blocksPlural let-i i18n="difficulty-box.blocks-behind">{{ i }} blocks behind</ng-template>
<ng-template #blocksSingular let-i i18n="difficulty-box.block-behind">{{ i }} block behind</ng-template>
</ng-container>
<ng-container *ngSwitchCase="'next'">
<span class="next-block" i18n="@@bdf0e930eb22431140a2eaeacd809cc5f8ebd38c">Next Block</span>
</ng-container>
</ng-container>
</div>

View File

@@ -1,22 +0,0 @@
.difficulty-tooltip {
position: fixed;
background: rgba(#11131f, 0.95);
border-radius: 4px;
box-shadow: 1px 1px 10px rgba(0,0,0,0.5);
color: #b1b1b1;
padding: 10px 15px;
text-align: left;
pointer-events: none;
max-width: 300px;
min-width: 200px;
text-align: center;
p {
margin: 0;
white-space: nowrap;
}
}
.next-block {
text-transform: lowercase;
}

View File

@@ -1,66 +0,0 @@
import { Component, ElementRef, ViewChild, Input, OnChanges } from '@angular/core';
interface EpochProgress {
base: string;
change: number;
progress: number;
minedBlocks: number;
remainingBlocks: number;
expectedBlocks: number;
newDifficultyHeight: number;
colorAdjustments: string;
colorPreviousAdjustments: string;
estimatedRetargetDate: number;
previousRetarget: number;
blocksUntilHalving: number;
timeUntilHalving: number;
}
const EPOCH_BLOCK_LENGTH = 2016; // Bitcoin mainnet
@Component({
selector: 'app-difficulty-tooltip',
templateUrl: './difficulty-tooltip.component.html',
styleUrls: ['./difficulty-tooltip.component.scss'],
})
export class DifficultyTooltipComponent implements OnChanges {
@Input() status: string | void;
@Input() progress: EpochProgress | void = null;
@Input() cursorPosition: { x: number, y: number };
mined: number;
ahead: number;
behind: number;
expected: number;
remaining: number;
isAhead: boolean;
isBehind: boolean;
tooltipPosition = { x: 0, y: 0 };
@ViewChild('tooltip') tooltipElement: ElementRef<HTMLCanvasElement>;
constructor() {}
ngOnChanges(changes): void {
if (changes.cursorPosition && changes.cursorPosition.currentValue) {
let x = changes.cursorPosition.currentValue.x;
let y = changes.cursorPosition.currentValue.y - 50;
if (this.tooltipElement) {
const elementBounds = this.tooltipElement.nativeElement.getBoundingClientRect();
x -= elementBounds.width / 2;
x = Math.min(Math.max(x, 20), (window.innerWidth - 20 - elementBounds.width));
}
this.tooltipPosition = { x, y };
}
if ((changes.progress || changes.status) && this.progress && this.status) {
this.remaining = this.progress.remainingBlocks;
this.expected = this.progress.expectedBlocks;
this.mined = this.progress.minedBlocks;
this.ahead = Math.max(0, this.mined - this.expected);
this.behind = Math.max(0, this.expected - this.mined);
this.isAhead = this.ahead > 0;
this.isBehind = this.behind > 0;
}
}
}

View File

@@ -3,100 +3,81 @@
<div class="card">
<div class="card-body more-padding">
<div class="difficulty-adjustment-container" *ngIf="(isLoadingWebSocket$ | async) === false && (difficultyEpoch$ | async) as epochData; else loadingDifficulty">
<div class="epoch-progress">
<svg class="epoch-blocks" height="22px" width="100%" viewBox="0 0 224 9" shape-rendering="crispEdges" preserveAspectRatio="none">
<defs>
<linearGradient id="diff-gradient" x1="0%" y1="0%" x2="100%" y2="0%" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#105fb0" />
<stop offset="100%" stop-color="#9339f4" />
</linearGradient>
<linearGradient id="diff-hover-gradient" x1="0%" y1="0%" x2="100%" y2="0%" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#2486eb" />
<stop offset="100%" stop-color="#ae6af7" />
</linearGradient>
</defs>
<rect
*ngFor="let rect of shapes"
[attr.x]="rect.x" [attr.y]="rect.y"
[attr.width]="rect.w" [attr.height]="rect.h"
class="rect {{rect.status}}"
[class.hover]="hoverSection && rect.status === hoverSection.status"
(pointerover)="onHover($event, rect);"
(pointerout)="onBlur($event);"
>
<animate
*ngIf="rect.status === 'next'"
attributeType="XML"
attributeName="fill"
[attr.values]="'#fff;' + (rect.expected ? '#D81B60' : '#2d3348') + ';#fff'"
dur="2s"
repeatCount="indefinite"/>
</rect>
</svg>
<div class="item">
<h5 class="card-title" i18n="difficulty-box.remaining">Remaining</h5>
<div class="card-text">
<ng-container *ngTemplateOutlet="epochData.remainingBlocks === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.remainingBlocks }"></ng-container>
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
</div>
<div class="symbol"><app-time kind="until" [time]="epochData.estimatedRetargetDate" [fastRender]="true"></app-time></div>
</div>
<div class="difficulty-stats">
<div class="item">
<div class="card-text">
~<app-time [time]="epochData.timeAvg / 1000" [fractionDigits]="1"></app-time>
</div>
<div class="symbol" i18n="difficulty-box.average-block-time">Average block time</div>
</div>
<div class="item">
<div *ngIf="epochData.remainingBlocks < 1870; else recentlyAdjusted" class="card-text bigger" [ngStyle]="{'color': epochData.colorAdjustments}">
<span *ngIf="epochData.change > 0; else arrowDownDifficulty" >
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
</span>
<ng-template #arrowDownDifficulty >
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
</ng-template>
{{ epochData.change | absolute | number: '1.2-2' }}
<span class="symbol">%</span>
</div>
<ng-template #recentlyAdjusted>
<div class="card-text">&#8212;</div>
<div class="item">
<h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5>
<div *ngIf="epochData.remainingBlocks < 1870; else recentlyAdjusted" class="card-text" [ngStyle]="{'color': epochData.colorAdjustments}">
<span *ngIf="epochData.change > 0; else arrowDownDifficulty" >
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
</span>
<ng-template #arrowDownDifficulty >
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
</ng-template>
<div class="symbol">
<span i18n="difficulty-box.previous">Previous</span>:
<span [ngStyle]="{'color': epochData.colorPreviousAdjustments}">
<span *ngIf="epochData.previousRetarget > 0; else arrowDownPreviousDifficulty" >
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
</span>
<ng-template #arrowDownPreviousDifficulty >
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
</ng-template>
{{ epochData.previousRetarget | absolute | number: '1.2-2' }} </span> %
</div>
{{ epochData.change | absolute | number: '1.2-2' }}
<span class="symbol">%</span>
</div>
<div class="item">
<div class="card-text"><app-time kind="until" [time]="epochData.estimatedRetargetDate" [fastRender]="true" [precision]="1"></app-time></div>
<div class="symbol">
{{ epochData.retargetDateString }}
</div>
<ng-template #recentlyAdjusted>
<div class="card-text">&#8212;</div>
</ng-template>
<div class="symbol">
<span i18n="difficulty-box.previous">Previous</span>:
<span [ngStyle]="{'color': epochData.colorPreviousAdjustments}">
<span *ngIf="epochData.previousRetarget > 0; else arrowDownPreviousDifficulty" >
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
</span>
<ng-template #arrowDownPreviousDifficulty >
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
</ng-template>
{{ epochData.previousRetarget | absolute | number: '1.2-2' }} </span> %
</div>
</div>
<div class="item" *ngIf="showProgress">
<h5 class="card-title" i18n="difficulty-box.current-period">Current Period</h5>
<div class="card-text">{{ epochData.progress | number: '1.2-2' }} <span class="symbol">%</span></div>
<div class="progress small-bar">
<div class="progress-bar" role="progressbar" style="width: 15%; background-color: #105fb0" [ngStyle]="{'width': epochData.base}">&nbsp;</div>
</div>
</div>
<div class="item" *ngIf="showHalving">
<h5 class="card-title" i18n="difficulty-box.next-halving">Next Halving</h5>
<div class="card-text">
<ng-container *ngTemplateOutlet="epochData.blocksUntilHalving === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.blocksUntilHalving }"></ng-container>
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
</div>
<div class="symbol"><app-time kind="until" [time]="epochData.timeUntilHalving" [fastRender]="true"></app-time></div>
</div>
</div>
</div>
</div>
</div>
<ng-template #loadingDifficulty>
<div class="epoch-progress">
<div class="skeleton-loader"></div>
</div>
<div class="difficulty-skeleton loading-container">
<div class="item">
<h5 class="card-title" i18n="difficulty-box.remaining">Remaining</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="difficulty-box.current-period">Current Period</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
@@ -104,10 +85,3 @@
</div>
</div>
</ng-template>
<app-difficulty-tooltip
*ngIf="hoverSection && (isLoadingWebSocket$ | async) === false && (difficultyEpoch$ | async) as epochData"
[cursorPosition]="tooltipPosition"
[status]="hoverSection.status"
[progress]="epochData"
></app-difficulty-tooltip>

View File

@@ -1,14 +1,8 @@
.difficulty-adjustment-container {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.difficulty-stats {
display: flex;
flex-direction: row;
justify-content: space-around;
height: 50.5px;
height: 76px;
.shared-block {
color: #ffffff66;
font-size: 12px;
@@ -30,28 +24,20 @@
}
}
.card-text {
font-size: 18px;
margin: auto;
font-size: 22px;
margin-top: -9px;
position: relative;
margin-bottom: 0.2rem;
&.bigger {
font-size: 20px;
margin-bottom: 0;
}
}
}
.difficulty-skeleton {
display: flex;
flex-direction: row;
justify-content: space-around;
height: 50.5px;
justify-content: space-between;
@media (min-width: 376px) {
flex-direction: row;
}
.item {
min-width: 120px;
max-width: 150px;
margin: 0;
width: -webkit-fill-available;
@@ -79,7 +65,7 @@
width: 100%;
display: block;
&:first-child {
margin: 10px auto 4px;
margin: 14px auto 0;
max-width: 80px;
}
&:last-child {
@@ -123,7 +109,7 @@
}
.loading-container {
min-height: 50.5px;
min-height: 76px;
}
.main-title {
@@ -147,7 +133,7 @@
text-align: center;
flex-direction: column;
justify-content: space-around;
padding: 20px;
padding: 24px 20px;
}
}
@@ -165,51 +151,4 @@
.symbol {
font-size: 13px;
white-space: nowrap;
}
.epoch-progress {
width: 100%;
height: 22px;
margin-bottom: 12px;
}
.epoch-blocks {
display: block;
width: 100%;
background: #2d3348;
.rect {
fill: #2d3348;
&.behind {
fill: #D81B60;
}
&.mined {
fill: url(#diff-gradient);
}
&.ahead {
fill: #1a9436;
}
&.hover {
fill: #535e84;
&.behind {
fill: #e94d86;
}
&.mined {
fill: url(#diff-hover-gradient);
}
&.ahead {
fill: #29d951;
}
}
}
}
.blocks-ahead {
color: #3bcc49;
}
.blocks-behind {
color: #D81B60;
}

View File

@@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, HostListener, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { combineLatest, Observable, timer } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { StateService } from '../..//services/state.service';
@@ -7,33 +7,16 @@ interface EpochProgress {
base: string;
change: number;
progress: number;
minedBlocks: number;
remainingBlocks: number;
expectedBlocks: number;
newDifficultyHeight: number;
colorAdjustments: string;
colorPreviousAdjustments: string;
estimatedRetargetDate: number;
retargetDateString: string;
previousRetarget: number;
blocksUntilHalving: number;
timeUntilHalving: number;
timeAvg: number;
}
type BlockStatus = 'mined' | 'behind' | 'ahead' | 'next' | 'remaining';
interface DiffShape {
x: number;
y: number;
w: number;
h: number;
status: BlockStatus;
expected: boolean;
}
const EPOCH_BLOCK_LENGTH = 2016; // Bitcoin mainnet
@Component({
selector: 'app-difficulty',
templateUrl: './difficulty.component.html',
@@ -41,27 +24,15 @@ const EPOCH_BLOCK_LENGTH = 2016; // Bitcoin mainnet
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DifficultyComponent implements OnInit {
@Input() showProgress = true;
@Input() showHalving = false;
@Input() showTitle = true;
isLoadingWebSocket$: Observable<boolean>;
difficultyEpoch$: Observable<EpochProgress>;
epochStart: number;
currentHeight: number;
currentIndex: number;
expectedHeight: number;
expectedIndex: number;
difference: number;
shapes: DiffShape[];
tooltipPosition = { x: 0, y: 0 };
hoverSection: DiffShape | void;
@Input() showProgress = true;
@Input() showHalving = false;
@Input() showTitle = true;
constructor(
public stateService: StateService,
@Inject(LOCALE_ID) private locale: string,
) { }
ngOnInit(): void {
@@ -94,110 +65,22 @@ export class DifficultyComponent implements OnInit {
const blocksUntilHalving = 210000 - (block.height % 210000);
const timeUntilHalving = new Date().getTime() + (blocksUntilHalving * 600000);
const newEpochStart = Math.floor(this.stateService.latestBlockHeight / EPOCH_BLOCK_LENGTH) * EPOCH_BLOCK_LENGTH;
const newExpectedHeight = Math.floor(newEpochStart + da.expectedBlocks);
if (newEpochStart !== this.epochStart || newExpectedHeight !== this.expectedHeight || this.currentHeight !== this.stateService.latestBlockHeight) {
this.epochStart = newEpochStart;
this.expectedHeight = newExpectedHeight;
this.currentHeight = this.stateService.latestBlockHeight;
this.currentIndex = this.currentHeight - this.epochStart;
this.expectedIndex = Math.min(this.expectedHeight - this.epochStart, 2016) - 1;
this.difference = this.currentIndex - this.expectedIndex;
this.shapes = [];
this.shapes = this.shapes.concat(this.blocksToShapes(
0, Math.min(this.currentIndex, this.expectedIndex), 'mined', true
));
this.shapes = this.shapes.concat(this.blocksToShapes(
this.currentIndex + 1, this.expectedIndex, 'behind', true
));
this.shapes = this.shapes.concat(this.blocksToShapes(
this.expectedIndex + 1, this.currentIndex, 'ahead', false
));
if (this.currentIndex < 2015) {
this.shapes = this.shapes.concat(this.blocksToShapes(
this.currentIndex + 1, this.currentIndex + 1, 'next', (this.expectedIndex > this.currentIndex)
));
}
this.shapes = this.shapes.concat(this.blocksToShapes(
Math.max(this.currentIndex + 2, this.expectedIndex + 1), 2105, 'remaining', false
));
}
let retargetDateString;
if (da.remainingBlocks > 1870) {
retargetDateString = (new Date(da.estimatedRetargetDate)).toLocaleDateString(this.locale, { month: 'long', day: 'numeric' });
} else {
retargetDateString = (new Date(da.estimatedRetargetDate)).toLocaleTimeString(this.locale, { month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });
}
const data = {
base: `${da.progressPercent.toFixed(2)}%`,
change: da.difficultyChange,
progress: da.progressPercent,
minedBlocks: this.currentIndex + 1,
remainingBlocks: da.remainingBlocks - 1,
expectedBlocks: Math.floor(da.expectedBlocks),
remainingBlocks: da.remainingBlocks,
colorAdjustments,
colorPreviousAdjustments,
newDifficultyHeight: da.nextRetargetHeight,
estimatedRetargetDate: da.estimatedRetargetDate,
retargetDateString,
previousRetarget: da.previousRetarget,
blocksUntilHalving,
timeUntilHalving,
timeAvg: da.timeAvg,
};
return data;
})
);
}
blocksToShapes(start: number, end: number, status: BlockStatus, expected: boolean = false): DiffShape[] {
const startY = start % 9;
const startX = Math.floor(start / 9);
const endY = (end % 9);
const endX = Math.floor(end / 9);
if (startX > endX) {
return [];
}
if (startX === endX) {
return [{
x: startX, y: startY, w: 1, h: 1 + endY - startY, status, expected
}];
}
const shapes = [];
shapes.push({
x: startX, y: startY, w: 1, h: 9 - startY, status, expected
});
shapes.push({
x: endX, y: 0, w: 1, h: endY + 1, status, expected
});
if (startX < endX - 1) {
shapes.push({
x: startX + 1, y: 0, w: endX - startX - 1, h: 9, status, expected
});
}
return shapes;
}
@HostListener('pointermove', ['$event'])
onPointerMove(event) {
this.tooltipPosition = { x: event.clientX, y: event.clientY };
}
onHover(event, rect): void {
this.hoverSection = rect;
}
onBlur(event): void {
this.hoverSection = null;
}
}

View File

@@ -1,4 +1,4 @@
<div class="fee-estimation-wrapper" *ngIf="(isLoading$ | async) === false && (recommendedFees$ | async) as recommendedFees; else loadingFees">
<div class="fee-estimation-wrapper" *ngIf="(isLoadingWebSocket$ | async) === false && (recommendedFees$ | async) as recommendedFees; else loadingFees">
<div class="d-flex">
<div class="fee-progress-bar" [style.background]="noPriority">
<span class="fee-label" i18n="fees-box.no-priority" i18n-ngbTooltip="Transaction feerate tooltip (economy)" ngbTooltip="Either 2x the minimum, or the Low Priority rate (whichever is lower)" placement="top">No Priority</span>

View File

@@ -1,9 +1,9 @@
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { StateService } from '../../services/state.service';
import { Observable, combineLatest } from 'rxjs';
import { Observable } from 'rxjs';
import { Recommendedfees } from '../../interfaces/websocket.interface';
import { feeLevels, mempoolFeeColors } from '../../app.constants';
import { map, startWith, tap } from 'rxjs/operators';
import { tap } from 'rxjs/operators';
@Component({
selector: 'app-fees-box',
@@ -12,7 +12,7 @@ import { map, startWith, tap } from 'rxjs/operators';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FeesBoxComponent implements OnInit {
isLoading$: Observable<boolean>;
isLoadingWebSocket$: Observable<boolean>;
recommendedFees$: Observable<Recommendedfees>;
gradient = 'linear-gradient(to right, #2e324e, #2e324e)';
noPriority = '#2e324e';
@@ -22,12 +22,7 @@ export class FeesBoxComponent implements OnInit {
) { }
ngOnInit(): void {
this.isLoading$ = combineLatest(
this.stateService.isLoadingWebSocket$.pipe(startWith(false)),
this.stateService.loadingIndicators$.pipe(startWith({ mempool: 0 })),
).pipe(map(([socket, indicators]) => {
return socket || (indicators.mempool != null && indicators.mempool !== 100);
}));
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
this.recommendedFees$ = this.stateService.recommendedFees$
.pipe(
tap((fees) => {

View File

@@ -2,7 +2,7 @@
<div class="mempool-blocks-container" [class.time-ltr]="timeLtr" *ngIf="(difficultyAdjustments$ | async) as da;">
<div class="flashing">
<ng-template ngFor let-projectedBlock [ngForOf]="mempoolBlocks$ | async" let-i="index" [ngForTrackBy]="trackByFn">
<div @blockEntryTrigger [@.disabled]="!animateEntry" [attr.data-cy]="'mempool-block-' + i" class="bitcoin-block text-center mempool-block" id="mempool-block-{{ i }}" [ngStyle]="mempoolBlockStyles[i]" [class.blink-bg]="projectedBlock.blink">
<div [attr.data-cy]="'mempool-block-' + i" class="bitcoin-block text-center mempool-block" id="mempool-block-{{ i }}" [ngStyle]="mempoolBlockStyles[i]" [class.blink-bg]="projectedBlock.blink">
<a draggable="false" [routerLink]="['/mempool-block/' | relativeUrl, i]"
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}">&nbsp;</a>
<div class="block-body">
@@ -26,7 +26,7 @@
<app-time kind="until" [time]="(1 * i) + now + 61000" [fastRender]="false" [fixedRender]="true"></app-time>
</ng-template>
<ng-template #timeDiffMainnet>
<app-time kind="until" [time]="da.timeAvg * (i + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true"></app-time>
<app-time kind="until" [time]="da.timeAvg * (i + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true" [forceFloorOnTimeIntervals]="['hour']"></app-time>
</ng-template>
</div>
<ng-template #mergedBlock>

View File

@@ -1,5 +1,5 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { Subscription, Observable, fromEvent, merge, of, combineLatest } from 'rxjs';
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, Input } from '@angular/core';
import { Subscription, Observable, fromEvent, merge, of, combineLatest, timer } from 'rxjs';
import { MempoolBlock } from '../../interfaces/websocket.interface';
import { StateService } from '../../services/state.service';
import { Router } from '@angular/router';
@@ -9,18 +9,11 @@ import { specialBlocks } from '../../app.constants';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { Location } from '@angular/common';
import { DifficultyAdjustment } from '../../interfaces/node-api.interface';
import { animate, style, transition, trigger } from '@angular/animations';
@Component({
selector: 'app-mempool-blocks',
templateUrl: './mempool-blocks.component.html',
styleUrls: ['./mempool-blocks.component.scss'],
animations: [trigger('blockEntryTrigger', [
transition(':enter', [
style({ transform: 'translateX(-155px)' }),
animate('2s 0s ease', style({ transform: '' })),
]),
])],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MempoolBlocksComponent implements OnInit, OnDestroy {
@@ -39,14 +32,12 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
isLoadingWebsocketSubscription: Subscription;
blockSubscription: Subscription;
networkSubscription: Subscription;
chainTipSubscription: Subscription;
network = '';
now = new Date().getTime();
timeOffset = 0;
showMiningInfo = false;
timeLtrSubscription: Subscription;
timeLtr: boolean;
animateEntry: boolean = false;
blockWidth = 125;
blockPadding = 30;
@@ -62,7 +53,6 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
resetTransitionTimeout: number;
chainTip: number = -1;
blockIndex = 1;
constructor(
@@ -79,8 +69,6 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
}
ngOnInit() {
this.chainTip = this.stateService.latestBlockHeight;
if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
this.enabledMiningInfoIfNeeded(this.location.path());
this.location.onUrlChange((url) => this.enabledMiningInfoIfNeeded(url));
@@ -128,7 +116,9 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
mempoolBlocks.forEach((block, i) => {
block.index = this.blockIndex + i;
block.height = lastBlock.height + i + 1;
block.blink = specialBlocks[block.height]?.networks.includes(this.stateService.network || 'mainnet') ? true : false;
if (this.stateService.network === '') {
block.blink = specialBlocks[block.height] ? true : false;
}
});
const stringifiedBlocks = JSON.stringify(mempoolBlocks);
@@ -165,24 +155,11 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
this.blockSubscription = this.stateService.blocks$
.subscribe(([block]) => {
if (this.chainTip === -1) {
this.animateEntry = block.height === this.stateService.latestBlockHeight;
} else {
this.animateEntry = block.height > this.chainTip;
}
this.chainTip = this.stateService.latestBlockHeight;
if ((block?.extras?.similarity == null || block?.extras?.similarity > 0.5) && !this.tabHidden) {
if (block?.extras?.matchRate >= 66 && !this.tabHidden) {
this.blockIndex++;
}
});
this.chainTipSubscription = this.stateService.chainTip$.subscribe((height) => {
if (this.chainTip === -1) {
this.chainTip = height;
}
});
this.networkSubscription = this.stateService.networkChanged$
.subscribe((network) => this.network = network);
@@ -218,12 +195,11 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
this.blockSubscription.unsubscribe();
this.networkSubscription.unsubscribe();
this.timeLtrSubscription.unsubscribe();
this.chainTipSubscription.unsubscribe();
clearTimeout(this.resetTransitionTimeout);
}
trackByFn(index: number, block: MempoolBlock) {
return (block.isStack) ? 'stack' : block.index;
return block.index;
}
reduceMempoolBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] {
@@ -238,10 +214,6 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
lastBlock.feeRange = lastBlock.feeRange.concat(block.feeRange);
lastBlock.feeRange.sort((a, b) => a - b);
lastBlock.medianFee = this.median(lastBlock.feeRange);
lastBlock.totalFees += block.totalFees;
}
if (blocks.length) {
blocks[blocks.length - 1].isStack = blocks[blocks.length - 1].blockVSize > this.stateService.blockVSize;
}
return blocks;
}
@@ -361,4 +333,4 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
}
return emptyBlocks;
}
}
}

View File

@@ -4,6 +4,7 @@
<div class="row row-cols-1 row-cols-md-2">
<!-- Temporary stuff here - Will be moved to a component once we have more useful data to show -->
<div class="col">
<div class="main-title">
<span [attr.data-cy]="'reward-stats'" i18n="mining.reward-stats">Reward stats</span>&nbsp;
@@ -21,7 +22,7 @@
<!-- difficulty adjustment -->
<div class="col">
<div class="main-title" i18n="dashboard.difficulty-adjustment">Difficulty Adjustment</div>
<app-difficulty-mining [attr.data-cy]="'difficulty-adjustment'" [showTitle]="false" [showProgress]="false" [showHalving]="true"></app-difficulty-mining>
<app-difficulty [attr.data-cy]="'difficulty-adjustment'" [showTitle]="false" [showProgress]="false" [showHalving]="true"></app-difficulty>
</div>
<!-- pool distribution -->

View File

@@ -32,7 +32,7 @@
</div>
<div class="card-header" *ngIf="!widget">
<div class="d-flex d-md-table-cell align-items-baseline">
<div class="d-flex d-md-block align-items-baseline">
<span i18n="mining.pools">Pools Ranking</span>
<button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
@@ -87,19 +87,19 @@
<table *ngIf="widget === false" class="table table-borderless text-center pools-table">
<thead>
<tr>
<th class="d-none d-md-table-cell" i18n="mining.rank">Rank</th>
<th class="d-none d-md-block" i18n="mining.rank">Rank</th>
<th class=""></th>
<th class="" i18n="mining.pool-name">Pool</th>
<th class="" *ngIf="this.miningWindowPreference === '24h'" i18n="mining.hashrate">Hashrate</th>
<th class="" i18n="master-page.blocks">Blocks</th>
<th *ngIf="auditAvailable" class="health text-right widget" i18n="latest-blocks.avg_health"
i18n-ngbTooltip="latest-blocks.avg_health" ngbTooltip="Avg Health" placement="bottom" #health [disableTooltip]="!isEllipsisActive(health)">Avg Health</th>
<th class="d-none d-md-table-cell" i18n="mining.empty-blocks">Empty blocks</th>
<th class="d-none d-md-block" i18n="mining.empty-blocks">Empty blocks</th>
</tr>
</thead>
<tbody [attr.data-cy]="'pools-table'" *ngIf="(miningStatsObservable$ | async) as miningStats">
<tr *ngFor="let pool of miningStats.pools">
<td class="d-none d-md-table-cell">{{ pool.rank }}</td>
<td class="d-none d-md-block">{{ pool.rank }}</td>
<td class="text-right">
<img width="25" height="25" src="{{ pool.logo }}" [alt]="pool.name + ' mining pool logo'" onError="this.src = '/resources/mining-pools/default.svg'">
</td>
@@ -107,7 +107,7 @@
<td class="" *ngIf="this.miningWindowPreference === '24h'">{{ pool.lastEstimatedHashrate }} {{
miningStats.miningUnits.hashrateUnit }}</td>
<td class="d-flex justify-content-center">
{{ pool.blockCount }}<span class="d-none d-md-table-cell">&nbsp;({{ pool.share }}%)</span>
{{ pool.blockCount }}<span class="d-none d-md-block">&nbsp;({{ pool.share }}%)</span>
</td>
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
<a
@@ -121,16 +121,16 @@
<span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span>
</ng-template>
</td>
<td class="d-none d-md-table-cell">{{ pool.emptyBlocks }} ({{ pool.emptyBlockRatio }}%)</td>
<td class="d-none d-md-block">{{ pool.emptyBlocks }} ({{ pool.emptyBlockRatio }}%)</td>
</tr>
<tr style="border-top: 1px solid #555">
<td class="d-none d-md-table-cell"></td>
<td class="d-none d-md-block"></td>
<td class="text-right"></td>
<td class=""><b i18n="mining.all-miners">All miners</b></td>
<td class="" *ngIf="this.miningWindowPreference === '24h'"><b>{{ miningStats.lastEstimatedHashrate}} {{
miningStats.miningUnits.hashrateUnit }}</b></td>
<td class=""><b>{{ miningStats.blockCount }}</b></td>
<td class="d-none d-md-table-cell"><b>{{ miningStats.totalEmptyBlock }} ({{ miningStats.totalEmptyBlockRatio
<td class="d-none d-md-block"><b>{{ miningStats.totalEmptyBlock }} ({{ miningStats.totalEmptyBlockRatio
}}%)</b></td>
</tr>
</tbody>

View File

@@ -162,10 +162,10 @@ export class PoolRankingComponent implements OnInit {
if (this.miningWindowPreference === '24h') {
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
pool.lastEstimatedHashrate.toString() + ' PH/s' +
`<br>` + $localize`${ i }:INTERPOLATION: blocks`;
`<br>` + $localize`${i} blocks`;
} else {
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
$localize`${ i }:INTERPOLATION: blocks`;
$localize`${i} blocks`;
}
}
},
@@ -195,15 +195,13 @@ export class PoolRankingComponent implements OnInit {
},
borderColor: '#000',
formatter: () => {
const percentage = totalShareOther.toFixed(2) + '%';
const i = totalBlockOther.toString();
if (this.miningWindowPreference === '24h') {
return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` +
return `<b style="color: white">${'Other'} (${totalShareOther.toFixed(2)}%)</b><br>` +
totalEstimatedHashrateOther.toString() + ' PH/s' +
`<br>` + $localize`${ i }:INTERPOLATION: blocks`;
`<br>` + totalBlockOther.toString() + ` blocks`;
} else {
return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` +
$localize`${ i }:INTERPOLATION: blocks`;
return `<b style="color: white">${'Other'} (${totalShareOther.toFixed(2)}%)</b><br>` +
totalBlockOther.toString() + ` blocks`;
}
}
},

View File

@@ -86,6 +86,11 @@ export class PoolPreviewComponent implements OnInit {
regexes += regex + '", "';
}
poolStats.pool.regexes = regexes.slice(0, -3);
poolStats.pool.addresses = poolStats.pool.addresses;
if (poolStats.reportedHashrate) {
poolStats.luck = poolStats.estimatedHashrate / poolStats.reportedHashrate * 100;
}
this.openGraphService.waitOver('pool-stats-' + this.slug);

View File

@@ -38,12 +38,12 @@
<tr *ngIf="!isMobile()" class="taller-row">
<td class="label addresses" i18n="mining.addresses">Addresses</td>
<td *ngIf="poolStats.pool.addresses.length else nodata" style="padding-top: 25px">
<a class="addresses-data" [routerLink]="['/address' | relativeUrl, poolStats.pool.addresses[0]]">
<a [routerLink]="['/address' | relativeUrl, poolStats.pool.addresses[0]]" class="first-address">
{{ poolStats.pool.addresses[0] }}
</a>
<div>
<div #collapse="ngbCollapse" [(ngbCollapse)]="gfg">
<a class="addresses-data" *ngFor="let address of poolStats.pool.addresses | slice: 1"
<a *ngFor="let address of poolStats.pool.addresses | slice: 1"
[routerLink]="['/address' | relativeUrl, address]">{{
address }}<br></a>
</div>
@@ -67,13 +67,13 @@
[attr.aria-expanded]="!gfg" aria-controls="collapseExample">
<span i18n="show-all">Show all</span> ({{ poolStats.pool.addresses.length }})
</button>
<a class="addresses-data" [routerLink]="['/address' | relativeUrl, poolStats.pool.addresses[0]]">
{{ poolStats.pool.addresses[0] | shortenString: 30 }}
<a [routerLink]="['/address' | relativeUrl, poolStats.pool.addresses[0]]">
{{ poolStats.pool.addresses[0] | shortenString: 40 }}
</a>
<div #collapse="ngbCollapse" [(ngbCollapse)]="gfg" style="width: 100%">
<a class="addresses-data" *ngFor="let address of poolStats.pool.addresses | slice: 1"
<a *ngFor="let address of poolStats.pool.addresses | slice: 1"
[routerLink]="['/address' | relativeUrl, address]">{{
address | shortenString: 30 }}<br></a>
address | shortenString: 40 }}<br></a>
</div>
</div>
</td>
@@ -88,25 +88,22 @@
<!-- Hashrate desktop -->
<tr *ngIf="!isMobile()" class="taller-row">
<td class="label" i18n="mining.hashrate-24h">Hashrate (24h)</td>
<td class="data">
<table class="table table-xs table-data">
<thead>
<tr>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.reward">Reward</th>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.hashrate">Hashrate (24h)</th>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="latest-blocks.avg_health" *ngIf="auditAvailable">Avg Health</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.estimated">Estimated</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported</th>
<th scope="col" class="block-count-title" style="width: 26%" i18n="mining.luck">Luck</th>
</tr>
</thead>
<tbody>
<td class="text-center"><app-amount [satoshis]="poolStats.totalReward" digitsInfo="1.0-0" [noFiat]="true"></app-amount></td>
<td class="text-center">{{ poolStats.estimatedHashrate | amountShortener : 1 : 'H/s' }}</td>
<td class="text-center" *ngIf="auditAvailable; else emptyTd"><span class="health-badge badge" [class.badge-success]="poolStats.avgBlockHealth >= 99"
[class.badge-warning]="poolStats.avgBlockHealth >= 75 && poolStats.avgBlockHealth < 99" [class.badge-danger]="poolStats.avgBlockHealth < 75"
*ngIf="poolStats.avgBlockHealth != null; else nullHealth">{{ poolStats.avgBlockHealth }}%</span>
<ng-template #nullHealth>
<span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span>
</ng-template>
</td>
<td>{{ poolStats.estimatedHashrate | amountShortener : 1 : 'H/s' }}</td>
<ng-template *ngIf="poolStats.luck; else noreported">
<td>{{ poolStats.reportedHashrate | amountShortener : 1 : 'H/s' }}</td>
<td>{{ formatNumber(poolStats.luck, this.locale, '1.2-2') }}%</td>
</ng-template>
</tbody>
</table>
</td>
@@ -114,46 +111,49 @@
<!-- Hashrate mobile -->
<tr *ngIf="isMobile()">
<td colspan="2">
<span class="label" i18n="mining.hashrate-24h">Hashrate (24h)</span>
<table class="table table-xs table-data">
<thead>
<tr>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.reward">Reward</th>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.hashrate">Hashrate (24h)</th>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="latest-blocks.avg_health" *ngIf="auditAvailable">Avg Health</th>
<th scope="col" class="block-count-title" style="width: 33%" i18n="mining.estimated">Estimated</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported</th>
<th scope="col" class="block-count-title" style="width: 30%" i18n="mining.luck">Luck</th>
</tr>
</thead>
<tbody>
<td class="text-center"><app-amount [satoshis]="poolStats.totalReward" digitsInfo="1.0-0" [noFiat]="true"></app-amount></td>
<td class="text-center">{{ poolStats.estimatedHashrate | amountShortener : 1 : 'H/s' }}</td>
<td *ngIf="auditAvailable; else emptyTd" class="text-center"><span class="health-badge badge" [class.badge-success]="poolStats.avgBlockHealth >= 99"
[class.badge-warning]="poolStats.avgBlockHealth >= 75 && poolStats.avgBlockHealth < 99" [class.badge-danger]="poolStats.avgBlockHealth < 75"
*ngIf="poolStats.avgBlockHealth != null; else nullHealth">{{ poolStats.avgBlockHealth }}%</span>
<ng-template #nullHealth>
<span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span>
</ng-template>
</td>
<td>{{ poolStats.estimatedHashrate | amountShortener : 1 : 'H/s' }}</td>
<ng-template *ngIf="poolStats.luck; else noreported">
<td>{{ poolStats.reportedHashrate | amountShortener : 1 : 'H/s' }}</td>
<td>{{ formatNumber(poolStats.luck, this.locale, '1.2-2') }}%</td>
</ng-template>
</tbody>
</table>
</td>
</tr>
<ng-template #noreported>
<td>~</td>
<td>~</td>
</ng-template>
<!-- Mined blocks desktop -->
<tr *ngIf="!isMobile()" class="taller-row">
<td class="label" i18n="mining.mined-blocks">Mined blocks</td>
<td class="data">
<table class="table table-xs table-data">
<thead>
<tr>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="1w">1w</th>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="all">All</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="24h">24h</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="1w">1w</th>
<th scope="col" class="block-count-title" style="width: 26%" i18n="all">All</th>
</tr>
</thead>
<tbody>
<td class="text-center">{{ formatNumber(poolStats.blockCount['24h'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
<td>{{ formatNumber(poolStats.blockCount['24h'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
poolStats.blockShare['24h'], this.locale, '1.0-0') }}%)</td>
<td class="text-center">{{ formatNumber(poolStats.blockCount['1w'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
<td>{{ formatNumber(poolStats.blockCount['1w'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
poolStats.blockShare['1w'], this.locale, '1.0-0') }}%)</td>
<td class="text-center">{{ formatNumber(poolStats.blockCount['all'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
<td>{{ formatNumber(poolStats.blockCount['all'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
poolStats.blockShare['all'], this.locale, '1.0-0') }}%)</td>
</tbody>
</table>
@@ -162,20 +162,21 @@
<!-- Mined blocks mobile -->
<tr *ngIf="isMobile()">
<td colspan=2>
<span class="label" i18n="mining.mined-blocks">Mined blocks</span>
<table class="table table-xs table-data">
<thead>
<tr>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="1w">1w</th>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="all">All</th>
<th scope="col" class="block-count-title" style="width: 33%" i18n="24h">24h</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="1w">1w</th>
<th scope="col" class="block-count-title" style="width: 30%" i18n="all">All</th>
</tr>
</thead>
<tbody>
<td class="text-center">{{ formatNumber(poolStats.blockCount['24h'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
<td>{{ formatNumber(poolStats.blockCount['24h'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
poolStats.blockShare['24h'], this.locale, '1.0-0') }}%)</td>
<td class="text-center">{{ formatNumber(poolStats.blockCount['1w'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
<td>{{ formatNumber(poolStats.blockCount['1w'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
poolStats.blockShare['1w'], this.locale, '1.0-0') }}%)</td>
<td class="text-center">{{ formatNumber(poolStats.blockCount['all'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
<td>{{ formatNumber(poolStats.blockCount['all'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
poolStats.blockShare['all'], this.locale, '1.0-0') }}%)</td>
</tbody>
</table>
@@ -212,9 +213,8 @@
<th class="timestamp" i18n="latest-blocks.timestamp">Timestamp</th>
<th class="mined" i18n="latest-blocks.mined">Mined</th>
<th class="coinbase text-left" i18n="latest-blocks.coinbasetag">Coinbase tag</th>
<th *ngIf="auditAvailable" class="health text-right" i18n="latest-blocks.health">Health</th>
<th class="reward text-right" i18n="latest-blocks.reward">Reward</th>
<th *ngIf="!auditAvailable" class="fees text-right" i18n="latest-blocks.fees">Fees</th>
<th class="fees text-right" i18n="latest-blocks.fees">Fees</th>
<th class="txs text-right" i18n="dashboard.txs">TXs</th>
<th class="size" i18n="latest-blocks.size">Size</th>
</thead>
@@ -234,24 +234,10 @@
{{ block.extras.coinbaseRaw | hex2ascii }}
</span>
</td>
<td *ngIf="auditAvailable" class="health text-right">
<a
class="health-badge badge"
[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="block.extras.matchRate != null; else nullHealth"
>{{ block.extras.matchRate }}%</a>
<ng-template #nullHealth>
<span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span>
</ng-template>
</td>
<td class="reward text-right">
<app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-2" [noFiat]="true"></app-amount>
</td>
<td *ngIf="!auditAvailable" class="fees text-right">
<td class="fees text-right">
<app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-2" [noFiat]="true"></app-amount>
</td>
<td class="txs text-right">
@@ -378,23 +364,24 @@
<!-- Hashrate desktop -->
<tr *ngIf="!isMobile()" class="taller-row">
<td class="label" i18n="mining.hashrate-24h">Hashrate (24h)</td>
<td class="data">
<table class="table table-xs table-data text-center">
<thead>
<tr>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.total-reward">Reward</th>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.estimated">Hashrate (24h)</th>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.luck" *ngIf="auditAvailable">Avg Health</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.estimated">Estimated</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported</th>
<th scope="col" class="block-count-title" style="width: 26%" i18n="mining.luck">Luck</th>
</tr>
</thead>
<tbody>
<td class="text-center">
<td>
<div class="skeleton-loader data"></div>
</td>
<td class="text-center">
<td>
<div class="skeleton-loader data"></div>
</td>
<td class="text-center" *ngIf="auditAvailable">
<td>
<div class="skeleton-loader data"></div>
</td>
</tbody>
@@ -404,22 +391,23 @@
<!-- Hashrate mobile -->
<tr *ngIf="isMobile()">
<td colspan="2">
<span class="label" i18n="mining.hashrate-24h">Hashrate (24h)</span>
<table class="table table-xs table-data text-center">
<thead>
<tr>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.total-reward">Reward</th>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.estimated">Hashrate (24h)</th>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.luck" *ngIf="auditAvailable">Avg Health</th>
<th scope="col" class="block-count-title" style="width: 33%" i18n="mining.estimated">Estimated</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported</th>
<th scope="col" class="block-count-title" style="width: 30%" i18n="mining.luck">Luck</th>
</tr>
</thead>
<tbody>
<td class="text-center">
<td>
<div class="skeleton-loader data"></div>
</td>
<td class="text-center">
<td>
<div class="skeleton-loader data"></div>
</td>
<td class="text-center" *ngIf="auditAvailable">
<td>
<div class="skeleton-loader data"></div>
</td>
</tbody>
@@ -429,23 +417,24 @@
<!-- Mined blocks desktop -->
<tr *ngIf="!isMobile()" class="taller-row">
<td class="label" i18n="mining.mined-blocks">Mined blocks</td>
<td class="data">
<table class="table table-xs table-data text-center">
<thead>
<tr>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="1w">1w</th>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="all">All</th>
<th scope="col" class="block-count-title" style="width: 37%">24h</th>
<th scope="col" class="block-count-title" style="width: 37%">1w</th>
<th scope="col" class="block-count-title" style="width: 26%" i18n="all">All</th>
</tr>
</thead>
<tbody>
<td class="text-center">
<td>
<div class="skeleton-loader data"></div>
</td>
<td class="text-center">
<td>
<div class="skeleton-loader data"></div>
</td>
<td class="text-center">
<td>
<div class="skeleton-loader data"></div>
</td>
</tbody>
@@ -455,22 +444,23 @@
<!-- Mined blocks mobile -->
<tr *ngIf="isMobile()">
<td colspan=2>
<span class="label" i18n="mining.mined-blocks">Mined blocks</span>
<table class="table table-xs table-data text-center">
<thead>
<tr>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="1w">1w</th>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="all">All</th>
<th scope="col" class="block-count-title" style="width: 33%">24h</th>
<th scope="col" class="block-count-title" style="width: 37%">1w</th>
<th scope="col" class="block-count-title" style="width: 30%" i18n="all">All</th>
</tr>
</thead>
<tbody>
<td class="text-center">
<td>
<div class="skeleton-loader data"></div>
</td>
<td class="text-center">
<td>
<div class="skeleton-loader data"></div>
</td>
<td class="text-center">
<td>
<div class="skeleton-loader data"></div>
</td>
</tbody>
@@ -485,8 +475,4 @@
</div>
</div>
</div>
</ng-template>
<ng-template #emptyTd>
<td class="text-center"></td>
</ng-template>

View File

@@ -68,11 +68,6 @@ div.scrollable {
vertical-align: top;
padding-top: 25px;
}
.addresses-data {
vertical-align: top;
font-family: monospace;
font-size: 14px;
}
.data {
text-align: right;
@@ -105,7 +100,7 @@ div.scrollable {
@media (max-width: 875px) {
padding-left: 50px;
}
@media (max-width: 685px) {
@media (max-width: 650px) {
display: none;
}
}
@@ -123,7 +118,7 @@ div.scrollable {
padding-right: 10px;
}
@media (max-width: 875px) {
padding-right: 20px;
padding-right: 50px;
}
@media (max-width: 567px) {
padding-right: 10px;
@@ -191,6 +186,10 @@ div.scrollable {
.block-count-title {
color: #4a68b9;
font-size: 14px;
text-align: left;
@media (max-width: 767.98px) {
text-align: center;
}
}
.table-data tr {

View File

@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { EChartsOption, graphic } from 'echarts';
import { BehaviorSubject, Observable } from 'rxjs';
import { BehaviorSubject, Observable, timer } from 'rxjs';
import { distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators';
import { BlockExtended, PoolStat } from '../../interfaces/node-api.interface';
import { ApiService } from '../../services/api.service';
@@ -35,8 +35,6 @@ export class PoolComponent implements OnInit {
blocks: BlockExtended[] = [];
slug: string = undefined;
auditAvailable = false;
loadMoreSubject: BehaviorSubject<number> = new BehaviorSubject(this.blocks[this.blocks.length - 1]?.height);
constructor(
@@ -46,7 +44,6 @@ export class PoolComponent implements OnInit {
public stateService: StateService,
private seoService: SeoService,
) {
this.auditAvailable = this.stateService.env.AUDIT;
}
ngOnInit(): void {
@@ -77,6 +74,11 @@ export class PoolComponent implements OnInit {
regexes += regex + '", "';
}
poolStats.pool.regexes = regexes.slice(0, -3);
poolStats.pool.addresses = poolStats.pool.addresses;
if (poolStats.reportedHashrate) {
poolStats.luck = poolStats.estimatedHashrate / poolStats.reportedHashrate * 100;
}
return Object.assign({
logo: `/resources/mining-pools/` + poolStats.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'

View File

@@ -50,14 +50,14 @@
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="mining.fees-per-block">Avg Block Fees</h5>
<h5 class="card-title" i18n="mining.rewards-per-tx">Reward Per Tx</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="mining.average-fee">Avg Tx Fee</h5>
<h5 class="card-title" i18n="mining.average-fee">Reward Per Tx</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>

View File

@@ -53,8 +53,3 @@ form {
margin-top: 1px;
margin-right: 2px;
}
input:focus {
box-shadow: none;
border-color: #1b1f2c;
}

View File

@@ -153,7 +153,7 @@ export class SearchFormComponent implements OnInit {
const matchesBlockHeight = this.regexBlockheight.test(searchText);
const matchesTxId = this.regexTransaction.test(searchText) && !this.regexBlockhash.test(searchText);
const matchesBlockHash = this.regexBlockhash.test(searchText);
const matchesAddress = !matchesTxId && this.regexAddress.test(searchText);
const matchesAddress = this.regexAddress.test(searchText);
if (matchesAddress && this.network === 'bisq') {
searchText = 'B' + searchText;
@@ -198,7 +198,7 @@ export class SearchFormComponent implements OnInit {
const searchText = result || this.searchForm.value.searchText.trim();
if (searchText) {
this.isSearching = true;
if (!this.regexTransaction.test(searchText) && this.regexAddress.test(searchText)) {
if (this.regexAddress.test(searchText)) {
this.navigate('/address/', searchText);
} else if (this.regexBlockhash.test(searchText) || this.regexBlockheight.test(searchText)) {
this.navigate('/block/', searchText);

View File

@@ -85,20 +85,21 @@ export class StartComponent implements OnInit, OnDestroy {
});
this.stateService.blocks$
.subscribe((blocks: any) => {
if (this.stateService.network !== '') {
return;
}
this.countdown = 0;
const block = blocks[0];
for (const sb in specialBlocks) {
if (specialBlocks[sb].networks.includes(this.stateService.network || 'mainnet')) {
const height = parseInt(sb, 10);
const diff = height - block.height;
if (diff > 0 && diff <= 1008) {
this.countdown = diff;
this.eventName = specialBlocks[sb].labelEvent;
}
const height = parseInt(sb, 10);
const diff = height - block.height;
if (diff > 0 && diff <= 1008) {
this.countdown = diff;
this.eventName = specialBlocks[sb].labelEvent;
}
}
if (specialBlocks[block.height] && specialBlocks[block.height].networks.includes(this.stateService.network || 'mainnet')) {
if (specialBlocks[block.height]) {
this.specialEvent = true;
this.eventName = specialBlocks[block.height].labelEventCompleted;
setTimeout(() => {

View File

@@ -10,16 +10,6 @@ import { dates } from '../../shared/i18n/dates';
export class TimeComponent implements OnInit, OnChanges, OnDestroy {
interval: number;
text: string;
units: string[] = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'];
precisionThresholds = {
year: 100,
month: 18,
week: 12,
day: 31,
hour: 48,
minute: 90,
second: 90
};
intervals = {};
@Input() time: number;
@@ -28,8 +18,7 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
@Input() fastRender = false;
@Input() fixedRender = false;
@Input() relative = false;
@Input() precision: number = 0;
@Input() fractionDigits: number = 0;
@Input() forceFloorOnTimeIntervals: string[];
constructor(
private ref: ChangeDetectorRef,
@@ -93,24 +82,18 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
}
let counter: number;
for (const [index, unit] of this.units.entries()) {
let precisionUnit = this.units[Math.min(this.units.length - 1, index + this.precision)];
counter = Math.floor(seconds / this.intervals[unit]);
const precisionCounter = Math.floor(seconds / this.intervals[precisionUnit]);
if (precisionCounter > this.precisionThresholds[precisionUnit]) {
precisionUnit = unit;
for (const i in this.intervals) {
if (this.kind !== 'until' || this.forceFloorOnTimeIntervals && this.forceFloorOnTimeIntervals.indexOf(i) > -1) {
counter = Math.floor(seconds / this.intervals[i]);
} else {
counter = Math.round(seconds / this.intervals[i]);
}
const dateStrings = dates(counter);
if (counter > 0) {
let rounded = Math.round(seconds / this.intervals[precisionUnit]);
if (this.fractionDigits) {
const roundFactor = Math.pow(10,this.fractionDigits);
rounded = Math.round((seconds / this.intervals[precisionUnit]) * roundFactor) / roundFactor;
}
const dateStrings = dates(rounded);
switch (this.kind) {
case 'since':
if (rounded === 1) {
switch (precisionUnit) { // singular (1 day)
if (counter === 1) {
switch (i) { // singular (1 day)
case 'year': return $localize`:@@time-since:${dateStrings.i18nYear}:DATE: ago`; break;
case 'month': return $localize`:@@time-since:${dateStrings.i18nMonth}:DATE: ago`; break;
case 'week': return $localize`:@@time-since:${dateStrings.i18nWeek}:DATE: ago`; break;
@@ -120,7 +103,7 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
case 'second': return $localize`:@@time-since:${dateStrings.i18nSecond}:DATE: ago`; break;
}
} else {
switch (precisionUnit) { // plural (2 days)
switch (i) { // plural (2 days)
case 'year': return $localize`:@@time-since:${dateStrings.i18nYears}:DATE: ago`; break;
case 'month': return $localize`:@@time-since:${dateStrings.i18nMonths}:DATE: ago`; break;
case 'week': return $localize`:@@time-since:${dateStrings.i18nWeeks}:DATE: ago`; break;
@@ -132,8 +115,8 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
}
break;
case 'until':
if (rounded === 1) {
switch (precisionUnit) { // singular (In ~1 day)
if (counter === 1) {
switch (i) { // singular (In ~1 day)
case 'year': return $localize`:@@time-until:In ~${dateStrings.i18nYear}:DATE:`; break;
case 'month': return $localize`:@@time-until:In ~${dateStrings.i18nMonth}:DATE:`; break;
case 'week': return $localize`:@@time-until:In ~${dateStrings.i18nWeek}:DATE:`; break;
@@ -143,7 +126,7 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
case 'second': return $localize`:@@time-until:In ~${dateStrings.i18nSecond}:DATE:`;
}
} else {
switch (precisionUnit) { // plural (In ~2 days)
switch (i) { // plural (In ~2 days)
case 'year': return $localize`:@@time-until:In ~${dateStrings.i18nYears}:DATE:`; break;
case 'month': return $localize`:@@time-until:In ~${dateStrings.i18nMonths}:DATE:`; break;
case 'week': return $localize`:@@time-until:In ~${dateStrings.i18nWeeks}:DATE:`; break;
@@ -155,8 +138,8 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
}
break;
case 'span':
if (rounded === 1) {
switch (precisionUnit) { // singular (1 day)
if (counter === 1) {
switch (i) { // singular (1 day)
case 'year': return $localize`:@@time-span:After ${dateStrings.i18nYear}:DATE:`; break;
case 'month': return $localize`:@@time-span:After ${dateStrings.i18nMonth}:DATE:`; break;
case 'week': return $localize`:@@time-span:After ${dateStrings.i18nWeek}:DATE:`; break;
@@ -166,7 +149,7 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
case 'second': return $localize`:@@time-span:After ${dateStrings.i18nSecond}:DATE:`; break;
}
} else {
switch (precisionUnit) { // plural (2 days)
switch (i) { // plural (2 days)
case 'year': return $localize`:@@time-span:After ${dateStrings.i18nYears}:DATE:`; break;
case 'month': return $localize`:@@time-span:After ${dateStrings.i18nMonths}:DATE:`; break;
case 'week': return $localize`:@@time-span:After ${dateStrings.i18nWeeks}:DATE:`; break;
@@ -178,8 +161,8 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
}
break;
default:
if (rounded === 1) {
switch (precisionUnit) { // singular (1 day)
if (counter === 1) {
switch (i) { // singular (1 day)
case 'year': return dateStrings.i18nYear; break;
case 'month': return dateStrings.i18nMonth; break;
case 'week': return dateStrings.i18nWeek; break;
@@ -189,7 +172,7 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
case 'second': return dateStrings.i18nSecond; break;
}
} else {
switch (precisionUnit) { // plural (2 days)
switch (i) { // plural (2 days)
case 'year': return dateStrings.i18nYears; break;
case 'month': return dateStrings.i18nMonths; break;
case 'week': return dateStrings.i18nWeeks; break;

View File

@@ -67,7 +67,7 @@
<td><app-time kind="span" [time]="tx.status.block_time - transactionTime" [fastRender]="true" [relative]="true"></app-time></td>
</tr>
</ng-template>
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet' && featuresEnabled">
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
<td class="td-width" i18n="transaction.features|Transaction features">Features</td>
<td>
<app-tx-features [tx]="tx"></app-tx-features>
@@ -119,7 +119,7 @@
<app-time kind="until" [time]="(60 * 1000 * txInBlockIndex) + now" [fastRender]="false" [fixedRender]="true"></app-time>
</ng-template>
<ng-template #timeEstimateDefault>
<app-time kind="until" *ngIf="(timeAvg$ | async) as timeAvg;" [time]="(timeAvg * txInBlockIndex) + now + timeAvg" [fastRender]="false" [fixedRender]="true"></app-time>
<app-time kind="until" *ngIf="(timeAvg$ | async) as timeAvg;" [time]="(timeAvg * txInBlockIndex) + now + timeAvg" [fastRender]="false" [fixedRender]="true" [forceFloorOnTimeIntervals]="['hour']"></app-time>
</ng-template>
</ng-template>
</ng-template>
@@ -468,7 +468,6 @@
<ng-template #feeTable>
<table class="table table-borderless table-striped">
<tbody>
<tr *ngIf="isMobile && (network === 'liquid' || network === 'liquidtestnet' || !featuresEnabled)"></tr>
<tr>
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
<td>{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [blockConversion]="blockConversion" [value]="tx.fee"></app-fiat></span></td>
@@ -489,7 +488,7 @@
<div class="effective-fee-container">
{{ tx.effectiveFeePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
<ng-template [ngIf]="tx.status.confirmed">
<app-tx-fee-rating class="ml-2 mr-2" *ngIf="tx.fee || tx.effectiveFeePerVsize" [tx]="tx"></app-tx-fee-rating>
<app-tx-fee-rating class="d-none d-lg-inline ml-2" *ngIf="tx.fee" [tx]="tx"></app-tx-fee-rating>
</ng-template>
</div>
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right" (click)="showCpfpDetails = !showCpfpDetails">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>

View File

@@ -23,7 +23,6 @@ import { BlockExtended, CpfpInfo } from '../../interfaces/node-api.interface';
import { LiquidUnblinding } from './liquid-ublinding';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { Price, PriceService } from '../../services/price.service';
import { isFeatureActive } from '../../bitcoin.utils';
@Component({
selector: 'app-transaction',
@@ -49,7 +48,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
blocksSubscription: Subscription;
queryParamsSubscription: Subscription;
urlFragmentSubscription: Subscription;
mempoolBlocksSubscription: Subscription;
fragmentParams: URLSearchParams;
rbfTransaction: undefined | Transaction;
replaced: boolean = false;
@@ -60,7 +58,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
fetchRbfHistory$ = new Subject<string>();
fetchCachedTx$ = new Subject<string>();
isCached: boolean = false;
now = Date.now();
now = new Date().getTime();
timeAvg$: Observable<number>;
liquidUnblinding = new LiquidUnblinding();
inputIndex: number;
@@ -76,12 +74,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
flowEnabled: boolean;
blockConversion: Price;
tooltipPosition: { x: number, y: number };
isMobile: boolean;
featuresEnabled: boolean;
segwitEnabled: boolean;
rbfEnabled: boolean;
taprootEnabled: boolean;
@ViewChild('graphContainer')
graphContainer: ElementRef;
@@ -205,7 +197,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
}
this.tx = tx;
this.setFeatures();
this.isCached = true;
if (tx.fee === undefined) {
this.tx.fee = 0;
@@ -300,7 +291,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
}
this.tx = tx;
this.setFeatures();
this.isCached = false;
if (tx.fee === undefined) {
this.tx.fee = 0;
@@ -309,6 +299,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.isLoadingTx = false;
this.error = undefined;
this.waitingForTransaction = false;
this.setMempoolBlocksSubscription();
this.websocketService.startTrackTransaction(tx.txid);
this.graphExpanded = false;
this.setupGraph();
@@ -356,7 +347,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.blocksSubscription = this.stateService.blocks$.subscribe(([block, txConfirmed]) => {
this.latestBlock = block;
if (txConfirmed && this.tx && !this.tx.status.confirmed) {
if (txConfirmed && this.tx) {
this.tx.status = {
confirmed: true,
block_height: block.height,
@@ -391,34 +382,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.setFlowEnabled();
this.setGraphSize();
});
this.mempoolBlocksSubscription = this.stateService.mempoolBlocks$.subscribe((mempoolBlocks) => {
if (!this.tx) {
return;
}
this.now = Date.now();
const txFeePerVSize =
this.tx.effectiveFeePerVsize || this.tx.fee / (this.tx.weight / 4);
let found = false;
this.txInBlockIndex = 0;
for (const block of mempoolBlocks) {
for (let i = 0; i < block.feeRange.length - 1 && !found; i++) {
if (
txFeePerVSize <= block.feeRange[i + 1] &&
txFeePerVSize >= block.feeRange[i]
) {
this.txInBlockIndex = mempoolBlocks.indexOf(block);
found = true;
}
}
}
if (!found && txFeePerVSize < mempoolBlocks[mempoolBlocks.length - 1].feeRange[0]) {
this.txInBlockIndex = 7;
}
});
}
ngAfterViewInit(): void {
@@ -435,6 +398,28 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
return of(false);
}
setMempoolBlocksSubscription() {
this.stateService.mempoolBlocks$.subscribe((mempoolBlocks) => {
if (!this.tx) {
return;
}
const txFeePerVSize =
this.tx.effectiveFeePerVsize || this.tx.fee / (this.tx.weight / 4);
for (const block of mempoolBlocks) {
for (let i = 0; i < block.feeRange.length - 1; i++) {
if (
txFeePerVSize <= block.feeRange[i + 1] &&
txFeePerVSize >= block.feeRange[i]
) {
this.txInBlockIndex = mempoolBlocks.indexOf(block);
}
}
}
});
}
getTransactionTime() {
this.apiService
.getTransactionTimes$([this.tx.txid])
@@ -443,23 +428,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
});
}
setFeatures(): void {
if (this.tx) {
this.segwitEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'segwit');
this.taprootEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'taproot');
this.rbfEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'rbf');
} else {
this.segwitEnabled = false;
this.taprootEnabled = false;
this.rbfEnabled = false;
}
this.featuresEnabled = this.segwitEnabled || this.taprootEnabled || this.rbfEnabled;
}
resetTransaction() {
this.error = undefined;
this.tx = null;
this.setFeatures();
this.waitingForTransaction = false;
this.isLoadingTx = true;
this.rbfTransaction = undefined;
@@ -524,11 +495,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
@HostListener('window:resize', ['$event'])
setGraphSize(): void {
this.isMobile = window.innerWidth < 850;
if (this.graphContainer) {
setTimeout(() => {
this.graphWidth = this.graphContainer.nativeElement.clientWidth;
}, 1);
this.graphWidth = this.graphContainer.nativeElement.clientWidth;
}
}
@@ -542,7 +510,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.queryParamsSubscription.unsubscribe();
this.flowPrefSubscription.unsubscribe();
this.urlFragmentSubscription.unsubscribe();
this.mempoolBlocksSubscription.unsubscribe();
this.leaveTransaction();
}
}

View File

@@ -1,7 +1,7 @@
import { Component, OnInit, Input, ChangeDetectionStrategy, OnChanges, Output, EventEmitter, ChangeDetectorRef } from '@angular/core';
import { StateService } from '../../services/state.service';
import { CacheService } from '../../services/cache.service';
import { Observable, ReplaySubject, BehaviorSubject, merge, Subscription, of, forkJoin } from 'rxjs';
import { Observable, ReplaySubject, BehaviorSubject, merge, Subscription, of } from 'rxjs';
import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.interface';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { environment } from '../../../environments/environment';
@@ -70,19 +70,12 @@ export class TransactionsListComponent implements OnInit, OnChanges {
.pipe(
switchMap((txIds) => {
if (!this.cached) {
// break list into batches of 50 (maximum supported by esplora)
const batches = [];
for (let i = 0; i < txIds.length; i += 50) {
batches.push(txIds.slice(i, i + 50));
}
return forkJoin(batches.map(batch => this.apiService.getOutspendsBatched$(batch)));
return this.apiService.getOutspendsBatched$(txIds);
} else {
return of([]);
}
}),
tap((batchedOutspends: Outspend[][][]) => {
// flatten batched results back into a single array
const outspends = batchedOutspends.flat(1);
tap((outspends: Outspend[][]) => {
if (!this.transactions) {
return;
}
@@ -184,9 +177,9 @@ export class TransactionsListComponent implements OnInit, OnChanges {
onScroll(): void {
const scrollHeight = document.body.scrollHeight;
const scrollTop = document.documentElement.scrollTop;
if (scrollHeight > 0) {
if (scrollHeight > 0){
const percentageScrolled = scrollTop * 100 / scrollHeight;
if (percentageScrolled > 50) {
if (percentageScrolled > 70){
this.loadMore.emit();
}
}

View File

@@ -56,25 +56,9 @@
</ng-container>
</ng-container>
<p *ngIf="line.value == null && line.confidential" i18n="shared.confidential">Confidential</p>
<p *ngIf="line.value != null">
<ng-template [ngIf]="line.asset && line.asset !== nativeAssetId" [ngIfElse]="defaultOutput">
<div *ngIf="assetsMinimal && assetsMinimal[line.asset] else assetNotFound">
<ng-container *ngTemplateOutlet="assetBox; context:{ $implicit: line }"></ng-container>
</div>
<ng-template #assetNotFound>
{{ line.value }} <span class="symbol">{{ line.asset | slice : 0 : 7 }}</span>
</ng-template>
</ng-template>
<ng-template #defaultOutput>
<app-amount [blockConversion]="blockConversion" [satoshis]="line.value"></app-amount>
</ng-template>
</p>
<p *ngIf="line.value != null"><app-amount [blockConversion]="blockConversion" [satoshis]="line.value"></app-amount></p>
<p *ngIf="line.type !== 'fee' && line.address" class="address">
<app-truncate [text]="line.address"></app-truncate>
</p>
</ng-template>
</div>
<ng-template #assetBox let-item>
{{ item.value / pow(10, assetsMinimal[item.asset][3]) | number: '1.' + assetsMinimal[item.asset][3] + '-' + assetsMinimal[item.asset][3] }} <span class="symbol">{{ assetsMinimal[item.asset][1] }}</span>
</ng-template>

View File

@@ -1,8 +1,6 @@
import { Component, ElementRef, ViewChild, Input, OnChanges, OnInit } from '@angular/core';
import { tap } from 'rxjs';
import { Price, PriceService } from '../../services/price.service';
import { StateService } from '../../services/state.service';
import { environment } from '../../../environments/environment';
interface Xput {
type: 'input' | 'output' | 'fee';
@@ -18,7 +16,6 @@ interface Xput {
pegout?: string;
confidential?: boolean;
timestamp?: number;
asset?: string;
}
@Component({
@@ -30,19 +27,13 @@ export class TxBowtieGraphTooltipComponent implements OnChanges {
@Input() line: Xput | void;
@Input() cursorPosition: { x: number, y: number };
@Input() isConnector: boolean = false;
@Input() assetsMinimal: any;
tooltipPosition = { x: 0, y: 0 };
blockConversion: Price;
nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId;
@ViewChild('tooltip') tooltipElement: ElementRef<HTMLCanvasElement>;
constructor(
private priceService: PriceService,
private stateService: StateService,
) {}
constructor(private priceService: PriceService) {}
ngOnChanges(changes): void {
if (changes.line?.currentValue) {
@@ -69,8 +60,4 @@ export class TxBowtieGraphTooltipComponent implements OnChanges {
this.tooltipPosition = { x, y };
}
}
pow(base: number, exponent: number): number {
return Math.pow(base, exponent);
}
}

View File

@@ -172,6 +172,5 @@
[line]="hoverLine"
[cursorPosition]="tooltipPosition"
[isConnector]="hoverConnector"
[assetsMinimal]="assetsMinimal"
></app-tx-bowtie-graph-tooltip>
</div>

View File

@@ -6,7 +6,6 @@ import { ReplaySubject, merge, Subscription, of } from 'rxjs';
import { tap, switchMap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { AssetsService } from '../../services/assets.service';
interface SvgLine {
path: string;
@@ -31,7 +30,6 @@ interface Xput {
pegout?: string;
confidential?: boolean;
timestamp?: number;
asset?: string;
}
@Component({
@@ -73,7 +71,6 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
zeroValueWidth = 60;
zeroValueThickness = 20;
hasLine: boolean;
assetsMinimal: any;
outspendsSubscription: Subscription;
refreshOutspends$: ReplaySubject<string> = new ReplaySubject();
@@ -98,7 +95,6 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
private relativeUrlPipe: RelativeUrlPipe,
private stateService: StateService,
private apiService: ApiService,
private assetsService: AssetsService,
@Inject(LOCALE_ID) private locale: string,
) {
if (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he')) {
@@ -109,12 +105,6 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
ngOnInit(): void {
this.initGraph();
if (this.network === 'liquid' || this.network === 'liquidtestnet') {
this.assetsService.getAssetsMinimalJson$.subscribe((assets) => {
this.assetsMinimal = assets;
});
}
this.outspendsSubscription = merge(
this.refreshOutspends$
.pipe(
@@ -172,8 +162,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
index: i,
pegout: v?.pegout?.scriptpubkey_address,
confidential: (this.isLiquid && v?.value === undefined),
timestamp: this.tx.status.block_time,
asset: v?.asset,
timestamp: this.tx.status.block_time
} as Xput;
});
@@ -193,8 +182,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
coinbase: v?.is_coinbase,
pegin: v?.is_pegin,
confidential: (this.isLiquid && v?.prevout?.value === undefined),
timestamp: this.tx.status.block_time,
asset: v?.prevout?.asset,
timestamp: this.tx.status.block_time
} as Xput;
});
@@ -220,8 +208,8 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
this.outputs = this.initLines('out', voutWithFee, totalValue, this.maxStrands);
this.middle = {
path: `M ${(this.width / 2) - this.midWidth} ${(this.height / 2) + 0.25} L ${(this.width / 2) + this.midWidth} ${(this.height / 2) + 0.25}`,
style: `stroke-width: ${this.combinedWeight + 0.5}; stroke: ${this.gradient[1]}`
path: `M ${(this.width / 2) - this.midWidth} ${(this.height / 2) + 0.5} L ${(this.width / 2) + this.midWidth} ${(this.height / 2) + 0.5}`,
style: `stroke-width: ${this.combinedWeight + 1}; stroke: ${this.gradient[1]}`
};
this.hasLine = this.inputs.reduce((line, put) => line || !put.zeroValue, false)
@@ -278,7 +266,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
const lineParams = weights.map((w, i) => {
return {
weight: w,
thickness: xputs[i].value === 0 ? this.zeroValueThickness : Math.min(this.combinedWeight + 0.5, Math.max(this.minWeight - 1, w) + 1),
thickness: xputs[i].value === 0 ? this.zeroValueThickness : Math.max(this.minWeight - 1, w) + 1,
offset: 0,
innerY: 0,
outerY: 0,
@@ -290,7 +278,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
// bounds of the middle segment
const innerTop = (this.height / 2) - (this.combinedWeight / 2);
const innerBottom = innerTop + this.combinedWeight + 0.5;
const innerBottom = innerTop + this.combinedWeight;
// tracks the visual bottom of the endpoints of the previous line
let lastOuter = 0;
let lastInner = innerTop;
@@ -315,7 +303,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
// set the vertical position of the (center of the) outer side of the line
line.outerY = lastOuter + (line.thickness / 2);
line.innerY = Math.min(innerBottom - (line.thickness / 2), Math.max(innerTop + (line.thickness / 2), lastInner + (line.weight / 2)));
line.innerY = Math.min(innerBottom + (line.thickness / 2), Math.max(innerTop + (line.thickness / 2), lastInner + (line.weight / 2)));
// special case to center single input/outputs
if (xputs.length === 1) {

View File

@@ -1,4 +1,4 @@
<ng-template [ngIf]="segwitEnabled">
<ng-template [ngIf]="!tx.status.confirmed || tx.status.block_height >= 477120">
<span *ngIf="segwitGains.realizedSegwitGains && !segwitGains.potentialSegwitGains; else segwitTwo" class="badge badge-success mr-1" i18n-ngbTooltip="ngbTooltip about segwit gains" ngbTooltip="This transaction saved {{ segwitGains.realizedSegwitGains * 100 | number: '1.0-0' }}% on fees by using native SegWit" placement="bottom" i18n="tx-features.tag.segwit|SegWit">SegWit</span>
<ng-template #segwitTwo>
<span *ngIf="segwitGains.realizedSegwitGains && segwitGains.potentialSegwitGains; else potentialP2shSegwitGains" class="badge badge-warning mr-1" i18n-ngbTooltip="ngbTooltip about double segwit gains" ngbTooltip="This transaction saved {{ segwitGains.realizedSegwitGains * 100 | number: '1.0-0' }}% on fees by using SegWit and could save {{ segwitGains.potentialSegwitGains * 100 | number : '1.0-0' }}% more by fully upgrading to native SegWit" placement="bottom" i18n="tx-features.tag.segwit|SegWit">SegWit</span>
@@ -8,7 +8,7 @@
</ng-template>
</ng-template>
<ng-template [ngIf]="taprootEnabled">
<ng-template [ngIf]="!tx.status.confirmed || tx.status.block_height >= 709632">
<span *ngIf="segwitGains.realizedTaprootGains && !segwitGains.potentialTaprootGains; else notFullyTaproot" class="badge badge-success mr-1" i18n-ngbTooltip="Tooltip about fees saved with taproot" ngbTooltip="This transaction uses Taproot and thereby saved at least {{ segwitGains.realizedTaprootGains * 100 | number: '1.0-0' }}% on fees" placement="bottom" i18n="tx-features.tag.taproot|Taproot">Taproot</span>
<ng-template #notFullyTaproot>
<span *ngIf="segwitGains.realizedTaprootGains && segwitGains.potentialTaprootGains; else noTaproot" class="badge badge-warning mr-1" i18n-ngbTooltip="Tooltip about fees that saved and could be saved with taproot" ngbTooltip="This transaction uses Taproot and already saved at least {{ segwitGains.realizedTaprootGains * 100 | number: '1.0-0' }}% on fees, but could save an additional {{ segwitGains.potentialTaprootGains * 100 | number: '1.0-0' }}% by fully using Taproot" placement="bottom" i18n="tx-features.tag.taproot|Taproot">Taproot</span>
@@ -24,7 +24,7 @@
</ng-template>
</ng-template>
<ng-template [ngIf]="rbfEnabled">
<ng-template [ngIf]="!tx.status.confirmed || tx.status.block_height > 399700">
<span *ngIf="isRbfTransaction; else rbfDisabled" class="badge badge-success" i18n-ngbTooltip="RBF tooltip" ngbTooltip="This transaction supports Replace-By-Fee (RBF) allowing fee bumping" placement="bottom" i18n="tx-features.tag.rbf|RBF">RBF</span>
<ng-template #rbfDisabled><span class="badge badge-danger mr-1" i18n-ngbTooltip="RBF disabled tooltip" ngbTooltip="This transaction does NOT support Replace-By-Fee (RBF) and cannot be fee bumped using this method" placement="bottom"><del i18n="tx-features.tag.rbf|RBF">RBF</del></span></ng-template>
</ng-template>

View File

@@ -1,7 +1,6 @@
import { Component, ChangeDetectionStrategy, OnChanges, Input } from '@angular/core';
import { calcSegwitFeeGains, isFeatureActive } from '../../bitcoin.utils';
import { calcSegwitFeeGains } from '../../bitcoin.utils';
import { Transaction } from '../../interfaces/electrs.interface';
import { StateService } from '../../services/state.service';
@Component({
selector: 'app-tx-features',
@@ -22,21 +21,12 @@ export class TxFeaturesComponent implements OnChanges {
isRbfTransaction: boolean;
isTaproot: boolean;
segwitEnabled: boolean;
rbfEnabled: boolean;
taprootEnabled: boolean;
constructor(
private stateService: StateService,
) { }
constructor() { }
ngOnChanges() {
if (!this.tx) {
return;
}
this.segwitEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'segwit');
this.taprootEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'taproot');
this.rbfEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'rbf');
this.segwitGains = calcSegwitFeeGains(this.tx);
this.isRbfTransaction = this.tx.vin.some((v) => v.sequence < 0xfffffffe);
this.isTaproot = this.tx.vin.some((v) => v.prevout && v.prevout.scriptpubkey_type === 'v1_p2tr');

View File

@@ -3,7 +3,6 @@ import { Transaction } from '../../interfaces/electrs.interface';
import { StateService } from '../../services/state.service';
import { Subscription } from 'rxjs';
import { BlockExtended } from '../../interfaces/node-api.interface';
import { CacheService } from '../../services/cache.service';
@Component({
selector: 'app-tx-fee-rating',
@@ -24,12 +23,12 @@ export class TxFeeRatingComponent implements OnInit, OnChanges, OnDestroy {
constructor(
private stateService: StateService,
private cacheService: CacheService,
private cd: ChangeDetectorRef,
) { }
ngOnInit() {
this.blocksSubscription = this.cacheService.loadedBlocks$.subscribe((block) => {
this.blocksSubscription = this.stateService.blocks$.subscribe(([block]) => {
this.blocks.push(block);
if (this.tx.status.confirmed && this.tx.status.block_height === block.height && block?.extras?.medianFee > 0) {
this.calculateRatings(block);
this.cd.markForCheck();
@@ -42,9 +41,8 @@ export class TxFeeRatingComponent implements OnInit, OnChanges, OnDestroy {
if (!this.tx.status.confirmed) {
return;
}
this.cacheService.loadBlock(this.tx.status.block_height);
const foundBlock = this.cacheService.getCachedBlock(this.tx.status.block_height) || null;
const foundBlock = this.blocks.find((b) => b.height === this.tx.status.block_height);
if (foundBlock && foundBlock?.extras?.medianFee > 0) {
this.calculateRatings(foundBlock);
}

View File

@@ -8859,21 +8859,6 @@ export const faqData = [
fragment: "what-is-full-mempool",
title: "What does it mean for the mempool to be \"full\"?",
},
{
type: "endpoint",
category: "advanced",
showConditions: bitcoinNetworks,
fragment: "how-big-is-mempool-used-by-mempool.space",
title: "How big is the mempool used by mempool.space?",
options: { officialOnly: true },
},
{
type: "endpoint",
category: "advanced",
showConditions: bitcoinNetworks,
fragment: "what-is-memory-usage",
title: "What is memory usage?",
},
{
type: "endpoint",
category: "advanced",

View File

@@ -1,4 +1,4 @@
<div *ngFor="let item of tabData">
<p *ngIf="( item.type === 'category' ) && ( item.showConditions.indexOf(network.val) > -1 )">{{ item.title }}</p>
<a *ngIf="( item.type !== 'category' ) && ( item.showConditions.indexOf(network.val) > -1 ) && ( !item.hasOwnProperty('options') || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('officialOnly') && item.options.officialOnly && officialMempoolInstance ) || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('auditOnly') && item.options.auditOnly && auditEnabled ) )" [routerLink]="['./']" fragment="{{ item.fragment }}" (click)="navLinkClick($event)">{{ item.title }}</a>
<a *ngIf="( item.type !== 'category' ) && ( item.showConditions.indexOf(network.val) > -1 ) && ( !item.hasOwnProperty('options') || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('auditOnly') && item.options.auditOnly && auditEnabled ) )" [routerLink]="['./']" fragment="{{ item.fragment }}" (click)="navLinkClick($event)">{{ item.title }}</a>
</div>

View File

@@ -16,7 +16,6 @@ export class ApiDocsNavComponent implements OnInit {
env: Env;
tabData: any[];
auditEnabled: boolean;
officialMempoolInstance: boolean;
constructor(
private stateService: StateService
@@ -24,7 +23,6 @@ export class ApiDocsNavComponent implements OnInit {
ngOnInit(): void {
this.env = this.stateService.env;
this.officialMempoolInstance = this.env.OFFICIAL_MEMPOOL_SPACE;
this.auditEnabled = this.env.AUDIT;
if (this.whichTab === 'rest') {
this.tabData = restApiDocsData;

View File

@@ -16,7 +16,7 @@
</div>
<div class="doc-item-container" *ngFor="let item of faq">
<div *ngIf="!item.hasOwnProperty('options') || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('officialOnly') && item.options.officialOnly && officialMempoolInstance ) || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('auditOnly') && item.options.auditOnly && auditEnabled )">
<div *ngIf="!item.hasOwnProperty('options') || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('auditOnly') && item.options.auditOnly && auditEnabled )">
<h3 *ngIf="item.type === 'category'">{{ item.title }}</h3>
<div *ngIf="item.type !== 'category'" class="endpoint-container" id="{{ item.fragment }}">
<a id="{{ item.fragment + '-tab-header' }}" class="section-header" (click)="anchorLinkClick( $event )" [routerLink]="['./']" fragment="{{ item.fragment }}"><table><tr><td>{{ item.title }}</td><td><span>{{ item.category }}</span></td></tr></table></a>
@@ -207,18 +207,6 @@
<p>When a Bitcoin transaction is made, it is stored in a Bitcoin node's mempool before it is confirmed into a block. When the rate of incoming transactions exceeds the rate transactions are confirmed, the mempool grows in size.</p><p>By default, Bitcoin Core allocates 300MB of memory for its mempool, so when a node's mempool grows big enough to use all 300MB of allocated memory, we say it's "full".</p><p>Once a node's mempool is using all of its allocated memory, it will start rejecting new transactions below a certain feerate threshold—so when this is the case, be extra sure to set a feerate that (at a minimum) exceeds that threshold. The current threshold feerate (and memory usage) are displayed right on Mempool's front page.</p>
</ng-template>
<ng-template type="how-big-is-mempool-used-by-mempool.space">
<p>mempool.space uses multiple Bitcoin nodes to obtain data: some with the default 300MB mempool memory limit (call these Small Nodes) and others with a much larger mempool memory limit (call these Big Nodes).</p>
<p>Many nodes on the Bitcoin network are configured to run with the default 300MB mempool memory setting. When all 300MB of memory are used up, such nodes will reject transactions below a certain threshold feerate. Running Small Nodes allows mempool.space to tell you what this threshold feerate is—this is the "Purging" feerate that shows on the front page when mempools are full, which you can use to be reasonably sure that your transaction will be widely propagated.</p>
<p>Big Node mempools are so big that they don't need to reject (or purge) transactions. Such nodes allow for mempool.space to provide you with information on any pending transaction it has received—no matter how congested the mempool is, and no matter how low-feerate or low-priority the transaction is.</p>
</ng-template>
<ng-template type="what-is-memory-usage">
<p>Memory usage on the front page refers to the real-time amount of system memory used by a Bitcoin node's mempool. This memory usage number is always higher than the total size of all pending transactions in the mempool due to indexes, pointers, and other overhead used by Bitcoin Core for storage and processing.</p>
<p>mempool.space shows the memory usage of a Bitcoin node that has a very high mempool memory limit. As a result, when mempools fill up, you may notice memory usage on mempool.space go beyond 300MB. This is not a mistake—this memory usage figure is high because it's for a Bitcoin node that isn't rejecting (or evicting) transactions. Consider it to be another data point to give you an idea of how congested the mempool is relative to the default memory limit of 300MB.</p>
<p>A Bitcoin node running the default 300MB mempool memory limit, like most Raspberry Pi nodes, will never go past 300MB of memory usage.</p>
</ng-template>
<ng-template type="why-empty-blocks">
<p>When a new block is found, mining pools send miners a block template with no transactions so they can start searching for the next block as soon as possible. They send a block template full of transactions right afterward, but a full block template is a bigger data transfer and takes slightly longer to reach miners.</p><p>In this intervening time, which is usually no more than 1-2 seconds, miners sometimes get lucky and find a new block using the empty block template.</p>
</ng-template>

Some files were not shown because too many files have changed in this diff Show More