Compare commits
293 Commits
mononaut/c
...
mononaut/v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe3059fbb0 | ||
|
|
a0d116c069 | ||
|
|
5fba2a2531 | ||
|
|
5349857692 | ||
|
|
819932bee9 | ||
|
|
fc6ebf3ccf | ||
|
|
c8decb9e46 | ||
|
|
563ce3cfb9 | ||
|
|
b3819dfb84 | ||
|
|
93f1a4e6d4 | ||
|
|
72c6ddef75 | ||
|
|
bb1ee90b0b | ||
|
|
2773bbfd00 | ||
|
|
d172233aff | ||
|
|
9c3e676391 | ||
|
|
4fed3a90a7 | ||
|
|
e7261d2613 | ||
|
|
80160fa37c | ||
|
|
e13b3ebfdc | ||
|
|
74cd15b8fd | ||
|
|
6715e20e0b | ||
|
|
57eddac7f0 | ||
|
|
af115b49aa | ||
|
|
332f9a2f5e | ||
|
|
34eef3553b | ||
|
|
9e4ce42b6a | ||
|
|
4c4a91ae95 | ||
|
|
93d46d5c5b | ||
|
|
8788d4f898 | ||
|
|
e28650c46c | ||
|
|
855c11f02c | ||
|
|
3f8e91bd46 | ||
|
|
6722e45109 | ||
|
|
414383638d | ||
|
|
2575b79c05 | ||
|
|
c7cab4c877 | ||
|
|
85c2f0ba30 | ||
|
|
edfbede704 | ||
|
|
5f60cb821a | ||
|
|
8486c1117d | ||
|
|
61f24562fd | ||
|
|
28de93d0ff | ||
|
|
1fd85b729d | ||
|
|
5681ae3f5c | ||
|
|
9d9e0976ae | ||
|
|
6180837636 | ||
|
|
17beaf7d4f | ||
|
|
ce8f471b27 | ||
|
|
b3e36fdd99 | ||
|
|
f971ddf1fa | ||
|
|
c0c37922c3 | ||
|
|
1eb9e58331 | ||
|
|
f8a35a110c | ||
|
|
c4d13fb5b7 | ||
|
|
53a44853b3 | ||
|
|
29aa3617d8 | ||
|
|
addf3e2521 | ||
|
|
5826f8fa1e | ||
|
|
965d89fd91 | ||
|
|
ed69591bcf | ||
|
|
f1f6c48128 | ||
|
|
f8bd062aa2 | ||
|
|
77835bcb9d | ||
|
|
bf5821c8c8 | ||
|
|
a2e23014f4 | ||
|
|
811c14a6bd | ||
|
|
a34d87148b | ||
|
|
a45a8db479 | ||
|
|
672f71c515 | ||
|
|
2c16bbb0e9 | ||
|
|
63f7709e82 | ||
|
|
15b13ef4a4 | ||
|
|
75303c7a34 | ||
|
|
1a6048f0ab | ||
|
|
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"
|
||||
|
||||
18
backend/package-lock.json
generated
18
backend/package-lock.json
generated
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "mempool-backend",
|
||||
"version": "2.5.0-dev",
|
||||
"version": "2.5.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mempool-backend",
|
||||
"version": "2.5.0-dev",
|
||||
"version": "2.5.0",
|
||||
"license": "GNU Affero General Public License v3.0",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.20.12",
|
||||
"@mempool/electrum-client": "^1.1.7",
|
||||
"@mempool/electrum-client": "1.1.9",
|
||||
"@types/node": "^16.18.11",
|
||||
"axios": "~0.27.2",
|
||||
"bitcoinjs-lib": "~6.1.0",
|
||||
@@ -1272,9 +1272,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mempool/electrum-client": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@mempool/electrum-client/-/electrum-client-1.1.8.tgz",
|
||||
"integrity": "sha512-6YP6UJstlk2GgC++NwPJthMPvLozyEMlqPq7RjvIWSwrL0smvM0Q0PAOohwZJtJFDWspuEUtNRF7aHQT2ztnYg==",
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@mempool/electrum-client/-/electrum-client-1.1.9.tgz",
|
||||
"integrity": "sha512-mlvPiCzUlaETpYW3i6V87A24jjMYgsebaXtUo3WQyyLnYUuxs0KiXQ2mnKh3h15j8Xg/hfxeGIi+5OC9u0nftQ==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@@ -7248,9 +7248,9 @@
|
||||
}
|
||||
},
|
||||
"@mempool/electrum-client": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@mempool/electrum-client/-/electrum-client-1.1.8.tgz",
|
||||
"integrity": "sha512-6YP6UJstlk2GgC++NwPJthMPvLozyEMlqPq7RjvIWSwrL0smvM0Q0PAOohwZJtJFDWspuEUtNRF7aHQT2ztnYg=="
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@mempool/electrum-client/-/electrum-client-1.1.9.tgz",
|
||||
"integrity": "sha512-mlvPiCzUlaETpYW3i6V87A24jjMYgsebaXtUo3WQyyLnYUuxs0KiXQ2mnKh3h15j8Xg/hfxeGIi+5OC9u0nftQ=="
|
||||
},
|
||||
"@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mempool-backend",
|
||||
"version": "2.5.0-dev",
|
||||
"version": "2.5.0",
|
||||
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
||||
"license": "GNU Affero General Public License v3.0",
|
||||
"homepage": "https://mempool.space",
|
||||
@@ -35,7 +35,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.20.12",
|
||||
"@mempool/electrum-client": "^1.1.7",
|
||||
"@mempool/electrum-client": "1.1.9",
|
||||
"@types/node": "^16.18.11",
|
||||
"axios": "~0.27.2",
|
||||
"bitcoinjs-lib": "~6.1.0",
|
||||
|
||||
@@ -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__"
|
||||
|
||||
@@ -14,18 +14,20 @@ describe('Mempool Difficulty Adjustment', () => {
|
||||
750134, // Current block height
|
||||
0.6280047707459726, // Previous retarget % (Passed through)
|
||||
'mainnet', // Network (if testnet, next value is non-zero)
|
||||
0, // If not testnet, not used
|
||||
0, // Latest block timestamp in seconds (only used if difficulty already locked in)
|
||||
],
|
||||
{ // Expected Result
|
||||
progressPercent: 9.027777777777777,
|
||||
difficultyChange: 12.562233927411782,
|
||||
difficultyChange: 13.180707740199772,
|
||||
estimatedRetargetDate: 1661895424692,
|
||||
remainingBlocks: 1834,
|
||||
remainingTime: 977591692,
|
||||
previousRetarget: 0.6280047707459726,
|
||||
previousTime: 1660820820,
|
||||
nextRetargetHeight: 751968,
|
||||
timeAvg: 533038,
|
||||
timeOffset: 0,
|
||||
expectedBlocks: 161.68833333333333,
|
||||
},
|
||||
],
|
||||
[ // Vector 2 (testnet)
|
||||
@@ -39,15 +41,40 @@ describe('Mempool Difficulty Adjustment', () => {
|
||||
],
|
||||
{ // Expected Result is same other than timeOffset
|
||||
progressPercent: 9.027777777777777,
|
||||
difficultyChange: 12.562233927411782,
|
||||
difficultyChange: 13.180707740199772,
|
||||
estimatedRetargetDate: 1661895424692,
|
||||
remainingBlocks: 1834,
|
||||
remainingTime: 977591692,
|
||||
previousTime: 1660820820,
|
||||
previousRetarget: 0.6280047707459726,
|
||||
nextRetargetHeight: 751968,
|
||||
timeAvg: 533038,
|
||||
timeOffset: -667000, // 11 min 7 seconds since last block (testnet only)
|
||||
// If we add time avg to abs(timeOffset) it makes exactly 1200000 ms, or 20 minutes
|
||||
expectedBlocks: 161.68833333333333,
|
||||
},
|
||||
],
|
||||
[ // Vector 3 (mainnet lock-in (epoch ending 788255))
|
||||
[ // Inputs
|
||||
dt('2023-04-20T09:57:33.000Z'), // Last DA time (in seconds)
|
||||
dt('2023-05-04T14:54:09.000Z'), // Current time (now) (in seconds)
|
||||
788255, // Current block height
|
||||
1.7220298879531821, // Previous retarget % (Passed through)
|
||||
'mainnet', // Network (if testnet, next value is non-zero)
|
||||
dt('2023-05-04T14:54:26.000Z'), // Latest block timestamp in seconds
|
||||
],
|
||||
{ // Expected Result
|
||||
progressPercent: 99.95039682539682,
|
||||
difficultyChange: -1.4512637555574193,
|
||||
estimatedRetargetDate: 1683212658129,
|
||||
remainingBlocks: 1,
|
||||
remainingTime: 609129,
|
||||
previousRetarget: 1.7220298879531821,
|
||||
previousTime: 1681984653,
|
||||
nextRetargetHeight: 788256,
|
||||
timeAvg: 609129,
|
||||
timeOffset: 0,
|
||||
expectedBlocks: 2045.66,
|
||||
},
|
||||
],
|
||||
] as [[number, number, number, number, string, number], DifficultyAdjustment][];
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import config from '../config';
|
||||
import logger from '../logger';
|
||||
import { TransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
|
||||
|
||||
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
|
||||
|
||||
class Audit {
|
||||
auditBlock(transactions: TransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: TransactionExtended })
|
||||
: { censored: string[], added: string[], fresh: string[], score: number } {
|
||||
: { 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 +17,8 @@ class Audit {
|
||||
const isCensored = {}; // missing, without excuse
|
||||
const isDisplaced = {};
|
||||
let displacedWeight = 0;
|
||||
let matchedWeight = 0;
|
||||
let projectedWeight = 0;
|
||||
|
||||
const inBlock = {};
|
||||
const inTemplate = {};
|
||||
@@ -37,12 +40,19 @@ class Audit {
|
||||
} else {
|
||||
isCensored[txid] = true;
|
||||
}
|
||||
displacedWeight += mempool[txid].weight;
|
||||
displacedWeight += mempool[txid]?.weight || 0;
|
||||
} else {
|
||||
matchedWeight += mempool[txid]?.weight || 0;
|
||||
}
|
||||
projectedWeight += mempool[txid]?.weight || 0;
|
||||
inTemplate[txid] = true;
|
||||
}
|
||||
|
||||
displacedWeight += (4000 - transactions[0].weight);
|
||||
if (transactions[0]) {
|
||||
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
|
||||
@@ -52,19 +62,24 @@ class Audit {
|
||||
let failures = 0;
|
||||
while (projectedBlocks[1] && index < projectedBlocks[1].transactionIds.length && failures < 500) {
|
||||
const txid = projectedBlocks[1].transactionIds[index];
|
||||
const fits = (mempool[txid].weight - displacedWeightRemaining) < 4000;
|
||||
const feeMatches = mempool[txid].effectiveFeePerVsize >= lastFeeRate;
|
||||
if (fits || feeMatches) {
|
||||
isDisplaced[txid] = true;
|
||||
if (fits) {
|
||||
lastFeeRate = Math.min(lastFeeRate, mempool[txid].effectiveFeePerVsize);
|
||||
const tx = mempool[txid];
|
||||
if (tx) {
|
||||
const fits = (tx.weight - displacedWeightRemaining) < 4000;
|
||||
const feeMatches = tx.effectiveFeePerVsize >= lastFeeRate;
|
||||
if (fits || feeMatches) {
|
||||
isDisplaced[txid] = true;
|
||||
if (fits) {
|
||||
lastFeeRate = Math.min(lastFeeRate, tx.effectiveFeePerVsize);
|
||||
}
|
||||
if (tx.firstSeen == null || (now - (tx?.firstSeen || 0)) > PROPAGATION_MARGIN) {
|
||||
displacedWeightRemaining -= tx.weight;
|
||||
}
|
||||
failures = 0;
|
||||
} else {
|
||||
failures++;
|
||||
}
|
||||
if (mempool[txid].firstSeen == null || (now - (mempool[txid]?.firstSeen || 0)) > PROPAGATION_MARGIN) {
|
||||
displacedWeightRemaining -= mempool[txid].weight;
|
||||
}
|
||||
failures = 0;
|
||||
} else {
|
||||
failures++;
|
||||
logger.warn('projected transaction missing from mempool cache');
|
||||
}
|
||||
index++;
|
||||
}
|
||||
@@ -78,17 +93,7 @@ class Audit {
|
||||
} else {
|
||||
if (!isDisplaced[tx.txid]) {
|
||||
added.push(tx.txid);
|
||||
} else {
|
||||
}
|
||||
let blockIndex = -1;
|
||||
let index = -1;
|
||||
projectedBlocks.forEach((block, bi) => {
|
||||
const i = block.transactionIds.indexOf(tx.txid);
|
||||
if (i >= 0) {
|
||||
blockIndex = bi;
|
||||
index = i;
|
||||
}
|
||||
});
|
||||
overflowWeight += tx.weight;
|
||||
}
|
||||
totalWeight += tx.weight;
|
||||
@@ -101,32 +106,39 @@ class Audit {
|
||||
index = projectedBlocks[0].transactionIds.length - 1;
|
||||
while (index >= 0) {
|
||||
const txid = projectedBlocks[0].transactionIds[index];
|
||||
if (overflowWeightRemaining > 0) {
|
||||
if (isCensored[txid]) {
|
||||
delete isCensored[txid];
|
||||
}
|
||||
if (mempool[txid].effectiveFeePerVsize > maxOverflowRate) {
|
||||
maxOverflowRate = mempool[txid].effectiveFeePerVsize;
|
||||
rateThreshold = (Math.ceil(maxOverflowRate * 100) / 100) + 0.005;
|
||||
}
|
||||
} else if (mempool[txid].effectiveFeePerVsize <= rateThreshold) { // tolerance of 0.01 sat/vb + rounding
|
||||
if (isCensored[txid]) {
|
||||
delete isCensored[txid];
|
||||
const tx = mempool[txid];
|
||||
if (tx) {
|
||||
if (overflowWeightRemaining > 0) {
|
||||
if (isCensored[txid]) {
|
||||
delete isCensored[txid];
|
||||
}
|
||||
if (tx.effectiveFeePerVsize > maxOverflowRate) {
|
||||
maxOverflowRate = tx.effectiveFeePerVsize;
|
||||
rateThreshold = (Math.ceil(maxOverflowRate * 100) / 100) + 0.005;
|
||||
}
|
||||
} else if (tx.effectiveFeePerVsize <= rateThreshold) { // tolerance of 0.01 sat/vb + rounding
|
||||
if (isCensored[txid]) {
|
||||
delete isCensored[txid];
|
||||
}
|
||||
}
|
||||
overflowWeightRemaining -= (mempool[txid]?.weight || 0);
|
||||
} else {
|
||||
logger.warn('projected transaction missing from mempool cache');
|
||||
}
|
||||
overflowWeightRemaining -= (mempool[txid]?.weight || 0);
|
||||
index--;
|
||||
}
|
||||
|
||||
const numCensored = Object.keys(isCensored).length;
|
||||
const numMatches = matches.length - 1; // adjust for coinbase tx
|
||||
const score = numMatches > 0 ? (numMatches / (numMatches + numCensored)) : 0;
|
||||
const similarity = projectedWeight ? matchedWeight / projectedWeight : 1;
|
||||
|
||||
return {
|
||||
censored: Object.keys(isCensored),
|
||||
added,
|
||||
fresh,
|
||||
score
|
||||
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);
|
||||
|
||||
@@ -16,7 +16,7 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
|
||||
super(bitcoinClient);
|
||||
|
||||
const electrumConfig = { client: 'mempool-v2', version: '1.4' };
|
||||
const electrumPersistencePolicy = { retryPeriod: 10000, maxRetry: 1000, callback: null };
|
||||
const electrumPersistencePolicy = { retryPeriod: 1000, maxRetry: Number.MAX_SAFE_INTEGER, callback: null };
|
||||
|
||||
const electrumCallbacks = {
|
||||
onConnect: (client, versionInfo) => { logger.info(`Connected to Electrum Server at ${config.ELECTRUM.HOST}:${config.ELECTRUM.PORT} (${JSON.stringify(versionInfo)})`); },
|
||||
|
||||
@@ -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
|
||||
@@ -510,6 +520,8 @@ class Blocks {
|
||||
}
|
||||
|
||||
public async $updateBlocks() {
|
||||
diskCache.lock();
|
||||
|
||||
let fastForwarded = false;
|
||||
const blockHeightTip = await bitcoinApi.$getBlockHeightTip();
|
||||
|
||||
@@ -548,7 +560,7 @@ class Blocks {
|
||||
}
|
||||
|
||||
while (this.currentBlockHeight < blockHeightTip) {
|
||||
if (this.currentBlockHeight < blockHeightTip - config.MEMPOOL.INITIAL_BLOCKS_AMOUNT) {
|
||||
if (this.currentBlockHeight === 0) {
|
||||
this.currentBlockHeight = blockHeightTip;
|
||||
} else {
|
||||
this.currentBlockHeight++;
|
||||
@@ -571,11 +583,10 @@ class Blocks {
|
||||
if (!fastForwarded) {
|
||||
const lastBlock = await blocksRepository.$getBlockByHeight(blockExtended.height - 1);
|
||||
if (lastBlock !== null && blockExtended.previousblockhash !== lastBlock.id) {
|
||||
logger.warn(`Chain divergence detected at block ${lastBlock.height}, re-indexing most recent data`);
|
||||
logger.warn(`Chain divergence detected at block ${lastBlock.height}, re-indexing most recent data`, logger.tags.mining);
|
||||
// We assume there won't be a reorg with more than 10 block depth
|
||||
await BlocksRepository.$deleteBlocksFrom(lastBlock.height - 10);
|
||||
await HashratesRepository.$deleteLastEntries();
|
||||
await BlocksSummariesRepository.$deleteBlocksFrom(lastBlock.height - 10);
|
||||
await cpfpRepository.$deleteClustersFrom(lastBlock.height - 10);
|
||||
for (let i = 10; i >= 0; --i) {
|
||||
const newBlock = await this.$indexBlock(lastBlock.height - i);
|
||||
@@ -586,7 +597,7 @@ class Blocks {
|
||||
}
|
||||
await mining.$indexDifficultyAdjustments();
|
||||
await DifficultyAdjustmentsRepository.$deleteLastAdjustment();
|
||||
logger.info(`Re-indexed 10 blocks and summaries. Also re-indexed the last difficulty adjustments. Will re-index latest hashrates in a few seconds.`);
|
||||
logger.info(`Re-indexed 10 blocks and summaries. Also re-indexed the last difficulty adjustments. Will re-index latest hashrates in a few seconds.`, logger.tags.mining);
|
||||
indexer.reindex();
|
||||
}
|
||||
await blocksRepository.$saveBlockInDatabase(blockExtended);
|
||||
@@ -641,13 +652,15 @@ 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();
|
||||
}
|
||||
|
||||
// wait for pending async callbacks to finish
|
||||
await Promise.all(callbackPromises);
|
||||
}
|
||||
|
||||
diskCache.unlock();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -718,7 +731,7 @@ class Blocks {
|
||||
|
||||
// Index the response if needed
|
||||
if (Common.blocksSummariesIndexingEnabled() === true) {
|
||||
await BlocksSummariesRepository.$saveSummary({height: block.height, mined: summary});
|
||||
await BlocksSummariesRepository.$saveTransactions(block.height, block.hash, summary.transactions);
|
||||
}
|
||||
|
||||
return summary.transactions;
|
||||
@@ -834,7 +847,7 @@ class Blocks {
|
||||
if (cleanBlock.fee_amt_percentiles === null) {
|
||||
const block = await bitcoinClient.getBlock(cleanBlock.hash, 2);
|
||||
const summary = this.summarizeBlock(block);
|
||||
await BlocksSummariesRepository.$saveSummary({ height: block.height, mined: summary });
|
||||
await BlocksSummariesRepository.$saveTransactions(cleanBlock.height, cleanBlock.hash, summary.transactions);
|
||||
cleanBlock.fee_amt_percentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(cleanBlock.hash);
|
||||
}
|
||||
if (cleanBlock.fee_amt_percentiles !== null) {
|
||||
|
||||
@@ -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[] = [];
|
||||
@@ -497,6 +497,7 @@ class DatabaseMigration {
|
||||
this.uniqueLog(logger.notice, this.blocksTruncatedMessage);
|
||||
await this.$executeQuery('DELETE FROM `pools`');
|
||||
await this.$executeQuery('ALTER TABLE pools AUTO_INCREMENT = 1');
|
||||
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
|
||||
this.uniqueLog(logger.notice, '`pools` table has been truncated`');
|
||||
await this.updateToSchemaVersion(56);
|
||||
}
|
||||
@@ -510,6 +511,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(
|
||||
@@ -22,31 +24,29 @@ export function calcDifficultyAdjustment(
|
||||
network: string,
|
||||
latestBlockTimestamp: number,
|
||||
): DifficultyAdjustment {
|
||||
const ESTIMATE_LAG_BLOCKS = 146; // For first 7.2% of epoch, don't estimate.
|
||||
const EPOCH_BLOCK_LENGTH = 2016; // Bitcoin mainnet
|
||||
const BLOCK_SECONDS_TARGET = 600; // Bitcoin mainnet
|
||||
const TESTNET_MAX_BLOCK_SECONDS = 1200; // Bitcoin testnet
|
||||
|
||||
const diffSeconds = nowSeconds - DATime;
|
||||
const diffSeconds = Math.max(0, nowSeconds - DATime);
|
||||
const blocksInEpoch = (blockHeight >= 0) ? blockHeight % EPOCH_BLOCK_LENGTH : 0;
|
||||
const progressPercent = (blockHeight >= 0) ? blocksInEpoch / EPOCH_BLOCK_LENGTH * 100 : 100;
|
||||
const remainingBlocks = EPOCH_BLOCK_LENGTH - blocksInEpoch;
|
||||
const nextRetargetHeight = (blockHeight >= 0) ? blockHeight + remainingBlocks : 0;
|
||||
const expectedBlocks = diffSeconds / BLOCK_SECONDS_TARGET;
|
||||
const actualTimespan = (blocksInEpoch === 2015 ? latestBlockTimestamp : nowSeconds) - DATime;
|
||||
|
||||
let difficultyChange = 0;
|
||||
let timeAvgSecs = BLOCK_SECONDS_TARGET;
|
||||
// Only calculate the estimate once we have 7.2% of blocks in current epoch
|
||||
if (blocksInEpoch >= ESTIMATE_LAG_BLOCKS) {
|
||||
timeAvgSecs = diffSeconds / blocksInEpoch;
|
||||
difficultyChange = (BLOCK_SECONDS_TARGET / timeAvgSecs - 1) * 100;
|
||||
// Max increase is x4 (+300%)
|
||||
if (difficultyChange > 300) {
|
||||
difficultyChange = 300;
|
||||
}
|
||||
// Max decrease is /4 (-75%)
|
||||
if (difficultyChange < -75) {
|
||||
difficultyChange = -75;
|
||||
}
|
||||
let timeAvgSecs = blocksInEpoch ? diffSeconds / blocksInEpoch : BLOCK_SECONDS_TARGET;
|
||||
|
||||
difficultyChange = (BLOCK_SECONDS_TARGET / (actualTimespan / (blocksInEpoch + 1)) - 1) * 100;
|
||||
// Max increase is x4 (+300%)
|
||||
if (difficultyChange > 300) {
|
||||
difficultyChange = 300;
|
||||
}
|
||||
// Max decrease is /4 (-75%)
|
||||
if (difficultyChange < -75) {
|
||||
difficultyChange = -75;
|
||||
}
|
||||
|
||||
// Testnet difficulty is set to 1 after 20 minutes of no blocks,
|
||||
@@ -74,9 +74,11 @@ export function calcDifficultyAdjustment(
|
||||
remainingBlocks,
|
||||
remainingTime,
|
||||
previousRetarget,
|
||||
previousTime: DATime,
|
||||
nextRetargetHeight,
|
||||
timeAvg,
|
||||
timeOffset,
|
||||
expectedBlocks,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -11,48 +11,96 @@ 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() { }
|
||||
private semaphore: { resume: (() => void)[], locks: number } = {
|
||||
resume: [],
|
||||
locks: 0,
|
||||
};
|
||||
|
||||
async $saveCacheToDisk(): Promise<void> {
|
||||
constructor() {
|
||||
if (!cluster.isPrimary) {
|
||||
return;
|
||||
}
|
||||
process.on('SIGINT', (e) => {
|
||||
this.$saveCacheToDisk(true);
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
const mempoolArray: TransactionExtended[] = [];
|
||||
for (const tx in mempool) {
|
||||
mempoolArray.push(mempool[tx]);
|
||||
if (mempool[tx] && !mempool[tx].deleteAfter) {
|
||||
mempoolArray.push(mempool[tx]);
|
||||
}
|
||||
}
|
||||
|
||||
Common.shuffleArray(mempoolArray);
|
||||
|
||||
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 this.$yield();
|
||||
await fsPromises.writeFile(DiskCache.TMP_FILE_NAME, JSON.stringify({
|
||||
network: config.MEMPOOL.NETWORK,
|
||||
cacheSchemaVersion: this.cacheSchemaVersion,
|
||||
blocks: blocks.getBlocks(),
|
||||
blockSummaries: blocks.getBlockSummaries(),
|
||||
mempool: {},
|
||||
mempoolArray: mempoolArray.splice(0, chunkSize),
|
||||
}), { flag: 'w' });
|
||||
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
|
||||
await this.$yield();
|
||||
await fsPromises.writeFile(DiskCache.TMP_FILE_NAMES.replace('{number}', i.toString()), JSON.stringify({
|
||||
mempool: {},
|
||||
mempoolArray: mempoolArray.splice(0, chunkSize),
|
||||
}), { flag: 'w' });
|
||||
}
|
||||
|
||||
await fsPromises.rename(DiskCache.TMP_FILE_NAME, DiskCache.FILE_NAME);
|
||||
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
|
||||
await fsPromises.rename(DiskCache.TMP_FILE_NAMES.replace('{number}', i.toString()), DiskCache.FILE_NAMES.replace('{number}', i.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug('Mempool and blocks data saved to disk cache');
|
||||
this.isWritingCache = false;
|
||||
} catch (e) {
|
||||
@@ -61,7 +109,7 @@ class DiskCache {
|
||||
}
|
||||
}
|
||||
|
||||
wipeCache() {
|
||||
wipeCache(): void {
|
||||
logger.notice(`Wiping nodejs backend cache/cache*.json files`);
|
||||
try {
|
||||
fs.unlinkSync(DiskCache.FILE_NAME);
|
||||
@@ -83,7 +131,7 @@ class DiskCache {
|
||||
}
|
||||
}
|
||||
|
||||
loadMempoolCache() {
|
||||
async $loadMempoolCache(): Promise<void> {
|
||||
if (!fs.existsSync(DiskCache.FILE_NAME)) {
|
||||
return;
|
||||
}
|
||||
@@ -97,6 +145,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) {
|
||||
@@ -123,13 +175,39 @@ class DiskCache {
|
||||
}
|
||||
}
|
||||
|
||||
memPool.setMempool(data.mempool);
|
||||
await memPool.$setMempool(data.mempool);
|
||||
blocks.setBlocks(data.blocks);
|
||||
blocks.setBlockSummaries(data.blockSummaries || []);
|
||||
} catch (e) {
|
||||
logger.warn('Failed to parse mempoool and blocks cache. Skipping. Reason: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
private $yield(): Promise<void> {
|
||||
if (this.semaphore.locks) {
|
||||
logger.debug('Pause writing mempool and blocks data to disk cache (async)');
|
||||
return new Promise((resolve) => {
|
||||
this.semaphore.resume.push(resolve);
|
||||
});
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
public lock(): void {
|
||||
this.semaphore.locks++;
|
||||
}
|
||||
|
||||
public unlock(): void {
|
||||
this.semaphore.locks = Math.max(0, this.semaphore.locks - 1);
|
||||
if (!this.semaphore.locks && this.semaphore.resume.length) {
|
||||
const nextResume = this.semaphore.resume.shift();
|
||||
if (nextResume) {
|
||||
logger.debug('Resume writing mempool and blocks data to disk cache (async)');
|
||||
nextResume();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new DiskCache();
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import logger from '../logger';
|
||||
import { MempoolBlock, TransactionExtended, ThreadTransaction, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor } from '../mempool.interfaces';
|
||||
import { MempoolBlock, TransactionExtended, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction } from '../mempool.interfaces';
|
||||
import { Common } from './common';
|
||||
import config from '../config';
|
||||
import { Worker } from 'worker_threads';
|
||||
@@ -10,6 +10,9 @@ class MempoolBlocks {
|
||||
private mempoolBlockDeltas: MempoolBlockDelta[] = [];
|
||||
private txSelectionWorker: Worker | null = null;
|
||||
|
||||
private nextUid: number = 1;
|
||||
private uidMap: Map<number, string> = new Map(); // map short numerical uids to full txids
|
||||
|
||||
constructor() {}
|
||||
|
||||
public getMempoolBlocks(): MempoolBlock[] {
|
||||
@@ -87,19 +90,34 @@ class MempoolBlocks {
|
||||
|
||||
private calculateMempoolBlocks(transactionsSorted: TransactionExtended[], prevBlocks: MempoolBlockWithTransactions[]): MempoolBlockWithTransactions[] {
|
||||
const mempoolBlocks: MempoolBlockWithTransactions[] = [];
|
||||
let blockWeight = 0;
|
||||
let blockSize = 0;
|
||||
let blockWeight = 0;
|
||||
let blockVsize = 0;
|
||||
let blockFees = 0;
|
||||
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
|
||||
let transactionIds: string[] = [];
|
||||
let transactions: TransactionExtended[] = [];
|
||||
transactionsSorted.forEach((tx) => {
|
||||
if (blockWeight + tx.weight <= config.MEMPOOL.BLOCK_WEIGHT_UNITS
|
||||
|| mempoolBlocks.length === config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT - 1) {
|
||||
blockWeight += tx.weight;
|
||||
blockVsize += tx.vsize;
|
||||
blockSize += tx.size;
|
||||
blockFees += tx.fee;
|
||||
transactions.push(tx);
|
||||
transactionIds.push(tx.txid);
|
||||
} else {
|
||||
mempoolBlocks.push(this.dataToMempoolBlocks(transactions, mempoolBlocks.length));
|
||||
blockVsize = 0;
|
||||
tx.position = {
|
||||
block: mempoolBlocks.length,
|
||||
vsize: blockVsize + (tx.vsize / 2),
|
||||
};
|
||||
blockVsize += tx.vsize;
|
||||
blockWeight = tx.weight;
|
||||
blockSize = tx.size;
|
||||
blockFees = tx.fee;
|
||||
transactionIds = [tx.txid];
|
||||
transactions = [tx];
|
||||
}
|
||||
});
|
||||
@@ -147,19 +165,29 @@ class MempoolBlocks {
|
||||
return mempoolBlockDeltas;
|
||||
}
|
||||
|
||||
public async makeBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, saveResults: boolean = false): Promise<MempoolBlockWithTransactions[]> {
|
||||
public async $makeBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, saveResults: boolean = false): Promise<MempoolBlockWithTransactions[]> {
|
||||
const start = Date.now();
|
||||
|
||||
// reset mempool short ids
|
||||
this.resetUids();
|
||||
for (const tx of Object.values(newMempool)) {
|
||||
this.setUid(tx);
|
||||
}
|
||||
|
||||
// prepare a stripped down version of the mempool with only the minimum necessary data
|
||||
// to reduce the overhead of passing this data to the worker thread
|
||||
const strippedMempool: { [txid: string]: ThreadTransaction } = {};
|
||||
const strippedMempool: Map<number, CompactThreadTransaction> = new Map();
|
||||
Object.values(newMempool).forEach(entry => {
|
||||
strippedMempool[entry.txid] = {
|
||||
txid: entry.txid,
|
||||
fee: entry.fee,
|
||||
weight: entry.weight,
|
||||
feePerVsize: entry.fee / (entry.weight / 4),
|
||||
effectiveFeePerVsize: entry.fee / (entry.weight / 4),
|
||||
vin: entry.vin.map(v => v.txid),
|
||||
};
|
||||
if (entry.uid != null) {
|
||||
strippedMempool.set(entry.uid, {
|
||||
uid: entry.uid,
|
||||
fee: entry.fee,
|
||||
weight: entry.weight,
|
||||
feePerVsize: entry.fee / (entry.weight / 4),
|
||||
effectiveFeePerVsize: entry.effectiveFeePerVsize || (entry.fee / (entry.weight / 4)),
|
||||
inputs: entry.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => uid != null) as number[],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// (re)initialize tx selection worker thread
|
||||
@@ -178,7 +206,7 @@ class MempoolBlocks {
|
||||
// run the block construction algorithm in a separate thread, and wait for a result
|
||||
let threadErrorListener;
|
||||
try {
|
||||
const workerResultPromise = new Promise<{ blocks: ThreadTransaction[][], clusters: { [root: string]: string[] } }>((resolve, reject) => {
|
||||
const workerResultPromise = new Promise<{ blocks: number[][], rates: Map<number, number>, clusters: Map<number, number[]> }>((resolve, reject) => {
|
||||
threadErrorListener = reject;
|
||||
this.txSelectionWorker?.once('message', (result): void => {
|
||||
resolve(result);
|
||||
@@ -186,103 +214,149 @@ class MempoolBlocks {
|
||||
this.txSelectionWorker?.once('error', reject);
|
||||
});
|
||||
this.txSelectionWorker.postMessage({ type: 'set', mempool: strippedMempool });
|
||||
const { blocks, clusters } = await workerResultPromise;
|
||||
const { blocks, rates, clusters } = this.convertResultTxids(await workerResultPromise);
|
||||
|
||||
// clean up thread error listener
|
||||
this.txSelectionWorker?.removeListener('error', threadErrorListener);
|
||||
|
||||
return this.processBlockTemplates(newMempool, blocks, clusters, saveResults);
|
||||
const processed = this.processBlockTemplates(newMempool, blocks, rates, clusters, saveResults);
|
||||
logger.debug(`makeBlockTemplates completed in ${(Date.now() - start)/1000} seconds`);
|
||||
return processed;
|
||||
} catch (e) {
|
||||
logger.err('makeBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
return this.mempoolBlocks;
|
||||
}
|
||||
|
||||
public async updateBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, added: TransactionExtended[], removed: string[], saveResults: boolean = false): Promise<void> {
|
||||
public async $updateBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, added: TransactionExtended[], removed: TransactionExtended[], saveResults: boolean = false): Promise<void> {
|
||||
if (!this.txSelectionWorker) {
|
||||
// need to reset the worker
|
||||
this.makeBlockTemplates(newMempool, saveResults);
|
||||
await this.$makeBlockTemplates(newMempool, saveResults);
|
||||
return;
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
for (const tx of Object.values(added)) {
|
||||
this.setUid(tx);
|
||||
}
|
||||
const removedUids = removed.map(tx => this.getUid(tx)).filter(uid => uid != null) as number[];
|
||||
// prepare a stripped down version of the mempool with only the minimum necessary data
|
||||
// to reduce the overhead of passing this data to the worker thread
|
||||
const addedStripped: ThreadTransaction[] = added.map(entry => {
|
||||
const addedStripped: CompactThreadTransaction[] = added.filter(entry => entry.uid != null).map(entry => {
|
||||
return {
|
||||
txid: entry.txid,
|
||||
uid: entry.uid || 0,
|
||||
fee: entry.fee,
|
||||
weight: entry.weight,
|
||||
feePerVsize: entry.fee / (entry.weight / 4),
|
||||
effectiveFeePerVsize: entry.fee / (entry.weight / 4),
|
||||
vin: entry.vin.map(v => v.txid),
|
||||
effectiveFeePerVsize: entry.effectiveFeePerVsize || (entry.fee / (entry.weight / 4)),
|
||||
inputs: entry.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => uid != null) as number[],
|
||||
};
|
||||
});
|
||||
|
||||
// run the block construction algorithm in a separate thread, and wait for a result
|
||||
let threadErrorListener;
|
||||
try {
|
||||
const workerResultPromise = new Promise<{ blocks: ThreadTransaction[][], clusters: { [root: string]: string[] } }>((resolve, reject) => {
|
||||
const workerResultPromise = new Promise<{ blocks: number[][], rates: Map<number, number>, clusters: Map<number, number[]> }>((resolve, reject) => {
|
||||
threadErrorListener = reject;
|
||||
this.txSelectionWorker?.once('message', (result): void => {
|
||||
resolve(result);
|
||||
});
|
||||
this.txSelectionWorker?.once('error', reject);
|
||||
});
|
||||
this.txSelectionWorker.postMessage({ type: 'update', added: addedStripped, removed });
|
||||
const { blocks, clusters } = await workerResultPromise;
|
||||
this.txSelectionWorker.postMessage({ type: 'update', added: addedStripped, removed: removedUids });
|
||||
const { blocks, rates, clusters } = this.convertResultTxids(await workerResultPromise);
|
||||
|
||||
this.removeUids(removedUids);
|
||||
|
||||
// clean up thread error listener
|
||||
this.txSelectionWorker?.removeListener('error', threadErrorListener);
|
||||
|
||||
this.processBlockTemplates(newMempool, blocks, clusters, saveResults);
|
||||
this.processBlockTemplates(newMempool, blocks, rates, clusters, saveResults);
|
||||
logger.debug(`updateBlockTemplates completed in ${(Date.now() - start) / 1000} seconds`);
|
||||
} catch (e) {
|
||||
logger.err('updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
private processBlockTemplates(mempool, blocks, clusters, saveResults): MempoolBlockWithTransactions[] {
|
||||
// update this thread's mempool with the results
|
||||
blocks.forEach(block => {
|
||||
block.forEach(tx => {
|
||||
if (tx.txid in mempool) {
|
||||
if (tx.effectiveFeePerVsize != null) {
|
||||
mempool[tx.txid].effectiveFeePerVsize = tx.effectiveFeePerVsize;
|
||||
}
|
||||
if (tx.cpfpRoot && tx.cpfpRoot in clusters) {
|
||||
const ancestors: Ancestor[] = [];
|
||||
const descendants: Ancestor[] = [];
|
||||
const cluster = clusters[tx.cpfpRoot];
|
||||
let matched = false;
|
||||
cluster.forEach(txid => {
|
||||
if (txid === tx.txid) {
|
||||
matched = true;
|
||||
} else {
|
||||
const relative = {
|
||||
txid: txid,
|
||||
fee: mempool[txid].fee,
|
||||
weight: mempool[txid].weight,
|
||||
};
|
||||
if (matched) {
|
||||
descendants.push(relative);
|
||||
} else {
|
||||
ancestors.push(relative);
|
||||
}
|
||||
}
|
||||
});
|
||||
mempool[tx.txid].ancestors = ancestors;
|
||||
mempool[tx.txid].descendants = descendants;
|
||||
mempool[tx.txid].bestDescendant = null;
|
||||
}
|
||||
mempool[tx.txid].cpfpChecked = tx.cpfpChecked;
|
||||
}
|
||||
});
|
||||
});
|
||||
private processBlockTemplates(mempool, blocks: string[][], rates: { [root: string]: number }, clusters: { [root: string]: string[] }, saveResults): MempoolBlockWithTransactions[] {
|
||||
for (const txid of Object.keys(rates)) {
|
||||
if (txid in mempool) {
|
||||
mempool[txid].effectiveFeePerVsize = rates[txid];
|
||||
}
|
||||
}
|
||||
|
||||
// unpack the condensed blocks into proper mempool blocks
|
||||
const mempoolBlocks = blocks.map((transactions, blockIndex) => {
|
||||
return this.dataToMempoolBlocks(transactions.map(tx => {
|
||||
return mempool[tx.txid] || null;
|
||||
}).filter(tx => !!tx), blockIndex);
|
||||
});
|
||||
const readyBlocks: { transactionIds, transactions, totalSize, totalWeight, totalFees }[] = [];
|
||||
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
|
||||
// update this thread's mempool with the results
|
||||
for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
|
||||
const block: string[] = blocks[blockIndex];
|
||||
let txid: string;
|
||||
let mempoolTx: TransactionExtended;
|
||||
let totalSize = 0;
|
||||
let totalVsize = 0;
|
||||
let totalWeight = 0;
|
||||
let totalFees = 0;
|
||||
const transactions: TransactionExtended[] = [];
|
||||
for (let txIndex = 0; txIndex < block.length; txIndex++) {
|
||||
txid = block[txIndex];
|
||||
if (txid) {
|
||||
mempoolTx = mempool[txid];
|
||||
// save position in projected blocks
|
||||
mempoolTx.position = {
|
||||
block: blockIndex,
|
||||
vsize: totalVsize + (mempoolTx.vsize / 2),
|
||||
};
|
||||
mempoolTx.cpfpChecked = true;
|
||||
|
||||
totalSize += mempoolTx.size;
|
||||
totalVsize += mempoolTx.vsize;
|
||||
totalWeight += mempoolTx.weight;
|
||||
totalFees += mempoolTx.fee;
|
||||
|
||||
transactions.push(mempoolTx);
|
||||
}
|
||||
}
|
||||
readyBlocks.push({
|
||||
transactionIds: block,
|
||||
transactions,
|
||||
totalSize,
|
||||
totalWeight,
|
||||
totalFees
|
||||
});
|
||||
}
|
||||
|
||||
for (const cluster of Object.values(clusters)) {
|
||||
for (const memberTxid of cluster) {
|
||||
if (memberTxid in mempool) {
|
||||
const mempoolTx = mempool[memberTxid];
|
||||
const ancestors: Ancestor[] = [];
|
||||
const descendants: Ancestor[] = [];
|
||||
let matched = false;
|
||||
cluster.forEach(txid => {
|
||||
if (txid === memberTxid) {
|
||||
matched = true;
|
||||
} else {
|
||||
const relative = {
|
||||
txid: txid,
|
||||
fee: mempool[txid].fee,
|
||||
weight: mempool[txid].weight,
|
||||
};
|
||||
if (matched) {
|
||||
descendants.push(relative);
|
||||
} else {
|
||||
ancestors.push(relative);
|
||||
}
|
||||
}
|
||||
});
|
||||
mempoolTx.ancestors = ancestors;
|
||||
mempoolTx.descendants = descendants;
|
||||
mempoolTx.bestDescendant = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mempoolBlocks = readyBlocks.map((b, i) => this.dataToMempoolBlocks(b.transactions, i));
|
||||
|
||||
if (saveResults) {
|
||||
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks);
|
||||
@@ -324,6 +398,56 @@ class MempoolBlocks {
|
||||
transactions: fitTransactions.map((tx) => Common.stripTransaction(tx)),
|
||||
};
|
||||
}
|
||||
|
||||
private resetUids(): void {
|
||||
this.uidMap.clear();
|
||||
this.nextUid = 1;
|
||||
}
|
||||
|
||||
private setUid(tx: TransactionExtended): number {
|
||||
const uid = this.nextUid;
|
||||
this.nextUid++;
|
||||
this.uidMap.set(uid, tx.txid);
|
||||
tx.uid = uid;
|
||||
return uid;
|
||||
}
|
||||
|
||||
private getUid(tx: TransactionExtended): number | void {
|
||||
if (tx?.uid != null && this.uidMap.has(tx.uid)) {
|
||||
return tx.uid;
|
||||
}
|
||||
}
|
||||
|
||||
private removeUids(uids: number[]): void {
|
||||
for (const uid of uids) {
|
||||
this.uidMap.delete(uid);
|
||||
}
|
||||
}
|
||||
|
||||
private convertResultTxids({ blocks, rates, clusters }: { blocks: number[][], rates: Map<number, number>, clusters: Map<number, number[]>})
|
||||
: { blocks: string[][], rates: { [root: string]: number }, clusters: { [root: string]: string[] }} {
|
||||
const convertedBlocks: string[][] = blocks.map(block => block.map(uid => {
|
||||
return this.uidMap.get(uid) || '';
|
||||
}));
|
||||
const convertedRates = {};
|
||||
for (const rateUid of rates.keys()) {
|
||||
const rateTxid = this.uidMap.get(rateUid);
|
||||
if (rateTxid) {
|
||||
convertedRates[rateTxid] = rates.get(rateUid);
|
||||
}
|
||||
}
|
||||
const convertedClusters = {};
|
||||
for (const rootUid of clusters.keys()) {
|
||||
const rootTxid = this.uidMap.get(rootUid);
|
||||
if (rootTxid) {
|
||||
const members = clusters.get(rootUid)?.map(uid => {
|
||||
return this.uidMap.get(uid);
|
||||
});
|
||||
convertedClusters[rootTxid] = members;
|
||||
}
|
||||
}
|
||||
return { blocks: convertedBlocks, rates: convertedRates, clusters: convertedClusters } as { blocks: string[][], rates: { [root: string]: number }, clusters: { [root: string]: string[] }};
|
||||
}
|
||||
}
|
||||
|
||||
export default new MempoolBlocks();
|
||||
|
||||
@@ -20,7 +20,7 @@ class Mempool {
|
||||
maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 };
|
||||
private mempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
|
||||
deletedTransactions: TransactionExtended[]) => void) | undefined;
|
||||
private asyncMempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
|
||||
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
|
||||
deletedTransactions: TransactionExtended[]) => Promise<void>) | undefined;
|
||||
|
||||
private txPerSecondArray: number[] = [];
|
||||
@@ -31,9 +31,13 @@ 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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,20 +71,20 @@ class Mempool {
|
||||
|
||||
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: TransactionExtended; },
|
||||
newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]) => Promise<void>) {
|
||||
this.asyncMempoolChangedCallback = fn;
|
||||
this.$asyncMempoolChangedCallback = fn;
|
||||
}
|
||||
|
||||
public getMempool(): { [txid: string]: TransactionExtended } {
|
||||
return this.mempoolCache;
|
||||
}
|
||||
|
||||
public setMempool(mempoolData: { [txId: string]: TransactionExtended }) {
|
||||
public async $setMempool(mempoolData: { [txId: string]: TransactionExtended }) {
|
||||
this.mempoolCache = mempoolData;
|
||||
if (this.mempoolChangedCallback) {
|
||||
this.mempoolChangedCallback(this.mempoolCache, [], []);
|
||||
}
|
||||
if (this.asyncMempoolChangedCallback) {
|
||||
this.asyncMempoolChangedCallback(this.mempoolCache, [], []);
|
||||
if (this.$asyncMempoolChangedCallback) {
|
||||
await this.$asyncMempoolChangedCallback(this.mempoolCache, [], []);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +132,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 +156,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 +169,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
|
||||
@@ -197,8 +222,8 @@ class Mempool {
|
||||
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
||||
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
|
||||
}
|
||||
if (this.asyncMempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
||||
await this.asyncMempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
|
||||
if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
||||
await this.$asyncMempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
|
||||
}
|
||||
|
||||
const end = new Date().getTime();
|
||||
@@ -230,7 +255,7 @@ class Mempool {
|
||||
}
|
||||
}
|
||||
|
||||
private deleteExpiredTransactions() {
|
||||
public deleteExpiredTransactions() {
|
||||
const now = new Date().getTime();
|
||||
for (const tx in this.mempoolCache) {
|
||||
const lazyDeleteAt = this.mempoolCache[tx].deleteAfter;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import config from '../config';
|
||||
import logger from '../logger';
|
||||
import { ThreadTransaction, MempoolBlockWithTransactions, AuditTransaction } from '../mempool.interfaces';
|
||||
import { CompactThreadTransaction, AuditTransaction } from '../mempool.interfaces';
|
||||
import { PairingHeap } from '../utils/pairing-heap';
|
||||
import { Common } from './common';
|
||||
import { parentPort } from 'worker_threads';
|
||||
|
||||
let mempool: { [txid: string]: ThreadTransaction } = {};
|
||||
let mempool: Map<number, CompactThreadTransaction> = new Map();
|
||||
|
||||
if (parentPort) {
|
||||
parentPort.on('message', (params) => {
|
||||
@@ -13,18 +13,18 @@ if (parentPort) {
|
||||
mempool = params.mempool;
|
||||
} else if (params.type === 'update') {
|
||||
params.added.forEach(tx => {
|
||||
mempool[tx.txid] = tx;
|
||||
mempool.set(tx.uid, tx);
|
||||
});
|
||||
params.removed.forEach(txid => {
|
||||
delete mempool[txid];
|
||||
params.removed.forEach(uid => {
|
||||
mempool.delete(uid);
|
||||
});
|
||||
}
|
||||
|
||||
const { blocks, clusters } = makeBlockTemplates(mempool);
|
||||
const { blocks, rates, clusters } = makeBlockTemplates(mempool);
|
||||
|
||||
// return the result to main thread.
|
||||
if (parentPort) {
|
||||
parentPort.postMessage({ blocks, clusters });
|
||||
parentPort.postMessage({ blocks, rates, clusters });
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -33,26 +33,25 @@ if (parentPort) {
|
||||
* Build projected mempool blocks using an approximation of the transaction selection algorithm from Bitcoin Core
|
||||
* (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp)
|
||||
*/
|
||||
function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction })
|
||||
: { blocks: ThreadTransaction[][], clusters: { [root: string]: string[] } } {
|
||||
function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
|
||||
: { blocks: number[][], rates: Map<number, number>, clusters: Map<number, number[]> } {
|
||||
const start = Date.now();
|
||||
const auditPool: { [txid: string]: AuditTransaction } = {};
|
||||
const auditPool: Map<number, AuditTransaction> = new Map();
|
||||
const mempoolArray: AuditTransaction[] = [];
|
||||
const restOfArray: ThreadTransaction[] = [];
|
||||
const cpfpClusters: { [root: string]: string[] } = {};
|
||||
const cpfpClusters: Map<number, number[]> = new Map();
|
||||
|
||||
// grab the top feerate txs up to maxWeight
|
||||
Object.values(mempool).sort((a, b) => b.feePerVsize - a.feePerVsize).forEach(tx => {
|
||||
mempool.forEach(tx => {
|
||||
tx.dirty = false;
|
||||
// initializing everything up front helps V8 optimize property access later
|
||||
auditPool[tx.txid] = {
|
||||
txid: tx.txid,
|
||||
auditPool.set(tx.uid, {
|
||||
uid: tx.uid,
|
||||
fee: tx.fee,
|
||||
weight: tx.weight,
|
||||
feePerVsize: tx.feePerVsize,
|
||||
effectiveFeePerVsize: tx.feePerVsize,
|
||||
vin: tx.vin,
|
||||
inputs: tx.inputs || [],
|
||||
relativesSet: false,
|
||||
ancestorMap: new Map<string, AuditTransaction>(),
|
||||
ancestorMap: new Map<number, AuditTransaction>(),
|
||||
children: new Set<AuditTransaction>(),
|
||||
ancestorFee: 0,
|
||||
ancestorWeight: 0,
|
||||
@@ -60,8 +59,8 @@ function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction })
|
||||
used: false,
|
||||
modified: false,
|
||||
modifiedNode: null,
|
||||
};
|
||||
mempoolArray.push(auditPool[tx.txid]);
|
||||
});
|
||||
mempoolArray.push(auditPool.get(tx.uid) as AuditTransaction);
|
||||
});
|
||||
|
||||
// Build relatives graph & calculate ancestor scores
|
||||
@@ -72,15 +71,28 @@ function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction })
|
||||
}
|
||||
|
||||
// Sort by descending ancestor score
|
||||
mempoolArray.sort((a, b) => (b.score || 0) - (a.score || 0));
|
||||
mempoolArray.sort((a, b) => {
|
||||
if (b.score === a.score) {
|
||||
// tie-break by uid for stability
|
||||
return a.uid < b.uid ? -1 : 1;
|
||||
} else {
|
||||
return (b.score || 0) - (a.score || 0);
|
||||
}
|
||||
});
|
||||
|
||||
// Build blocks by greedily choosing the highest feerate package
|
||||
// (i.e. the package rooted in the transaction with the best ancestor score)
|
||||
const blocks: ThreadTransaction[][] = [];
|
||||
const blocks: number[][] = [];
|
||||
let blockWeight = 4000;
|
||||
let blockSize = 0;
|
||||
let transactions: AuditTransaction[] = [];
|
||||
const modified: PairingHeap<AuditTransaction> = new PairingHeap((a, b): boolean => (a.score || 0) > (b.score || 0));
|
||||
const modified: PairingHeap<AuditTransaction> = new PairingHeap((a, b): boolean => {
|
||||
if (a.score === b.score) {
|
||||
// tie-break by uid for stability
|
||||
return a.uid > b.uid;
|
||||
} else {
|
||||
return (a.score || 0) > (b.score || 0);
|
||||
}
|
||||
});
|
||||
let overflow: AuditTransaction[] = [];
|
||||
let failures = 0;
|
||||
let top = 0;
|
||||
@@ -107,30 +119,36 @@ function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction })
|
||||
|
||||
if (nextTx && !nextTx?.used) {
|
||||
// Check if the package fits into this block
|
||||
if (blockWeight + nextTx.ancestorWeight < config.MEMPOOL.BLOCK_WEIGHT_UNITS) {
|
||||
if (blocks.length >= 7 || (blockWeight + nextTx.ancestorWeight < config.MEMPOOL.BLOCK_WEIGHT_UNITS)) {
|
||||
const ancestors: AuditTransaction[] = Array.from(nextTx.ancestorMap.values());
|
||||
// sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
|
||||
const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx];
|
||||
let isCluster = false;
|
||||
if (sortedTxSet.length > 1) {
|
||||
cpfpClusters[nextTx.txid] = sortedTxSet.map(tx => tx.txid);
|
||||
cpfpClusters.set(nextTx.uid, sortedTxSet.map(tx => tx.uid));
|
||||
isCluster = true;
|
||||
}
|
||||
const effectiveFeeRate = nextTx.ancestorFee / (nextTx.ancestorWeight / 4);
|
||||
const used: AuditTransaction[] = [];
|
||||
while (sortedTxSet.length) {
|
||||
const ancestor = sortedTxSet.pop();
|
||||
const mempoolTx = mempool[ancestor.txid];
|
||||
const mempoolTx = mempool.get(ancestor.uid);
|
||||
if (!mempoolTx) {
|
||||
continue;
|
||||
}
|
||||
ancestor.used = true;
|
||||
ancestor.usedBy = nextTx.txid;
|
||||
ancestor.usedBy = nextTx.uid;
|
||||
// update original copy of this tx with effective fee rate & relatives data
|
||||
mempoolTx.effectiveFeePerVsize = effectiveFeeRate;
|
||||
if (isCluster) {
|
||||
mempoolTx.cpfpRoot = nextTx.txid;
|
||||
if (mempoolTx.effectiveFeePerVsize !== effectiveFeeRate) {
|
||||
mempoolTx.effectiveFeePerVsize = effectiveFeeRate;
|
||||
mempoolTx.dirty = true;
|
||||
}
|
||||
if (mempoolTx.cpfpRoot !== nextTx.uid) {
|
||||
mempoolTx.cpfpRoot = isCluster ? nextTx.uid : null;
|
||||
mempoolTx.dirty;
|
||||
}
|
||||
mempoolTx.cpfpChecked = true;
|
||||
transactions.push(ancestor);
|
||||
blockSize += ancestor.size;
|
||||
blockWeight += ancestor.weight;
|
||||
used.push(ancestor);
|
||||
}
|
||||
@@ -156,11 +174,10 @@ function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction })
|
||||
if ((exceededPackageTries || queueEmpty) && blocks.length < 7) {
|
||||
// construct this block
|
||||
if (transactions.length) {
|
||||
blocks.push(transactions.map(t => mempool[t.txid]));
|
||||
blocks.push(transactions.map(t => t.uid));
|
||||
}
|
||||
// reset for the next block
|
||||
transactions = [];
|
||||
blockSize = 0;
|
||||
blockWeight = 4000;
|
||||
|
||||
// 'overflow' packages didn't fit in this block, but are valid candidates for the next
|
||||
@@ -175,50 +192,38 @@ function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction })
|
||||
overflow = [];
|
||||
}
|
||||
}
|
||||
// pack any leftover transactions into the last block
|
||||
for (const tx of overflow) {
|
||||
if (!tx || tx?.used) {
|
||||
continue;
|
||||
}
|
||||
blockWeight += tx.weight;
|
||||
const mempoolTx = mempool[tx.txid];
|
||||
// update original copy of this tx with effective fee rate & relatives data
|
||||
mempoolTx.effectiveFeePerVsize = tx.score;
|
||||
if (tx.ancestorMap.size > 0) {
|
||||
cpfpClusters[tx.txid] = Array.from(tx.ancestorMap?.values()).map(a => a.txid);
|
||||
mempoolTx.cpfpRoot = tx.txid;
|
||||
}
|
||||
mempoolTx.cpfpChecked = true;
|
||||
transactions.push(tx);
|
||||
tx.used = true;
|
||||
|
||||
if (overflow.length > 0) {
|
||||
logger.warn('GBT overflow list unexpectedly non-empty after final block constructed');
|
||||
}
|
||||
const blockTransactions = transactions.map(t => mempool[t.txid]);
|
||||
restOfArray.forEach(tx => {
|
||||
blockWeight += tx.weight;
|
||||
tx.effectiveFeePerVsize = tx.feePerVsize;
|
||||
tx.cpfpChecked = false;
|
||||
blockTransactions.push(tx);
|
||||
});
|
||||
if (blockTransactions.length) {
|
||||
blocks.push(blockTransactions);
|
||||
// add the final unbounded block if it contains any transactions
|
||||
if (transactions.length > 0) {
|
||||
blocks.push(transactions.map(t => t.uid));
|
||||
}
|
||||
|
||||
// get map of dirty transactions
|
||||
const rates = new Map<number, number>();
|
||||
for (const tx of mempool.values()) {
|
||||
if (tx?.dirty) {
|
||||
rates.set(tx.uid, tx.effectiveFeePerVsize || tx.feePerVsize);
|
||||
}
|
||||
}
|
||||
transactions = [];
|
||||
|
||||
const end = Date.now();
|
||||
const time = end - start;
|
||||
logger.debug('Mempool templates calculated in ' + time / 1000 + ' seconds');
|
||||
|
||||
return { blocks, clusters: cpfpClusters };
|
||||
return { blocks, rates, clusters: cpfpClusters };
|
||||
}
|
||||
|
||||
// traverse in-mempool ancestors
|
||||
// recursion unavoidable, but should be limited to depth < 25 by mempool policy
|
||||
function setRelatives(
|
||||
tx: AuditTransaction,
|
||||
mempool: { [txid: string]: AuditTransaction },
|
||||
mempool: Map<number, AuditTransaction>,
|
||||
): void {
|
||||
for (const parent of tx.vin) {
|
||||
const parentTx = mempool[parent];
|
||||
for (const parent of tx.inputs) {
|
||||
const parentTx = mempool.get(parent);
|
||||
if (parentTx && !tx.ancestorMap?.has(parent)) {
|
||||
tx.ancestorMap.set(parent, parentTx);
|
||||
parentTx.children.add(tx);
|
||||
@@ -227,7 +232,7 @@ function setRelatives(
|
||||
setRelatives(parentTx, mempool);
|
||||
}
|
||||
parentTx.ancestorMap.forEach((ancestor) => {
|
||||
tx.ancestorMap.set(ancestor.txid, ancestor);
|
||||
tx.ancestorMap.set(ancestor.uid, ancestor);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -245,7 +250,7 @@ function setRelatives(
|
||||
// avoids recursion to limit call stack depth
|
||||
function updateDescendants(
|
||||
rootTx: AuditTransaction,
|
||||
mempool: { [txid: string]: AuditTransaction },
|
||||
mempool: Map<number, AuditTransaction>,
|
||||
modified: PairingHeap<AuditTransaction>,
|
||||
): void {
|
||||
const descendantSet: Set<AuditTransaction> = new Set();
|
||||
@@ -261,9 +266,9 @@ function updateDescendants(
|
||||
});
|
||||
while (descendants.length) {
|
||||
descendantTx = descendants.pop();
|
||||
if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.txid)) {
|
||||
if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.uid)) {
|
||||
// remove tx as ancestor
|
||||
descendantTx.ancestorMap.delete(rootTx.txid);
|
||||
descendantTx.ancestorMap.delete(rootTx.uid);
|
||||
descendantTx.ancestorFee -= rootTx.fee;
|
||||
descendantTx.ancestorWeight -= rootTx.weight;
|
||||
tmpScore = descendantTx.score;
|
||||
|
||||
@@ -211,6 +211,7 @@ class WebsocketHandler {
|
||||
if (!_blocks) {
|
||||
_blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT);
|
||||
}
|
||||
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||
return {
|
||||
'mempoolInfo': memPool.getMempoolInfo(),
|
||||
'vBytesPerSecond': memPool.getVBytesPerSecond(),
|
||||
@@ -220,7 +221,7 @@ class WebsocketHandler {
|
||||
'transactions': memPool.getLatestTransactions(),
|
||||
'backendInfo': backendInfo.getBackendInfo(),
|
||||
'loadingIndicators': loadingIndicators.getLoadingIndicators(),
|
||||
'da': difficultyAdjustment.getDifficultyAdjustment(),
|
||||
'da': da?.previousTime ? da : undefined,
|
||||
'fees': feeApi.getRecommendedFee(),
|
||||
...this.extraInitProperties
|
||||
};
|
||||
@@ -246,14 +247,14 @@ class WebsocketHandler {
|
||||
});
|
||||
}
|
||||
|
||||
async handleMempoolChange(newMempool: { [txid: string]: TransactionExtended },
|
||||
async $handleMempoolChange(newMempool: { [txid: string]: TransactionExtended },
|
||||
newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]): Promise<void> {
|
||||
if (!this.wss) {
|
||||
throw new Error('WebSocket.Server is not set');
|
||||
}
|
||||
|
||||
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
|
||||
await mempoolBlocks.updateBlockTemplates(newMempool, newTransactions, deletedTransactions.map(tx => tx.txid), true);
|
||||
await mempoolBlocks.$updateBlockTemplates(newMempool, newTransactions, deletedTransactions, true);
|
||||
} else {
|
||||
mempoolBlocks.updateMempoolBlocks(newMempool, true);
|
||||
}
|
||||
@@ -278,7 +279,9 @@ class WebsocketHandler {
|
||||
response['mempoolInfo'] = mempoolInfo;
|
||||
response['vBytesPerSecond'] = vBytesPerSecond;
|
||||
response['transactions'] = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx));
|
||||
response['da'] = da;
|
||||
if (da?.previousTime) {
|
||||
response['da'] = da;
|
||||
}
|
||||
response['fees'] = recommendedFees;
|
||||
}
|
||||
|
||||
@@ -422,17 +425,23 @@ class WebsocketHandler {
|
||||
|
||||
if (config.MEMPOOL.AUDIT) {
|
||||
let projectedBlocks;
|
||||
let auditMempool = _memPool;
|
||||
// template calculation functions have mempool side effects, so calculate audits using
|
||||
// a cloned copy of the mempool if we're running a different algorithm for mempool updates
|
||||
const auditMempool = (config.MEMPOOL.ADVANCED_GBT_AUDIT === config.MEMPOOL.ADVANCED_GBT_MEMPOOL) ? _memPool : deepClone(_memPool);
|
||||
if (config.MEMPOOL.ADVANCED_GBT_AUDIT) {
|
||||
projectedBlocks = await mempoolBlocks.makeBlockTemplates(auditMempool, false);
|
||||
const separateAudit = config.MEMPOOL.ADVANCED_GBT_AUDIT !== config.MEMPOOL.ADVANCED_GBT_MEMPOOL;
|
||||
if (separateAudit) {
|
||||
auditMempool = deepClone(_memPool);
|
||||
if (config.MEMPOOL.ADVANCED_GBT_AUDIT) {
|
||||
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false);
|
||||
} else {
|
||||
projectedBlocks = mempoolBlocks.updateMempoolBlocks(auditMempool, false);
|
||||
}
|
||||
} else {
|
||||
projectedBlocks = mempoolBlocks.updateMempoolBlocks(auditMempool, false);
|
||||
projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
|
||||
}
|
||||
|
||||
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,20 +473,24 @@ class WebsocketHandler {
|
||||
|
||||
if (block.extras) {
|
||||
block.extras.matchRate = matchRate;
|
||||
block.extras.similarity = similarity;
|
||||
}
|
||||
}
|
||||
} else if (block.extras) {
|
||||
const mBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
|
||||
if (mBlocks?.length && mBlocks[0].transactions) {
|
||||
block.extras.similarity = Common.getSimilarity(mBlocks[0], transactions);
|
||||
}
|
||||
}
|
||||
|
||||
const removed: string[] = [];
|
||||
// Update mempool to remove transactions included in the new block
|
||||
for (const txId of txIds) {
|
||||
delete _memPool[txId];
|
||||
removed.push(txId);
|
||||
rbfCache.evict(txId);
|
||||
}
|
||||
|
||||
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
|
||||
await mempoolBlocks.updateBlockTemplates(_memPool, [], removed, true);
|
||||
await mempoolBlocks.$makeBlockTemplates(_memPool, true);
|
||||
} else {
|
||||
mempoolBlocks.updateMempoolBlocks(_memPool, true);
|
||||
}
|
||||
@@ -499,7 +512,7 @@ class WebsocketHandler {
|
||||
const response = {
|
||||
'block': block,
|
||||
'mempoolInfo': memPool.getMempoolInfo(),
|
||||
'da': da,
|
||||
'da': da?.previousTime ? da : undefined,
|
||||
'fees': fees,
|
||||
};
|
||||
|
||||
|
||||
@@ -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': '',
|
||||
|
||||
@@ -120,7 +120,7 @@ class Server {
|
||||
await poolsUpdater.updatePoolsJson(); // Needs to be done before loading the disk cache because we sometimes wipe it
|
||||
await syncAssets.syncAssets$();
|
||||
if (config.MEMPOOL.ENABLED) {
|
||||
diskCache.loadMempoolCache();
|
||||
await diskCache.$loadMempoolCache();
|
||||
}
|
||||
|
||||
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && cluster.isPrimary) {
|
||||
@@ -179,6 +179,7 @@ class Server {
|
||||
}
|
||||
}
|
||||
await blocks.$updateBlocks();
|
||||
memPool.deleteExpiredTransactions();
|
||||
await memPool.$updateMempool();
|
||||
indexer.$run();
|
||||
|
||||
@@ -205,6 +206,8 @@ class Server {
|
||||
setTimeout(this.runMainUpdateLoop.bind(this), 1000 * this.currentBackendRetryInterval);
|
||||
this.currentBackendRetryInterval *= 2;
|
||||
this.currentBackendRetryInterval = Math.min(this.currentBackendRetryInterval, 60);
|
||||
} finally {
|
||||
diskCache.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,11 +218,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) {
|
||||
@@ -237,7 +240,7 @@ class Server {
|
||||
websocketHandler.setupConnectionHandling();
|
||||
if (config.MEMPOOL.ENABLED) {
|
||||
statistics.setNewStatisticsEntryCallback(websocketHandler.handleNewStatistic.bind(websocketHandler));
|
||||
memPool.setAsyncMempoolChangedCallback(websocketHandler.handleMempoolChange.bind(websocketHandler));
|
||||
memPool.setAsyncMempoolChangedCallback(websocketHandler.$handleMempoolChange.bind(websocketHandler));
|
||||
blocks.setNewAsyncBlockCallback(websocketHandler.handleNewBlock.bind(websocketHandler));
|
||||
}
|
||||
priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -80,17 +80,22 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
|
||||
bestDescendant?: BestDescendant | null;
|
||||
cpfpChecked?: boolean;
|
||||
deleteAfter?: number;
|
||||
position?: {
|
||||
block: number,
|
||||
vsize: number,
|
||||
};
|
||||
uid?: number;
|
||||
}
|
||||
|
||||
export interface AuditTransaction {
|
||||
txid: string;
|
||||
uid: number;
|
||||
fee: number;
|
||||
weight: number;
|
||||
feePerVsize: number;
|
||||
effectiveFeePerVsize: number;
|
||||
vin: string[];
|
||||
inputs: number[];
|
||||
relativesSet: boolean;
|
||||
ancestorMap: Map<string, AuditTransaction>;
|
||||
ancestorMap: Map<number, AuditTransaction>;
|
||||
children: Set<AuditTransaction>;
|
||||
ancestorFee: number;
|
||||
ancestorWeight: number;
|
||||
@@ -100,13 +105,25 @@ export interface AuditTransaction {
|
||||
modifiedNode: HeapNode<AuditTransaction>;
|
||||
}
|
||||
|
||||
export interface CompactThreadTransaction {
|
||||
uid: number;
|
||||
fee: number;
|
||||
weight: number;
|
||||
feePerVsize: number;
|
||||
effectiveFeePerVsize?: number;
|
||||
inputs: number[];
|
||||
cpfpRoot?: string;
|
||||
cpfpChecked?: boolean;
|
||||
dirty?: boolean;
|
||||
}
|
||||
|
||||
export interface ThreadTransaction {
|
||||
txid: string;
|
||||
fee: number;
|
||||
weight: number;
|
||||
feePerVsize: number;
|
||||
effectiveFeePerVsize?: number;
|
||||
vin: string[];
|
||||
inputs: number[];
|
||||
cpfpRoot?: string;
|
||||
cpfpChecked?: boolean;
|
||||
}
|
||||
@@ -153,6 +170,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;
|
||||
@@ -308,9 +326,11 @@ export interface IDifficultyAdjustment {
|
||||
remainingBlocks: number;
|
||||
remainingTime: number;
|
||||
previousRetarget: number;
|
||||
previousTime: number;
|
||||
nextRetargetHeight: number;
|
||||
timeAvg: number;
|
||||
timeOffset: number;
|
||||
expectedBlocks: number;
|
||||
}
|
||||
|
||||
export interface IndexedDifficultyAdjustment {
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
@@ -417,30 +466,6 @@ class BlocksRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get one block by hash
|
||||
*/
|
||||
public async $getBlockByHash(hash: string): Promise<object | null> {
|
||||
try {
|
||||
const query = `
|
||||
SELECT ${BLOCK_DB_FIELDS}
|
||||
FROM blocks
|
||||
JOIN pools ON blocks.pool_id = pools.id
|
||||
WHERE hash = ?;
|
||||
`;
|
||||
const [rows]: any[] = await DB.query(query, [hash]);
|
||||
|
||||
if (rows.length <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await this.formatDbBlockIntoExtendedBlock(rows[0]);
|
||||
} catch (e) {
|
||||
logger.err(`Cannot get indexed block ${hash}. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return blocks difficulty
|
||||
*/
|
||||
@@ -550,7 +575,6 @@ class BlocksRepository {
|
||||
if (blocks[idx].previous_block_hash !== blocks[idx - 1].hash) {
|
||||
logger.warn(`Chain divergence detected at block ${blocks[idx - 1].height}`);
|
||||
await this.$deleteBlocksFrom(blocks[idx - 1].height);
|
||||
await BlocksSummariesRepository.$deleteBlocksFrom(blocks[idx - 1].height);
|
||||
await HashratesRepository.$deleteHashratesFromTimestamp(blocks[idx - 1].timestamp - 604800);
|
||||
await DifficultyAdjustmentsRepository.$deleteAdjustementsFromHeight(blocks[idx - 1].height);
|
||||
return false;
|
||||
@@ -570,7 +594,7 @@ class BlocksRepository {
|
||||
* Delete blocks from the database from blockHeight
|
||||
*/
|
||||
public async $deleteBlocksFrom(blockHeight: number) {
|
||||
logger.info(`Delete newer blocks from height ${blockHeight} from the database`);
|
||||
logger.info(`Delete newer blocks from height ${blockHeight} from the database`, logger.tags.mining);
|
||||
|
||||
try {
|
||||
await DB.query(`DELETE FROM blocks where height >= ${blockHeight}`);
|
||||
@@ -748,6 +772,7 @@ class BlocksRepository {
|
||||
SELECT height
|
||||
FROM compact_cpfp_clusters
|
||||
WHERE height <= ? AND height >= ?
|
||||
GROUP BY height
|
||||
ORDER BY height DESC;
|
||||
`, [currentBlockHeight, minHeight]);
|
||||
|
||||
@@ -928,6 +953,7 @@ class BlocksRepository {
|
||||
}
|
||||
|
||||
// If we're missing block summary related field, check if we can populate them on the fly now
|
||||
// This is for example triggered upon re-org
|
||||
if (Common.blocksSummariesIndexingEnabled() &&
|
||||
(extras.medianFeeAmt === null || extras.feePercentiles === null))
|
||||
{
|
||||
@@ -935,7 +961,7 @@ class BlocksRepository {
|
||||
if (extras.feePercentiles === null) {
|
||||
const block = await bitcoinClient.getBlock(dbBlk.id, 2);
|
||||
const summary = blocks.summarizeBlock(block);
|
||||
await BlocksSummariesRepository.$saveSummary({ height: block.height, mined: summary });
|
||||
await BlocksSummariesRepository.$saveTransactions(dbBlk.height, dbBlk.hash, summary.transactions);
|
||||
extras.feePercentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(dbBlk.id);
|
||||
}
|
||||
if (extras.feePercentiles !== null) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import DB from '../database';
|
||||
import logger from '../logger';
|
||||
import { BlockSummary } from '../mempool.interfaces';
|
||||
import { BlockSummary, TransactionStripped } from '../mempool.interfaces';
|
||||
|
||||
class BlocksSummariesRepository {
|
||||
public async $getByBlockId(id: string): Promise<BlockSummary | undefined> {
|
||||
@@ -17,7 +17,7 @@ class BlocksSummariesRepository {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public async $saveSummary(params: { height: number, mined?: BlockSummary}) {
|
||||
public async $saveSummary(params: { height: number, mined?: BlockSummary}): Promise<void> {
|
||||
const blockId = params.mined?.id;
|
||||
try {
|
||||
const transactions = JSON.stringify(params.mined?.transactions || []);
|
||||
@@ -37,6 +37,20 @@ class BlocksSummariesRepository {
|
||||
}
|
||||
}
|
||||
|
||||
public async $saveTransactions(blockHeight: number, blockId: string, transactions: TransactionStripped[]): Promise<void> {
|
||||
try {
|
||||
const transactionsStr = JSON.stringify(transactions);
|
||||
await DB.query(`
|
||||
INSERT INTO blocks_summaries
|
||||
SET height = ?, transactions = ?, id = ?
|
||||
ON DUPLICATE KEY UPDATE transactions = ?`,
|
||||
[blockHeight, transactionsStr, blockId, transactionsStr]);
|
||||
} catch (e: any) {
|
||||
logger.debug(`Cannot save block summary transactions for ${blockId}. Reason: ${e instanceof Error ? e.message : e}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public async $saveTemplate(params: { height: number, template: BlockSummary}) {
|
||||
const blockId = params.template?.id;
|
||||
try {
|
||||
@@ -68,19 +82,6 @@ class BlocksSummariesRepository {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete blocks from the database from blockHeight
|
||||
*/
|
||||
public async $deleteBlocksFrom(blockHeight: number) {
|
||||
logger.info(`Delete newer blocks summary from height ${blockHeight} from the database`);
|
||||
|
||||
try {
|
||||
await DB.query(`DELETE FROM blocks_summaries where height >= ${blockHeight}`);
|
||||
} catch (e) {
|
||||
logger.err('Cannot delete indexed blocks summaries. Reason: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fee percentiles if the block has already been indexed, [] otherwise
|
||||
*
|
||||
|
||||
@@ -220,7 +220,7 @@ class HashratesRepository {
|
||||
* Delete hashrates from the database from timestamp
|
||||
*/
|
||||
public async $deleteHashratesFromTimestamp(timestamp: number) {
|
||||
logger.info(`Delete newer hashrates from timestamp ${new Date(timestamp * 1000).toUTCString()} from the database`);
|
||||
logger.info(`Delete newer hashrates from timestamp ${new Date(timestamp * 1000).toUTCString()} from the database`, logger.tags.mining);
|
||||
|
||||
try {
|
||||
await DB.query(`DELETE FROM hashrates WHERE hashrate_timestamp >= FROM_UNIXTIME(?)`, [timestamp]);
|
||||
|
||||
@@ -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}" != "" ];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
|
||||
|
||||
@@ -106,13 +106,15 @@ https://www.transifex.com/mempool/mempool/dashboard/
|
||||
|
||||
* Arabic @baro0k
|
||||
* Czech @pixelmade2
|
||||
* Danish @pierrevendelboe
|
||||
* German @Emzy
|
||||
* English (default)
|
||||
* Spanish @maxhodler @bisqes
|
||||
* Persian @techmix
|
||||
* French @Bayernatoor
|
||||
* Korean @kcalvinalvinn
|
||||
* Korean @kcalvinalvinn @sogoagain
|
||||
* Italian @HodlBits
|
||||
* Lithuanian @eimze21
|
||||
* Hebrew @rapidlab309
|
||||
* Georgian @wyd_idk
|
||||
* Hungarian @btcdragonlord
|
||||
|
||||
@@ -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)');
|
||||
});
|
||||
|
||||
|
||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mempool-frontend",
|
||||
"version": "2.5.0-dev",
|
||||
"version": "2.5.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mempool-frontend",
|
||||
"version": "2.5.0-dev",
|
||||
"version": "2.5.0",
|
||||
"license": "GNU Affero General Public License v3.0",
|
||||
"dependencies": {
|
||||
"@angular-devkit/build-angular": "^14.2.10",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mempool-frontend",
|
||||
"version": "2.5.0-dev",
|
||||
"version": "2.5.0",
|
||||
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
||||
"license": "GNU Affero General Public License v3.0",
|
||||
"homepage": "https://mempool.space",
|
||||
|
||||
@@ -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>
|
||||
@@ -185,7 +201,7 @@
|
||||
<span>Umbrel</span>
|
||||
</a>
|
||||
<a href="https://github.com/rootzoll/raspiblitz" target="_blank" title="RaspiBlitz">
|
||||
<img class="image" src="/resources/profile/raspiblitz.jpg" />
|
||||
<img class="image" src="/resources/profile/raspiblitz.svg" />
|
||||
<span>RaspiBlitz</span>
|
||||
</a>
|
||||
<a href="https://github.com/mynodebtc/mynode" target="_blank" title="MyNode">
|
||||
@@ -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,18 +2,19 @@
|
||||
<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">
|
||||
<app-time kind="since" [time]="diffChange.timestamp" [fastRender]="true"></app-time>
|
||||
<td class="">
|
||||
<a [routerLink]="['/block' | relativeUrl, diffChange.height]">{{ diffChange.height }}</a>
|
||||
</td>
|
||||
<td class="date text-left">
|
||||
<app-time kind="since" [time]="diffChange.timestamp" [fastRender]="true" [precision]="1"></app-time>
|
||||
</td>
|
||||
<td class="text-right">{{ diffChange.difficultyShorten }}</td>
|
||||
<td class="text-right" [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'">
|
||||
@@ -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" [precision]="1"></app-time></div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5>
|
||||
<div *ngIf="epochData.remainingBlocks < 1870; else recentlyAdjusted" class="card-text" [ngStyle]="{'color': epochData.colorAdjustments}">
|
||||
<span *ngIf="epochData.change > 0; else arrowDownDifficulty" >
|
||||
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
|
||||
</span>
|
||||
<ng-template #arrowDownDifficulty >
|
||||
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
|
||||
</ng-template>
|
||||
{{ epochData.change | absolute | number: '1.2-2' }}
|
||||
<span class="symbol">%</span>
|
||||
</div>
|
||||
<ng-template #recentlyAdjusted>
|
||||
<div class="card-text">—</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" [precision]="1"></app-time></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #loadingDifficulty>
|
||||
<div class="difficulty-skeleton loading-container">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="difficulty-box.remaining">Remaining</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="difficulty-box.next-halving">Next Halving</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,160 @@
|
||||
.difficulty-adjustment-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
height: 76px;
|
||||
.shared-block {
|
||||
color: #ffffff66;
|
||||
font-size: 12px;
|
||||
}
|
||||
.item {
|
||||
padding: 0 5px;
|
||||
width: 100%;
|
||||
max-width: 150px;
|
||||
&:nth-child(1) {
|
||||
display: none;
|
||||
@media (min-width: 485px) {
|
||||
display: table-cell;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
}
|
||||
.card-text {
|
||||
font-size: 22px;
|
||||
margin-top: -9px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.difficulty-skeleton {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@media (min-width: 376px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
.item {
|
||||
min-width: 120px;
|
||||
max-width: 150px;
|
||||
margin: 0;
|
||||
width: -webkit-fill-available;
|
||||
@media (min-width: 376px) {
|
||||
margin: 0 auto 0px;
|
||||
}
|
||||
&:first-child{
|
||||
display: none;
|
||||
@media (min-width: 485px) {
|
||||
display: block;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.card-text {
|
||||
.skeleton-loader {
|
||||
width: 100%;
|
||||
display: block;
|
||||
&:first-child {
|
||||
margin: 14px auto 0;
|
||||
max-width: 80px;
|
||||
}
|
||||
&:last-child {
|
||||
margin: 10px auto 0;
|
||||
max-width: 120px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #1d1f31;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
color: #4a68b9;
|
||||
font-size: 1rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.progress {
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
background-color: #2d3348;
|
||||
height: 1.1rem;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.skeleton-loader {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.more-padding {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.small-bar {
|
||||
height: 8px;
|
||||
top: -4px;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
min-height: 76px;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
margin-top: -13px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.card-wrapper {
|
||||
.card {
|
||||
height: auto !important;
|
||||
}
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex: inherit;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
padding: 24px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.retarget-sign {
|
||||
margin-right: -3px;
|
||||
font-size: 14px;
|
||||
top: -2px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.previous-retarget-sign {
|
||||
margin-right: -2px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.symbol {
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -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" [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 bigger" [ngStyle]="{'color': epochData.colorAdjustments}">
|
||||
<span *ngIf="epochData.change > 0; else arrowDownDifficulty" >
|
||||
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
|
||||
</span>
|
||||
<ng-template #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" [precision]="1"></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,20 +30,28 @@
|
||||
}
|
||||
}
|
||||
.card-text {
|
||||
font-size: 22px;
|
||||
margin-top: -9px;
|
||||
font-size: 18px;
|
||||
margin: auto;
|
||||
position: relative;
|
||||
margin-bottom: 0.2rem;
|
||||
&.bigger {
|
||||
font-size: 20px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.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 +79,7 @@
|
||||
width: 100%;
|
||||
display: block;
|
||||
&:first-child {
|
||||
margin: 14px auto 0;
|
||||
margin: 10px auto 4px;
|
||||
max-width: 80px;
|
||||
}
|
||||
&:last-child {
|
||||
@@ -109,7 +123,7 @@
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
min-height: 76px;
|
||||
min-height: 50.5px;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
@@ -133,7 +147,7 @@
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
padding: 24px 20px;
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,4 +165,51 @@
|
||||
|
||||
.symbol {
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.epoch-progress {
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.epoch-blocks {
|
||||
display: block;
|
||||
width: 100%;
|
||||
background: #2d3348;
|
||||
|
||||
.rect {
|
||||
fill: #2d3348;
|
||||
|
||||
&.behind {
|
||||
fill: #D81B60;
|
||||
}
|
||||
&.mined {
|
||||
fill: url(#diff-gradient);
|
||||
}
|
||||
&.ahead {
|
||||
fill: #1a9436;
|
||||
}
|
||||
|
||||
&.hover {
|
||||
fill: #535e84;
|
||||
&.behind {
|
||||
fill: #e94d86;
|
||||
}
|
||||
&.mined {
|
||||
fill: url(#diff-hover-gradient);
|
||||
}
|
||||
&.ahead {
|
||||
fill: #29d951;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.blocks-ahead {
|
||||
color: #3bcc49;
|
||||
}
|
||||
.blocks-behind {
|
||||
color: #D81B60;
|
||||
}
|
||||
@@ -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,4 +1,4 @@
|
||||
<div class="fee-estimation-wrapper" *ngIf="(isLoadingWebSocket$ | async) === false && (recommendedFees$ | async) as recommendedFees; else loadingFees">
|
||||
<div class="fee-estimation-wrapper" *ngIf="(isLoading$ | async) === false && (recommendedFees$ | async) as recommendedFees; else loadingFees">
|
||||
<div class="d-flex">
|
||||
<div class="fee-progress-bar" [style.background]="noPriority">
|
||||
<span class="fee-label" i18n="fees-box.no-priority" i18n-ngbTooltip="Transaction feerate tooltip (economy)" ngbTooltip="Either 2x the minimum, or the Low Priority rate (whichever is lower)" placement="top">No Priority</span>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Observable, combineLatest } from 'rxjs';
|
||||
import { Recommendedfees } from '../../interfaces/websocket.interface';
|
||||
import { feeLevels, mempoolFeeColors } from '../../app.constants';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { map, startWith, tap } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'app-fees-box',
|
||||
@@ -12,7 +12,7 @@ import { tap } from 'rxjs/operators';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FeesBoxComponent implements OnInit {
|
||||
isLoadingWebSocket$: Observable<boolean>;
|
||||
isLoading$: Observable<boolean>;
|
||||
recommendedFees$: Observable<Recommendedfees>;
|
||||
gradient = 'linear-gradient(to right, #2e324e, #2e324e)';
|
||||
noPriority = '#2e324e';
|
||||
@@ -22,7 +22,12 @@ export class FeesBoxComponent implements OnInit {
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
|
||||
this.isLoading$ = combineLatest(
|
||||
this.stateService.isLoadingWebSocket$.pipe(startWith(false)),
|
||||
this.stateService.loadingIndicators$.pipe(startWith({ mempool: 0 })),
|
||||
).pipe(map(([socket, indicators]) => {
|
||||
return socket || (indicators.mempool != null && indicators.mempool !== 100);
|
||||
}));
|
||||
this.recommendedFees$ = this.stateService.recommendedFees$
|
||||
.pipe(
|
||||
tap((fees) => {
|
||||
|
||||
@@ -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">
|
||||
@@ -26,7 +26,7 @@
|
||||
<app-time kind="until" [time]="(1 * i) + now + 61000" [fastRender]="false" [fixedRender]="true"></app-time>
|
||||
</ng-template>
|
||||
<ng-template #timeDiffMainnet>
|
||||
<app-time kind="until" [time]="da.timeAvg * (i + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true" [forceFloorOnTimeIntervals]="['hour']"></app-time>
|
||||
<app-time kind="until" [time]="da.timeAvg * (i + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true"></app-time>
|
||||
</ng-template>
|
||||
</div>
|
||||
<ng-template #mergedBlock>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ export class SearchFormComponent implements OnInit {
|
||||
const matchesBlockHeight = this.regexBlockheight.test(searchText);
|
||||
const matchesTxId = this.regexTransaction.test(searchText) && !this.regexBlockhash.test(searchText);
|
||||
const matchesBlockHash = this.regexBlockhash.test(searchText);
|
||||
const matchesAddress = this.regexAddress.test(searchText);
|
||||
const matchesAddress = !matchesTxId && this.regexAddress.test(searchText);
|
||||
|
||||
if (matchesAddress && this.network === 'bisq') {
|
||||
searchText = 'B' + searchText;
|
||||
@@ -198,7 +198,7 @@ export class SearchFormComponent implements OnInit {
|
||||
const searchText = result || this.searchForm.value.searchText.trim();
|
||||
if (searchText) {
|
||||
this.isSearching = true;
|
||||
if (this.regexAddress.test(searchText)) {
|
||||
if (!this.regexTransaction.test(searchText) && this.regexAddress.test(searchText)) {
|
||||
this.navigate('/address/', searchText);
|
||||
} else if (this.regexBlockhash.test(searchText) || this.regexBlockheight.test(searchText)) {
|
||||
this.navigate('/block/', searchText);
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -10,6 +10,16 @@ import { dates } from '../../shared/i18n/dates';
|
||||
export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
||||
interval: number;
|
||||
text: string;
|
||||
units: string[] = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'];
|
||||
precisionThresholds = {
|
||||
year: 100,
|
||||
month: 18,
|
||||
week: 12,
|
||||
day: 31,
|
||||
hour: 48,
|
||||
minute: 90,
|
||||
second: 90
|
||||
};
|
||||
intervals = {};
|
||||
|
||||
@Input() time: number;
|
||||
@@ -18,7 +28,8 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() fastRender = false;
|
||||
@Input() fixedRender = false;
|
||||
@Input() relative = false;
|
||||
@Input() forceFloorOnTimeIntervals: string[];
|
||||
@Input() precision: number = 0;
|
||||
@Input() fractionDigits: number = 0;
|
||||
|
||||
constructor(
|
||||
private ref: ChangeDetectorRef,
|
||||
@@ -82,18 +93,24 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
let counter: number;
|
||||
for (const i in this.intervals) {
|
||||
if (this.kind !== 'until' || this.forceFloorOnTimeIntervals && this.forceFloorOnTimeIntervals.indexOf(i) > -1) {
|
||||
counter = Math.floor(seconds / this.intervals[i]);
|
||||
} else {
|
||||
counter = Math.round(seconds / this.intervals[i]);
|
||||
for (const [index, unit] of this.units.entries()) {
|
||||
let precisionUnit = this.units[Math.min(this.units.length - 1, index + this.precision)];
|
||||
counter = Math.floor(seconds / this.intervals[unit]);
|
||||
const precisionCounter = Math.floor(seconds / this.intervals[precisionUnit]);
|
||||
if (precisionCounter > this.precisionThresholds[precisionUnit]) {
|
||||
precisionUnit = unit;
|
||||
}
|
||||
const dateStrings = dates(counter);
|
||||
if (counter > 0) {
|
||||
let rounded = Math.round(seconds / this.intervals[precisionUnit]);
|
||||
if (this.fractionDigits) {
|
||||
const roundFactor = Math.pow(10,this.fractionDigits);
|
||||
rounded = Math.round((seconds / this.intervals[precisionUnit]) * roundFactor) / roundFactor;
|
||||
}
|
||||
const dateStrings = dates(rounded);
|
||||
switch (this.kind) {
|
||||
case 'since':
|
||||
if (counter === 1) {
|
||||
switch (i) { // singular (1 day)
|
||||
if (rounded === 1) {
|
||||
switch (precisionUnit) { // singular (1 day)
|
||||
case 'year': return $localize`:@@time-since:${dateStrings.i18nYear}:DATE: ago`; break;
|
||||
case 'month': return $localize`:@@time-since:${dateStrings.i18nMonth}:DATE: ago`; break;
|
||||
case 'week': return $localize`:@@time-since:${dateStrings.i18nWeek}:DATE: ago`; break;
|
||||
@@ -103,7 +120,7 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
||||
case 'second': return $localize`:@@time-since:${dateStrings.i18nSecond}:DATE: ago`; break;
|
||||
}
|
||||
} else {
|
||||
switch (i) { // plural (2 days)
|
||||
switch (precisionUnit) { // plural (2 days)
|
||||
case 'year': return $localize`:@@time-since:${dateStrings.i18nYears}:DATE: ago`; break;
|
||||
case 'month': return $localize`:@@time-since:${dateStrings.i18nMonths}:DATE: ago`; break;
|
||||
case 'week': return $localize`:@@time-since:${dateStrings.i18nWeeks}:DATE: ago`; break;
|
||||
@@ -115,8 +132,8 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
break;
|
||||
case 'until':
|
||||
if (counter === 1) {
|
||||
switch (i) { // singular (In ~1 day)
|
||||
if (rounded === 1) {
|
||||
switch (precisionUnit) { // singular (In ~1 day)
|
||||
case 'year': return $localize`:@@time-until:In ~${dateStrings.i18nYear}:DATE:`; break;
|
||||
case 'month': return $localize`:@@time-until:In ~${dateStrings.i18nMonth}:DATE:`; break;
|
||||
case 'week': return $localize`:@@time-until:In ~${dateStrings.i18nWeek}:DATE:`; break;
|
||||
@@ -126,7 +143,7 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
||||
case 'second': return $localize`:@@time-until:In ~${dateStrings.i18nSecond}:DATE:`;
|
||||
}
|
||||
} else {
|
||||
switch (i) { // plural (In ~2 days)
|
||||
switch (precisionUnit) { // plural (In ~2 days)
|
||||
case 'year': return $localize`:@@time-until:In ~${dateStrings.i18nYears}:DATE:`; break;
|
||||
case 'month': return $localize`:@@time-until:In ~${dateStrings.i18nMonths}:DATE:`; break;
|
||||
case 'week': return $localize`:@@time-until:In ~${dateStrings.i18nWeeks}:DATE:`; break;
|
||||
@@ -138,8 +155,8 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
break;
|
||||
case 'span':
|
||||
if (counter === 1) {
|
||||
switch (i) { // singular (1 day)
|
||||
if (rounded === 1) {
|
||||
switch (precisionUnit) { // singular (1 day)
|
||||
case 'year': return $localize`:@@time-span:After ${dateStrings.i18nYear}:DATE:`; break;
|
||||
case 'month': return $localize`:@@time-span:After ${dateStrings.i18nMonth}:DATE:`; break;
|
||||
case 'week': return $localize`:@@time-span:After ${dateStrings.i18nWeek}:DATE:`; break;
|
||||
@@ -149,7 +166,7 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
||||
case 'second': return $localize`:@@time-span:After ${dateStrings.i18nSecond}:DATE:`; break;
|
||||
}
|
||||
} else {
|
||||
switch (i) { // plural (2 days)
|
||||
switch (precisionUnit) { // plural (2 days)
|
||||
case 'year': return $localize`:@@time-span:After ${dateStrings.i18nYears}:DATE:`; break;
|
||||
case 'month': return $localize`:@@time-span:After ${dateStrings.i18nMonths}:DATE:`; break;
|
||||
case 'week': return $localize`:@@time-span:After ${dateStrings.i18nWeeks}:DATE:`; break;
|
||||
@@ -161,8 +178,8 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (counter === 1) {
|
||||
switch (i) { // singular (1 day)
|
||||
if (rounded === 1) {
|
||||
switch (precisionUnit) { // singular (1 day)
|
||||
case 'year': return dateStrings.i18nYear; break;
|
||||
case 'month': return dateStrings.i18nMonth; break;
|
||||
case 'week': return dateStrings.i18nWeek; break;
|
||||
@@ -172,7 +189,7 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
||||
case 'second': return dateStrings.i18nSecond; break;
|
||||
}
|
||||
} else {
|
||||
switch (i) { // plural (2 days)
|
||||
switch (precisionUnit) { // plural (2 days)
|
||||
case 'year': return dateStrings.i18nYears; break;
|
||||
case 'month': return dateStrings.i18nMonths; break;
|
||||
case 'week': return dateStrings.i18nWeeks; break;
|
||||
|
||||
@@ -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>
|
||||
@@ -119,7 +119,7 @@
|
||||
<app-time kind="until" [time]="(60 * 1000 * txInBlockIndex) + now" [fastRender]="false" [fixedRender]="true"></app-time>
|
||||
</ng-template>
|
||||
<ng-template #timeEstimateDefault>
|
||||
<app-time kind="until" *ngIf="(timeAvg$ | async) as timeAvg;" [time]="(timeAvg * txInBlockIndex) + now + timeAvg" [fastRender]="false" [fixedRender]="true" [forceFloorOnTimeIntervals]="['hour']"></app-time>
|
||||
<app-time kind="until" *ngIf="(timeAvg$ | async) as timeAvg;" [time]="(timeAvg * txInBlockIndex) + now + timeAvg" [fastRender]="false" [fixedRender]="true"></app-time>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
@@ -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',
|
||||
@@ -48,6 +49,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
blocksSubscription: Subscription;
|
||||
queryParamsSubscription: Subscription;
|
||||
urlFragmentSubscription: Subscription;
|
||||
mempoolBlocksSubscription: Subscription;
|
||||
fragmentParams: URLSearchParams;
|
||||
rbfTransaction: undefined | Transaction;
|
||||
replaced: boolean = false;
|
||||
@@ -58,7 +60,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
fetchRbfHistory$ = new Subject<string>();
|
||||
fetchCachedTx$ = new Subject<string>();
|
||||
isCached: boolean = false;
|
||||
now = new Date().getTime();
|
||||
now = Date.now();
|
||||
timeAvg$: Observable<number>;
|
||||
liquidUnblinding = new LiquidUnblinding();
|
||||
inputIndex: number;
|
||||
@@ -74,6 +76,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 +205,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 +300,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
this.tx = tx;
|
||||
this.setFeatures();
|
||||
this.isCached = false;
|
||||
if (tx.fee === undefined) {
|
||||
this.tx.fee = 0;
|
||||
@@ -299,7 +309,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.isLoadingTx = false;
|
||||
this.error = undefined;
|
||||
this.waitingForTransaction = false;
|
||||
this.setMempoolBlocksSubscription();
|
||||
this.websocketService.startTrackTransaction(tx.txid);
|
||||
this.graphExpanded = false;
|
||||
this.setupGraph();
|
||||
@@ -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,
|
||||
@@ -382,6 +391,34 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.setFlowEnabled();
|
||||
this.setGraphSize();
|
||||
});
|
||||
|
||||
this.mempoolBlocksSubscription = this.stateService.mempoolBlocks$.subscribe((mempoolBlocks) => {
|
||||
if (!this.tx) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.now = Date.now();
|
||||
|
||||
const txFeePerVSize =
|
||||
this.tx.effectiveFeePerVsize || this.tx.fee / (this.tx.weight / 4);
|
||||
|
||||
let found = false;
|
||||
this.txInBlockIndex = 0;
|
||||
for (const block of mempoolBlocks) {
|
||||
for (let i = 0; i < block.feeRange.length - 1 && !found; i++) {
|
||||
if (
|
||||
txFeePerVSize <= block.feeRange[i + 1] &&
|
||||
txFeePerVSize >= block.feeRange[i]
|
||||
) {
|
||||
this.txInBlockIndex = mempoolBlocks.indexOf(block);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!found && txFeePerVSize < mempoolBlocks[mempoolBlocks.length - 1].feeRange[0]) {
|
||||
this.txInBlockIndex = 7;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
@@ -398,28 +435,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
return of(false);
|
||||
}
|
||||
|
||||
setMempoolBlocksSubscription() {
|
||||
this.stateService.mempoolBlocks$.subscribe((mempoolBlocks) => {
|
||||
if (!this.tx) {
|
||||
return;
|
||||
}
|
||||
|
||||
const txFeePerVSize =
|
||||
this.tx.effectiveFeePerVsize || this.tx.fee / (this.tx.weight / 4);
|
||||
|
||||
for (const block of mempoolBlocks) {
|
||||
for (let i = 0; i < block.feeRange.length - 1; i++) {
|
||||
if (
|
||||
txFeePerVSize <= block.feeRange[i + 1] &&
|
||||
txFeePerVSize >= block.feeRange[i]
|
||||
) {
|
||||
this.txInBlockIndex = mempoolBlocks.indexOf(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getTransactionTime() {
|
||||
this.apiService
|
||||
.getTransactionTimes$([this.tx.txid])
|
||||
@@ -428,9 +443,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 +524,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,6 +542,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.queryParamsSubscription.unsubscribe();
|
||||
this.flowPrefSubscription.unsubscribe();
|
||||
this.urlFragmentSubscription.unsubscribe();
|
||||
this.mempoolBlocksSubscription.unsubscribe();
|
||||
this.leaveTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -177,9 +184,9 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
onScroll(): void {
|
||||
const scrollHeight = document.body.scrollHeight;
|
||||
const scrollTop = document.documentElement.scrollTop;
|
||||
if (scrollHeight > 0){
|
||||
if (scrollHeight > 0) {
|
||||
const percentageScrolled = scrollTop * 100 / scrollHeight;
|
||||
if (percentageScrolled > 70){
|
||||
if (percentageScrolled > 50) {
|
||||
this.loadMore.emit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user