Compare commits
219 Commits
mononaut/c
...
v2.5.0-rc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae6a408c05 | ||
|
|
1015cbfa94 | ||
|
|
876feef53f | ||
|
|
f5f0329d39 | ||
|
|
80a7b6d8d5 | ||
|
|
f72e17c12e | ||
|
|
f570b2762f | ||
|
|
e2fda99578 | ||
|
|
d7d45146c8 | ||
|
|
45dbc6c6f6 | ||
|
|
d76e3a5939 | ||
|
|
cb8fdb5e8d | ||
|
|
d337bf3ee2 | ||
|
|
758e4d4f4c | ||
|
|
cd2bda4b49 | ||
|
|
493ea0641d | ||
|
|
ca1b6553c9 | ||
|
|
d479715d8e | ||
|
|
e3109a8fec | ||
|
|
e6bc5bef33 | ||
|
|
d82a7169b7 | ||
|
|
ba48b6f7ce | ||
|
|
8e1cf997f7 | ||
|
|
70d8548c92 | ||
|
|
cce7dd917f | ||
|
|
3dafb284a9 | ||
|
|
6a599a9a30 | ||
|
|
74fb292633 | ||
|
|
c6e063ea2f | ||
|
|
81d563381a | ||
|
|
870e895144 | ||
|
|
343d1345e2 | ||
|
|
517cf613c1 | ||
|
|
d54bcc898b | ||
|
|
704e1741ed | ||
|
|
ad5ce6dba4 | ||
|
|
1ed20a95df | ||
|
|
1718ddd4c3 | ||
|
|
32c2db2153 | ||
|
|
0abb9cbb7c | ||
|
|
e2e71c7a46 | ||
|
|
e27bdd3e2b | ||
|
|
5839ed428e | ||
|
|
af6d115dbb | ||
|
|
194968d16f | ||
|
|
30686bd322 | ||
|
|
175c645777 | ||
|
|
587a259843 | ||
|
|
64749ca726 | ||
|
|
8f2493dadb | ||
|
|
7d8ea075d9 | ||
|
|
328327e5dc | ||
|
|
fd1816d451 | ||
|
|
72f25b873c | ||
|
|
994656953c | ||
|
|
ed46232b83 | ||
|
|
dca18a1c66 | ||
|
|
c291ee1789 | ||
|
|
caf15351f7 | ||
|
|
6be9f23790 | ||
|
|
adc51f6217 | ||
|
|
ec8a46ede6 | ||
|
|
b78fdf5a23 | ||
|
|
7c2493f3fa | ||
|
|
b0a0ad11b4 | ||
|
|
377f71eb52 | ||
|
|
eefe343973 | ||
|
|
41a6674fad | ||
|
|
effba92729 | ||
|
|
c9f4bdda17 | ||
|
|
ef54385068 | ||
|
|
b8f77c4be4 | ||
|
|
b5c2073414 | ||
|
|
25aacb5046 | ||
|
|
c24724dcdf | ||
|
|
64ab14f995 | ||
|
|
4a64c0dfa5 | ||
|
|
0ebe0a5dc9 | ||
|
|
c683a52a01 | ||
|
|
5fbdd0bb2a | ||
|
|
599881366b | ||
|
|
4c294b010d | ||
|
|
418c32e334 | ||
|
|
12b605e5cc | ||
|
|
870a7e51b1 | ||
|
|
cdfde05452 | ||
|
|
ce24b8bb0a | ||
|
|
1b2810ec0e | ||
|
|
c8e84ec056 | ||
|
|
7bf8fea9f2 | ||
|
|
3458c5af71 | ||
|
|
3e46dabf7b | ||
|
|
a5dd141934 | ||
|
|
881af309ab | ||
|
|
374ff50a62 | ||
|
|
2b9e63dbc5 | ||
|
|
9f453deceb | ||
|
|
0b88d94573 | ||
|
|
536114853c | ||
|
|
4d281277d6 | ||
|
|
c97a722a3b | ||
|
|
adacb42d1a | ||
|
|
b6427d6f67 | ||
|
|
433acb7b1d | ||
|
|
d015ee7824 | ||
|
|
ecfb980e75 | ||
|
|
cfdbd30956 | ||
|
|
7acfec2406 | ||
|
|
070ee10fb0 | ||
|
|
446b0de8f3 | ||
|
|
99b7fc8814 | ||
|
|
defb88a474 | ||
|
|
7392535182 | ||
|
|
130ae8c3a5 | ||
|
|
f4f8b2b271 | ||
|
|
477d09412b | ||
|
|
87bc7917d8 | ||
|
|
9030d95207 | ||
|
|
5e4131b474 | ||
|
|
3bf96dafde | ||
|
|
2f4dba895c | ||
|
|
a5e281706f | ||
|
|
eba0e1c25a | ||
|
|
97d82042c0 | ||
|
|
8bd05987e5 | ||
|
|
7e676dbaf0 | ||
|
|
760f3193d9 | ||
|
|
72663b30df | ||
|
|
8995283a58 | ||
|
|
a8ac6aedf7 | ||
|
|
9613247283 | ||
|
|
ab96a17e80 | ||
|
|
3b080ee5fb | ||
|
|
24a8cca758 | ||
|
|
dbad3af8ba | ||
|
|
cdddf3a8b2 | ||
|
|
b675bd8d55 | ||
|
|
cb04d67d07 | ||
|
|
b1c6c5cbb6 | ||
|
|
7f8f72e9d6 | ||
|
|
a1ea77ee50 | ||
|
|
19e2d687d0 | ||
|
|
4be8016eb1 | ||
|
|
627c913d5e | ||
|
|
3c5165603b | ||
|
|
513277cef7 | ||
|
|
72a9b71901 | ||
|
|
60bef0eeb6 | ||
|
|
eeb97bdb81 | ||
|
|
8407c07b88 | ||
|
|
951cf2daf9 | ||
|
|
31cfbf6625 | ||
|
|
5dc52250e6 | ||
|
|
6b544ac179 | ||
|
|
2ad9bf57f7 | ||
|
|
e0e97f0d5e | ||
|
|
c72024b4e3 | ||
|
|
093469c164 | ||
|
|
24d9977919 | ||
|
|
7b2ea9c4c8 | ||
|
|
6b4650f3cd | ||
|
|
e971846b7e | ||
|
|
23ea5d582b | ||
|
|
6196860387 | ||
|
|
368f858ff2 | ||
|
|
ef0cc9d2db | ||
|
|
39051e94e3 | ||
|
|
e1f0bb9901 | ||
|
|
097eed0baa | ||
|
|
25c7c84705 | ||
|
|
7e873e6637 | ||
|
|
36e1777b96 | ||
|
|
35a05a420d | ||
|
|
1be5c6ec53 | ||
|
|
e4aa3c2091 | ||
|
|
4263977d99 | ||
|
|
5bd9be7ab1 | ||
|
|
74c95bfdf5 | ||
|
|
a1c0a58b9d | ||
|
|
d3d67627f3 | ||
|
|
4a0d9cb66f | ||
|
|
a2d673f9ed | ||
|
|
8dccaee0b0 | ||
|
|
ccc413a800 | ||
|
|
4d7e23064c | ||
|
|
a22a62836e | ||
|
|
dd01371b61 | ||
|
|
46d89ac837 | ||
|
|
796566e7ae | ||
|
|
3f234431fb | ||
|
|
778e2f9b64 | ||
|
|
6327ce7c89 | ||
|
|
cec8445223 | ||
|
|
548a6ea664 | ||
|
|
9cef9b67c1 | ||
|
|
42228dc70f | ||
|
|
63dd9fd09e | ||
|
|
cd3c1ed82e | ||
|
|
304089b3d0 | ||
|
|
c60903565e | ||
|
|
37e94249df | ||
|
|
d2337ae4e8 | ||
|
|
7557c47502 | ||
|
|
a8214bcbbd | ||
|
|
fcf51e2af8 | ||
|
|
c657e622eb | ||
|
|
526e46b8e4 | ||
|
|
ead7a13ff0 | ||
|
|
c3c0696844 | ||
|
|
2907054a01 | ||
|
|
64408bfd16 | ||
|
|
ca690bf123 | ||
|
|
0243428fe9 | ||
|
|
901d32d8f7 | ||
|
|
8adacd4a0e | ||
|
|
b8a3c15ed2 | ||
|
|
3e31e68a19 | ||
|
|
5eae84bb75 | ||
|
|
5a5ebe8435 |
26
.github/workflows/get_image_digest.yml
vendored
Normal file
26
.github/workflows/get_image_digest.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
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 }}
|
||||
@@ -1,13 +1,13 @@
|
||||
# The Mempool Open Source Project™ [](https://dashboard.cypress.io/projects/ry4br7/runs)
|
||||
|
||||
https://user-images.githubusercontent.com/232186/222445818-234aa6c9-c233-4c52-b3f0-e32b8232893b.mp4
|
||||
https://user-images.githubusercontent.com/93150691/226236121-375ea64f-b4a1-4cc0-8fad-a6fb33226840.mp4
|
||||
|
||||
<br>
|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
# 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.
|
||||
|
||||
@@ -171,52 +171,58 @@ Helpful link: https://gist.github.com/System-Glitch/cb4e87bf1ae3fec9925725bb3ebe
|
||||
|
||||
Run bitcoind on regtest:
|
||||
```
|
||||
bitcoind -regtest -rpcport=8332
|
||||
bitcoind -regtest
|
||||
```
|
||||
|
||||
Create a new wallet, if needed:
|
||||
```
|
||||
bitcoin-cli -regtest -rpcport=8332 createwallet test
|
||||
bitcoin-cli -regtest createwallet test
|
||||
```
|
||||
|
||||
Load wallet (this command may take a while if you have lot of UTXOs):
|
||||
```
|
||||
bitcoin-cli -regtest -rpcport=8332 loadwallet test
|
||||
bitcoin-cli -regtest loadwallet test
|
||||
```
|
||||
|
||||
Get a new address:
|
||||
```
|
||||
address=$(./src/bitcoin-cli -regtest -rpcport=8332 getnewaddress)
|
||||
address=$(bitcoin-cli -regtest 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 -rpcport=8332 generatetoaddress 101 $address
|
||||
bitcoin-cli -regtest generatetoaddress 101 $address
|
||||
```
|
||||
|
||||
Send 0.1 BTC at 5 sat/vB to another address:
|
||||
```
|
||||
./src/bitcoin-cli -named -regtest -rpcport=8332 sendtoaddress address=$(./src/bitcoin-cli -regtest -rpcport=8332 getnewaddress) amount=0.1 fee_rate=5
|
||||
bitcoin-cli -named -regtest sendtoaddress address=$(bitcoin-cli -regtest getnewaddress) amount=0.1 fee_rate=5
|
||||
```
|
||||
|
||||
See more example of `sendtoaddress`:
|
||||
```
|
||||
./src/bitcoin-cli sendtoaddress # will print the help
|
||||
bitcoin-cli sendtoaddress # will print the help
|
||||
```
|
||||
|
||||
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.
|
||||
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.
|
||||
```
|
||||
#!/bin/bash
|
||||
address=$(./src/bitcoin-cli -regtest -rpcport=8332 getnewaddress)
|
||||
address=$(bitcoin-cli -regtest getnewaddress)
|
||||
bitcoin-cli -regtest generatetoaddress 101 $address
|
||||
for i in {1..1000000}
|
||||
do
|
||||
./src/bitcoin-cli -regtest -rpcport=8332 -named sendtoaddress address=$address amount=0.01 fee_rate=$(jot -r 1 1 100)
|
||||
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
|
||||
done
|
||||
```
|
||||
|
||||
Generate block at regular interval (every 10 seconds in this example):
|
||||
```
|
||||
watch -n 10 "./src/bitcoin-cli -regtest -rpcport=8332 generatetoaddress 1 $address"
|
||||
watch -n 10 "bitcoin-cli -regtest generatetoaddress 1 $address"
|
||||
```
|
||||
|
||||
### Mining pools update
|
||||
|
||||
@@ -27,13 +27,15 @@
|
||||
"AUDIT": false,
|
||||
"ADVANCED_GBT_AUDIT": false,
|
||||
"ADVANCED_GBT_MEMPOOL": false,
|
||||
"CPFP_INDEXING": false
|
||||
"CPFP_INDEXING": false,
|
||||
"DISK_CACHE_BLOCK_INTERVAL": 6
|
||||
},
|
||||
"CORE_RPC": {
|
||||
"HOST": "127.0.0.1",
|
||||
"PORT": 8332,
|
||||
"USERNAME": "mempool",
|
||||
"PASSWORD": "mempool"
|
||||
"PASSWORD": "mempool",
|
||||
"TIMEOUT": 60000
|
||||
},
|
||||
"ELECTRUM": {
|
||||
"HOST": "127.0.0.1",
|
||||
@@ -47,7 +49,8 @@
|
||||
"HOST": "127.0.0.1",
|
||||
"PORT": 8332,
|
||||
"USERNAME": "mempool",
|
||||
"PASSWORD": "mempool"
|
||||
"PASSWORD": "mempool",
|
||||
"TIMEOUT": 60000
|
||||
},
|
||||
"DATABASE": {
|
||||
"ENABLED": true,
|
||||
@@ -91,7 +94,8 @@
|
||||
"LND": {
|
||||
"TLS_CERT_PATH": "tls.cert",
|
||||
"MACAROON_PATH": "readonly.macaroon",
|
||||
"REST_API_URL": "https://localhost:8080"
|
||||
"REST_API_URL": "https://localhost:8080",
|
||||
"TIMEOUT": 10000
|
||||
},
|
||||
"CLIGHTNING": {
|
||||
"SOCKET": "lightning-rpc"
|
||||
|
||||
@@ -28,13 +28,15 @@
|
||||
"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__"
|
||||
"MAX_BLOCKS_BULK_QUERY": "__MEMPOOL_MAX_BLOCKS_BULK_QUERY__",
|
||||
"DISK_CACHE_BLOCK_INTERVAL": "__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__"
|
||||
},
|
||||
"CORE_RPC": {
|
||||
"HOST": "__CORE_RPC_HOST__",
|
||||
"PORT": 15,
|
||||
"USERNAME": "__CORE_RPC_USERNAME__",
|
||||
"PASSWORD": "__CORE_RPC_PASSWORD__"
|
||||
"PASSWORD": "__CORE_RPC_PASSWORD__",
|
||||
"TIMEOUT": "__CORE_RPC_TIMEOUT__"
|
||||
},
|
||||
"ELECTRUM": {
|
||||
"HOST": "__ELECTRUM_HOST__",
|
||||
@@ -48,7 +50,8 @@
|
||||
"HOST": "__SECOND_CORE_RPC_HOST__",
|
||||
"PORT": 17,
|
||||
"USERNAME": "__SECOND_CORE_RPC_USERNAME__",
|
||||
"PASSWORD": "__SECOND_CORE_RPC_PASSWORD__"
|
||||
"PASSWORD": "__SECOND_CORE_RPC_PASSWORD__",
|
||||
"TIMEOUT": "__SECOND_CORE_RPC_TIMEOUT__"
|
||||
},
|
||||
"DATABASE": {
|
||||
"ENABLED": false,
|
||||
@@ -107,7 +110,8 @@
|
||||
"LND": {
|
||||
"TLS_CERT_PATH": "",
|
||||
"MACAROON_PATH": "",
|
||||
"REST_API_URL": "https://localhost:8080"
|
||||
"REST_API_URL": "https://localhost:8080",
|
||||
"TIMEOUT": 10000
|
||||
},
|
||||
"CLIGHTNING": {
|
||||
"SOCKET": "__CLIGHTNING_SOCKET__"
|
||||
|
||||
@@ -23,9 +23,11 @@ describe('Mempool Difficulty Adjustment', () => {
|
||||
remainingBlocks: 1834,
|
||||
remainingTime: 977591692,
|
||||
previousRetarget: 0.6280047707459726,
|
||||
previousTime: 1660820820,
|
||||
nextRetargetHeight: 751968,
|
||||
timeAvg: 533038,
|
||||
timeOffset: 0,
|
||||
expectedBlocks: 161.68833333333333,
|
||||
},
|
||||
],
|
||||
[ // Vector 2 (testnet)
|
||||
@@ -43,11 +45,13 @@ describe('Mempool Difficulty Adjustment', () => {
|
||||
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,
|
||||
},
|
||||
],
|
||||
] as [[number, number, number, number, string, number], DifficultyAdjustment][];
|
||||
|
||||
@@ -42,6 +42,7 @@ 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 });
|
||||
@@ -52,14 +53,16 @@ describe('Mempool Backend Config', () => {
|
||||
HOST: '127.0.0.1',
|
||||
PORT: 8332,
|
||||
USERNAME: 'mempool',
|
||||
PASSWORD: 'mempool'
|
||||
PASSWORD: 'mempool',
|
||||
TIMEOUT: 60000
|
||||
});
|
||||
|
||||
expect(config.SECOND_CORE_RPC).toStrictEqual({
|
||||
HOST: '127.0.0.1',
|
||||
PORT: 8332,
|
||||
USERNAME: 'mempool',
|
||||
PASSWORD: 'mempool'
|
||||
PASSWORD: 'mempool',
|
||||
TIMEOUT: 60000
|
||||
});
|
||||
|
||||
expect(config.DATABASE).toStrictEqual({
|
||||
@@ -106,6 +109,13 @@ 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'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first
|
||||
|
||||
class Audit {
|
||||
auditBlock(transactions: TransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: TransactionExtended })
|
||||
: { censored: string[], added: string[], fresh: string[], score: number } {
|
||||
: { censored: string[], added: string[], fresh: string[], score: number, similarity: number } {
|
||||
if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
|
||||
return { censored: [], added: [], fresh: [], score: 0 };
|
||||
return { censored: [], added: [], fresh: [], score: 0, similarity: 1 };
|
||||
}
|
||||
|
||||
const matches: string[] = []; // present in both mined block and template
|
||||
@@ -16,6 +16,8 @@ class Audit {
|
||||
const isCensored = {}; // missing, without excuse
|
||||
const isDisplaced = {};
|
||||
let displacedWeight = 0;
|
||||
let matchedWeight = 0;
|
||||
let projectedWeight = 0;
|
||||
|
||||
const inBlock = {};
|
||||
const inTemplate = {};
|
||||
@@ -38,11 +40,16 @@ class Audit {
|
||||
isCensored[txid] = true;
|
||||
}
|
||||
displacedWeight += mempool[txid].weight;
|
||||
} else {
|
||||
matchedWeight += mempool[txid].weight;
|
||||
}
|
||||
projectedWeight += mempool[txid].weight;
|
||||
inTemplate[txid] = true;
|
||||
}
|
||||
|
||||
displacedWeight += (4000 - transactions[0].weight);
|
||||
projectedWeight += transactions[0].weight;
|
||||
matchedWeight += 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
|
||||
@@ -121,12 +128,14 @@ class Audit {
|
||||
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
|
||||
score,
|
||||
similarity,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ const nodeRpcCredentials: BitcoinRpcCredentials = {
|
||||
port: config.CORE_RPC.PORT,
|
||||
user: config.CORE_RPC.USERNAME,
|
||||
pass: config.CORE_RPC.PASSWORD,
|
||||
timeout: 60000,
|
||||
timeout: config.CORE_RPC.TIMEOUT,
|
||||
};
|
||||
|
||||
export default new bitcoin.Client(nodeRpcCredentials);
|
||||
|
||||
@@ -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: 60000,
|
||||
timeout: config.SECOND_CORE_RPC.TIMEOUT,
|
||||
};
|
||||
|
||||
export default new bitcoin.Client(nodeRpcCredentials);
|
||||
|
||||
@@ -143,7 +143,10 @@ class Blocks {
|
||||
* @returns BlockSummary
|
||||
*/
|
||||
public summarizeBlock(block: IBitcoinApi.VerboseBlock): BlockSummary {
|
||||
const stripped = block.tx.map((tx) => {
|
||||
if (Common.isLiquid()) {
|
||||
block = this.convertLiquidFees(block);
|
||||
}
|
||||
const stripped = block.tx.map((tx: IBitcoinApi.VerboseTransaction) => {
|
||||
return {
|
||||
txid: tx.txid,
|
||||
vsize: tx.weight / 4,
|
||||
@@ -158,6 +161,13 @@ 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
|
||||
@@ -641,7 +651,7 @@ class Blocks {
|
||||
if (this.newBlockCallbacks.length) {
|
||||
this.newBlockCallbacks.forEach((cb) => cb(blockExtended, txIds, transactions));
|
||||
}
|
||||
if (!memPool.hasPriority()) {
|
||||
if (!memPool.hasPriority() && (block.height % config.MEMPOOL.DISK_CACHE_BLOCK_INTERVAL === 0)) {
|
||||
diskCache.$saveCacheToDisk();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CpfpInfo, TransactionExtended, TransactionStripped } from '../mempool.interfaces';
|
||||
import { CpfpInfo, MempoolBlockWithTransactions, TransactionExtended, TransactionStripped } from '../mempool.interfaces';
|
||||
import config from '../config';
|
||||
import { NodeSocket } from '../repositories/NodesSocketsRepository';
|
||||
import { isIP } from 'net';
|
||||
@@ -164,6 +164,30 @@ 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';
|
||||
|
||||
@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
|
||||
import { RowDataPacket } from 'mysql2';
|
||||
|
||||
class DatabaseMigration {
|
||||
private static currentVersion = 58;
|
||||
private static currentVersion = 59;
|
||||
private queryTimeout = 3600_000;
|
||||
private statisticsAddedIndexed = false;
|
||||
private uniqueLogs: string[] = [];
|
||||
@@ -510,6 +510,11 @@ 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`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,9 +9,11 @@ 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(
|
||||
@@ -32,12 +34,12 @@ export function calcDifficultyAdjustment(
|
||||
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;
|
||||
|
||||
let difficultyChange = 0;
|
||||
let timeAvgSecs = BLOCK_SECONDS_TARGET;
|
||||
let timeAvgSecs = diffSeconds / blocksInEpoch;
|
||||
// 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) {
|
||||
@@ -74,9 +76,11 @@ export function calcDifficultyAdjustment(
|
||||
remainingBlocks,
|
||||
remainingTime,
|
||||
previousRetarget,
|
||||
previousTime: DATime,
|
||||
nextRetargetHeight,
|
||||
timeAvg,
|
||||
timeOffset,
|
||||
expectedBlocks,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -11,23 +11,33 @@ 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;
|
||||
|
||||
constructor() { }
|
||||
constructor() {
|
||||
if (!cluster.isPrimary) {
|
||||
return;
|
||||
}
|
||||
process.on('SIGINT', (e) => {
|
||||
this.$saveCacheToDisk(true);
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
async $saveCacheToDisk(): Promise<void> {
|
||||
async $saveCacheToDisk(sync: boolean = false): 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 (async)...');
|
||||
logger.debug(`Writing mempool and blocks data to disk cache (${ sync ? 'sync' : 'async' })...`);
|
||||
this.isWritingCache = true;
|
||||
|
||||
const mempool = memPool.getMempool();
|
||||
@@ -40,19 +50,48 @@ class DiskCache {
|
||||
|
||||
const chunkSize = Math.floor(mempoolArray.length / DiskCache.CHUNK_FILES);
|
||||
|
||||
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({
|
||||
if (sync) {
|
||||
fs.writeFileSync(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++) {
|
||||
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 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 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) {
|
||||
@@ -61,7 +100,7 @@ class DiskCache {
|
||||
}
|
||||
}
|
||||
|
||||
wipeCache() {
|
||||
wipeCache(): void {
|
||||
logger.notice(`Wiping nodejs backend cache/cache*.json files`);
|
||||
try {
|
||||
fs.unlinkSync(DiskCache.FILE_NAME);
|
||||
@@ -83,7 +122,7 @@ class DiskCache {
|
||||
}
|
||||
}
|
||||
|
||||
loadMempoolCache() {
|
||||
loadMempoolCache(): void {
|
||||
if (!fs.existsSync(DiskCache.FILE_NAME)) {
|
||||
return;
|
||||
}
|
||||
@@ -97,6 +136,10 @@ 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) {
|
||||
|
||||
@@ -417,24 +417,24 @@ class NodesApi {
|
||||
|
||||
if (!ispList[isp1]) {
|
||||
ispList[isp1] = {
|
||||
id: channel.isp1ID.toString(),
|
||||
ids: [channel.isp1ID],
|
||||
capacity: 0,
|
||||
channels: 0,
|
||||
nodes: {},
|
||||
};
|
||||
} else if (ispList[isp1].id.indexOf(channel.isp1ID) === -1) {
|
||||
ispList[isp1].id += ',' + channel.isp1ID.toString();
|
||||
} else if (ispList[isp1].ids.includes(channel.isp1ID) === false) {
|
||||
ispList[isp1].ids.push(channel.isp1ID);
|
||||
}
|
||||
|
||||
if (!ispList[isp2]) {
|
||||
ispList[isp2] = {
|
||||
id: channel.isp2ID.toString(),
|
||||
ids: [channel.isp2ID],
|
||||
capacity: 0,
|
||||
channels: 0,
|
||||
nodes: {},
|
||||
};
|
||||
} else if (ispList[isp2].id.indexOf(channel.isp2ID) === -1) {
|
||||
ispList[isp2].id += ',' + channel.isp2ID.toString();
|
||||
} else if (ispList[isp2].ids.includes(channel.isp2ID) === false) {
|
||||
ispList[isp2].ids.push(channel.isp2ID);
|
||||
}
|
||||
|
||||
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].id,
|
||||
ispList[isp].ids.sort((a, b) => a - b).join(','),
|
||||
isp,
|
||||
ispList[isp].capacity,
|
||||
ispList[isp].channels,
|
||||
|
||||
@@ -4,21 +4,29 @@ 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) {
|
||||
if (!config.LIGHTNING.ENABLED) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
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: 10000
|
||||
timeout: config.LND.TIMEOUT
|
||||
};
|
||||
} 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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,11 @@ 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);
|
||||
@@ -128,6 +133,16 @@ 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 {
|
||||
@@ -142,7 +157,10 @@ class Mempool {
|
||||
}
|
||||
hasChange = true;
|
||||
newTransactions.push(transaction);
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
if (config.MEMPOOL.BACKEND === 'esplora' && e.response?.status === 404) {
|
||||
this.missingTxCount++;
|
||||
}
|
||||
logger.debug(`Error finding transaction '${txid}' in the mempool: ` + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
@@ -152,6 +170,14 @@ 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
|
||||
|
||||
@@ -13,6 +13,7 @@ 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;
|
||||
@@ -141,6 +142,9 @@ 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);
|
||||
@@ -162,6 +166,8 @@ class Mining {
|
||||
},
|
||||
estimatedHashrate: currentEstimatedHashrate * (blockCount24h / totalBlock24h),
|
||||
reportedHashrate: null,
|
||||
avgBlockHealth: avgHealth,
|
||||
totalReward: totalReward,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -432,7 +432,7 @@ class WebsocketHandler {
|
||||
}
|
||||
|
||||
if (Common.indexingEnabled() && memPool.isInSync()) {
|
||||
const { censored, added, fresh, score } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
|
||||
const { censored, added, fresh, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
|
||||
const matchRate = Math.round(score * 100 * 100) / 100;
|
||||
|
||||
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions.map((tx) => {
|
||||
@@ -464,8 +464,14 @@ 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[] = [];
|
||||
|
||||
@@ -33,6 +33,7 @@ interface IConfig {
|
||||
ADVANCED_GBT_MEMPOOL: boolean;
|
||||
CPFP_INDEXING: boolean;
|
||||
MAX_BLOCKS_BULK_QUERY: number;
|
||||
DISK_CACHE_BLOCK_INTERVAL: number;
|
||||
};
|
||||
ESPLORA: {
|
||||
REST_API_URL: string;
|
||||
@@ -51,6 +52,7 @@ interface IConfig {
|
||||
TLS_CERT_PATH: string;
|
||||
MACAROON_PATH: string;
|
||||
REST_API_URL: string;
|
||||
TIMEOUT: number;
|
||||
};
|
||||
CLIGHTNING: {
|
||||
SOCKET: string;
|
||||
@@ -65,12 +67,14 @@ 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;
|
||||
@@ -155,6 +159,7 @@ 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',
|
||||
@@ -168,13 +173,15 @@ const defaults: IConfig = {
|
||||
'HOST': '127.0.0.1',
|
||||
'PORT': 8332,
|
||||
'USERNAME': 'mempool',
|
||||
'PASSWORD': 'mempool'
|
||||
'PASSWORD': 'mempool',
|
||||
'TIMEOUT': 60000,
|
||||
},
|
||||
'SECOND_CORE_RPC': {
|
||||
'HOST': '127.0.0.1',
|
||||
'PORT': 8332,
|
||||
'USERNAME': 'mempool',
|
||||
'PASSWORD': 'mempool'
|
||||
'PASSWORD': 'mempool',
|
||||
'TIMEOUT': 60000,
|
||||
},
|
||||
'DATABASE': {
|
||||
'ENABLED': true,
|
||||
@@ -214,6 +221,7 @@ const defaults: IConfig = {
|
||||
'TLS_CERT_PATH': '',
|
||||
'MACAROON_PATH': '',
|
||||
'REST_API_URL': 'https://localhost:8080',
|
||||
'TIMEOUT': 10000,
|
||||
},
|
||||
'CLIGHTNING': {
|
||||
'SOCKET': '',
|
||||
|
||||
@@ -215,11 +215,11 @@ class Server {
|
||||
await lightningStatsUpdater.$startService();
|
||||
await forensicsService.$startService();
|
||||
} catch(e) {
|
||||
logger.err(`Nodejs lightning backend crashed. Restarting in 1 minute. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||
logger.err(`Exception in $runLightningBackend. Restarting in 1 minute. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||
await Common.sleep$(1000 * 60);
|
||||
this.$runLightningBackend();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
setUpWebsocketHandling(): void {
|
||||
if (this.wss) {
|
||||
|
||||
@@ -69,6 +69,10 @@ 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) {
|
||||
|
||||
@@ -153,6 +153,7 @@ 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;
|
||||
|
||||
@@ -330,6 +330,55 @@ 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
|
||||
*/
|
||||
@@ -748,6 +797,7 @@ class BlocksRepository {
|
||||
SELECT height
|
||||
FROM compact_cpfp_clusters
|
||||
WHERE height <= ? AND height >= ?
|
||||
GROUP BY height
|
||||
ORDER BY height DESC;
|
||||
`, [currentBlockHeight, minHeight]);
|
||||
|
||||
|
||||
@@ -22,12 +22,15 @@ class LightningStatsUpdater {
|
||||
* Update the latest entry for each node every config.LIGHTNING.STATS_REFRESH_INTERVAL seconds
|
||||
*/
|
||||
private async $logStatsDaily(): Promise<void> {
|
||||
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);
|
||||
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)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,9 @@ class PoolsUpdater {
|
||||
treeUrl: string = config.MEMPOOL.POOLS_JSON_TREE_URL;
|
||||
|
||||
public async updatePoolsJson(): Promise<void> {
|
||||
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) {
|
||||
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false ||
|
||||
config.MEMPOOL.ENABLED === false
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -73,6 +73,11 @@ 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;
|
||||
}
|
||||
@@ -88,7 +93,7 @@ class PriceUpdater {
|
||||
if (this.historyInserted === false && config.DATABASE.ENABLED === true) {
|
||||
await this.$insertHistoricalPrices();
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
logger.err(`Cannot save BTC prices in db. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ 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.
|
||||
@@ -112,6 +113,7 @@ 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
|
||||
},
|
||||
```
|
||||
|
||||
@@ -143,6 +145,7 @@ Corresponding `docker-compose.yml` overrides:
|
||||
MEMPOOL_ADVANCED_GBT_MEMPOOL: ""
|
||||
MEMPOOL_CPFP_INDEXING: ""
|
||||
MAX_BLOCKS_BULK_QUERY: ""
|
||||
DISK_CACHE_BLOCK_INTERVAL: ""
|
||||
...
|
||||
```
|
||||
|
||||
@@ -158,7 +161,8 @@ Corresponding `docker-compose.yml` overrides:
|
||||
"HOST": "127.0.0.1",
|
||||
"PORT": 8332,
|
||||
"USERNAME": "mempool",
|
||||
"PASSWORD": "mempool"
|
||||
"PASSWORD": "mempool",
|
||||
"TIMEOUT": 60000
|
||||
},
|
||||
```
|
||||
|
||||
@@ -170,6 +174,7 @@ Corresponding `docker-compose.yml` overrides:
|
||||
CORE_RPC_PORT: ""
|
||||
CORE_RPC_USERNAME: ""
|
||||
CORE_RPC_PASSWORD: ""
|
||||
CORE_RPC_TIMEOUT: 60000
|
||||
...
|
||||
```
|
||||
|
||||
@@ -219,7 +224,8 @@ Corresponding `docker-compose.yml` overrides:
|
||||
"HOST": "127.0.0.1",
|
||||
"PORT": 8332,
|
||||
"USERNAME": "mempool",
|
||||
"PASSWORD": "mempool"
|
||||
"PASSWORD": "mempool",
|
||||
"TIMEOUT": 60000
|
||||
},
|
||||
```
|
||||
|
||||
@@ -231,6 +237,7 @@ Corresponding `docker-compose.yml` overrides:
|
||||
SECOND_CORE_RPC_PORT: ""
|
||||
SECOND_CORE_RPC_USERNAME: ""
|
||||
SECOND_CORE_RPC_PASSWORD: ""
|
||||
SECOND_CORE_RPC_TIMEOUT: ""
|
||||
...
|
||||
```
|
||||
|
||||
@@ -403,6 +410,7 @@ Corresponding `docker-compose.yml` overrides:
|
||||
"TLS_CERT_PATH": ""
|
||||
"MACAROON_PATH": ""
|
||||
"REST_API_URL": "https://localhost:8080"
|
||||
"TIMEOUT": 10000
|
||||
}
|
||||
```
|
||||
|
||||
@@ -413,6 +421,7 @@ Corresponding `docker-compose.yml` overrides:
|
||||
LND_TLS_CERT_PATH: ""
|
||||
LND_MACAROON_PATH: ""
|
||||
LND_REST_API_URL: "https://localhost:8080"
|
||||
LND_TIMEOUT: 10000
|
||||
...
|
||||
```
|
||||
|
||||
@@ -432,3 +441,26 @@ 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"
|
||||
...
|
||||
```
|
||||
|
||||
@@ -17,6 +17,7 @@ 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
|
||||
|
||||
@@ -26,13 +26,15 @@
|
||||
"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__
|
||||
"MAX_BLOCKS_BULK_QUERY": __MEMPOOL_MAX_BLOCKS_BULK_QUERY__,
|
||||
"DISK_CACHE_BLOCK_INTERVAL": __MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__
|
||||
},
|
||||
"CORE_RPC": {
|
||||
"HOST": "__CORE_RPC_HOST__",
|
||||
"PORT": __CORE_RPC_PORT__,
|
||||
"USERNAME": "__CORE_RPC_USERNAME__",
|
||||
"PASSWORD": "__CORE_RPC_PASSWORD__"
|
||||
"PASSWORD": "__CORE_RPC_PASSWORD__",
|
||||
"TIMEOUT": __CORE_RPC_TIMEOUT__
|
||||
},
|
||||
"ELECTRUM": {
|
||||
"HOST": "__ELECTRUM_HOST__",
|
||||
@@ -46,7 +48,8 @@
|
||||
"HOST": "__SECOND_CORE_RPC_HOST__",
|
||||
"PORT": __SECOND_CORE_RPC_PORT__,
|
||||
"USERNAME": "__SECOND_CORE_RPC_USERNAME__",
|
||||
"PASSWORD": "__SECOND_CORE_RPC_PASSWORD__"
|
||||
"PASSWORD": "__SECOND_CORE_RPC_PASSWORD__",
|
||||
"TIMEOUT": __SECOND_CORE_RPC_TIMEOUT__
|
||||
},
|
||||
"DATABASE": {
|
||||
"ENABLED": __DATABASE_ENABLED__,
|
||||
@@ -83,7 +86,8 @@
|
||||
"LND": {
|
||||
"TLS_CERT_PATH": "__LND_TLS_CERT_PATH__",
|
||||
"MACAROON_PATH": "__LND_MACAROON_PATH__",
|
||||
"REST_API_URL": "__LND_REST_API_URL__"
|
||||
"REST_API_URL": "__LND_REST_API_URL__",
|
||||
"TIMEOUT": "__LND_TIMEOUT__"
|
||||
},
|
||||
"CLIGHTNING": {
|
||||
"SOCKET": "__CLIGHTNING_SOCKET__"
|
||||
@@ -107,5 +111,11 @@
|
||||
"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__"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ __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}
|
||||
@@ -31,12 +30,14 @@ __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}
|
||||
@@ -51,6 +52,7 @@ __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}
|
||||
@@ -108,10 +110,18 @@ __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
|
||||
@@ -135,7 +145,6 @@ 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
|
||||
@@ -144,11 +153,13 @@ 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
|
||||
@@ -160,6 +171,7 @@ 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
|
||||
@@ -211,8 +223,16 @@ 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
|
||||
|
||||
@@ -10,6 +10,10 @@ 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}" = "9735" ];then
|
||||
export LIGHTNING=true
|
||||
fi
|
||||
|
||||
# Runtime overrides - read env vars defined in docker compose
|
||||
|
||||
__TESTNET_ENABLED__=${TESTNET_ENABLED:=false}
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
#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
3
frontend/.gitignore
vendored
@@ -54,7 +54,8 @@ src/resources/assets-testnet.json
|
||||
src/resources/assets-testnet.minimal.json
|
||||
src/resources/pools.json
|
||||
src/resources/mining-pools/*
|
||||
src/resources/*.mp4
|
||||
src/resources/**/*.mp4
|
||||
src/resources/**/*.vtt
|
||||
|
||||
# environment config
|
||||
mempool-frontend-config.json
|
||||
|
||||
@@ -111,7 +111,7 @@ https://www.transifex.com/mempool/mempool/dashboard/
|
||||
* Spanish @maxhodler @bisqes
|
||||
* Persian @techmix
|
||||
* French @Bayernatoor
|
||||
* Korean @kcalvinalvinn
|
||||
* Korean @kcalvinalvinn @sogoagain
|
||||
* Italian @HodlBits
|
||||
* Hebrew @rapidlab309
|
||||
* Georgian @wyd_idk
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
"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/"
|
||||
|
||||
@@ -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', '');
|
||||
cy.get('.table-tx-vin tr:nth-child(1)').should('have.class', 'ng-star-inserted');
|
||||
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', '');
|
||||
cy.get('.table-tx-vout tr:nth-child(2)').should('have.class', '');
|
||||
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) .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', '');
|
||||
cy.get('.table-tx-vout tr').should('have.class', '');
|
||||
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('.error-unblinded').contains('Error: Invalid blinding data (invalid hex)');
|
||||
});
|
||||
|
||||
|
||||
@@ -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', '');
|
||||
cy.get('.table-tx-vin tr:nth-child(1)').should('have.class', 'ng-star-inserted');
|
||||
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', '');
|
||||
cy.get('.table-tx-vout tr:nth-child(2)').should('have.class', '');
|
||||
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) .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', '');
|
||||
cy.get('.table-tx-vout tr').should('have.class', '');
|
||||
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('.error-unblinded').contains('Error: Invalid blinding data (invalid hex)');
|
||||
});
|
||||
|
||||
|
||||
@@ -87,9 +87,9 @@ export const languages: Language[] = [
|
||||
{ code: 'ar', name: 'العربية' }, // Arabic
|
||||
// { code: 'bg', name: 'Български' }, // Bulgarian
|
||||
// { code: 'bs', name: 'Bosanski' }, // Bosnian
|
||||
{ code: 'ca', name: 'Català' }, // Catalan
|
||||
// { code: 'ca', name: 'Català' }, // Catalan
|
||||
{ code: 'cs', name: 'Čeština' }, // Czech
|
||||
// { code: 'da', name: 'Dansk' }, // Danish
|
||||
{ code: 'da', name: 'Dansk' }, // Danish
|
||||
{ code: 'de', name: 'Deutsch' }, // German
|
||||
// { code: 'et', name: 'Eesti' }, // Estonian
|
||||
// { code: 'el', name: 'Ελληνικά' }, // Greek
|
||||
@@ -136,13 +136,90 @@ export const languages: Language[] = [
|
||||
];
|
||||
|
||||
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: 'Halving 🥳',
|
||||
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'],
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -254,3 +254,30 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,23 @@
|
||||
<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/mempool-promo.mp4" poster="/resources/mempool-promo.jpg" controls loop playsinline [autoplay]="true" [muted]="true"></video>
|
||||
<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>
|
||||
|
||||
<div class="enterprise-sponsor" id="enterprise-sponsors">
|
||||
<h3 i18n="about.sponsors.enterprise.withRocket">Enterprise Sponsors 🚀</h3>
|
||||
@@ -209,7 +225,7 @@
|
||||
<span>EmbassyOS</span>
|
||||
</a>
|
||||
<a href="https://github.com/btcpayserver/btcpayserver" target="_blank" title="BTCPay Server">
|
||||
<img class="image" src="/resources/profile/btcpayserver.svg" />
|
||||
<img class="image not-rounded" src="/resources/profile/btcpayserver.svg" />
|
||||
<span>BTCPay</span>
|
||||
</a>
|
||||
<a href="https://github.com/bisq-network/bisq" target="_blank" title="Bisq">
|
||||
@@ -268,6 +284,26 @@
|
||||
<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>
|
||||
|
||||
@@ -383,6 +419,12 @@
|
||||
<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>
|
||||
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.image.not-rounded {
|
||||
border-radius: 0;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.intro {
|
||||
margin: 25px auto 30px;
|
||||
width: 250px;
|
||||
@@ -36,9 +42,11 @@
|
||||
|
||||
video {
|
||||
width: 640px;
|
||||
height: 360px;
|
||||
max-width: 90%;
|
||||
margin-top: 0;
|
||||
@media (min-width: 768px) {
|
||||
height: 360px;
|
||||
}
|
||||
}
|
||||
|
||||
.social-icons {
|
||||
@@ -51,9 +59,13 @@
|
||||
.enterprise-sponsor,
|
||||
.community-integrations-sponsor,
|
||||
.maintainers {
|
||||
margin-top: 68px;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 68px;
|
||||
scroll-margin: 30px;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
margin-top: 68px;
|
||||
}
|
||||
}
|
||||
|
||||
.maintainers {
|
||||
@@ -199,6 +211,16 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,6 +234,11 @@
|
||||
}
|
||||
|
||||
.community-integrations-sponsor {
|
||||
max-width: 965px;
|
||||
max-width: 1110px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.community-integrations-sponsor img.image {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ export class AboutComponent implements OnInit {
|
||||
tap(() => this.goToAnchor())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.goToAnchor();
|
||||
}
|
||||
@@ -90,4 +90,8 @@ export class AboutComponent implements OnInit {
|
||||
this.showNavigateToSponsor = true;
|
||||
}
|
||||
}
|
||||
|
||||
showSubtitles(language) {
|
||||
return ( this.locale.startsWith( language ) && !this.locale.startsWith('en') );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<ng-container *ngIf="!noFiat && (viewFiat$ | async) && (conversions$ | async) as conversions; else viewFiatVin">
|
||||
<span class="fiat" *ngIf="blockConversion; else noblockconversion">
|
||||
{{ addPlus && satoshis >= 0 ? '+' : '' }}
|
||||
{{
|
||||
{{ addPlus && satoshis >= 0 ? '+' : '' }}{{
|
||||
(
|
||||
(blockConversion.price[currency] > -1 ? blockConversion.price[currency] : null) ??
|
||||
(blockConversion.price['USD'] > -1 ? blockConversion.price['USD'] * blockConversion.exchangeRates['USD' + currency] : null) ?? 0
|
||||
@@ -9,8 +8,7 @@
|
||||
}}
|
||||
</span>
|
||||
<ng-template #noblockconversion>
|
||||
<span class="fiat">{{ addPlus && satoshis >= 0 ? '+' : '' }}
|
||||
{{ (conversions[currency] > -1 ? conversions[currency] : 0) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency }}
|
||||
<span class="fiat">{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ (conversions[currency] > -1 ? conversions[currency] : 0) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency }}
|
||||
</span>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
@@ -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[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>
|
||||
<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>
|
||||
</tr>
|
||||
<tr *ngIf="block?.extras?.medianFee != undefined">
|
||||
<td class="td-width" i18n="block.median-fee">Median fee</td>
|
||||
|
||||
@@ -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]="(specialBlocks[block.height] !== undefined)">
|
||||
[class.blink-bg]="isSpecial(block.height)">
|
||||
<a draggable="false" [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }"
|
||||
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a>
|
||||
<div [attr.data-cy]="'bitcoin-block-' + i + '-height'" class="block-height">
|
||||
|
||||
@@ -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;
|
||||
@Input() count: number = 8;
|
||||
@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() loadingTip: boolean = false;
|
||||
@Input() connected: boolean = true;
|
||||
|
||||
@@ -31,6 +31,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
dynamicBlocksAmount: number = 8;
|
||||
emptyBlocks: BlockExtended[] = this.mountEmptyBlocks();
|
||||
markHeight: number;
|
||||
chainTip: number;
|
||||
blocksSubscription: Subscription;
|
||||
blockPageSubscription: Subscription;
|
||||
networkSubscription: Subscription;
|
||||
@@ -73,6 +74,7 @@ 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)) {
|
||||
@@ -107,7 +109,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.blocks.unshift(block);
|
||||
this.blocks = this.blocks.slice(0, this.dynamicBlocksAmount);
|
||||
|
||||
if (txConfirmed) {
|
||||
if (txConfirmed && block.height > this.chainTip) {
|
||||
this.markHeight = block.height;
|
||||
this.moveArrowToPosition(true, true);
|
||||
} else {
|
||||
@@ -115,7 +117,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
this.blockStyles = [];
|
||||
if (this.blocksFilled) {
|
||||
if (this.blocksFilled && block.height > this.chainTip) {
|
||||
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, i ? -155 : -205)));
|
||||
setTimeout(() => {
|
||||
this.blockStyles = [];
|
||||
@@ -129,6 +131,8 @@ 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 {
|
||||
@@ -265,6 +269,10 @@ 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);
|
||||
|
||||
@@ -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="text-left" [class]="widget ? 'widget' : ''">
|
||||
<td class="height 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,7 +89,6 @@
|
||||
<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'">
|
||||
@@ -98,7 +97,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-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<td *ngIf="auditAvailable" class="health text-right" [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}">
|
||||
|
||||
@@ -51,7 +51,12 @@ tr, td, th {
|
||||
.pool.widget {
|
||||
width: 40%;
|
||||
padding-left: 24px;
|
||||
@media (max-width: 376px) {
|
||||
@media (min-width: 768px) AND (max-width: 926px) {
|
||||
padding-left: 0px;
|
||||
width: 60%;
|
||||
}
|
||||
@media (max-width: 430px) {
|
||||
padding-left: 0px;
|
||||
width: 60%;
|
||||
}
|
||||
}
|
||||
@@ -59,6 +64,10 @@ 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 {
|
||||
@@ -69,6 +78,12 @@ 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%;
|
||||
@@ -92,7 +107,7 @@ tr, td, th {
|
||||
|
||||
.mined {
|
||||
width: 13%;
|
||||
@media (max-width: 576px) {
|
||||
@media (max-width: 730px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -138,7 +153,7 @@ tr, td, th {
|
||||
|
||||
.fees {
|
||||
width: 8%;
|
||||
@media (max-width: 650px) {
|
||||
@media (max-width: 820px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -163,6 +178,16 @@ 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 {
|
||||
@@ -189,10 +214,10 @@ tr, td, th {
|
||||
|
||||
.health {
|
||||
width: 10%;
|
||||
@media (max-width: 1000px) {
|
||||
@media (max-width: 1105px) {
|
||||
width: 13%;
|
||||
}
|
||||
@media (max-width: 950px) {
|
||||
@media (max-width: 560px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -202,7 +227,7 @@ tr, td, th {
|
||||
}
|
||||
.health.widget {
|
||||
width: 25%;
|
||||
@media (max-width: 1000px) {
|
||||
@media (max-width: 1105px) {
|
||||
display: none;
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
@@ -242,4 +267,4 @@ tr, td, th {
|
||||
vertical-align: middle;
|
||||
max-width: 50vw;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,8 +87,8 @@ export class BlocksList implements OnInit, OnDestroy {
|
||||
this.stateService.blocks$
|
||||
.pipe(
|
||||
switchMap((block) => {
|
||||
if (block[0].height < this.lastBlockHeight) {
|
||||
return []; // Return an empty stream so the last pipe is not executed
|
||||
if (block[0].height <= this.lastBlockHeight) {
|
||||
return [null]; // Return an empty stream so the last pipe is not executed
|
||||
}
|
||||
this.lastBlockHeight = block[0].height;
|
||||
return [block];
|
||||
@@ -101,14 +101,16 @@ export class BlocksList implements OnInit, OnDestroy {
|
||||
this.lastPage = this.page;
|
||||
return blocks[0];
|
||||
}
|
||||
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';
|
||||
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);
|
||||
}
|
||||
acc.unshift(blocks[1][0]);
|
||||
acc = acc.slice(0, this.widget ? 6 : 15);
|
||||
return acc;
|
||||
}, [])
|
||||
);
|
||||
|
||||
@@ -2,17 +2,18 @@
|
||||
<table class="table latest-adjustments">
|
||||
<thead>
|
||||
<tr>
|
||||
<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>
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody *ngIf="(hashrateObservable$ | async) as data">
|
||||
<tr *ngFor="let diffChange of data">
|
||||
<td class="d-none d-md-block"><a [routerLink]="['/block' | relativeUrl, diffChange.height]">{{ diffChange.height
|
||||
}}</a></td>
|
||||
<td class="text-left">
|
||||
<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"></app-time>
|
||||
</td>
|
||||
<td class="text-right">{{ diffChange.difficultyShorten }}</td>
|
||||
@@ -23,8 +24,8 @@
|
||||
</tbody>
|
||||
<tbody *ngIf="isLoading">
|
||||
<tr *ngFor="let item of [1,2,3,4,5,6]">
|
||||
<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=""><span class="skeleton-loader"></span></td>
|
||||
<td class="date 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>
|
||||
|
||||
@@ -17,3 +17,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date {
|
||||
@media (min-width: 767px) AND (max-width: 991px) {
|
||||
display: none;
|
||||
}
|
||||
@media (max-width: 500px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<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"></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">—</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}"> </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"></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>
|
||||
@@ -0,0 +1,159 @@
|
||||
.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;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<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>
|
||||
@@ -0,0 +1,22 @@
|
||||
.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;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,81 +3,100 @@
|
||||
<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"></app-time></div>
|
||||
<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>
|
||||
<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 class="difficulty-stats">
|
||||
<div class="item">
|
||||
<div class="card-text">
|
||||
~<app-time [time]="epochData.timeAvg / 1000" [forceFloorOnTimeIntervals]="['minute']" [fractionDigits]="1"></app-time>
|
||||
</div>
|
||||
<div class="symbol" i18n="difficulty-box.average-block-time">Average block time</div>
|
||||
</div>
|
||||
<ng-template #recentlyAdjusted>
|
||||
<div class="card-text">—</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>
|
||||
<div class="item">
|
||||
<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 #arrowDownPreviousDifficulty >
|
||||
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
|
||||
<ng-template #arrowDownDifficulty >
|
||||
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
|
||||
</ng-template>
|
||||
{{ epochData.previousRetarget | absolute | number: '1.2-2' }} </span> %
|
||||
{{ epochData.change | absolute | number: '1.2-2' }}
|
||||
<span class="symbol">%</span>
|
||||
</div>
|
||||
<ng-template #recentlyAdjusted>
|
||||
<div class="card-text">—</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>
|
||||
<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}"> </div>
|
||||
<div class="item">
|
||||
<div class="card-text"><app-time kind="until" [time]="epochData.estimatedRetargetDate" [fastRender]="true"></app-time></div>
|
||||
<div class="symbol">
|
||||
{{ epochData.retargetDateString }}
|
||||
</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>
|
||||
@@ -85,3 +104,10 @@
|
||||
</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>
|
||||
@@ -1,8 +1,14 @@
|
||||
.difficulty-adjustment-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.difficulty-stats {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
height: 76px;
|
||||
height: 50.5px;
|
||||
.shared-block {
|
||||
color: #ffffff66;
|
||||
font-size: 12px;
|
||||
@@ -24,8 +30,8 @@
|
||||
}
|
||||
}
|
||||
.card-text {
|
||||
font-size: 22px;
|
||||
margin-top: -9px;
|
||||
font-size: 20px;
|
||||
margin: auto;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
@@ -33,11 +39,14 @@
|
||||
|
||||
.difficulty-skeleton {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
height: 50.5px;
|
||||
@media (min-width: 376px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
.item {
|
||||
min-width: 120px;
|
||||
max-width: 150px;
|
||||
margin: 0;
|
||||
width: -webkit-fill-available;
|
||||
@@ -65,7 +74,7 @@
|
||||
width: 100%;
|
||||
display: block;
|
||||
&:first-child {
|
||||
margin: 14px auto 0;
|
||||
margin: 10px auto 4px;
|
||||
max-width: 80px;
|
||||
}
|
||||
&:last-child {
|
||||
@@ -109,7 +118,7 @@
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
min-height: 76px;
|
||||
min-height: 50.5px;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
@@ -133,7 +142,7 @@
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
padding: 24px 20px;
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,4 +160,50 @@
|
||||
|
||||
.symbol {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, HostListener, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { combineLatest, Observable, timer } from 'rxjs';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
import { StateService } from '../..//services/state.service';
|
||||
@@ -7,16 +7,33 @@ 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',
|
||||
@@ -24,15 +41,27 @@ interface EpochProgress {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DifficultyComponent implements OnInit {
|
||||
isLoadingWebSocket$: Observable<boolean>;
|
||||
difficultyEpoch$: Observable<EpochProgress>;
|
||||
|
||||
@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;
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -65,22 +94,110 @@ 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,
|
||||
remainingBlocks: da.remainingBlocks,
|
||||
minedBlocks: this.currentIndex + 1,
|
||||
remainingBlocks: da.remainingBlocks - 1,
|
||||
expectedBlocks: Math.floor(da.expectedBlocks),
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
|
||||
import { EChartsOption, graphic } from 'echarts';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { merge, Observable, of } from 'rxjs';
|
||||
import { map, mergeMap, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { formatNumber } from '@angular/common';
|
||||
@@ -84,77 +84,84 @@ export class HashrateChartComponent implements OnInit {
|
||||
}
|
||||
});
|
||||
|
||||
this.hashrateObservable$ = this.radioGroupForm.get('dateSpan').valueChanges
|
||||
.pipe(
|
||||
startWith(this.radioGroupForm.controls.dateSpan.value),
|
||||
switchMap((timespan) => {
|
||||
if (!this.widget && !firstRun) {
|
||||
this.storageService.setValue('miningWindowPreference', timespan);
|
||||
}
|
||||
this.timespan = timespan;
|
||||
firstRun = false;
|
||||
this.miningWindowPreference = timespan;
|
||||
this.isLoading = true;
|
||||
return this.apiService.getHistoricalHashrate$(timespan)
|
||||
.pipe(
|
||||
tap((response) => {
|
||||
const data = response.body;
|
||||
this.hashrateObservable$ = merge(
|
||||
this.radioGroupForm.get('dateSpan').valueChanges
|
||||
.pipe(
|
||||
startWith(this.radioGroupForm.controls.dateSpan.value),
|
||||
switchMap((timespan) => {
|
||||
if (!this.widget && !firstRun) {
|
||||
this.storageService.setValue('miningWindowPreference', timespan);
|
||||
}
|
||||
this.timespan = timespan;
|
||||
firstRun = false;
|
||||
this.miningWindowPreference = timespan;
|
||||
this.isLoading = true;
|
||||
return this.apiService.getHistoricalHashrate$(this.timespan);
|
||||
})
|
||||
),
|
||||
this.stateService.chainTip$
|
||||
.pipe(
|
||||
switchMap(() => {
|
||||
return this.apiService.getHistoricalHashrate$(this.timespan);
|
||||
})
|
||||
)
|
||||
).pipe(
|
||||
tap((response: any) => {
|
||||
const data = response.body;
|
||||
|
||||
// We generate duplicated data point so the tooltip works nicely
|
||||
const diffFixed = [];
|
||||
let diffIndex = 1;
|
||||
let hashIndex = 0;
|
||||
while (hashIndex < data.hashrates.length) {
|
||||
if (diffIndex >= data.difficulty.length) {
|
||||
while (hashIndex < data.hashrates.length) {
|
||||
diffFixed.push({
|
||||
timestamp: data.hashrates[hashIndex].timestamp,
|
||||
difficulty: data.difficulty.length > 0 ? data.difficulty[data.difficulty.length - 1].difficulty : null
|
||||
});
|
||||
++hashIndex;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
while (hashIndex < data.hashrates.length && diffIndex < data.difficulty.length &&
|
||||
data.hashrates[hashIndex].timestamp <= data.difficulty[diffIndex].time
|
||||
) {
|
||||
diffFixed.push({
|
||||
timestamp: data.hashrates[hashIndex].timestamp,
|
||||
difficulty: data.difficulty[diffIndex - 1].difficulty
|
||||
});
|
||||
++hashIndex;
|
||||
}
|
||||
++diffIndex;
|
||||
}
|
||||
|
||||
let maResolution = 15;
|
||||
const hashrateMa = [];
|
||||
for (let i = maResolution - 1; i < data.hashrates.length; ++i) {
|
||||
let avg = 0;
|
||||
for (let y = maResolution - 1; y >= 0; --y) {
|
||||
avg += data.hashrates[i - y].avgHashrate;
|
||||
}
|
||||
avg /= maResolution;
|
||||
hashrateMa.push([data.hashrates[i].timestamp * 1000, avg]);
|
||||
}
|
||||
|
||||
this.prepareChartOptions({
|
||||
hashrates: data.hashrates.map(val => [val.timestamp * 1000, val.avgHashrate]),
|
||||
difficulty: diffFixed.map(val => [val.timestamp * 1000, val.difficulty]),
|
||||
hashrateMa: hashrateMa,
|
||||
// We generate duplicated data point so the tooltip works nicely
|
||||
const diffFixed = [];
|
||||
let diffIndex = 1;
|
||||
let hashIndex = 0;
|
||||
while (hashIndex < data.hashrates.length) {
|
||||
if (diffIndex >= data.difficulty.length) {
|
||||
while (hashIndex < data.hashrates.length) {
|
||||
diffFixed.push({
|
||||
timestamp: data.hashrates[hashIndex].timestamp,
|
||||
difficulty: data.difficulty.length > 0 ? data.difficulty[data.difficulty.length - 1].difficulty : null
|
||||
});
|
||||
this.isLoading = false;
|
||||
}),
|
||||
map((response) => {
|
||||
const data = response.body;
|
||||
return {
|
||||
blockCount: parseInt(response.headers.get('x-total-count'), 10),
|
||||
currentDifficulty: data.currentDifficulty,
|
||||
currentHashrate: data.currentHashrate,
|
||||
};
|
||||
}),
|
||||
);
|
||||
++hashIndex;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
while (hashIndex < data.hashrates.length && diffIndex < data.difficulty.length &&
|
||||
data.hashrates[hashIndex].timestamp <= data.difficulty[diffIndex].time
|
||||
) {
|
||||
diffFixed.push({
|
||||
timestamp: data.hashrates[hashIndex].timestamp,
|
||||
difficulty: data.difficulty[diffIndex - 1].difficulty
|
||||
});
|
||||
++hashIndex;
|
||||
}
|
||||
++diffIndex;
|
||||
}
|
||||
|
||||
let maResolution = 15;
|
||||
const hashrateMa = [];
|
||||
for (let i = maResolution - 1; i < data.hashrates.length; ++i) {
|
||||
let avg = 0;
|
||||
for (let y = maResolution - 1; y >= 0; --y) {
|
||||
avg += data.hashrates[i - y].avgHashrate;
|
||||
}
|
||||
avg /= maResolution;
|
||||
hashrateMa.push([data.hashrates[i].timestamp * 1000, avg]);
|
||||
}
|
||||
|
||||
this.prepareChartOptions({
|
||||
hashrates: data.hashrates.map(val => [val.timestamp * 1000, val.avgHashrate]),
|
||||
difficulty: diffFixed.map(val => [val.timestamp * 1000, val.difficulty]),
|
||||
hashrateMa: hashrateMa,
|
||||
});
|
||||
this.isLoading = false;
|
||||
}),
|
||||
map((response) => {
|
||||
const data = response.body;
|
||||
return {
|
||||
blockCount: parseInt(response.headers.get('x-total-count'), 10),
|
||||
currentDifficulty: data.currentDifficulty,
|
||||
currentHashrate: data.currentHashrate,
|
||||
};
|
||||
}),
|
||||
share()
|
||||
);
|
||||
|
||||
@@ -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 [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 @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">
|
||||
<a draggable="false" [routerLink]="['/mempool-block/' | relativeUrl, i]"
|
||||
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a>
|
||||
<div class="block-body">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, Input } from '@angular/core';
|
||||
import { Subscription, Observable, fromEvent, merge, of, combineLatest, timer } from 'rxjs';
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
||||
import { Subscription, Observable, fromEvent, merge, of, combineLatest } from 'rxjs';
|
||||
import { MempoolBlock } from '../../interfaces/websocket.interface';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Router } from '@angular/router';
|
||||
@@ -9,11 +9,18 @@ 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 {
|
||||
@@ -32,12 +39,14 @@ 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;
|
||||
@@ -53,6 +62,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
||||
|
||||
resetTransitionTimeout: number;
|
||||
|
||||
chainTip: number = -1;
|
||||
blockIndex = 1;
|
||||
|
||||
constructor(
|
||||
@@ -69,6 +79,8 @@ 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));
|
||||
@@ -116,9 +128,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
||||
mempoolBlocks.forEach((block, i) => {
|
||||
block.index = this.blockIndex + i;
|
||||
block.height = lastBlock.height + i + 1;
|
||||
if (this.stateService.network === '') {
|
||||
block.blink = specialBlocks[block.height] ? true : false;
|
||||
}
|
||||
block.blink = specialBlocks[block.height]?.networks.includes(this.stateService.network || 'mainnet') ? true : false;
|
||||
});
|
||||
|
||||
const stringifiedBlocks = JSON.stringify(mempoolBlocks);
|
||||
@@ -155,11 +165,24 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.blockSubscription = this.stateService.blocks$
|
||||
.subscribe(([block]) => {
|
||||
if (block?.extras?.matchRate >= 66 && !this.tabHidden) {
|
||||
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) {
|
||||
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);
|
||||
|
||||
@@ -195,11 +218,12 @@ 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.index;
|
||||
return (block.isStack) ? 'stack' : block.index;
|
||||
}
|
||||
|
||||
reduceMempoolBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] {
|
||||
@@ -214,6 +238,10 @@ 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;
|
||||
}
|
||||
@@ -333,4 +361,4 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
return emptyBlocks;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
<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>
|
||||
@@ -22,7 +21,7 @@
|
||||
<!-- difficulty adjustment -->
|
||||
<div class="col">
|
||||
<div class="main-title" i18n="dashboard.difficulty-adjustment">Difficulty Adjustment</div>
|
||||
<app-difficulty [attr.data-cy]="'difficulty-adjustment'" [showTitle]="false" [showProgress]="false" [showHalving]="true"></app-difficulty>
|
||||
<app-difficulty-mining [attr.data-cy]="'difficulty-adjustment'" [showTitle]="false" [showProgress]="false" [showHalving]="true"></app-difficulty-mining>
|
||||
</div>
|
||||
|
||||
<!-- pool distribution -->
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</div>
|
||||
|
||||
<div class="card-header" *ngIf="!widget">
|
||||
<div class="d-flex d-md-block align-items-baseline">
|
||||
<div class="d-flex d-md-table-cell 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-block" i18n="mining.rank">Rank</th>
|
||||
<th class="d-none d-md-table-cell" 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-block" i18n="mining.empty-blocks">Empty blocks</th>
|
||||
<th class="d-none d-md-table-cell" 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-block">{{ pool.rank }}</td>
|
||||
<td class="d-none d-md-table-cell">{{ 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-block"> ({{ pool.share }}%)</span>
|
||||
{{ pool.blockCount }}<span class="d-none d-md-table-cell"> ({{ 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-block">{{ pool.emptyBlocks }} ({{ pool.emptyBlockRatio }}%)</td>
|
||||
<td class="d-none d-md-table-cell">{{ pool.emptyBlocks }} ({{ pool.emptyBlockRatio }}%)</td>
|
||||
</tr>
|
||||
<tr style="border-top: 1px solid #555">
|
||||
<td class="d-none d-md-block"></td>
|
||||
<td class="d-none d-md-table-cell"></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-block"><b>{{ miningStats.totalEmptyBlock }} ({{ miningStats.totalEmptyBlockRatio
|
||||
<td class="d-none d-md-table-cell"><b>{{ miningStats.totalEmptyBlock }} ({{ miningStats.totalEmptyBlockRatio
|
||||
}}%)</b></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, Input, NgZone, OnInit, HostBinding
|
||||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { EChartsOption, PieSeriesOption } from 'echarts';
|
||||
import { concat, Observable } from 'rxjs';
|
||||
import { merge, Observable } from 'rxjs';
|
||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { StorageService } from '../..//services/storage.service';
|
||||
@@ -73,7 +73,7 @@ export class PoolRankingComponent implements OnInit {
|
||||
}
|
||||
});
|
||||
|
||||
this.miningStatsObservable$ = concat(
|
||||
this.miningStatsObservable$ = merge(
|
||||
this.radioGroupForm.get('dateSpan').valueChanges
|
||||
.pipe(
|
||||
startWith(this.radioGroupForm.controls.dateSpan.value), // (trigger when the page loads)
|
||||
@@ -89,7 +89,7 @@ export class PoolRankingComponent implements OnInit {
|
||||
return this.miningService.getMiningStats(this.miningWindowPreference);
|
||||
})
|
||||
),
|
||||
this.stateService.blocks$
|
||||
this.stateService.chainTip$
|
||||
.pipe(
|
||||
switchMap(() => {
|
||||
return this.miningService.getMiningStats(this.miningWindowPreference);
|
||||
@@ -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} blocks`;
|
||||
`<br>` + $localize`${ i }:INTERPOLATION: blocks`;
|
||||
} else {
|
||||
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
|
||||
$localize`${i} blocks`;
|
||||
$localize`${ i }:INTERPOLATION: blocks`;
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -195,13 +195,15 @@ 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">${'Other'} (${totalShareOther.toFixed(2)}%)</b><br>` +
|
||||
return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` +
|
||||
totalEstimatedHashrateOther.toString() + ' PH/s' +
|
||||
`<br>` + totalBlockOther.toString() + ` blocks`;
|
||||
`<br>` + $localize`${ i }:INTERPOLATION: blocks`;
|
||||
} else {
|
||||
return `<b style="color: white">${'Other'} (${totalShareOther.toFixed(2)}%)</b><br>` +
|
||||
totalBlockOther.toString() + ` blocks`;
|
||||
return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` +
|
||||
$localize`${ i }:INTERPOLATION: blocks`;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -86,11 +86,6 @@ 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);
|
||||
|
||||
|
||||
@@ -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 [routerLink]="['/address' | relativeUrl, poolStats.pool.addresses[0]]" class="first-address">
|
||||
<a class="addresses-data" [routerLink]="['/address' | relativeUrl, poolStats.pool.addresses[0]]">
|
||||
{{ poolStats.pool.addresses[0] }}
|
||||
</a>
|
||||
<div>
|
||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="gfg">
|
||||
<a *ngFor="let address of poolStats.pool.addresses | slice: 1"
|
||||
<a class="addresses-data" *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 [routerLink]="['/address' | relativeUrl, poolStats.pool.addresses[0]]">
|
||||
{{ poolStats.pool.addresses[0] | shortenString: 40 }}
|
||||
<a class="addresses-data" [routerLink]="['/address' | relativeUrl, poolStats.pool.addresses[0]]">
|
||||
{{ poolStats.pool.addresses[0] | shortenString: 30 }}
|
||||
</a>
|
||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="gfg" style="width: 100%">
|
||||
<a *ngFor="let address of poolStats.pool.addresses | slice: 1"
|
||||
<a class="addresses-data" *ngFor="let address of poolStats.pool.addresses | slice: 1"
|
||||
[routerLink]="['/address' | relativeUrl, address]">{{
|
||||
address | shortenString: 40 }}<br></a>
|
||||
address | shortenString: 30 }}<br></a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -88,22 +88,25 @@
|
||||
|
||||
<!-- 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" 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>
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<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>
|
||||
<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>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
@@ -111,49 +114,46 @@
|
||||
<!-- 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" 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>
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<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>
|
||||
<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>
|
||||
</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" 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>
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<td>{{ formatNumber(poolStats.blockCount['24h'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
|
||||
<td class="text-center">{{ formatNumber(poolStats.blockCount['24h'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
|
||||
poolStats.blockShare['24h'], this.locale, '1.0-0') }}%)</td>
|
||||
<td>{{ formatNumber(poolStats.blockCount['1w'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
|
||||
<td class="text-center">{{ formatNumber(poolStats.blockCount['1w'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
|
||||
poolStats.blockShare['1w'], this.locale, '1.0-0') }}%)</td>
|
||||
<td>{{ formatNumber(poolStats.blockCount['all'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
|
||||
<td class="text-center">{{ formatNumber(poolStats.blockCount['all'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
|
||||
poolStats.blockShare['all'], this.locale, '1.0-0') }}%)</td>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -162,21 +162,20 @@
|
||||
<!-- 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" 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>
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<td>{{ formatNumber(poolStats.blockCount['24h'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
|
||||
<td class="text-center">{{ formatNumber(poolStats.blockCount['24h'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
|
||||
poolStats.blockShare['24h'], this.locale, '1.0-0') }}%)</td>
|
||||
<td>{{ formatNumber(poolStats.blockCount['1w'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
|
||||
<td class="text-center">{{ formatNumber(poolStats.blockCount['1w'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
|
||||
poolStats.blockShare['1w'], this.locale, '1.0-0') }}%)</td>
|
||||
<td>{{ formatNumber(poolStats.blockCount['all'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
|
||||
<td class="text-center">{{ formatNumber(poolStats.blockCount['all'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
|
||||
poolStats.blockShare['all'], this.locale, '1.0-0') }}%)</td>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -213,8 +212,9 @@
|
||||
<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 class="fees text-right" i18n="latest-blocks.fees">Fees</th>
|
||||
<th *ngIf="!auditAvailable" 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,10 +234,24 @@
|
||||
{{ 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 class="fees text-right">
|
||||
<td *ngIf="!auditAvailable" class="fees text-right">
|
||||
<app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-2" [noFiat]="true"></app-amount>
|
||||
</td>
|
||||
<td class="txs text-right">
|
||||
@@ -364,24 +378,23 @@
|
||||
|
||||
<!-- 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" 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>
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<td>
|
||||
<td class="text-center">
|
||||
<div class="skeleton-loader data"></div>
|
||||
</td>
|
||||
<td>
|
||||
<td class="text-center">
|
||||
<div class="skeleton-loader data"></div>
|
||||
</td>
|
||||
<td>
|
||||
<td class="text-center" *ngIf="auditAvailable">
|
||||
<div class="skeleton-loader data"></div>
|
||||
</td>
|
||||
</tbody>
|
||||
@@ -391,23 +404,22 @@
|
||||
<!-- 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" 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>
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<td>
|
||||
<td class="text-center">
|
||||
<div class="skeleton-loader data"></div>
|
||||
</td>
|
||||
<td>
|
||||
<td class="text-center">
|
||||
<div class="skeleton-loader data"></div>
|
||||
</td>
|
||||
<td>
|
||||
<td class="text-center" *ngIf="auditAvailable">
|
||||
<div class="skeleton-loader data"></div>
|
||||
</td>
|
||||
</tbody>
|
||||
@@ -417,24 +429,23 @@
|
||||
|
||||
<!-- 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" 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>
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<td>
|
||||
<td class="text-center">
|
||||
<div class="skeleton-loader data"></div>
|
||||
</td>
|
||||
<td>
|
||||
<td class="text-center">
|
||||
<div class="skeleton-loader data"></div>
|
||||
</td>
|
||||
<td>
|
||||
<td class="text-center">
|
||||
<div class="skeleton-loader data"></div>
|
||||
</td>
|
||||
</tbody>
|
||||
@@ -444,23 +455,22 @@
|
||||
<!-- 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" 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>
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<td>
|
||||
<td class="text-center">
|
||||
<div class="skeleton-loader data"></div>
|
||||
</td>
|
||||
<td>
|
||||
<td class="text-center">
|
||||
<div class="skeleton-loader data"></div>
|
||||
</td>
|
||||
<td>
|
||||
<td class="text-center">
|
||||
<div class="skeleton-loader data"></div>
|
||||
</td>
|
||||
</tbody>
|
||||
@@ -475,4 +485,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #emptyTd>
|
||||
<td class="text-center"></td>
|
||||
</ng-template>
|
||||
@@ -68,6 +68,11 @@ div.scrollable {
|
||||
vertical-align: top;
|
||||
padding-top: 25px;
|
||||
}
|
||||
.addresses-data {
|
||||
vertical-align: top;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.data {
|
||||
text-align: right;
|
||||
@@ -100,7 +105,7 @@ div.scrollable {
|
||||
@media (max-width: 875px) {
|
||||
padding-left: 50px;
|
||||
}
|
||||
@media (max-width: 650px) {
|
||||
@media (max-width: 685px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -118,7 +123,7 @@ div.scrollable {
|
||||
padding-right: 10px;
|
||||
}
|
||||
@media (max-width: 875px) {
|
||||
padding-right: 50px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
@media (max-width: 567px) {
|
||||
padding-right: 10px;
|
||||
@@ -186,10 +191,6 @@ div.scrollable {
|
||||
.block-count-title {
|
||||
color: #4a68b9;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
@media (max-width: 767.98px) {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.table-data tr {
|
||||
|
||||
@@ -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, timer } from 'rxjs';
|
||||
import { BehaviorSubject, Observable } 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,6 +35,8 @@ 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(
|
||||
@@ -44,6 +46,7 @@ export class PoolComponent implements OnInit {
|
||||
public stateService: StateService,
|
||||
private seoService: SeoService,
|
||||
) {
|
||||
this.auditAvailable = this.stateService.env.AUDIT;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -74,11 +77,6 @@ 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'
|
||||
|
||||
@@ -50,14 +50,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="mining.rewards-per-tx">Reward Per Tx</h5>
|
||||
<h5 class="card-title" i18n="mining.fees-per-block">Avg Block Fees</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">Reward Per Tx</h5>
|
||||
<h5 class="card-title" i18n="mining.average-fee">Avg Tx Fee</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<form [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate>
|
||||
<div class="d-flex">
|
||||
<div class="search-box-container mr-2">
|
||||
<input (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="Explore the full Bitcoin ecosystem">
|
||||
<input autofocus (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="Explore the full Bitcoin ecosystem">
|
||||
<app-search-results #searchResults [hidden]="dropdownHidden" [results]="typeAhead$ | async" (selectedResult)="selectedResult($event)"></app-search-results>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -53,3 +53,8 @@ form {
|
||||
margin-top: 1px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
box-shadow: none;
|
||||
border-color: #1b1f2c;
|
||||
}
|
||||
|
||||
@@ -85,21 +85,20 @@ 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) {
|
||||
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[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;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (specialBlocks[block.height]) {
|
||||
if (specialBlocks[block.height] && specialBlocks[block.height].networks.includes(this.stateService.network || 'mainnet')) {
|
||||
this.specialEvent = true;
|
||||
this.eventName = specialBlocks[block.height].labelEventCompleted;
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -19,6 +19,7 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() fixedRender = false;
|
||||
@Input() relative = false;
|
||||
@Input() forceFloorOnTimeIntervals: string[];
|
||||
@Input() fractionDigits: number = 0;
|
||||
|
||||
constructor(
|
||||
private ref: ChangeDetectorRef,
|
||||
@@ -88,7 +89,12 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
||||
} else {
|
||||
counter = Math.round(seconds / this.intervals[i]);
|
||||
}
|
||||
const dateStrings = dates(counter);
|
||||
let rounded = counter;
|
||||
if (this.fractionDigits) {
|
||||
const roundFactor = Math.pow(10,this.fractionDigits);
|
||||
rounded = Math.round((seconds / this.intervals[i]) * roundFactor) / roundFactor;
|
||||
}
|
||||
const dateStrings = dates(rounded);
|
||||
if (counter > 0) {
|
||||
switch (this.kind) {
|
||||
case 'since':
|
||||
|
||||
@@ -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'">
|
||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet' && featuresEnabled">
|
||||
<td class="td-width" i18n="transaction.features|Transaction features">Features</td>
|
||||
<td>
|
||||
<app-tx-features [tx]="tx"></app-tx-features>
|
||||
@@ -468,6 +468,7 @@
|
||||
<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>
|
||||
@@ -488,7 +489,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="d-none d-lg-inline ml-2" *ngIf="tx.fee" [tx]="tx"></app-tx-fee-rating>
|
||||
<app-tx-fee-rating class="ml-2 mr-2" *ngIf="tx.fee || tx.effectiveFeePerVsize" [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>
|
||||
|
||||
@@ -23,6 +23,7 @@ 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',
|
||||
@@ -74,6 +75,12 @@ 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;
|
||||
@@ -197,6 +204,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
this.tx = tx;
|
||||
this.setFeatures();
|
||||
this.isCached = true;
|
||||
if (tx.fee === undefined) {
|
||||
this.tx.fee = 0;
|
||||
@@ -291,6 +299,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
this.tx = tx;
|
||||
this.setFeatures();
|
||||
this.isCached = false;
|
||||
if (tx.fee === undefined) {
|
||||
this.tx.fee = 0;
|
||||
@@ -347,7 +356,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.blocksSubscription = this.stateService.blocks$.subscribe(([block, txConfirmed]) => {
|
||||
this.latestBlock = block;
|
||||
|
||||
if (txConfirmed && this.tx) {
|
||||
if (txConfirmed && this.tx && !this.tx.status.confirmed) {
|
||||
this.tx.status = {
|
||||
confirmed: true,
|
||||
block_height: block.height,
|
||||
@@ -428,9 +437,23 @@ 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;
|
||||
@@ -495,8 +518,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
setGraphSize(): void {
|
||||
this.isMobile = window.innerWidth < 850;
|
||||
if (this.graphContainer) {
|
||||
this.graphWidth = this.graphContainer.nativeElement.clientWidth;
|
||||
setTimeout(() => {
|
||||
this.graphWidth = this.graphContainer.nativeElement.clientWidth;
|
||||
}, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 } from 'rxjs';
|
||||
import { Observable, ReplaySubject, BehaviorSubject, merge, Subscription, of, forkJoin } from 'rxjs';
|
||||
import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.interface';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { environment } from '../../../environments/environment';
|
||||
@@ -70,12 +70,19 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
.pipe(
|
||||
switchMap((txIds) => {
|
||||
if (!this.cached) {
|
||||
return this.apiService.getOutspendsBatched$(txIds);
|
||||
// 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)));
|
||||
} else {
|
||||
return of([]);
|
||||
}
|
||||
}),
|
||||
tap((outspends: Outspend[][]) => {
|
||||
tap((batchedOutspends: Outspend[][][]) => {
|
||||
// flatten batched results back into a single array
|
||||
const outspends = batchedOutspends.flat(1);
|
||||
if (!this.transactions) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -56,9 +56,25 @@
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<p *ngIf="line.value == null && line.confidential" i18n="shared.confidential">Confidential</p>
|
||||
<p *ngIf="line.value != null"><app-amount [blockConversion]="blockConversion" [satoshis]="line.value"></app-amount></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.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>
|
||||
@@ -1,6 +1,8 @@
|
||||
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';
|
||||
@@ -16,6 +18,7 @@ interface Xput {
|
||||
pegout?: string;
|
||||
confidential?: boolean;
|
||||
timestamp?: number;
|
||||
asset?: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -27,13 +30,19 @@ 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) {}
|
||||
constructor(
|
||||
private priceService: PriceService,
|
||||
private stateService: StateService,
|
||||
) {}
|
||||
|
||||
ngOnChanges(changes): void {
|
||||
if (changes.line?.currentValue) {
|
||||
@@ -60,4 +69,8 @@ export class TxBowtieGraphTooltipComponent implements OnChanges {
|
||||
this.tooltipPosition = { x, y };
|
||||
}
|
||||
}
|
||||
|
||||
pow(base: number, exponent: number): number {
|
||||
return Math.pow(base, exponent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,5 +172,6 @@
|
||||
[line]="hoverLine"
|
||||
[cursorPosition]="tooltipPosition"
|
||||
[isConnector]="hoverConnector"
|
||||
[assetsMinimal]="assetsMinimal"
|
||||
></app-tx-bowtie-graph-tooltip>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ 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;
|
||||
@@ -30,6 +31,7 @@ interface Xput {
|
||||
pegout?: string;
|
||||
confidential?: boolean;
|
||||
timestamp?: number;
|
||||
asset?: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -71,6 +73,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||
zeroValueWidth = 60;
|
||||
zeroValueThickness = 20;
|
||||
hasLine: boolean;
|
||||
assetsMinimal: any;
|
||||
|
||||
outspendsSubscription: Subscription;
|
||||
refreshOutspends$: ReplaySubject<string> = new ReplaySubject();
|
||||
@@ -95,6 +98,7 @@ 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')) {
|
||||
@@ -105,6 +109,12 @@ 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(
|
||||
@@ -162,7 +172,8 @@ 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
|
||||
timestamp: this.tx.status.block_time,
|
||||
asset: v?.asset,
|
||||
} as Xput;
|
||||
});
|
||||
|
||||
@@ -182,7 +193,8 @@ 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
|
||||
timestamp: this.tx.status.block_time,
|
||||
asset: v?.prevout?.asset,
|
||||
} as Xput;
|
||||
});
|
||||
|
||||
@@ -208,8 +220,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.5} L ${(this.width / 2) + this.midWidth} ${(this.height / 2) + 0.5}`,
|
||||
style: `stroke-width: ${this.combinedWeight + 1}; stroke: ${this.gradient[1]}`
|
||||
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]}`
|
||||
};
|
||||
|
||||
this.hasLine = this.inputs.reduce((line, put) => line || !put.zeroValue, false)
|
||||
@@ -266,7 +278,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||
const lineParams = weights.map((w, i) => {
|
||||
return {
|
||||
weight: w,
|
||||
thickness: xputs[i].value === 0 ? this.zeroValueThickness : Math.max(this.minWeight - 1, w) + 1,
|
||||
thickness: xputs[i].value === 0 ? this.zeroValueThickness : Math.min(this.combinedWeight + 0.5, Math.max(this.minWeight - 1, w) + 1),
|
||||
offset: 0,
|
||||
innerY: 0,
|
||||
outerY: 0,
|
||||
@@ -278,7 +290,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;
|
||||
const innerBottom = innerTop + this.combinedWeight + 0.5;
|
||||
// tracks the visual bottom of the endpoints of the previous line
|
||||
let lastOuter = 0;
|
||||
let lastInner = innerTop;
|
||||
@@ -303,7 +315,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) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<ng-template [ngIf]="!tx.status.confirmed || tx.status.block_height >= 477120">
|
||||
<ng-template [ngIf]="segwitEnabled">
|
||||
<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]="!tx.status.confirmed || tx.status.block_height >= 709632">
|
||||
<ng-template [ngIf]="taprootEnabled">
|
||||
<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]="!tx.status.confirmed || tx.status.block_height > 399700">
|
||||
<ng-template [ngIf]="rbfEnabled">
|
||||
<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>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component, ChangeDetectionStrategy, OnChanges, Input } from '@angular/core';
|
||||
import { calcSegwitFeeGains } from '../../bitcoin.utils';
|
||||
import { calcSegwitFeeGains, isFeatureActive } from '../../bitcoin.utils';
|
||||
import { Transaction } from '../../interfaces/electrs.interface';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tx-features',
|
||||
@@ -21,12 +22,21 @@ export class TxFeaturesComponent implements OnChanges {
|
||||
isRbfTransaction: boolean;
|
||||
isTaproot: boolean;
|
||||
|
||||
constructor() { }
|
||||
segwitEnabled: boolean;
|
||||
rbfEnabled: boolean;
|
||||
taprootEnabled: boolean;
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
) { }
|
||||
|
||||
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');
|
||||
|
||||
@@ -3,6 +3,7 @@ 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',
|
||||
@@ -23,12 +24,12 @@ export class TxFeeRatingComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private cacheService: CacheService,
|
||||
private cd: ChangeDetectorRef,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.blocksSubscription = this.stateService.blocks$.subscribe(([block]) => {
|
||||
this.blocks.push(block);
|
||||
this.blocksSubscription = this.cacheService.loadedBlocks$.subscribe((block) => {
|
||||
if (this.tx.status.confirmed && this.tx.status.block_height === block.height && block?.extras?.medianFee > 0) {
|
||||
this.calculateRatings(block);
|
||||
this.cd.markForCheck();
|
||||
@@ -41,8 +42,9 @@ export class TxFeeRatingComponent implements OnInit, OnChanges, OnDestroy {
|
||||
if (!this.tx.status.confirmed) {
|
||||
return;
|
||||
}
|
||||
this.cacheService.loadBlock(this.tx.status.block_height);
|
||||
|
||||
const foundBlock = this.blocks.find((b) => b.height === this.tx.status.block_height);
|
||||
const foundBlock = this.cacheService.getCachedBlock(this.tx.status.block_height) || null;
|
||||
if (foundBlock && foundBlock?.extras?.medianFee > 0) {
|
||||
this.calculateRatings(foundBlock);
|
||||
}
|
||||
|
||||
@@ -8859,6 +8859,21 @@ 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",
|
||||
|
||||
@@ -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('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('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>
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,7 @@ export class ApiDocsNavComponent implements OnInit {
|
||||
env: Env;
|
||||
tabData: any[];
|
||||
auditEnabled: boolean;
|
||||
officialMempoolInstance: boolean;
|
||||
|
||||
constructor(
|
||||
private stateService: StateService
|
||||
@@ -23,6 +24,7 @@ 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;
|
||||
|
||||
@@ -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('auditOnly') && item.options.auditOnly && auditEnabled )">
|
||||
<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 )">
|
||||
<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,6 +207,18 @@
|
||||
<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>
|
||||
|
||||
@@ -33,9 +33,11 @@ export interface DifficultyAdjustment {
|
||||
remainingBlocks: number;
|
||||
remainingTime: number;
|
||||
previousRetarget: number;
|
||||
previousTime: number;
|
||||
nextRetargetHeight: number;
|
||||
timeAvg: number;
|
||||
timeOffset: number;
|
||||
expectedBlocks: number;
|
||||
}
|
||||
|
||||
export interface AddressInformation {
|
||||
@@ -105,8 +107,8 @@ export interface PoolStat {
|
||||
'1w': number,
|
||||
};
|
||||
estimatedHashrate: number;
|
||||
reportedHashrate: number;
|
||||
luck?: number;
|
||||
avgBlockHealth: number;
|
||||
totalReward: number;
|
||||
}
|
||||
|
||||
export interface BlockExtension {
|
||||
@@ -116,6 +118,7 @@ export interface BlockExtension {
|
||||
reward?: number;
|
||||
coinbaseRaw?: string;
|
||||
matchRate?: number;
|
||||
similarity?: number;
|
||||
pool?: {
|
||||
id: number;
|
||||
name: string;
|
||||
|
||||
@@ -43,6 +43,7 @@ export interface MempoolBlock {
|
||||
totalFees: number;
|
||||
feeRange: number[];
|
||||
index: number;
|
||||
isStack?: boolean;
|
||||
}
|
||||
|
||||
export interface MempoolBlockWithTransactions extends MempoolBlock {
|
||||
|
||||
@@ -36,25 +36,25 @@
|
||||
<ng-template #tableHeader>
|
||||
<thead>
|
||||
<th class="alias text-left" i18n="lightning.alias">Alias</th>
|
||||
<th class="alias text-left d-none d-md-table-cell"> </th>
|
||||
<th class="alias text-left d-none d-md-table-cell" i18n="status">Status</th>
|
||||
<th *ngIf="status !== 'closed'" class="channels text-left d-none d-md-table-cell" i18n="transaction.fee-rate|Transaction fee rate">Fee rate</th>
|
||||
<th *ngIf="status === 'closed'" class="channels text-left d-none d-md-table-cell" i18n="channels.closing_date">Closing date</th>
|
||||
<th class="capacity text-right d-none d-md-table-cell" i18n="lightning.capacity">Capacity</th>
|
||||
<th class="capacity text-right" i18n="channels.id">Channel ID</th>
|
||||
<th class="nodedetails text-left"> </th>
|
||||
<th class="status text-left" i18n="status">Status</th>
|
||||
<th class="feerate text-left" *ngIf="status !== 'closed'" i18n="transaction.fee-rate|Transaction fee rate">Fee rate</th>
|
||||
<th class="feerate text-left" *ngIf="status === 'closed'" i18n="channels.closing_date">Closing date</th>
|
||||
<th class="liquidity text-right" i18n="lightning.capacity">Capacity</th>
|
||||
<th class="channelid text-right" i18n="channels.id">Channel ID</th>
|
||||
</thead>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #tableTemplate let-channel let-node="node">
|
||||
<td class="alias text-left">
|
||||
<div>{{ node.alias || '?' }}</div>
|
||||
<app-truncate [text]="node.alias || '?'" [maxWidth]="200" [lastChars]="6"></app-truncate>
|
||||
<div class="second-line">
|
||||
<app-truncate [text]="node.public_key" [maxWidth]="200" [lastChars]="6" [link]="['/lightning/node' | relativeUrl, node.public_key]">
|
||||
<app-clipboard [text]="node.public_key" size="small"></app-clipboard>
|
||||
</app-truncate>
|
||||
</div>
|
||||
</td>
|
||||
<td class="alias text-left d-none d-md-table-cell">
|
||||
<td class="nodedetails text-left">
|
||||
<div class="second-line"><ng-container *ngTemplateOutlet="xChannels; context: {$implicit: node.channels }"></ng-container></div>
|
||||
<div class="second-line">
|
||||
<app-amount *ngIf="node.capacity > 100000000; else smallnode" [satoshis]="node.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
|
||||
@@ -64,7 +64,7 @@
|
||||
</ng-template>
|
||||
</div>
|
||||
</td>
|
||||
<td class="d-none d-md-table-cell">
|
||||
<td class="status">
|
||||
<span class="badge rounded-pill badge-secondary" *ngIf="channel.status === 0" i18n="status.inactive">Inactive</span>
|
||||
<span class="badge rounded-pill badge-success" *ngIf="channel.status === 1" i18n="status.active">Active</span>
|
||||
<ng-template [ngIf]="channel.status === 2">
|
||||
@@ -74,20 +74,20 @@
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</td>
|
||||
<td *ngIf="status !== 'closed'" class="capacity text-left d-none d-md-table-cell">
|
||||
<td *ngIf="status !== 'closed'" class="feerate text-left">
|
||||
{{ channel.fee_rate }} <span class="symbol">ppm ({{ channel.fee_rate / 10000 | number }}%)</span>
|
||||
</td>
|
||||
<td *ngIf="status === 'closed'" class="capacity text-left d-none d-md-table-cell">
|
||||
<td *ngIf="status === 'closed'" class="feerate text-left">
|
||||
<app-timestamp [unixTime]="channel.closing_date"></app-timestamp>
|
||||
</td>
|
||||
<td class="capacity text-right d-none d-md-table-cell">
|
||||
<td class="liquidity text-right">
|
||||
<app-amount *ngIf="channel.capacity > 100000000; else smallchannel" [satoshis]="channel.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
|
||||
<ng-template #smallchannel>
|
||||
{{ channel.capacity | amountShortener: 1 }}
|
||||
<span class="sats" i18n="shared.sats">sats</span>
|
||||
</ng-template>
|
||||
</td>
|
||||
<td class="capacity text-right">
|
||||
</td>
|
||||
<td class="channelid text-right">
|
||||
<a [routerLink]="['/lightning/channel' | relativeUrl, channel.id]">{{ channel.short_id }}</a>
|
||||
</td>
|
||||
</ng-template>
|
||||
@@ -100,19 +100,19 @@
|
||||
<td class="alias text-left" style="width: 370px;">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
<td class="alias text-left">
|
||||
<td class="nodedetails text-left">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
<td class="capacity text-left d-none d-md-table-cell">
|
||||
<td class="status text-left">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
<td class="channels text-left d-none d-md-table-cell">
|
||||
<td class="feerate text-left">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
<td class="channels text-right d-none d-md-table-cell">
|
||||
<td class="liquidity text-right">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
<td class="channels text-left">
|
||||
<td class="channelid text-left">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -31,4 +31,36 @@
|
||||
@media (max-width: 435px) {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.alias {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.feerate {
|
||||
@media (max-width: 815px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
@media (max-width: 710px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.nodedetails {
|
||||
@media (max-width: 600px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.liquidity {
|
||||
@media (max-width: 500px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.channelid {
|
||||
padding-right: 0;
|
||||
}
|
||||
@@ -2,7 +2,10 @@
|
||||
color: #4a68b9;
|
||||
font-size: 10px;
|
||||
margin-bottom: 4px;
|
||||
font-size: 1rem;
|
||||
font-size: 1rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
|
||||
@@ -146,7 +146,7 @@ export class NodeStatisticsChartComponent implements OnInit {
|
||||
padding: 10,
|
||||
data: [
|
||||
{
|
||||
name: 'Channels',
|
||||
name: $localize`:@@807cf11e6ac1cde912496f764c176bdfdd6b7e19:Channels`,
|
||||
inactiveColor: 'rgb(110, 112, 121)',
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
@@ -154,7 +154,7 @@ export class NodeStatisticsChartComponent implements OnInit {
|
||||
icon: 'roundRect',
|
||||
},
|
||||
{
|
||||
name: 'Capacity',
|
||||
name: $localize`:@@ce9dfdc6dccb28dc75a78c704e09dc18fb02dcfa:Capacity`,
|
||||
inactiveColor: 'rgb(110, 112, 121)',
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
@@ -201,7 +201,7 @@ export class NodeStatisticsChartComponent implements OnInit {
|
||||
series: data.channels.length === 0 ? [] : [
|
||||
{
|
||||
zlevel: 1,
|
||||
name: 'Channels',
|
||||
name: $localize`:@@807cf11e6ac1cde912496f764c176bdfdd6b7e19:Channels`,
|
||||
showSymbol: false,
|
||||
symbol: 'none',
|
||||
data: data.channels,
|
||||
@@ -224,7 +224,7 @@ export class NodeStatisticsChartComponent implements OnInit {
|
||||
{
|
||||
zlevel: 0,
|
||||
yAxisIndex: 1,
|
||||
name: 'Capacity',
|
||||
name: $localize`:@@ce9dfdc6dccb28dc75a78c704e09dc18fb02dcfa:Capacity`,
|
||||
showSymbol: false,
|
||||
symbol: 'none',
|
||||
stack: 'Total',
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
</tr>
|
||||
<tr *ngIf="(avgChannelDistance$ | async) as avgDistance;">
|
||||
<td i18n="lightning.avg-distance" class="text-truncate">Avg channel distance</td>
|
||||
<td class="direction-ltr">{{ avgDistance | number : '1.0-0' }} <span class="symbol">km</span> <span class="separator">/</span> {{ kmToMiles(avgDistance) | number : '1.0-0' }} <span class="symbol">mi</span></td>
|
||||
<td class="direction-ltr">{{ avgDistance | amountShortener: 1 }} <span class="symbol">km</span> <span class="separator">·</span>{{ kmToMiles(avgDistance) | amountShortener: 1 }} <span class="symbol">mi</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -108,5 +108,6 @@ app-fiat {
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin: 0 1em;
|
||||
margin: 0 0.25em;
|
||||
color: slategrey;
|
||||
}
|
||||
|
||||
@@ -18,5 +18,8 @@
|
||||
<div class="text-center loading-spinner" [class]="style" *ngIf="isLoading && !disableSpinner">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
<div *ngIf="showIndexingInProgress" class="indexing-message">
|
||||
<span class="badge badge-pill badge-warning" i18n="lightning.indexing-in-progress">Indexing in progress</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user