Compare commits
380 Commits
v2.5.0-dev
...
v2.5.0-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a210a3faf2 | ||
|
|
b8edcbadf4 | ||
|
|
07987ff4b6 | ||
|
|
e7e0a64ca2 | ||
|
|
484c503f6d | ||
|
|
c59ab2a129 | ||
|
|
8e668be703 | ||
|
|
7a7172bb64 | ||
|
|
685433fe4c | ||
|
|
79f6ae3b6f | ||
|
|
e54e896e56 | ||
|
|
3126a559a0 | ||
|
|
132e848fdc | ||
|
|
0d92779971 | ||
|
|
5ff5275b36 | ||
|
|
534f2e2781 | ||
|
|
2cd98c7c04 | ||
|
|
75459729ad | ||
|
|
2b411aad0a | ||
|
|
229dd7718a | ||
|
|
13b52c427c | ||
|
|
fc778e1e25 | ||
|
|
f6813f1d1c | ||
|
|
1db11d1d67 | ||
|
|
12b130cfdc | ||
|
|
175bcf7467 | ||
|
|
0b54035e80 | ||
|
|
92807dbdde | ||
|
|
059d5a94a9 | ||
|
|
501ca1832b | ||
|
|
ddc7de0d4a | ||
|
|
59f1b031c8 | ||
|
|
3d45054e38 | ||
|
|
38c890626b | ||
|
|
c7d61a3be4 | ||
|
|
0b37a02435 | ||
|
|
03a3320e45 | ||
|
|
6d075842f4 | ||
|
|
ead60aaa21 | ||
|
|
0fd672a741 | ||
|
|
6741a2b226 | ||
|
|
100c1b292a | ||
|
|
7e5c0a4c46 | ||
|
|
8117b9799c | ||
|
|
afc5c6786b | ||
|
|
c7cca500fa | ||
|
|
5f1a71cc9b | ||
|
|
734c953714 | ||
|
|
ba10df69b7 | ||
|
|
ded11892f5 | ||
|
|
609f68eb24 | ||
|
|
5e1f54e862 | ||
|
|
dc7d5bc94d | ||
|
|
35ae672177 | ||
|
|
8f0830f6d1 | ||
|
|
0c96a11150 | ||
|
|
cf89ded14d | ||
|
|
a9e766046f | ||
|
|
030889250f | ||
|
|
50993d3b95 | ||
|
|
95e8789ba9 | ||
|
|
194e4b4c80 | ||
|
|
272b6d2437 | ||
|
|
89293b4358 | ||
|
|
c682a8e3ff | ||
|
|
cc30da0b4d | ||
|
|
6d6dd09d11 | ||
|
|
f2ad184d1f | ||
|
|
ab5308e1c8 | ||
|
|
205d832d31 | ||
|
|
3e7270d1c5 | ||
|
|
fa515402bf | ||
|
|
9b6a012476 | ||
|
|
3406758fd2 | ||
|
|
cc93674591 | ||
|
|
c9fc77490f | ||
|
|
ddb4fbac5c | ||
|
|
3eb4ea9048 | ||
|
|
6d99d0a9ce | ||
|
|
d43a9cc5ea | ||
|
|
a3a2adac02 | ||
|
|
c8aea18c5e | ||
|
|
c2f45f9bc1 | ||
|
|
208946a8bf | ||
|
|
0e8e5dc3a9 | ||
|
|
f1122384dd | ||
|
|
2290f98011 | ||
|
|
b0e3022ddb | ||
|
|
acd633530f | ||
|
|
f73dc59f49 | ||
|
|
e627122239 | ||
|
|
201b32bdcd | ||
|
|
6ec9c2f816 | ||
|
|
de04914851 | ||
|
|
5fc3b8b70c | ||
|
|
276470474d | ||
|
|
1461cb1b17 | ||
|
|
c43e4bb71b | ||
|
|
92538b1a48 | ||
|
|
fa519a0d8f | ||
|
|
da10b36524 | ||
|
|
c2b6316c8b | ||
|
|
6ada839282 | ||
|
|
7de068368c | ||
|
|
0d797ff7fd | ||
|
|
fe8cdb5867 | ||
|
|
74dbd6cee1 | ||
|
|
0b7182715f | ||
|
|
e08902b85b | ||
|
|
7d3ec63335 | ||
|
|
584f443f56 | ||
|
|
4f3296566a | ||
|
|
1309a63430 | ||
|
|
ca33a629cf | ||
|
|
311774103e | ||
|
|
e72cdb42e8 | ||
|
|
6f807b7a2c | ||
|
|
7f83b4be28 | ||
|
|
802d38c363 | ||
|
|
38311e191b | ||
|
|
a1c5769d0d | ||
|
|
01a727a344 | ||
|
|
6cd1f9e870 | ||
|
|
d107286344 | ||
|
|
330ab9682b | ||
|
|
2b94849881 | ||
|
|
37bf67aa38 | ||
|
|
28d5ec34b3 | ||
|
|
eeea6cd9c8 | ||
|
|
7bafeefa95 | ||
|
|
dc86f41e03 | ||
|
|
2f7aacaf3b | ||
|
|
446d76980a | ||
|
|
92dbba64e6 | ||
|
|
43bb3aa50b | ||
|
|
5198cc51dc | ||
|
|
56e00d7ea9 | ||
|
|
5e72ecfdc9 | ||
|
|
6c1457e257 | ||
|
|
7e01a22265 | ||
|
|
cb7e25d646 | ||
|
|
2653e7bf39 | ||
|
|
d8d8a52445 | ||
|
|
3e50941351 | ||
|
|
b9a761fb88 | ||
|
|
b1d490972b | ||
|
|
786d73625a | ||
|
|
08ad6a0da3 | ||
|
|
38cb45e026 | ||
|
|
24dba5a2ef | ||
|
|
a32f960c4a | ||
|
|
9345b1609f | ||
|
|
4abd77fe31 | ||
|
|
a9760326f2 | ||
|
|
ed184824d4 | ||
|
|
9d5717f30d | ||
|
|
547b60fce7 | ||
|
|
b7bf2ec666 | ||
|
|
9b5d8fdad6 | ||
|
|
782d4b391b | ||
|
|
19e778c4b5 | ||
|
|
4bc5de306a | ||
|
|
47c61842f5 | ||
|
|
672001af72 | ||
|
|
5da8f2b6dc | ||
|
|
9df0e602d3 | ||
|
|
8a367fc6fd | ||
|
|
a33562a47a | ||
|
|
fc7024351e | ||
|
|
d3d4f93f85 | ||
|
|
14ec427f5e | ||
|
|
2c1f38aa9d | ||
|
|
eb2abefabc | ||
|
|
90912af62d | ||
|
|
adcc1ba4f0 | ||
|
|
a0b6719105 | ||
|
|
c2ab0bc715 | ||
|
|
010e9f2bb1 | ||
|
|
373e02a5b0 | ||
|
|
d36b239dbe | ||
|
|
eb03fc18ad | ||
|
|
a7c511fc1c | ||
|
|
5b6f713ef3 | ||
|
|
1b3bc0ef4e | ||
|
|
2022d3f6d5 | ||
|
|
695d81a3f6 | ||
|
|
29f7c89c53 | ||
|
|
7232c4755d | ||
|
|
88fa6bffb5 | ||
|
|
235ac204b4 | ||
|
|
e051758ca7 | ||
|
|
be3acf8694 | ||
|
|
2020cd74e9 | ||
|
|
67cbbda04b | ||
|
|
5957b71774 | ||
|
|
b0198de7e8 | ||
|
|
8cc252642b | ||
|
|
5e5daca600 | ||
|
|
cfb4fdb7a4 | ||
|
|
dbc2d752bc | ||
|
|
7c7273b696 | ||
|
|
34500f7d47 | ||
|
|
f18226bd01 | ||
|
|
c1e741a025 | ||
|
|
2a6ac4a5da | ||
|
|
34d5a2f9c0 | ||
|
|
3654178c83 | ||
|
|
5df54b6b3e | ||
|
|
8bd3e14652 | ||
|
|
ddcd387848 | ||
|
|
ef27aca6e4 | ||
|
|
997e8a4624 | ||
|
|
d65f267122 | ||
|
|
d32d97fbaf | ||
|
|
65bfe8163c | ||
|
|
b069196c27 | ||
|
|
38255a5452 | ||
|
|
48e2df3f7a | ||
|
|
4fc355a05d | ||
|
|
7c6349f2ba | ||
|
|
899d6558ec | ||
|
|
02820b0e68 | ||
|
|
4bb6a3800c | ||
|
|
b6d4e6b993 | ||
|
|
de46f7c10e | ||
|
|
69a36e17a8 | ||
|
|
06eeaf68e8 | ||
|
|
f789334d47 | ||
|
|
387a51d87e | ||
|
|
64426fa9c9 | ||
|
|
9c6799e193 | ||
|
|
8d6a0f867b | ||
|
|
057456504c | ||
|
|
45273f9309 | ||
|
|
2cbb7231a7 | ||
|
|
bee573fdb8 | ||
|
|
12bd89dade | ||
|
|
e24fd8e275 | ||
|
|
8c4a8f3a71 | ||
|
|
38ec5ef957 | ||
|
|
dbb6f267f4 | ||
|
|
23a4ab461e | ||
|
|
b657eb4e7d | ||
|
|
f3eb403c17 | ||
|
|
b6343ddc2d | ||
|
|
d86f045150 | ||
|
|
e2e50ac6bf | ||
|
|
6d28259515 | ||
|
|
968d7b827b | ||
|
|
832ccdac46 | ||
|
|
39afa4cda1 | ||
|
|
702ff2796a | ||
|
|
cb576ce601 | ||
|
|
e14fff45d6 | ||
|
|
847aa1ba13 | ||
|
|
58371bbd7d | ||
|
|
f3faf99c15 | ||
|
|
a5c4f8e2f3 | ||
|
|
27c39ef557 | ||
|
|
9e0a91efd2 | ||
|
|
601a559784 | ||
|
|
0e0ac363cf | ||
|
|
b31642e554 | ||
|
|
5f87cc6d37 | ||
|
|
b89d526379 | ||
|
|
67429d83b5 | ||
|
|
5c6060780b | ||
|
|
06a89bc1a7 | ||
|
|
022785a555 | ||
|
|
69baf97445 | ||
|
|
04fa08085d | ||
|
|
9bb897307f | ||
|
|
f3c947685a | ||
|
|
dffe9fa4e6 | ||
|
|
20bef70390 | ||
|
|
ae9439a991 | ||
|
|
9964f1ab14 | ||
|
|
f27abb1421 | ||
|
|
ee6766e34c | ||
|
|
76764936f9 | ||
|
|
596c7afecb | ||
|
|
ffad5e2a30 | ||
|
|
8da476c48c | ||
|
|
5bfc8a9d58 | ||
|
|
670f85b1f5 | ||
|
|
82a4212b72 | ||
|
|
cfa8a9a7d6 | ||
|
|
b77fe0dca2 | ||
|
|
81d35d9401 | ||
|
|
2742acf6ee | ||
|
|
8a2b144e29 | ||
|
|
3e66e4d6db | ||
|
|
61e8892204 | ||
|
|
543c4feaf9 | ||
|
|
992ea6da3c | ||
|
|
f3cfc7f80b | ||
|
|
4c170b08f4 | ||
|
|
d3b3c7df21 | ||
|
|
893aa03622 | ||
|
|
f4df51dd21 | ||
|
|
3e41e512ad | ||
|
|
7bdde13b40 | ||
|
|
7ec0e3ac86 | ||
|
|
02340d57dd | ||
|
|
1e6ea0b5f5 | ||
|
|
5d9bcce5cd | ||
|
|
39dd8ebe07 | ||
|
|
e5ec152002 | ||
|
|
e77f48abd4 | ||
|
|
3b692d05bc | ||
|
|
6895eb0b05 | ||
|
|
61333b2286 | ||
|
|
1240a3f115 | ||
|
|
f70ff9b402 | ||
|
|
5cdb0c5ce9 | ||
|
|
3971814710 | ||
|
|
c5d4e86e0e | ||
|
|
ad7e7795f9 | ||
|
|
71e00f66c9 | ||
|
|
5d21a61840 | ||
|
|
8ef88e9f39 | ||
|
|
ddb1e97ce0 | ||
|
|
b638719e72 | ||
|
|
4fee471992 | ||
|
|
5365f61121 | ||
|
|
4924c521a4 | ||
|
|
43d56a2121 | ||
|
|
bede502f2d | ||
|
|
6005bbea49 | ||
|
|
3653e75810 | ||
|
|
66c99e2f3b | ||
|
|
9876805bc3 | ||
|
|
d08e5e293c | ||
|
|
6635238934 | ||
|
|
001f7a4fd7 | ||
|
|
3ba4fd454e | ||
|
|
52b2ee4f35 | ||
|
|
bfac856eb2 | ||
|
|
42dec95738 | ||
|
|
6eacbf80d8 | ||
|
|
be7e2c2c80 | ||
|
|
e428565d50 | ||
|
|
50cc424679 | ||
|
|
d288da1e18 | ||
|
|
0c1993e264 | ||
|
|
75fd036ec2 | ||
|
|
626a1a2977 | ||
|
|
d1cedbb981 | ||
|
|
0df796f873 | ||
|
|
c10ace8fb5 | ||
|
|
5d3ee50bca | ||
|
|
be2b72eea7 | ||
|
|
1af38456f3 | ||
|
|
0a4c1c24af | ||
|
|
54c44565fb | ||
|
|
b86d8bd836 | ||
|
|
5610afde36 | ||
|
|
d88e12fc6e | ||
|
|
95156eebd1 | ||
|
|
8f0a9a9dd2 | ||
|
|
42189ec1a5 | ||
|
|
f2889fc05c | ||
|
|
6e235924d8 | ||
|
|
15caef10d6 | ||
|
|
21db64b2a5 | ||
|
|
d07bf30737 | ||
|
|
135fbfc4f3 | ||
|
|
03c6a7c54f | ||
|
|
9d3d3ed5f8 | ||
|
|
619a6bd34d | ||
|
|
d9c967b529 | ||
|
|
0e716165e5 | ||
|
|
4154d3081d | ||
|
|
5d1c5b51dd | ||
|
|
19467de809 | ||
|
|
bd4cf980bd | ||
|
|
9b1fc1e000 | ||
|
|
90db8c15f2 | ||
|
|
f062132636 | ||
|
|
9c09c00fab |
44
.github/dependabot.yml
vendored
44
.github/dependabot.yml
vendored
@@ -1,20 +1,28 @@
|
|||||||
version: 2
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: npm
|
- package-ecosystem: npm
|
||||||
directory: "/backend"
|
directory: "/backend"
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: daily
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
- package-ecosystem: npm
|
ignore:
|
||||||
directory: "/frontend"
|
- update-types: ["version-update:semver-major"]
|
||||||
schedule:
|
- package-ecosystem: npm
|
||||||
interval: daily
|
directory: "/frontend"
|
||||||
open-pull-requests-limit: 10
|
schedule:
|
||||||
- package-ecosystem: docker
|
interval: daily
|
||||||
directory: "/docker/backend"
|
open-pull-requests-limit: 10
|
||||||
schedule:
|
ignore:
|
||||||
interval: weekly
|
- update-types: ["version-update:semver-major"]
|
||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: docker
|
||||||
directory: "/"
|
directory: "/docker/backend"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: daily
|
||||||
|
ignore:
|
||||||
|
- update-types: ["version-update:semver-major"]
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
ignore:
|
||||||
|
- update-types: ["version-update:semver-major"]
|
||||||
|
|||||||
8
.github/workflows/on-tag.yml
vendored
8
.github/workflows/on-tag.yml
vendored
@@ -68,24 +68,24 @@ jobs:
|
|||||||
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
||||||
|
|
||||||
- name: Checkout project
|
- name: Checkout project
|
||||||
uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
|
uses: actions/checkout@e2f20e631ae6d7dd3b768f56a5d2af784dd54791 # v2.5.0
|
||||||
|
|
||||||
- name: Init repo for Dockerization
|
- name: Init repo for Dockerization
|
||||||
run: docker/init.sh "$TAG"
|
run: docker/init.sh "$TAG"
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@27d0a4f181a40b142cce983c5393082c365d1480 # v1
|
uses: docker/setup-qemu-action@e81a89b1732b9c48d79cd809d8d81d79c4647a18 # v2.1.0
|
||||||
id: qemu
|
id: qemu
|
||||||
|
|
||||||
- name: Setup Docker buildx action
|
- name: Setup Docker buildx action
|
||||||
uses: docker/setup-buildx-action@94ab11c41e45d028884a99163086648e898eed25 # v1
|
uses: docker/setup-buildx-action@8c0edbc76e98fa90f69d9a2c020dcb50019dc325 # v2.2.1
|
||||||
id: buildx
|
id: buildx
|
||||||
|
|
||||||
- name: Available platforms
|
- name: Available platforms
|
||||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||||
|
|
||||||
- name: Cache Docker layers
|
- name: Cache Docker layers
|
||||||
uses: actions/cache@661fd3eb7f2f20d8c7c84bc2b0509efd7a826628 # v2
|
uses: actions/cache@9b0c1fce7a93df8e3bb8926b0d6e9d89e92f20a7 # v3.0.11
|
||||||
id: cache
|
id: cache
|
||||||
with:
|
with:
|
||||||
path: /tmp/.buildx-cache
|
path: /tmp/.buildx-cache
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,3 +3,5 @@ data
|
|||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
backend/mempool-config.json
|
backend/mempool-config.json
|
||||||
*.swp
|
*.swp
|
||||||
|
frontend/src/resources/config.template.js
|
||||||
|
frontend/src/resources/config.js
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"MEMPOOL": {
|
"MEMPOOL": {
|
||||||
"NETWORK": "mainnet",
|
"NETWORK": "mainnet",
|
||||||
"BACKEND": "electrum",
|
"BACKEND": "electrum",
|
||||||
|
"ENABLED": true,
|
||||||
"HTTP_PORT": 8999,
|
"HTTP_PORT": 8999,
|
||||||
"SPAWN_CLUSTER_PROCS": 0,
|
"SPAWN_CLUSTER_PROCS": 0,
|
||||||
"API_URL_PREFIX": "/api/v1/",
|
"API_URL_PREFIX": "/api/v1/",
|
||||||
@@ -23,7 +24,10 @@
|
|||||||
"STDOUT_LOG_MIN_PRIORITY": "debug",
|
"STDOUT_LOG_MIN_PRIORITY": "debug",
|
||||||
"AUTOMATIC_BLOCK_REINDEXING": false,
|
"AUTOMATIC_BLOCK_REINDEXING": false,
|
||||||
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json",
|
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json",
|
||||||
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master"
|
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
|
||||||
|
"ADVANCED_GBT_AUDIT": false,
|
||||||
|
"ADVANCED_GBT_MEMPOOL": false,
|
||||||
|
"TRANSACTION_INDEXING": false
|
||||||
},
|
},
|
||||||
"CORE_RPC": {
|
"CORE_RPC": {
|
||||||
"HOST": "127.0.0.1",
|
"HOST": "127.0.0.1",
|
||||||
@@ -80,7 +84,9 @@
|
|||||||
"BACKEND": "lnd",
|
"BACKEND": "lnd",
|
||||||
"STATS_REFRESH_INTERVAL": 600,
|
"STATS_REFRESH_INTERVAL": 600,
|
||||||
"GRAPH_REFRESH_INTERVAL": 600,
|
"GRAPH_REFRESH_INTERVAL": 600,
|
||||||
"LOGGER_UPDATE_INTERVAL": 30
|
"LOGGER_UPDATE_INTERVAL": 30,
|
||||||
|
"FORENSICS_INTERVAL": 43200,
|
||||||
|
"FORENSICS_RATE_LIMIT": 20
|
||||||
},
|
},
|
||||||
"LND": {
|
"LND": {
|
||||||
"TLS_CERT_PATH": "tls.cert",
|
"TLS_CERT_PATH": "tls.cert",
|
||||||
|
|||||||
3667
backend/package-lock.json
generated
3667
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -34,35 +34,35 @@
|
|||||||
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\""
|
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.18.6",
|
"@babel/core": "^7.20.5",
|
||||||
"@mempool/electrum-client": "^1.1.7",
|
"@mempool/electrum-client": "^1.1.7",
|
||||||
"@types/node": "^16.11.41",
|
"@types/node": "^16.11.41",
|
||||||
"axios": "~0.27.2",
|
"axios": "~0.27.2",
|
||||||
"bitcoinjs-lib": "6.0.2",
|
"bitcoinjs-lib": "~6.0.2",
|
||||||
"crypto-js": "^4.0.0",
|
"crypto-js": "~4.1.1",
|
||||||
"express": "^4.18.0",
|
"express": "~4.18.2",
|
||||||
"maxmind": "^4.3.6",
|
"maxmind": "~4.3.8",
|
||||||
"mysql2": "2.3.3",
|
"mysql2": "~2.3.3",
|
||||||
"node-worker-threads-pool": "^1.5.1",
|
"node-worker-threads-pool": "~1.5.1",
|
||||||
"socks-proxy-agent": "~7.0.0",
|
"socks-proxy-agent": "~7.0.0",
|
||||||
"typescript": "~4.7.4",
|
"typescript": "~4.7.4",
|
||||||
"ws": "~8.8.0"
|
"ws": "~8.11.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.18.6",
|
"@babel/core": "^7.20.5",
|
||||||
"@babel/code-frame": "^7.18.6",
|
"@babel/code-frame": "^7.18.6",
|
||||||
"@types/compression": "^1.7.2",
|
"@types/compression": "^1.7.2",
|
||||||
"@types/crypto-js": "^4.1.1",
|
"@types/crypto-js": "^4.1.1",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.14",
|
||||||
"@types/jest": "^28.1.4",
|
"@types/jest": "^29.2.3",
|
||||||
"@types/ws": "~8.5.3",
|
"@types/ws": "~8.5.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.30.5",
|
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||||
"@typescript-eslint/parser": "^5.30.5",
|
"@typescript-eslint/parser": "^5.45.0",
|
||||||
"eslint": "^8.19.0",
|
"eslint": "^8.28.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"jest": "^28.1.2",
|
"jest": "^29.3.1",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.8.0",
|
||||||
"ts-jest": "^28.0.5",
|
"ts-jest": "^29.0.3",
|
||||||
"ts-node": "^10.8.2"
|
"ts-node": "^10.9.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
{
|
{
|
||||||
"MEMPOOL": {
|
"MEMPOOL": {
|
||||||
|
"ENABLED": true,
|
||||||
"NETWORK": "__MEMPOOL_NETWORK__",
|
"NETWORK": "__MEMPOOL_NETWORK__",
|
||||||
"BACKEND": "__MEMPOOL_BACKEND__",
|
"BACKEND": "__MEMPOOL_BACKEND__",
|
||||||
|
"ENABLED": true,
|
||||||
"BLOCKS_SUMMARIES_INDEXING": true,
|
"BLOCKS_SUMMARIES_INDEXING": true,
|
||||||
"HTTP_PORT": 1,
|
"HTTP_PORT": 1,
|
||||||
"SPAWN_CLUSTER_PROCS": 2,
|
"SPAWN_CLUSTER_PROCS": 2,
|
||||||
@@ -23,7 +25,10 @@
|
|||||||
"STDOUT_LOG_MIN_PRIORITY": "__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__",
|
"STDOUT_LOG_MIN_PRIORITY": "__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__",
|
||||||
"INDEXING_BLOCKS_AMOUNT": 14,
|
"INDEXING_BLOCKS_AMOUNT": 14,
|
||||||
"POOLS_JSON_TREE_URL": "__POOLS_JSON_TREE_URL__",
|
"POOLS_JSON_TREE_URL": "__POOLS_JSON_TREE_URL__",
|
||||||
"POOLS_JSON_URL": "__POOLS_JSON_URL__"
|
"POOLS_JSON_URL": "__POOLS_JSON_URL__",
|
||||||
|
"ADVANCED_GBT_AUDIT": "__ADVANCED_GBT_AUDIT__",
|
||||||
|
"ADVANCED_GBT_MEMPOOL": "__ADVANCED_GBT_MEMPOOL__",
|
||||||
|
"TRANSACTION_INDEXING": "__TRANSACTION_INDEXING__"
|
||||||
},
|
},
|
||||||
"CORE_RPC": {
|
"CORE_RPC": {
|
||||||
"HOST": "__CORE_RPC_HOST__",
|
"HOST": "__CORE_RPC_HOST__",
|
||||||
@@ -95,7 +100,9 @@
|
|||||||
"TOPOLOGY_FOLDER": "__LIGHTNING_TOPOLOGY_FOLDER__",
|
"TOPOLOGY_FOLDER": "__LIGHTNING_TOPOLOGY_FOLDER__",
|
||||||
"STATS_REFRESH_INTERVAL": 600,
|
"STATS_REFRESH_INTERVAL": 600,
|
||||||
"GRAPH_REFRESH_INTERVAL": 600,
|
"GRAPH_REFRESH_INTERVAL": 600,
|
||||||
"LOGGER_UPDATE_INTERVAL": 30
|
"LOGGER_UPDATE_INTERVAL": 30,
|
||||||
|
"FORENSICS_INTERVAL": 43200,
|
||||||
|
"FORENSICS_RATE_LIMIT": "__FORENSICS_RATE_LIMIT__"
|
||||||
},
|
},
|
||||||
"LND": {
|
"LND": {
|
||||||
"TLS_CERT_PATH": "",
|
"TLS_CERT_PATH": "",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ describe('Mempool Backend Config', () => {
|
|||||||
const config = jest.requireActual('../config').default;
|
const config = jest.requireActual('../config').default;
|
||||||
|
|
||||||
expect(config.MEMPOOL).toStrictEqual({
|
expect(config.MEMPOOL).toStrictEqual({
|
||||||
|
ENABLED: true,
|
||||||
NETWORK: 'mainnet',
|
NETWORK: 'mainnet',
|
||||||
BACKEND: 'none',
|
BACKEND: 'none',
|
||||||
BLOCKS_SUMMARIES_INDEXING: false,
|
BLOCKS_SUMMARIES_INDEXING: false,
|
||||||
@@ -36,7 +37,10 @@ describe('Mempool Backend Config', () => {
|
|||||||
USER_AGENT: 'mempool',
|
USER_AGENT: 'mempool',
|
||||||
STDOUT_LOG_MIN_PRIORITY: 'debug',
|
STDOUT_LOG_MIN_PRIORITY: 'debug',
|
||||||
POOLS_JSON_TREE_URL: 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
|
POOLS_JSON_TREE_URL: 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
|
||||||
POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json'
|
POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json',
|
||||||
|
ADVANCED_GBT_AUDIT: false,
|
||||||
|
ADVANCED_GBT_MEMPOOL: false,
|
||||||
|
TRANSACTION_INDEXING: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true });
|
expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true });
|
||||||
|
|||||||
138
backend/src/api/audit.ts
Normal file
138
backend/src/api/audit.ts
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import config from '../config';
|
||||||
|
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||||
|
import { Common } from './common';
|
||||||
|
import { TransactionExtended, MempoolBlockWithTransactions, AuditScore } from '../mempool.interfaces';
|
||||||
|
import blocksRepository from '../repositories/BlocksRepository';
|
||||||
|
import blocksAuditsRepository from '../repositories/BlocksAuditsRepository';
|
||||||
|
import blocks from '../api/blocks';
|
||||||
|
|
||||||
|
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 } {
|
||||||
|
if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
|
||||||
|
return { censored: [], added: [], fresh: [], score: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const matches: string[] = []; // present in both mined block and template
|
||||||
|
const added: string[] = []; // present in mined block, not in template
|
||||||
|
const fresh: string[] = []; // missing, but firstSeen within PROPAGATION_MARGIN
|
||||||
|
const isCensored = {}; // missing, without excuse
|
||||||
|
const isDisplaced = {};
|
||||||
|
let displacedWeight = 0;
|
||||||
|
|
||||||
|
const inBlock = {};
|
||||||
|
const inTemplate = {};
|
||||||
|
|
||||||
|
const now = Math.round((Date.now() / 1000));
|
||||||
|
for (const tx of transactions) {
|
||||||
|
inBlock[tx.txid] = tx;
|
||||||
|
}
|
||||||
|
// coinbase is always expected
|
||||||
|
if (transactions[0]) {
|
||||||
|
inTemplate[transactions[0].txid] = true;
|
||||||
|
}
|
||||||
|
// look for transactions that were expected in the template, but missing from the mined block
|
||||||
|
for (const txid of projectedBlocks[0].transactionIds) {
|
||||||
|
if (!inBlock[txid]) {
|
||||||
|
// tx is recent, may have reached the miner too late for inclusion
|
||||||
|
if (mempool[txid]?.firstSeen != null && (now - (mempool[txid]?.firstSeen || 0)) <= PROPAGATION_MARGIN) {
|
||||||
|
fresh.push(txid);
|
||||||
|
} else {
|
||||||
|
isCensored[txid] = true;
|
||||||
|
}
|
||||||
|
displacedWeight += mempool[txid].weight;
|
||||||
|
}
|
||||||
|
inTemplate[txid] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
displacedWeight += (4000 - transactions[0].weight);
|
||||||
|
|
||||||
|
// we can expect an honest miner to include 'displaced' transactions in place of recent arrivals and censored txs
|
||||||
|
// these displaced transactions should occupy the first N weight units of the next projected block
|
||||||
|
let displacedWeightRemaining = displacedWeight;
|
||||||
|
let index = 0;
|
||||||
|
let lastFeeRate = Infinity;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
if (mempool[txid].firstSeen == null || (now - (mempool[txid]?.firstSeen || 0)) > PROPAGATION_MARGIN) {
|
||||||
|
displacedWeightRemaining -= mempool[txid].weight;
|
||||||
|
}
|
||||||
|
failures = 0;
|
||||||
|
} else {
|
||||||
|
failures++;
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// mark unexpected transactions in the mined block as 'added'
|
||||||
|
let overflowWeight = 0;
|
||||||
|
let totalWeight = 0;
|
||||||
|
for (const tx of transactions) {
|
||||||
|
if (inTemplate[tx.txid]) {
|
||||||
|
matches.push(tx.txid);
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// transactions missing from near the end of our template are probably not being censored
|
||||||
|
let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight);
|
||||||
|
let maxOverflowRate = 0;
|
||||||
|
let rateThreshold = 0;
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
overflowWeightRemaining -= (mempool[txid]?.weight || 0);
|
||||||
|
index--;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numCensored = Object.keys(isCensored).length;
|
||||||
|
const score = matches.length > 0 ? (matches.length / (matches.length + numCensored)) : 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
censored: Object.keys(isCensored),
|
||||||
|
added,
|
||||||
|
fresh,
|
||||||
|
score
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new Audit();
|
||||||
@@ -3,13 +3,14 @@ import { IEsploraApi } from './esplora-api.interface';
|
|||||||
export interface AbstractBitcoinApi {
|
export interface AbstractBitcoinApi {
|
||||||
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>;
|
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>;
|
||||||
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>;
|
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>;
|
||||||
|
$getTransactionHex(txId: string): Promise<string>;
|
||||||
$getBlockHeightTip(): Promise<number>;
|
$getBlockHeightTip(): Promise<number>;
|
||||||
$getBlockHashTip(): Promise<string>;
|
$getBlockHashTip(): Promise<string>;
|
||||||
$getTxIdsForBlock(hash: string): Promise<string[]>;
|
$getTxIdsForBlock(hash: string): Promise<string[]>;
|
||||||
$getBlockHash(height: number): Promise<string>;
|
$getBlockHash(height: number): Promise<string>;
|
||||||
$getBlockHeader(hash: string): Promise<string>;
|
$getBlockHeader(hash: string): Promise<string>;
|
||||||
$getBlock(hash: string): Promise<IEsploraApi.Block>;
|
$getBlock(hash: string): Promise<IEsploraApi.Block>;
|
||||||
$getRawBlock(hash: string): Promise<string>;
|
$getRawBlock(hash: string): Promise<Buffer>;
|
||||||
$getAddress(address: string): Promise<IEsploraApi.Address>;
|
$getAddress(address: string): Promise<IEsploraApi.Address>;
|
||||||
$getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
|
$getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
|
||||||
$getAddressPrefix(prefix: string): string[];
|
$getAddressPrefix(prefix: string): string[];
|
||||||
|
|||||||
@@ -57,6 +57,11 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$getTransactionHex(txId: string): Promise<string> {
|
||||||
|
return this.$getRawTransaction(txId, true)
|
||||||
|
.then((tx) => tx.hex || '');
|
||||||
|
}
|
||||||
|
|
||||||
$getBlockHeightTip(): Promise<number> {
|
$getBlockHeightTip(): Promise<number> {
|
||||||
return this.bitcoindClient.getChainTips()
|
return this.bitcoindClient.getChainTips()
|
||||||
.then((result: IBitcoinApi.ChainTips[]) => {
|
.then((result: IBitcoinApi.ChainTips[]) => {
|
||||||
@@ -76,7 +81,7 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
.then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx);
|
.then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
$getRawBlock(hash: string): Promise<string> {
|
$getRawBlock(hash: string): Promise<Buffer> {
|
||||||
return this.bitcoindClient.getBlock(hash, 0)
|
return this.bitcoindClient.getBlock(hash, 0)
|
||||||
.then((raw: string) => Buffer.from(raw, "hex"));
|
.then((raw: string) => Buffer.from(raw, "hex"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Application, Request, Response } from 'express';
|
import { Application, Request, Response } from 'express';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import websocketHandler from '../websocket-handler';
|
import websocketHandler from '../websocket-handler';
|
||||||
import mempool from '../mempool';
|
import mempool from '../mempool';
|
||||||
@@ -16,13 +17,14 @@ import logger from '../../logger';
|
|||||||
import blocks from '../blocks';
|
import blocks from '../blocks';
|
||||||
import bitcoinClient from './bitcoin-client';
|
import bitcoinClient from './bitcoin-client';
|
||||||
import difficultyAdjustment from '../difficulty-adjustment';
|
import difficultyAdjustment from '../difficulty-adjustment';
|
||||||
|
import transactionRepository from '../../repositories/TransactionRepository';
|
||||||
|
|
||||||
class BitcoinRoutes {
|
class BitcoinRoutes {
|
||||||
public initRoutes(app: Application) {
|
public initRoutes(app: Application) {
|
||||||
app
|
app
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'transaction-times', this.getTransactionTimes)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'transaction-times', this.getTransactionTimes)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'outspends', this.$getBatchedOutspends)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'outspends', this.$getBatchedOutspends)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'cpfp/:txId', this.getCpfpInfo)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'cpfp/:txId', this.$getCpfpInfo)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'difficulty-adjustment', this.getDifficultyChange)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'difficulty-adjustment', this.getDifficultyChange)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/recommended', this.getRecommendedFees)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/recommended', this.getRecommendedFees)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/mempool-blocks', this.getMempoolBlocks)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/mempool-blocks', this.getMempoolBlocks)
|
||||||
@@ -87,7 +89,9 @@ class BitcoinRoutes {
|
|||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks', this.getBlocks.bind(this))
|
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks', this.getBlocks.bind(this))
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this))
|
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this))
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions);
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary)
|
||||||
|
.post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion)
|
||||||
;
|
;
|
||||||
|
|
||||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||||
@@ -185,29 +189,36 @@ class BitcoinRoutes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCpfpInfo(req: Request, res: Response) {
|
private async $getCpfpInfo(req: Request, res: Response) {
|
||||||
if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
|
if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
|
||||||
res.status(501).send(`Invalid transaction ID.`);
|
res.status(501).send(`Invalid transaction ID.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tx = mempool.getMempool()[req.params.txId];
|
const tx = mempool.getMempool()[req.params.txId];
|
||||||
if (!tx) {
|
if (tx) {
|
||||||
res.status(404).send(`Transaction doesn't exist in the mempool.`);
|
if (tx?.cpfpChecked) {
|
||||||
|
res.json({
|
||||||
|
ancestors: tx.ancestors,
|
||||||
|
bestDescendant: tx.bestDescendant || null,
|
||||||
|
descendants: tx.descendants || null,
|
||||||
|
effectiveFeePerVsize: tx.effectiveFeePerVsize || null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cpfpInfo = Common.setRelativesAndGetCpfpInfo(tx, mempool.getMempool());
|
||||||
|
|
||||||
|
res.json(cpfpInfo);
|
||||||
return;
|
return;
|
||||||
|
} else {
|
||||||
|
const cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
|
||||||
|
if (cpfpInfo) {
|
||||||
|
res.json(cpfpInfo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
res.status(404).send(`Transaction has no CPFP info available.`);
|
||||||
if (tx.cpfpChecked) {
|
|
||||||
res.json({
|
|
||||||
ancestors: tx.ancestors,
|
|
||||||
bestDescendant: tx.bestDescendant || null,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cpfpInfo = Common.setRelativesAndGetCpfpInfo(tx, mempool.getMempool());
|
|
||||||
|
|
||||||
res.json(cpfpInfo);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getBackendInfo(req: Request, res: Response) {
|
private getBackendInfo(req: Request, res: Response) {
|
||||||
@@ -241,6 +252,74 @@ class BitcoinRoutes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes the PSBT as text/plain body, parses it, and adds the full
|
||||||
|
* parent transaction to each input that doesn't already have it.
|
||||||
|
* This is used for BTCPayServer / Trezor users which need access to
|
||||||
|
* the full parent transaction even with segwit inputs.
|
||||||
|
* It will respond with a text/plain PSBT in the same format (hex|base64).
|
||||||
|
*/
|
||||||
|
private async postPsbtCompletion(req: Request, res: Response): Promise<void> {
|
||||||
|
res.setHeader('content-type', 'text/plain');
|
||||||
|
const notFoundError = `Couldn't get transaction hex for parent of input`;
|
||||||
|
try {
|
||||||
|
let psbt: bitcoinjs.Psbt;
|
||||||
|
let format: 'hex' | 'base64';
|
||||||
|
let isModified = false;
|
||||||
|
try {
|
||||||
|
psbt = bitcoinjs.Psbt.fromBase64(req.body);
|
||||||
|
format = 'base64';
|
||||||
|
} catch (e1) {
|
||||||
|
try {
|
||||||
|
psbt = bitcoinjs.Psbt.fromHex(req.body);
|
||||||
|
format = 'hex';
|
||||||
|
} catch (e2) {
|
||||||
|
throw new Error(`Unable to parse PSBT`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [index, input] of psbt.data.inputs.entries()) {
|
||||||
|
if (!input.nonWitnessUtxo) {
|
||||||
|
// Buffer.from ensures it won't be modified in place by reverse()
|
||||||
|
const txid = Buffer.from(psbt.txInputs[index].hash)
|
||||||
|
.reverse()
|
||||||
|
.toString('hex');
|
||||||
|
|
||||||
|
let transactionHex: string;
|
||||||
|
// If missing transaction, return 404 status error
|
||||||
|
try {
|
||||||
|
transactionHex = await bitcoinApi.$getTransactionHex(txid);
|
||||||
|
if (!transactionHex) {
|
||||||
|
throw new Error('');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`${notFoundError} #${index} @ ${txid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
psbt.updateInput(index, {
|
||||||
|
nonWitnessUtxo: Buffer.from(transactionHex, 'hex'),
|
||||||
|
});
|
||||||
|
if (!isModified) {
|
||||||
|
isModified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isModified) {
|
||||||
|
res.send(format === 'hex' ? psbt.toHex() : psbt.toBase64());
|
||||||
|
} else {
|
||||||
|
// Not modified
|
||||||
|
// 422 Unprocessable Entity
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422
|
||||||
|
res.status(422).send(`Psbt had no missing nonWitnessUtxos.`);
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e instanceof Error && new RegExp(notFoundError).test(e.message)) {
|
||||||
|
res.status(404).send(e.message);
|
||||||
|
} else {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async getTransactionStatus(req: Request, res: Response) {
|
private async getTransactionStatus(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true);
|
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true);
|
||||||
@@ -254,6 +333,16 @@ class BitcoinRoutes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getStrippedBlockTransactions(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const transactions = await blocks.$getStrippedBlockTransactions(req.params.hash);
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
|
||||||
|
res.json(transactions);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async getBlock(req: Request, res: Response) {
|
private async getBlock(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const block = await blocks.$getBlock(req.params.hash);
|
const block = await blocks.$getBlock(req.params.hash);
|
||||||
@@ -286,9 +375,9 @@ class BitcoinRoutes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getStrippedBlockTransactions(req: Request, res: Response) {
|
private async getBlockAuditSummary(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const transactions = await blocks.$getStrippedBlockTransactions(req.params.hash);
|
const transactions = await blocks.$getBlockAuditSummary(req.params.hash);
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
|
||||||
res.json(transactions);
|
res.json(transactions);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ class ElectrsApi implements AbstractBitcoinApi {
|
|||||||
.then((response) => response.data);
|
.then((response) => response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$getTransactionHex(txId: string): Promise<string> {
|
||||||
|
return axios.get<string>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/hex', this.axiosConfig)
|
||||||
|
.then((response) => response.data);
|
||||||
|
}
|
||||||
|
|
||||||
$getBlockHeightTip(): Promise<number> {
|
$getBlockHeightTip(): Promise<number> {
|
||||||
return axios.get<number>(config.ESPLORA.REST_API_URL + '/blocks/tip/height', this.axiosConfig)
|
return axios.get<number>(config.ESPLORA.REST_API_URL + '/blocks/tip/height', this.axiosConfig)
|
||||||
.then((response) => response.data);
|
.then((response) => response.data);
|
||||||
@@ -50,9 +55,9 @@ class ElectrsApi implements AbstractBitcoinApi {
|
|||||||
.then((response) => response.data);
|
.then((response) => response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
$getRawBlock(hash: string): Promise<string> {
|
$getRawBlock(hash: string): Promise<Buffer> {
|
||||||
return axios.get<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + "/raw", this.axiosConfig)
|
return axios.get<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + "/raw", { ...this.axiosConfig, responseType: 'arraybuffer' })
|
||||||
.then((response) => response.data);
|
.then((response) => { return Buffer.from(response.data); });
|
||||||
}
|
}
|
||||||
|
|
||||||
$getAddress(address: string): Promise<IEsploraApi.Address> {
|
$getAddress(address: string): Promise<IEsploraApi.Address> {
|
||||||
|
|||||||
@@ -20,10 +20,14 @@ import indexer from '../indexer';
|
|||||||
import fiatConversion from './fiat-conversion';
|
import fiatConversion from './fiat-conversion';
|
||||||
import poolsParser from './pools-parser';
|
import poolsParser from './pools-parser';
|
||||||
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
|
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
|
||||||
|
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
|
||||||
|
import cpfpRepository from '../repositories/CpfpRepository';
|
||||||
|
import transactionRepository from '../repositories/TransactionRepository';
|
||||||
import mining from './mining/mining';
|
import mining from './mining/mining';
|
||||||
import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmentsRepository';
|
import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmentsRepository';
|
||||||
import PricesRepository from '../repositories/PricesRepository';
|
import PricesRepository from '../repositories/PricesRepository';
|
||||||
import priceUpdater from '../tasks/price-updater';
|
import priceUpdater from '../tasks/price-updater';
|
||||||
|
import { Block } from 'bitcoinjs-lib';
|
||||||
|
|
||||||
class Blocks {
|
class Blocks {
|
||||||
private blocks: BlockExtended[] = [];
|
private blocks: BlockExtended[] = [];
|
||||||
@@ -33,6 +37,7 @@ class Blocks {
|
|||||||
private lastDifficultyAdjustmentTime = 0;
|
private lastDifficultyAdjustmentTime = 0;
|
||||||
private previousDifficultyRetarget = 0;
|
private previousDifficultyRetarget = 0;
|
||||||
private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
|
private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
|
||||||
|
private newAsyncBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => Promise<void>)[] = [];
|
||||||
|
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
@@ -56,6 +61,10 @@ class Blocks {
|
|||||||
this.newBlockCallbacks.push(fn);
|
this.newBlockCallbacks.push(fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setNewAsyncBlockCallback(fn: (block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => Promise<void>) {
|
||||||
|
this.newAsyncBlockCallbacks.push(fn);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the list of transaction for a block
|
* Return the list of transaction for a block
|
||||||
* @param blockHash
|
* @param blockHash
|
||||||
@@ -129,7 +138,7 @@ class Blocks {
|
|||||||
const stripped = block.tx.map((tx) => {
|
const stripped = block.tx.map((tx) => {
|
||||||
return {
|
return {
|
||||||
txid: tx.txid,
|
txid: tx.txid,
|
||||||
vsize: tx.vsize,
|
vsize: tx.weight / 4,
|
||||||
fee: tx.fee ? Math.round(tx.fee * 100000000) : 0,
|
fee: tx.fee ? Math.round(tx.fee * 100000000) : 0,
|
||||||
value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0) * 100000000)
|
value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0) * 100000000)
|
||||||
};
|
};
|
||||||
@@ -186,14 +195,18 @@ class Blocks {
|
|||||||
if (!pool) { // We should never have this situation in practise
|
if (!pool) { // We should never have this situation in practise
|
||||||
logger.warn(`Cannot assign pool to block ${blockExtended.height} and 'unknown' pool does not exist. ` +
|
logger.warn(`Cannot assign pool to block ${blockExtended.height} and 'unknown' pool does not exist. ` +
|
||||||
`Check your "pools" table entries`);
|
`Check your "pools" table entries`);
|
||||||
return blockExtended;
|
} else {
|
||||||
|
blockExtended.extras.pool = {
|
||||||
|
id: pool.id,
|
||||||
|
name: pool.name,
|
||||||
|
slug: pool.slug,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
blockExtended.extras.pool = {
|
const auditScore = await BlocksAuditsRepository.$getBlockAuditScore(block.id);
|
||||||
id: pool.id,
|
if (auditScore != null) {
|
||||||
name: pool.name,
|
blockExtended.extras.matchRate = auditScore.matchRate;
|
||||||
slug: pool.slug,
|
}
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return blockExtended;
|
return blockExtended;
|
||||||
@@ -250,7 +263,7 @@ class Blocks {
|
|||||||
/**
|
/**
|
||||||
* [INDEXING] Index all blocks summaries for the block txs visualization
|
* [INDEXING] Index all blocks summaries for the block txs visualization
|
||||||
*/
|
*/
|
||||||
public async $generateBlocksSummariesDatabase() {
|
public async $generateBlocksSummariesDatabase(): Promise<void> {
|
||||||
if (Common.blocksSummariesIndexingEnabled() === false) {
|
if (Common.blocksSummariesIndexingEnabled() === false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -306,6 +319,57 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [INDEXING] Index transaction CPFP data for all blocks
|
||||||
|
*/
|
||||||
|
public async $generateCPFPDatabase(): Promise<void> {
|
||||||
|
if (Common.cpfpIndexingEnabled() === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get all indexed block hash
|
||||||
|
const unindexedBlocks = await blocksRepository.$getCPFPUnindexedBlocks();
|
||||||
|
|
||||||
|
if (!unindexedBlocks?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
let count = 0;
|
||||||
|
let countThisRun = 0;
|
||||||
|
let timer = new Date().getTime() / 1000;
|
||||||
|
const startedAt = new Date().getTime() / 1000;
|
||||||
|
|
||||||
|
for (const block of unindexedBlocks) {
|
||||||
|
// Logging
|
||||||
|
const elapsedSeconds = Math.max(1, new Date().getTime() / 1000 - timer);
|
||||||
|
if (elapsedSeconds > 5) {
|
||||||
|
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
||||||
|
const blockPerSeconds = Math.max(1, countThisRun / elapsedSeconds);
|
||||||
|
const progress = Math.round(count / unindexedBlocks.length * 10000) / 100;
|
||||||
|
logger.debug(`Indexing cpfp clusters for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${count}/${unindexedBlocks.length} (${progress}%) | elapsed: ${runningFor} seconds`);
|
||||||
|
timer = new Date().getTime() / 1000;
|
||||||
|
countThisRun = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.$indexCPFP(block.hash, block.height); // Calculate and save CPFP data for transactions in this block
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
count++;
|
||||||
|
countThisRun++;
|
||||||
|
}
|
||||||
|
if (count > 0) {
|
||||||
|
logger.notice(`CPFP indexing completed: indexed ${count} blocks`);
|
||||||
|
} else {
|
||||||
|
logger.debug(`CPFP indexing completed: indexed ${count} blocks`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`CPFP indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [INDEXING] Index all blocks metadata for the mining dashboard
|
* [INDEXING] Index all blocks metadata for the mining dashboard
|
||||||
*/
|
*/
|
||||||
@@ -349,7 +413,7 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
++indexedThisRun;
|
++indexedThisRun;
|
||||||
++totalIndexed;
|
++totalIndexed;
|
||||||
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
|
const elapsedSeconds = Math.max(1, new Date().getTime() / 1000 - timer);
|
||||||
if (elapsedSeconds > 5 || blockHeight === lastBlockToIndex) {
|
if (elapsedSeconds > 5 || blockHeight === lastBlockToIndex) {
|
||||||
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
||||||
const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds);
|
const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds);
|
||||||
@@ -439,6 +503,9 @@ class Blocks {
|
|||||||
const blockExtended: BlockExtended = await this.$getBlockExtended(block, transactions);
|
const blockExtended: BlockExtended = await this.$getBlockExtended(block, transactions);
|
||||||
const blockSummary: BlockSummary = this.summarizeBlock(verboseBlock);
|
const blockSummary: BlockSummary = this.summarizeBlock(verboseBlock);
|
||||||
|
|
||||||
|
// start async callbacks
|
||||||
|
const callbackPromises = this.newAsyncBlockCallbacks.map((cb) => cb(blockExtended, txIds, transactions));
|
||||||
|
|
||||||
if (Common.indexingEnabled()) {
|
if (Common.indexingEnabled()) {
|
||||||
if (!fastForwarded) {
|
if (!fastForwarded) {
|
||||||
const lastBlock = await blocksRepository.$getBlockByHeight(blockExtended.height - 1);
|
const lastBlock = await blocksRepository.$getBlockByHeight(blockExtended.height - 1);
|
||||||
@@ -448,9 +515,13 @@ class Blocks {
|
|||||||
await BlocksRepository.$deleteBlocksFrom(lastBlock['height'] - 10);
|
await BlocksRepository.$deleteBlocksFrom(lastBlock['height'] - 10);
|
||||||
await HashratesRepository.$deleteLastEntries();
|
await HashratesRepository.$deleteLastEntries();
|
||||||
await BlocksSummariesRepository.$deleteBlocksFrom(lastBlock['height'] - 10);
|
await BlocksSummariesRepository.$deleteBlocksFrom(lastBlock['height'] - 10);
|
||||||
|
await cpfpRepository.$deleteClustersFrom(lastBlock['height'] - 10);
|
||||||
for (let i = 10; i >= 0; --i) {
|
for (let i = 10; i >= 0; --i) {
|
||||||
const newBlock = await this.$indexBlock(lastBlock['height'] - i);
|
const newBlock = await this.$indexBlock(lastBlock['height'] - i);
|
||||||
await this.$getStrippedBlockTransactions(newBlock.id, true, true);
|
await this.$getStrippedBlockTransactions(newBlock.id, true, true);
|
||||||
|
if (config.MEMPOOL.TRANSACTION_INDEXING) {
|
||||||
|
await this.$indexCPFP(newBlock.id, lastBlock['height'] - i);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await mining.$indexDifficultyAdjustments();
|
await mining.$indexDifficultyAdjustments();
|
||||||
await DifficultyAdjustmentsRepository.$deleteLastAdjustment();
|
await DifficultyAdjustmentsRepository.$deleteLastAdjustment();
|
||||||
@@ -476,6 +547,9 @@ class Blocks {
|
|||||||
if (Common.blocksSummariesIndexingEnabled() === true) {
|
if (Common.blocksSummariesIndexingEnabled() === true) {
|
||||||
await this.$getStrippedBlockTransactions(blockExtended.id, true);
|
await this.$getStrippedBlockTransactions(blockExtended.id, true);
|
||||||
}
|
}
|
||||||
|
if (config.MEMPOOL.TRANSACTION_INDEXING) {
|
||||||
|
this.$indexCPFP(blockExtended.id, this.currentBlockHeight);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -509,6 +583,9 @@ class Blocks {
|
|||||||
if (!memPool.hasPriority()) {
|
if (!memPool.hasPriority()) {
|
||||||
diskCache.$saveCacheToDisk();
|
diskCache.$saveCacheToDisk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wait for pending async callbacks to finish
|
||||||
|
await Promise.all(callbackPromises);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -574,7 +651,7 @@ class Blocks {
|
|||||||
if (skipMemoryCache === false) {
|
if (skipMemoryCache === false) {
|
||||||
// Check the memory cache
|
// Check the memory cache
|
||||||
const cachedSummary = this.getBlockSummaries().find((b) => b.id === hash);
|
const cachedSummary = this.getBlockSummaries().find((b) => b.id === hash);
|
||||||
if (cachedSummary) {
|
if (cachedSummary?.transactions?.length) {
|
||||||
return cachedSummary.transactions;
|
return cachedSummary.transactions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -582,7 +659,7 @@ class Blocks {
|
|||||||
// Check if it's indexed in db
|
// Check if it's indexed in db
|
||||||
if (skipDBLookup === false && Common.blocksSummariesIndexingEnabled() === true) {
|
if (skipDBLookup === false && Common.blocksSummariesIndexingEnabled() === true) {
|
||||||
const indexedSummary = await BlocksSummariesRepository.$getByBlockId(hash);
|
const indexedSummary = await BlocksSummariesRepository.$getByBlockId(hash);
|
||||||
if (indexedSummary !== undefined) {
|
if (indexedSummary !== undefined && indexedSummary?.transactions?.length) {
|
||||||
return indexedSummary.transactions;
|
return indexedSummary.transactions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -635,6 +712,22 @@ class Blocks {
|
|||||||
return returnBlocks;
|
return returnBlocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $getBlockAuditSummary(hash: string): Promise<any> {
|
||||||
|
let summary;
|
||||||
|
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
|
||||||
|
summary = await BlocksAuditsRepository.$getBlockAudit(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback to non-audited transaction summary
|
||||||
|
if (!summary?.transactions?.length) {
|
||||||
|
const strippedTransactions = await this.$getStrippedBlockTransactions(hash);
|
||||||
|
summary = {
|
||||||
|
transactions: strippedTransactions
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
public getLastDifficultyAdjustmentTime(): number {
|
public getLastDifficultyAdjustmentTime(): number {
|
||||||
return this.lastDifficultyAdjustmentTime;
|
return this.lastDifficultyAdjustmentTime;
|
||||||
}
|
}
|
||||||
@@ -646,6 +739,62 @@ class Blocks {
|
|||||||
public getCurrentBlockHeight(): number {
|
public getCurrentBlockHeight(): number {
|
||||||
return this.currentBlockHeight;
|
return this.currentBlockHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $indexCPFP(hash: string, height: number): Promise<void> {
|
||||||
|
let transactions;
|
||||||
|
if (false/*Common.blocksSummariesIndexingEnabled()*/) {
|
||||||
|
transactions = await this.$getStrippedBlockTransactions(hash);
|
||||||
|
const rawBlock = await bitcoinApi.$getRawBlock(hash);
|
||||||
|
const block = Block.fromBuffer(rawBlock);
|
||||||
|
const txMap = {};
|
||||||
|
for (const tx of block.transactions || []) {
|
||||||
|
txMap[tx.getId()] = tx;
|
||||||
|
}
|
||||||
|
for (const tx of transactions) {
|
||||||
|
if (txMap[tx.txid]?.ins) {
|
||||||
|
tx.vin = txMap[tx.txid].ins.map(vin => {
|
||||||
|
return {
|
||||||
|
txid: vin.hash
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const block = await bitcoinClient.getBlock(hash, 2);
|
||||||
|
transactions = block.tx.map(tx => {
|
||||||
|
tx.vsize = tx.weight / 4;
|
||||||
|
return tx;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let cluster: TransactionStripped[] = [];
|
||||||
|
let ancestors: { [txid: string]: boolean } = {};
|
||||||
|
for (let i = transactions.length - 1; i >= 0; i--) {
|
||||||
|
const tx = transactions[i];
|
||||||
|
if (!ancestors[tx.txid]) {
|
||||||
|
let totalFee = 0;
|
||||||
|
let totalVSize = 0;
|
||||||
|
cluster.forEach(tx => {
|
||||||
|
totalFee += tx?.fee || 0;
|
||||||
|
totalVSize += tx.vsize;
|
||||||
|
});
|
||||||
|
const effectiveFeePerVsize = (totalFee * 100_000_000) / totalVSize;
|
||||||
|
if (cluster.length > 1) {
|
||||||
|
await cpfpRepository.$saveCluster(height, cluster.map(tx => { return { txid: tx.txid, weight: tx.vsize * 4, fee: (tx.fee || 0) * 100_000_000 }; }), effectiveFeePerVsize);
|
||||||
|
for (const tx of cluster) {
|
||||||
|
await transactionRepository.$setCluster(tx.txid, cluster[0].txid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cluster = [];
|
||||||
|
ancestors = {};
|
||||||
|
}
|
||||||
|
cluster.push(tx);
|
||||||
|
tx.vin.forEach(vin => {
|
||||||
|
ancestors[vin.txid] = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await blocksRepository.$setCPFPIndexed(hash);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Blocks();
|
export default new Blocks();
|
||||||
|
|||||||
@@ -187,6 +187,13 @@ export class Common {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static cpfpIndexingEnabled(): boolean {
|
||||||
|
return (
|
||||||
|
Common.indexingEnabled() &&
|
||||||
|
config.MEMPOOL.TRANSACTION_INDEXING === true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static setDateMidnight(date: Date): void {
|
static setDateMidnight(date: Date): void {
|
||||||
date.setUTCHours(0);
|
date.setUTCHours(0);
|
||||||
date.setUTCMinutes(0);
|
date.setUTCMinutes(0);
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import logger from '../logger';
|
|||||||
import { Common } from './common';
|
import { Common } from './common';
|
||||||
|
|
||||||
class DatabaseMigration {
|
class DatabaseMigration {
|
||||||
private static currentVersion = 40;
|
private static currentVersion = 49;
|
||||||
private queryTimeout = 120000;
|
private queryTimeout = 3600_000;
|
||||||
private statisticsAddedIndexed = false;
|
private statisticsAddedIndexed = false;
|
||||||
private uniqueLogs: string[] = [];
|
private uniqueLogs: string[] = [];
|
||||||
|
|
||||||
@@ -107,18 +107,22 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery(this.getCreateStatisticsQuery(), await this.$checkIfTableExists('statistics'));
|
await this.$executeQuery(this.getCreateStatisticsQuery(), await this.$checkIfTableExists('statistics'));
|
||||||
if (databaseSchemaVersion < 2 && this.statisticsAddedIndexed === false) {
|
if (databaseSchemaVersion < 2 && this.statisticsAddedIndexed === false) {
|
||||||
await this.$executeQuery(`CREATE INDEX added ON statistics (added);`);
|
await this.$executeQuery(`CREATE INDEX added ON statistics (added);`);
|
||||||
|
await this.updateToSchemaVersion(2);
|
||||||
}
|
}
|
||||||
if (databaseSchemaVersion < 3) {
|
if (databaseSchemaVersion < 3) {
|
||||||
await this.$executeQuery(this.getCreatePoolsTableQuery(), await this.$checkIfTableExists('pools'));
|
await this.$executeQuery(this.getCreatePoolsTableQuery(), await this.$checkIfTableExists('pools'));
|
||||||
|
await this.updateToSchemaVersion(3);
|
||||||
}
|
}
|
||||||
if (databaseSchemaVersion < 4) {
|
if (databaseSchemaVersion < 4) {
|
||||||
await this.$executeQuery('DROP table IF EXISTS blocks;');
|
await this.$executeQuery('DROP table IF EXISTS blocks;');
|
||||||
await this.$executeQuery(this.getCreateBlocksTableQuery(), await this.$checkIfTableExists('blocks'));
|
await this.$executeQuery(this.getCreateBlocksTableQuery(), await this.$checkIfTableExists('blocks'));
|
||||||
|
await this.updateToSchemaVersion(4);
|
||||||
}
|
}
|
||||||
if (databaseSchemaVersion < 5 && isBitcoin === true) {
|
if (databaseSchemaVersion < 5 && isBitcoin === true) {
|
||||||
this.uniqueLog(logger.notice, this.blocksTruncatedMessage);
|
this.uniqueLog(logger.notice, this.blocksTruncatedMessage);
|
||||||
await this.$executeQuery('TRUNCATE blocks;'); // Need to re-index
|
await this.$executeQuery('TRUNCATE blocks;'); // Need to re-index
|
||||||
await this.$executeQuery('ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"');
|
||||||
|
await this.updateToSchemaVersion(5);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 6 && isBitcoin === true) {
|
if (databaseSchemaVersion < 6 && isBitcoin === true) {
|
||||||
@@ -141,11 +145,13 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('ALTER TABLE blocks ADD `nonce` bigint unsigned NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE blocks ADD `nonce` bigint unsigned NOT NULL DEFAULT "0"');
|
||||||
await this.$executeQuery('ALTER TABLE blocks ADD `merkle_root` varchar(65) NOT NULL DEFAULT ""');
|
await this.$executeQuery('ALTER TABLE blocks ADD `merkle_root` varchar(65) NOT NULL DEFAULT ""');
|
||||||
await this.$executeQuery('ALTER TABLE blocks ADD `previous_block_hash` varchar(65) NULL');
|
await this.$executeQuery('ALTER TABLE blocks ADD `previous_block_hash` varchar(65) NULL');
|
||||||
|
await this.updateToSchemaVersion(6);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 7 && isBitcoin === true) {
|
if (databaseSchemaVersion < 7 && isBitcoin === true) {
|
||||||
await this.$executeQuery('DROP table IF EXISTS hashrates;');
|
await this.$executeQuery('DROP table IF EXISTS hashrates;');
|
||||||
await this.$executeQuery(this.getCreateDailyStatsTableQuery(), await this.$checkIfTableExists('hashrates'));
|
await this.$executeQuery(this.getCreateDailyStatsTableQuery(), await this.$checkIfTableExists('hashrates'));
|
||||||
|
await this.updateToSchemaVersion(7);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 8 && isBitcoin === true) {
|
if (databaseSchemaVersion < 8 && isBitcoin === true) {
|
||||||
@@ -155,6 +161,7 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('ALTER TABLE `hashrates` ADD `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST');
|
await this.$executeQuery('ALTER TABLE `hashrates` ADD `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST');
|
||||||
await this.$executeQuery('ALTER TABLE `hashrates` ADD `share` float NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE `hashrates` ADD `share` float NOT NULL DEFAULT "0"');
|
||||||
await this.$executeQuery('ALTER TABLE `hashrates` ADD `type` enum("daily", "weekly") DEFAULT "daily"');
|
await this.$executeQuery('ALTER TABLE `hashrates` ADD `type` enum("daily", "weekly") DEFAULT "daily"');
|
||||||
|
await this.updateToSchemaVersion(8);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 9 && isBitcoin === true) {
|
if (databaseSchemaVersion < 9 && isBitcoin === true) {
|
||||||
@@ -162,10 +169,12 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index
|
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index
|
||||||
await this.$executeQuery('ALTER TABLE `state` CHANGE `name` `name` varchar(100)');
|
await this.$executeQuery('ALTER TABLE `state` CHANGE `name` `name` varchar(100)');
|
||||||
await this.$executeQuery('ALTER TABLE `hashrates` ADD UNIQUE `hashrate_timestamp_pool_id` (`hashrate_timestamp`, `pool_id`)');
|
await this.$executeQuery('ALTER TABLE `hashrates` ADD UNIQUE `hashrate_timestamp_pool_id` (`hashrate_timestamp`, `pool_id`)');
|
||||||
|
await this.updateToSchemaVersion(9);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 10 && isBitcoin === true) {
|
if (databaseSchemaVersion < 10 && isBitcoin === true) {
|
||||||
await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `blockTimestamp` (`blockTimestamp`)');
|
await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `blockTimestamp` (`blockTimestamp`)');
|
||||||
|
await this.updateToSchemaVersion(10);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 11 && isBitcoin === true) {
|
if (databaseSchemaVersion < 11 && isBitcoin === true) {
|
||||||
@@ -178,11 +187,13 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('ALTER TABLE blocks MODIFY `reward` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE blocks MODIFY `reward` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
|
||||||
await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` INT UNSIGNED NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` INT UNSIGNED NOT NULL DEFAULT "0"');
|
||||||
await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` INT UNSIGNED NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` INT UNSIGNED NOT NULL DEFAULT "0"');
|
||||||
|
await this.updateToSchemaVersion(11);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 12 && isBitcoin === true) {
|
if (databaseSchemaVersion < 12 && isBitcoin === true) {
|
||||||
// No need to re-index because the new data type can contain larger values
|
// No need to re-index because the new data type can contain larger values
|
||||||
await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
|
||||||
|
await this.updateToSchemaVersion(12);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 13 && isBitcoin === true) {
|
if (databaseSchemaVersion < 13 && isBitcoin === true) {
|
||||||
@@ -190,6 +201,7 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
|
||||||
await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
|
||||||
await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee_rate` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee_rate` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
|
||||||
|
await this.updateToSchemaVersion(13);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 14 && isBitcoin === true) {
|
if (databaseSchemaVersion < 14 && isBitcoin === true) {
|
||||||
@@ -197,37 +209,45 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index
|
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index
|
||||||
await this.$executeQuery('ALTER TABLE `hashrates` DROP FOREIGN KEY `hashrates_ibfk_1`');
|
await this.$executeQuery('ALTER TABLE `hashrates` DROP FOREIGN KEY `hashrates_ibfk_1`');
|
||||||
await this.$executeQuery('ALTER TABLE `hashrates` MODIFY `pool_id` SMALLINT UNSIGNED NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE `hashrates` MODIFY `pool_id` SMALLINT UNSIGNED NOT NULL DEFAULT "0"');
|
||||||
|
await this.updateToSchemaVersion(14);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 16 && isBitcoin === true) {
|
if (databaseSchemaVersion < 16 && isBitcoin === true) {
|
||||||
this.uniqueLog(logger.notice, this.hashratesTruncatedMessage);
|
this.uniqueLog(logger.notice, this.hashratesTruncatedMessage);
|
||||||
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index because we changed timestamps
|
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index because we changed timestamps
|
||||||
|
await this.updateToSchemaVersion(16);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 17 && isBitcoin === true) {
|
if (databaseSchemaVersion < 17 && isBitcoin === true) {
|
||||||
await this.$executeQuery('ALTER TABLE `pools` ADD `slug` CHAR(50) NULL');
|
await this.$executeQuery('ALTER TABLE `pools` ADD `slug` CHAR(50) NULL');
|
||||||
|
await this.updateToSchemaVersion(17);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 18 && isBitcoin === true) {
|
if (databaseSchemaVersion < 18 && isBitcoin === true) {
|
||||||
await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `hash` (`hash`);');
|
await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `hash` (`hash`);');
|
||||||
|
await this.updateToSchemaVersion(18);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 19) {
|
if (databaseSchemaVersion < 19) {
|
||||||
await this.$executeQuery(this.getCreateRatesTableQuery(), await this.$checkIfTableExists('rates'));
|
await this.$executeQuery(this.getCreateRatesTableQuery(), await this.$checkIfTableExists('rates'));
|
||||||
|
await this.updateToSchemaVersion(19);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 20 && isBitcoin === true) {
|
if (databaseSchemaVersion < 20 && isBitcoin === true) {
|
||||||
await this.$executeQuery(this.getCreateBlocksSummariesTableQuery(), await this.$checkIfTableExists('blocks_summaries'));
|
await this.$executeQuery(this.getCreateBlocksSummariesTableQuery(), await this.$checkIfTableExists('blocks_summaries'));
|
||||||
|
await this.updateToSchemaVersion(20);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 21) {
|
if (databaseSchemaVersion < 21) {
|
||||||
await this.$executeQuery('DROP TABLE IF EXISTS `rates`');
|
await this.$executeQuery('DROP TABLE IF EXISTS `rates`');
|
||||||
await this.$executeQuery(this.getCreatePricesTableQuery(), await this.$checkIfTableExists('prices'));
|
await this.$executeQuery(this.getCreatePricesTableQuery(), await this.$checkIfTableExists('prices'));
|
||||||
|
await this.updateToSchemaVersion(21);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 22 && isBitcoin === true) {
|
if (databaseSchemaVersion < 22 && isBitcoin === true) {
|
||||||
await this.$executeQuery('DROP TABLE IF EXISTS `difficulty_adjustments`');
|
await this.$executeQuery('DROP TABLE IF EXISTS `difficulty_adjustments`');
|
||||||
await this.$executeQuery(this.getCreateDifficultyAdjustmentsTableQuery(), await this.$checkIfTableExists('difficulty_adjustments'));
|
await this.$executeQuery(this.getCreateDifficultyAdjustmentsTableQuery(), await this.$checkIfTableExists('difficulty_adjustments'));
|
||||||
|
await this.updateToSchemaVersion(22);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 23) {
|
if (databaseSchemaVersion < 23) {
|
||||||
@@ -240,11 +260,13 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('ALTER TABLE `prices` ADD `CHF` float DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE `prices` ADD `CHF` float DEFAULT "0"');
|
||||||
await this.$executeQuery('ALTER TABLE `prices` ADD `AUD` float DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE `prices` ADD `AUD` float DEFAULT "0"');
|
||||||
await this.$executeQuery('ALTER TABLE `prices` ADD `JPY` float DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE `prices` ADD `JPY` float DEFAULT "0"');
|
||||||
|
await this.updateToSchemaVersion(23);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 24 && isBitcoin == true) {
|
if (databaseSchemaVersion < 24 && isBitcoin == true) {
|
||||||
await this.$executeQuery('DROP TABLE IF EXISTS `blocks_audits`');
|
await this.$executeQuery('DROP TABLE IF EXISTS `blocks_audits`');
|
||||||
await this.$executeQuery(this.getCreateBlocksAuditsTableQuery(), await this.$checkIfTableExists('blocks_audits'));
|
await this.$executeQuery(this.getCreateBlocksAuditsTableQuery(), await this.$checkIfTableExists('blocks_audits'));
|
||||||
|
await this.updateToSchemaVersion(24);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 25 && isBitcoin === true) {
|
if (databaseSchemaVersion < 25 && isBitcoin === true) {
|
||||||
@@ -252,6 +274,7 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery(this.getCreateNodesQuery(), await this.$checkIfTableExists('nodes'));
|
await this.$executeQuery(this.getCreateNodesQuery(), await this.$checkIfTableExists('nodes'));
|
||||||
await this.$executeQuery(this.getCreateChannelsQuery(), await this.$checkIfTableExists('channels'));
|
await this.$executeQuery(this.getCreateChannelsQuery(), await this.$checkIfTableExists('channels'));
|
||||||
await this.$executeQuery(this.getCreateNodesStatsQuery(), await this.$checkIfTableExists('node_stats'));
|
await this.$executeQuery(this.getCreateNodesStatsQuery(), await this.$checkIfTableExists('node_stats'));
|
||||||
|
await this.updateToSchemaVersion(25);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 26 && isBitcoin === true) {
|
if (databaseSchemaVersion < 26 && isBitcoin === true) {
|
||||||
@@ -262,6 +285,7 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD tor_nodes int(11) NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD tor_nodes int(11) NOT NULL DEFAULT "0"');
|
||||||
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_nodes int(11) NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_nodes int(11) NOT NULL DEFAULT "0"');
|
||||||
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD unannounced_nodes int(11) NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD unannounced_nodes int(11) NOT NULL DEFAULT "0"');
|
||||||
|
await this.updateToSchemaVersion(26);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 27 && isBitcoin === true) {
|
if (databaseSchemaVersion < 27 && isBitcoin === true) {
|
||||||
@@ -271,6 +295,7 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_capacity bigint(20) unsigned NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_capacity bigint(20) unsigned NOT NULL DEFAULT "0"');
|
||||||
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_fee_rate int(11) unsigned NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_fee_rate int(11) unsigned NOT NULL DEFAULT "0"');
|
||||||
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_base_fee_mtokens bigint(20) unsigned NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_base_fee_mtokens bigint(20) unsigned NOT NULL DEFAULT "0"');
|
||||||
|
await this.updateToSchemaVersion(27);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 28 && isBitcoin === true) {
|
if (databaseSchemaVersion < 28 && isBitcoin === true) {
|
||||||
@@ -280,6 +305,7 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery(`TRUNCATE lightning_stats`);
|
await this.$executeQuery(`TRUNCATE lightning_stats`);
|
||||||
await this.$executeQuery(`TRUNCATE node_stats`);
|
await this.$executeQuery(`TRUNCATE node_stats`);
|
||||||
await this.$executeQuery(`ALTER TABLE lightning_stats MODIFY added DATE`);
|
await this.$executeQuery(`ALTER TABLE lightning_stats MODIFY added DATE`);
|
||||||
|
await this.updateToSchemaVersion(28);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 29 && isBitcoin === true) {
|
if (databaseSchemaVersion < 29 && isBitcoin === true) {
|
||||||
@@ -291,41 +317,50 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('ALTER TABLE `nodes` ADD subdivision_id int(11) unsigned NULL DEFAULT NULL');
|
await this.$executeQuery('ALTER TABLE `nodes` ADD subdivision_id int(11) unsigned NULL DEFAULT NULL');
|
||||||
await this.$executeQuery('ALTER TABLE `nodes` ADD longitude double NULL DEFAULT NULL');
|
await this.$executeQuery('ALTER TABLE `nodes` ADD longitude double NULL DEFAULT NULL');
|
||||||
await this.$executeQuery('ALTER TABLE `nodes` ADD latitude double NULL DEFAULT NULL');
|
await this.$executeQuery('ALTER TABLE `nodes` ADD latitude double NULL DEFAULT NULL');
|
||||||
|
await this.updateToSchemaVersion(29);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 30 && isBitcoin === true) {
|
if (databaseSchemaVersion < 30 && isBitcoin === true) {
|
||||||
await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization") NOT NULL');
|
await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization") NOT NULL');
|
||||||
|
await this.updateToSchemaVersion(30);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 31 && isBitcoin == true) { // Link blocks to prices
|
if (databaseSchemaVersion < 31 && isBitcoin == true) { // Link blocks to prices
|
||||||
await this.$executeQuery('ALTER TABLE `prices` ADD `id` int NULL AUTO_INCREMENT UNIQUE');
|
await this.$executeQuery('ALTER TABLE `prices` ADD `id` int NULL AUTO_INCREMENT UNIQUE');
|
||||||
await this.$executeQuery('DROP TABLE IF EXISTS `blocks_prices`');
|
await this.$executeQuery('DROP TABLE IF EXISTS `blocks_prices`');
|
||||||
await this.$executeQuery(this.getCreateBlocksPricesTableQuery(), await this.$checkIfTableExists('blocks_prices'));
|
await this.$executeQuery(this.getCreateBlocksPricesTableQuery(), await this.$checkIfTableExists('blocks_prices'));
|
||||||
|
await this.updateToSchemaVersion(31);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 32 && isBitcoin == true) {
|
if (databaseSchemaVersion < 32 && isBitcoin == true) {
|
||||||
await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD `template` JSON DEFAULT "[]"');
|
await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD `template` JSON DEFAULT "[]"');
|
||||||
|
await this.updateToSchemaVersion(32);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 33 && isBitcoin == true) {
|
if (databaseSchemaVersion < 33 && isBitcoin == true) {
|
||||||
await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization", "country_iso_code") NOT NULL');
|
await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization", "country_iso_code") NOT NULL');
|
||||||
|
await this.updateToSchemaVersion(33);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 34 && isBitcoin == true) {
|
if (databaseSchemaVersion < 34 && isBitcoin == true) {
|
||||||
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_tor_nodes int(11) NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_tor_nodes int(11) NOT NULL DEFAULT "0"');
|
||||||
|
await this.updateToSchemaVersion(34);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 35 && isBitcoin == true) {
|
if (databaseSchemaVersion < 35 && isBitcoin == true) {
|
||||||
await this.$executeQuery('DELETE from `lightning_stats` WHERE added > "2021-09-19"');
|
await this.$executeQuery('DELETE from `lightning_stats` WHERE added > "2021-09-19"');
|
||||||
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD CONSTRAINT added_unique UNIQUE (added);');
|
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD CONSTRAINT added_unique UNIQUE (added);');
|
||||||
|
await this.updateToSchemaVersion(35);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 36 && isBitcoin == true) {
|
if (databaseSchemaVersion < 36 && isBitcoin == true) {
|
||||||
await this.$executeQuery('ALTER TABLE `nodes` ADD status TINYINT NOT NULL DEFAULT "1"');
|
await this.$executeQuery('ALTER TABLE `nodes` ADD status TINYINT NOT NULL DEFAULT "1"');
|
||||||
|
await this.updateToSchemaVersion(36);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 37 && isBitcoin == true) {
|
if (databaseSchemaVersion < 37 && isBitcoin == true) {
|
||||||
await this.$executeQuery(this.getCreateLNNodesSocketsTableQuery(), await this.$checkIfTableExists('nodes_sockets'));
|
await this.$executeQuery(this.getCreateLNNodesSocketsTableQuery(), await this.$checkIfTableExists('nodes_sockets'));
|
||||||
|
await this.updateToSchemaVersion(37);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 38 && isBitcoin == true) {
|
if (databaseSchemaVersion < 38 && isBitcoin == true) {
|
||||||
@@ -336,17 +371,76 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery(`TRUNCATE node_stats`);
|
await this.$executeQuery(`TRUNCATE node_stats`);
|
||||||
await this.$executeQuery('ALTER TABLE `lightning_stats` CHANGE `added` `added` timestamp NULL');
|
await this.$executeQuery('ALTER TABLE `lightning_stats` CHANGE `added` `added` timestamp NULL');
|
||||||
await this.$executeQuery('ALTER TABLE `node_stats` CHANGE `added` `added` timestamp NULL');
|
await this.$executeQuery('ALTER TABLE `node_stats` CHANGE `added` `added` timestamp NULL');
|
||||||
|
await this.updateToSchemaVersion(38);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 39 && isBitcoin === true) {
|
if (databaseSchemaVersion < 39 && isBitcoin === true) {
|
||||||
await this.$executeQuery('ALTER TABLE `nodes` ADD alias_search TEXT NULL DEFAULT NULL AFTER `alias`');
|
await this.$executeQuery('ALTER TABLE `nodes` ADD alias_search TEXT NULL DEFAULT NULL AFTER `alias`');
|
||||||
await this.$executeQuery('ALTER TABLE nodes ADD FULLTEXT(alias_search)');
|
await this.$executeQuery('ALTER TABLE nodes ADD FULLTEXT(alias_search)');
|
||||||
|
await this.updateToSchemaVersion(39);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 40 && isBitcoin === true) {
|
if (databaseSchemaVersion < 40 && isBitcoin === true) {
|
||||||
await this.$executeQuery('ALTER TABLE `nodes` ADD capacity bigint(20) unsigned DEFAULT NULL');
|
await this.$executeQuery('ALTER TABLE `nodes` ADD capacity bigint(20) unsigned DEFAULT NULL');
|
||||||
await this.$executeQuery('ALTER TABLE `nodes` ADD channels int(11) unsigned DEFAULT NULL');
|
await this.$executeQuery('ALTER TABLE `nodes` ADD channels int(11) unsigned DEFAULT NULL');
|
||||||
await this.$executeQuery('ALTER TABLE `nodes` ADD INDEX `capacity` (`capacity`);');
|
await this.$executeQuery('ALTER TABLE `nodes` ADD INDEX `capacity` (`capacity`);');
|
||||||
|
await this.updateToSchemaVersion(40);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 41 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery('UPDATE channels SET closing_reason = NULL WHERE closing_reason = 1');
|
||||||
|
await this.updateToSchemaVersion(41);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 42 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery('ALTER TABLE `channels` ADD closing_resolved tinyint(1) DEFAULT 0');
|
||||||
|
await this.updateToSchemaVersion(42);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 43 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery(this.getCreateLNNodeRecordsTableQuery(), await this.$checkIfTableExists('nodes_records'));
|
||||||
|
await this.updateToSchemaVersion(43);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 44 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery('UPDATE blocks_summaries SET template = NULL');
|
||||||
|
await this.updateToSchemaVersion(44);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 45 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD fresh_txs JSON DEFAULT "[]"');
|
||||||
|
await this.updateToSchemaVersion(45);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 46) {
|
||||||
|
await this.$executeQuery(`ALTER TABLE blocks MODIFY blockTimestamp timestamp NOT NULL DEFAULT 0`);
|
||||||
|
await this.updateToSchemaVersion(46);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 47) {
|
||||||
|
await this.$executeQuery('ALTER TABLE `blocks` ADD cpfp_indexed tinyint(1) DEFAULT 0');
|
||||||
|
await this.$executeQuery(this.getCreateCPFPTableQuery(), await this.$checkIfTableExists('cpfp_clusters'));
|
||||||
|
await this.$executeQuery(this.getCreateTransactionsTableQuery(), await this.$checkIfTableExists('transactions'));
|
||||||
|
await this.updateToSchemaVersion(47);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 48 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery('ALTER TABLE `channels` ADD source_checked tinyint(1) DEFAULT 0');
|
||||||
|
await this.$executeQuery('ALTER TABLE `channels` ADD closing_fee bigint(20) unsigned DEFAULT 0');
|
||||||
|
await this.$executeQuery('ALTER TABLE `channels` ADD node1_funding_balance bigint(20) unsigned DEFAULT 0');
|
||||||
|
await this.$executeQuery('ALTER TABLE `channels` ADD node2_funding_balance bigint(20) unsigned DEFAULT 0');
|
||||||
|
await this.$executeQuery('ALTER TABLE `channels` ADD node1_closing_balance bigint(20) unsigned DEFAULT 0');
|
||||||
|
await this.$executeQuery('ALTER TABLE `channels` ADD node2_closing_balance bigint(20) unsigned DEFAULT 0');
|
||||||
|
await this.$executeQuery('ALTER TABLE `channels` ADD funding_ratio float unsigned DEFAULT NULL');
|
||||||
|
await this.$executeQuery('ALTER TABLE `channels` ADD closed_by varchar(66) DEFAULT NULL');
|
||||||
|
await this.$executeQuery('ALTER TABLE `channels` ADD single_funded tinyint(1) DEFAULT 0');
|
||||||
|
await this.$executeQuery('ALTER TABLE `channels` ADD outputs JSON DEFAULT "[]"');
|
||||||
|
await this.updateToSchemaVersion(48);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 49 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery('TRUNCATE TABLE `blocks_audits`');
|
||||||
|
await this.updateToSchemaVersion(49);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,6 +579,10 @@ class DatabaseMigration {
|
|||||||
return `UPDATE state SET number = ${DatabaseMigration.currentVersion} WHERE name = 'schema_version';`;
|
return `UPDATE state SET number = ${DatabaseMigration.currentVersion} WHERE name = 'schema_version';`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async updateToSchemaVersion(version): Promise<void> {
|
||||||
|
await this.$executeQuery(`UPDATE state SET number = ${version} WHERE name = 'schema_version';`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Print current database version
|
* Print current database version
|
||||||
*/
|
*/
|
||||||
@@ -783,6 +881,38 @@ class DatabaseMigration {
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getCreateLNNodeRecordsTableQuery(): string {
|
||||||
|
return `CREATE TABLE IF NOT EXISTS nodes_records (
|
||||||
|
public_key varchar(66) NOT NULL,
|
||||||
|
type int(10) unsigned NOT NULL,
|
||||||
|
payload blob NOT NULL,
|
||||||
|
UNIQUE KEY public_key_type (public_key, type),
|
||||||
|
INDEX (public_key),
|
||||||
|
FOREIGN KEY (public_key)
|
||||||
|
REFERENCES nodes (public_key)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCreateCPFPTableQuery(): string {
|
||||||
|
return `CREATE TABLE IF NOT EXISTS cpfp_clusters (
|
||||||
|
root varchar(65) NOT NULL,
|
||||||
|
height int(10) NOT NULL,
|
||||||
|
txs JSON DEFAULT NULL,
|
||||||
|
fee_rate double unsigned NOT NULL,
|
||||||
|
PRIMARY KEY (root)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCreateTransactionsTableQuery(): string {
|
||||||
|
return `CREATE TABLE IF NOT EXISTS transactions (
|
||||||
|
txid varchar(65) NOT NULL,
|
||||||
|
cluster varchar(65) DEFAULT NULL,
|
||||||
|
PRIMARY KEY (txid),
|
||||||
|
FOREIGN KEY (cluster) REFERENCES cpfp_clusters (root) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
|
}
|
||||||
|
|
||||||
public async $truncateIndexedData(tables: string[]) {
|
public async $truncateIndexedData(tables: string[]) {
|
||||||
const allowedTables = ['blocks', 'hashrates', 'prices'];
|
const allowedTables = ['blocks', 'hashrates', 'prices'];
|
||||||
|
|
||||||
|
|||||||
@@ -117,6 +117,32 @@ class ChannelsApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $getUnresolvedClosedChannels(): Promise<any[]> {
|
||||||
|
try {
|
||||||
|
const query = `SELECT * FROM channels WHERE status = 2 AND closing_reason = 2 AND closing_resolved = 0 AND closing_transaction_id != ''`;
|
||||||
|
const [rows]: any = await DB.query(query);
|
||||||
|
return rows;
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('$getUnresolvedClosedChannels error: ' + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getChannelsWithoutSourceChecked(): Promise<any[]> {
|
||||||
|
try {
|
||||||
|
const query = `
|
||||||
|
SELECT channels.*
|
||||||
|
FROM channels
|
||||||
|
WHERE channels.source_checked != 1
|
||||||
|
`;
|
||||||
|
const [rows]: any = await DB.query(query);
|
||||||
|
return rows;
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('$getUnresolvedClosedChannels error: ' + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async $getChannelsWithoutCreatedDate(): Promise<any[]> {
|
public async $getChannelsWithoutCreatedDate(): Promise<any[]> {
|
||||||
try {
|
try {
|
||||||
const query = `SELECT * FROM channels WHERE created IS NULL`;
|
const query = `SELECT * FROM channels WHERE created IS NULL`;
|
||||||
@@ -246,6 +272,108 @@ class ChannelsApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $getChannelByClosingId(transactionId: string): Promise<any> {
|
||||||
|
try {
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
channels.*
|
||||||
|
FROM channels
|
||||||
|
WHERE channels.closing_transaction_id = ?
|
||||||
|
`;
|
||||||
|
const [rows]: any = await DB.query(query, [transactionId]);
|
||||||
|
if (rows.length > 0) {
|
||||||
|
rows[0].outputs = JSON.parse(rows[0].outputs);
|
||||||
|
return rows[0];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('$getChannelByClosingId error: ' + (e instanceof Error ? e.message : e));
|
||||||
|
// don't throw - this data isn't essential
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getChannelsByOpeningId(transactionId: string): Promise<any> {
|
||||||
|
try {
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
channels.*
|
||||||
|
FROM channels
|
||||||
|
WHERE channels.transaction_id = ?
|
||||||
|
`;
|
||||||
|
const [rows]: any = await DB.query(query, [transactionId]);
|
||||||
|
if (rows.length > 0) {
|
||||||
|
return rows.map(row => {
|
||||||
|
row.outputs = JSON.parse(row.outputs);
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('$getChannelsByOpeningId error: ' + (e instanceof Error ? e.message : e));
|
||||||
|
// don't throw - this data isn't essential
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $updateClosingInfo(channelInfo: { id: string, node1_closing_balance: number, node2_closing_balance: number, closed_by: string | null, closing_fee: number, outputs: ILightningApi.ForensicOutput[]}): Promise<void> {
|
||||||
|
try {
|
||||||
|
const query = `
|
||||||
|
UPDATE channels SET
|
||||||
|
node1_closing_balance = ?,
|
||||||
|
node2_closing_balance = ?,
|
||||||
|
closed_by = ?,
|
||||||
|
closing_fee = ?,
|
||||||
|
outputs = ?
|
||||||
|
WHERE channels.id = ?
|
||||||
|
`;
|
||||||
|
await DB.query<ResultSetHeader>(query, [
|
||||||
|
channelInfo.node1_closing_balance || 0,
|
||||||
|
channelInfo.node2_closing_balance || 0,
|
||||||
|
channelInfo.closed_by,
|
||||||
|
channelInfo.closing_fee || 0,
|
||||||
|
JSON.stringify(channelInfo.outputs),
|
||||||
|
channelInfo.id,
|
||||||
|
]);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('$updateClosingInfo error: ' + (e instanceof Error ? e.message : e));
|
||||||
|
// don't throw - this data isn't essential
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $updateOpeningInfo(channelInfo: { id: string, node1_funding_balance: number, node2_funding_balance: number, funding_ratio: number, single_funded: boolean | void }): Promise<void> {
|
||||||
|
try {
|
||||||
|
const query = `
|
||||||
|
UPDATE channels SET
|
||||||
|
node1_funding_balance = ?,
|
||||||
|
node2_funding_balance = ?,
|
||||||
|
funding_ratio = ?,
|
||||||
|
single_funded = ?
|
||||||
|
WHERE channels.id = ?
|
||||||
|
`;
|
||||||
|
await DB.query<ResultSetHeader>(query, [
|
||||||
|
channelInfo.node1_funding_balance || 0,
|
||||||
|
channelInfo.node2_funding_balance || 0,
|
||||||
|
channelInfo.funding_ratio,
|
||||||
|
channelInfo.single_funded ? 1 : 0,
|
||||||
|
channelInfo.id,
|
||||||
|
]);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('$updateOpeningInfo error: ' + (e instanceof Error ? e.message : e));
|
||||||
|
// don't throw - this data isn't essential
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $markChannelSourceChecked(id: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const query = `
|
||||||
|
UPDATE channels
|
||||||
|
SET source_checked = 1
|
||||||
|
WHERE id = ?
|
||||||
|
`;
|
||||||
|
await DB.query<ResultSetHeader>(query, [id]);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('$markChannelSourceChecked error: ' + (e instanceof Error ? e.message : e));
|
||||||
|
// don't throw - this data isn't essential
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async $getChannelsForNode(public_key: string, index: number, length: number, status: string): Promise<any[]> {
|
public async $getChannelsForNode(public_key: string, index: number, length: number, status: string): Promise<any[]> {
|
||||||
try {
|
try {
|
||||||
let channelStatusFilter;
|
let channelStatusFilter;
|
||||||
@@ -374,11 +502,15 @@ class ChannelsApi {
|
|||||||
'transaction_id': channel.transaction_id,
|
'transaction_id': channel.transaction_id,
|
||||||
'transaction_vout': channel.transaction_vout,
|
'transaction_vout': channel.transaction_vout,
|
||||||
'closing_transaction_id': channel.closing_transaction_id,
|
'closing_transaction_id': channel.closing_transaction_id,
|
||||||
|
'closing_fee': channel.closing_fee,
|
||||||
'closing_reason': channel.closing_reason,
|
'closing_reason': channel.closing_reason,
|
||||||
'closing_date': channel.closing_date,
|
'closing_date': channel.closing_date,
|
||||||
'updated_at': channel.updated_at,
|
'updated_at': channel.updated_at,
|
||||||
'created': channel.created,
|
'created': channel.created,
|
||||||
'status': channel.status,
|
'status': channel.status,
|
||||||
|
'funding_ratio': channel.funding_ratio,
|
||||||
|
'closed_by': channel.closed_by,
|
||||||
|
'single_funded': !!channel.single_funded,
|
||||||
'node_left': {
|
'node_left': {
|
||||||
'alias': channel.alias_left,
|
'alias': channel.alias_left,
|
||||||
'public_key': channel.node1_public_key,
|
'public_key': channel.node1_public_key,
|
||||||
@@ -393,6 +525,9 @@ class ChannelsApi {
|
|||||||
'updated_at': channel.node1_updated_at,
|
'updated_at': channel.node1_updated_at,
|
||||||
'longitude': channel.node1_longitude,
|
'longitude': channel.node1_longitude,
|
||||||
'latitude': channel.node1_latitude,
|
'latitude': channel.node1_latitude,
|
||||||
|
'funding_balance': channel.node1_funding_balance,
|
||||||
|
'closing_balance': channel.node1_closing_balance,
|
||||||
|
'initiated_close': channel.closed_by === channel.node1_public_key ? true : undefined,
|
||||||
},
|
},
|
||||||
'node_right': {
|
'node_right': {
|
||||||
'alias': channel.alias_right,
|
'alias': channel.alias_right,
|
||||||
@@ -408,6 +543,9 @@ class ChannelsApi {
|
|||||||
'updated_at': channel.node2_updated_at,
|
'updated_at': channel.node2_updated_at,
|
||||||
'longitude': channel.node2_longitude,
|
'longitude': channel.node2_longitude,
|
||||||
'latitude': channel.node2_latitude,
|
'latitude': channel.node2_latitude,
|
||||||
|
'funding_balance': channel.node2_funding_balance,
|
||||||
|
'closing_balance': channel.node2_closing_balance,
|
||||||
|
'initiated_close': channel.closed_by === channel.node2_public_key ? true : undefined,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ class NodesApi {
|
|||||||
nodes.longitude, nodes.latitude,
|
nodes.longitude, nodes.latitude,
|
||||||
geo_names_country.names as country, geo_names_iso.names as isoCode
|
geo_names_country.names as country, geo_names_iso.names as isoCode
|
||||||
FROM nodes
|
FROM nodes
|
||||||
LEFT JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country'
|
JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country'
|
||||||
LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
|
JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
|
||||||
WHERE status = 1 AND nodes.as_number IS NOT NULL
|
WHERE status = 1 AND nodes.as_number IS NOT NULL
|
||||||
ORDER BY capacity
|
ORDER BY capacity
|
||||||
`;
|
`;
|
||||||
@@ -105,6 +105,18 @@ class NodesApi {
|
|||||||
node.closed_channel_count = rows[0].closed_channel_count;
|
node.closed_channel_count = rows[0].closed_channel_count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Custom records
|
||||||
|
query = `
|
||||||
|
SELECT type, payload
|
||||||
|
FROM nodes_records
|
||||||
|
WHERE public_key = ?
|
||||||
|
`;
|
||||||
|
[rows] = await DB.query(query, [public_key]);
|
||||||
|
node.custom_records = {};
|
||||||
|
for (const record of rows) {
|
||||||
|
node.custom_records[record.type] = Buffer.from(record.payload, 'binary').toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err(`Cannot get node information for ${public_key}. Reason: ${(e instanceof Error ? e.message : e)}`);
|
logger.err(`Cannot get node information for ${public_key}. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||||
@@ -129,6 +141,56 @@ class NodesApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $getFeeHistogram(node_public_key: string): Promise<unknown> {
|
||||||
|
try {
|
||||||
|
const inQuery = `
|
||||||
|
SELECT CASE WHEN fee_rate <= 10.0 THEN CEIL(fee_rate)
|
||||||
|
WHEN (fee_rate > 10.0 and fee_rate <= 100.0) THEN CEIL(fee_rate / 10.0) * 10.0
|
||||||
|
WHEN (fee_rate > 100.0 and fee_rate <= 1000.0) THEN CEIL(fee_rate / 100.0) * 100.0
|
||||||
|
WHEN fee_rate > 1000.0 THEN CEIL(fee_rate / 1000.0) * 1000.0
|
||||||
|
END as bucket,
|
||||||
|
count(short_id) as count,
|
||||||
|
sum(capacity) as capacity
|
||||||
|
FROM (
|
||||||
|
SELECT CASE WHEN node1_public_key = ? THEN node2_fee_rate WHEN node2_public_key = ? THEN node1_fee_rate END as fee_rate,
|
||||||
|
short_id as short_id,
|
||||||
|
capacity as capacity
|
||||||
|
FROM channels
|
||||||
|
WHERE status = 1 AND (channels.node1_public_key = ? OR channels.node2_public_key = ?)
|
||||||
|
) as fee_rate_table
|
||||||
|
GROUP BY bucket;
|
||||||
|
`;
|
||||||
|
const [inRows]: any[] = await DB.query(inQuery, [node_public_key, node_public_key, node_public_key, node_public_key]);
|
||||||
|
|
||||||
|
const outQuery = `
|
||||||
|
SELECT CASE WHEN fee_rate <= 10.0 THEN CEIL(fee_rate)
|
||||||
|
WHEN (fee_rate > 10.0 and fee_rate <= 100.0) THEN CEIL(fee_rate / 10.0) * 10.0
|
||||||
|
WHEN (fee_rate > 100.0 and fee_rate <= 1000.0) THEN CEIL(fee_rate / 100.0) * 100.0
|
||||||
|
WHEN fee_rate > 1000.0 THEN CEIL(fee_rate / 1000.0) * 1000.0
|
||||||
|
END as bucket,
|
||||||
|
count(short_id) as count,
|
||||||
|
sum(capacity) as capacity
|
||||||
|
FROM (
|
||||||
|
SELECT CASE WHEN node1_public_key = ? THEN node1_fee_rate WHEN node2_public_key = ? THEN node2_fee_rate END as fee_rate,
|
||||||
|
short_id as short_id,
|
||||||
|
capacity as capacity
|
||||||
|
FROM channels
|
||||||
|
WHERE status = 1 AND (channels.node1_public_key = ? OR channels.node2_public_key = ?)
|
||||||
|
) as fee_rate_table
|
||||||
|
GROUP BY bucket;
|
||||||
|
`;
|
||||||
|
const [outRows]: any[] = await DB.query(outQuery, [node_public_key, node_public_key, node_public_key, node_public_key]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
incoming: inRows.length > 0 ? inRows : [],
|
||||||
|
outgoing: outRows.length > 0 ? outRows : [],
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Cannot get node fee distribution for ${node_public_key}. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async $getAllNodes(): Promise<any> {
|
public async $getAllNodes(): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const query = `SELECT * FROM nodes`;
|
const query = `SELECT * FROM nodes`;
|
||||||
@@ -462,7 +524,41 @@ class NodesApi {
|
|||||||
|
|
||||||
public async $getNodesPerISP(ISPId: string) {
|
public async $getNodesPerISP(ISPId: string) {
|
||||||
try {
|
try {
|
||||||
const query = `
|
let query = `
|
||||||
|
SELECT channels.node1_public_key AS node1PublicKey, isp1.id as isp1ID,
|
||||||
|
channels.node2_public_key AS node2PublicKey, isp2.id as isp2ID
|
||||||
|
FROM channels
|
||||||
|
JOIN nodes node1 ON node1.public_key = channels.node1_public_key
|
||||||
|
JOIN nodes node2 ON node2.public_key = channels.node2_public_key
|
||||||
|
JOIN geo_names isp1 ON isp1.id = node1.as_number
|
||||||
|
JOIN geo_names isp2 ON isp2.id = node2.as_number
|
||||||
|
WHERE channels.status = 1 AND (node1.as_number IN (?) OR node2.as_number IN (?))
|
||||||
|
ORDER BY short_id DESC
|
||||||
|
`;
|
||||||
|
|
||||||
|
const IPSIds = ISPId.split(',');
|
||||||
|
const [rows]: any = await DB.query(query, [IPSIds, IPSIds]);
|
||||||
|
if (!rows || rows.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodes = {};
|
||||||
|
|
||||||
|
const intISPIds: number[] = [];
|
||||||
|
for (const ispId of IPSIds) {
|
||||||
|
intISPIds.push(parseInt(ispId, 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const channel of rows) {
|
||||||
|
if (intISPIds.includes(channel.isp1ID)) {
|
||||||
|
nodes[channel.node1PublicKey] = true;
|
||||||
|
}
|
||||||
|
if (intISPIds.includes(channel.isp2ID)) {
|
||||||
|
nodes[channel.node2PublicKey] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query = `
|
||||||
SELECT nodes.public_key, CAST(COALESCE(nodes.capacity, 0) as INT) as capacity, CAST(COALESCE(nodes.channels, 0) as INT) as channels,
|
SELECT nodes.public_key, CAST(COALESCE(nodes.capacity, 0) as INT) as capacity, CAST(COALESCE(nodes.channels, 0) as INT) as channels,
|
||||||
nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at,
|
nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at,
|
||||||
geo_names_city.names as city, geo_names_country.names as country,
|
geo_names_city.names as city, geo_names_country.names as country,
|
||||||
@@ -473,17 +569,18 @@ class NodesApi {
|
|||||||
LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city'
|
LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city'
|
||||||
LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
|
LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
|
||||||
LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = nodes.subdivision_id AND geo_names_subdivision.type = 'division'
|
LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = nodes.subdivision_id AND geo_names_subdivision.type = 'division'
|
||||||
WHERE nodes.as_number IN (?)
|
WHERE nodes.public_key IN (?)
|
||||||
ORDER BY capacity DESC
|
ORDER BY capacity DESC
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const [rows]: any = await DB.query(query, [ISPId.split(',')]);
|
const [rows2]: any = await DB.query(query, [Object.keys(nodes)]);
|
||||||
for (let i = 0; i < rows.length; ++i) {
|
for (let i = 0; i < rows2.length; ++i) {
|
||||||
rows[i].country = JSON.parse(rows[i].country);
|
rows2[i].country = JSON.parse(rows2[i].country);
|
||||||
rows[i].city = JSON.parse(rows[i].city);
|
rows2[i].city = JSON.parse(rows2[i].city);
|
||||||
rows[i].subdivision = JSON.parse(rows[i].subdivision);
|
rows2[i].subdivision = JSON.parse(rows2[i].subdivision);
|
||||||
}
|
}
|
||||||
return rows;
|
return rows2;
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err(`Cannot get nodes for ISP id ${ISPId}. Reason: ${e instanceof Error ? e.message : e}`);
|
logger.err(`Cannot get nodes for ISP id ${ISPId}. Reason: ${e instanceof Error ? e.message : e}`);
|
||||||
throw e;
|
throw e;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class NodesRoutes {
|
|||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/rankings/connectivity', this.$getTopNodesByChannels)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/rankings/connectivity', this.$getTopNodesByChannels)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/rankings/age', this.$getOldestNodes)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/rankings/age', this.$getOldestNodes)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key/statistics', this.$getHistoricalNodeStats)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key/statistics', this.$getHistoricalNodeStats)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key/fees/histogram', this.$getFeeHistogram)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key', this.$getNode)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key', this.$getNode)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/group/:name', this.$getNodeGroup)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/group/:name', this.$getNodeGroup)
|
||||||
;
|
;
|
||||||
@@ -95,6 +96,22 @@ class NodesRoutes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async $getFeeHistogram(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const node = await nodesApi.$getFeeHistogram(req.params.public_key);
|
||||||
|
if (!node) {
|
||||||
|
res.status(404).send('Node not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.header('Pragma', 'public');
|
||||||
|
res.header('Cache-control', 'public');
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
|
res.json(node);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async $getNodesRanking(req: Request, res: Response): Promise<void> {
|
private async $getNodesRanking(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const topCapacityNodes = await nodesApi.$getTopCapacityNodes(false);
|
const topCapacityNodes = await nodesApi.$getTopCapacityNodes(false);
|
||||||
|
|||||||
@@ -7,6 +7,15 @@ import { Common } from '../../common';
|
|||||||
* Convert a clightning "listnode" entry to a lnd node entry
|
* Convert a clightning "listnode" entry to a lnd node entry
|
||||||
*/
|
*/
|
||||||
export function convertNode(clNode: any): ILightningApi.Node {
|
export function convertNode(clNode: any): ILightningApi.Node {
|
||||||
|
let custom_records: { [type: number]: string } | undefined = undefined;
|
||||||
|
if (clNode.option_will_fund) {
|
||||||
|
try {
|
||||||
|
custom_records = { '1': Buffer.from(clNode.option_will_fund.compact_lease || '', 'hex').toString('base64') };
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Cannot decode option_will_fund compact_lease for ${clNode.nodeid}). Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
custom_records = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
alias: clNode.alias ?? '',
|
alias: clNode.alias ?? '',
|
||||||
color: `#${clNode.color ?? ''}`,
|
color: `#${clNode.color ?? ''}`,
|
||||||
@@ -23,6 +32,7 @@ export function convertNode(clNode: any): ILightningApi.Node {
|
|||||||
};
|
};
|
||||||
}) ?? [],
|
}) ?? [],
|
||||||
last_update: clNode?.last_timestamp ?? 0,
|
last_update: clNode?.last_timestamp ?? 0,
|
||||||
|
custom_records
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,6 +80,8 @@ export async function convertAndmergeBidirectionalChannels(clChannels: any[]): P
|
|||||||
logger.info(`Building partial channels from clightning output. Channels processed: ${channelProcessed + 1} of ${keys.length}`);
|
logger.info(`Building partial channels from clightning output. Channels processed: ${channelProcessed + 1} of ${keys.length}`);
|
||||||
loggerTimer = new Date().getTime() / 1000;
|
loggerTimer = new Date().getTime() / 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
channelProcessed++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return consolidatedChannelList;
|
return consolidatedChannelList;
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export namespace ILightningApi {
|
|||||||
}[];
|
}[];
|
||||||
color: string;
|
color: string;
|
||||||
features: { [key: number]: Feature };
|
features: { [key: number]: Feature };
|
||||||
|
custom_records?: { [type: number]: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Info {
|
export interface Info {
|
||||||
@@ -82,4 +83,10 @@ export namespace ILightningApi {
|
|||||||
is_required: boolean;
|
is_required: boolean;
|
||||||
is_known: boolean;
|
is_known: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ForensicOutput {
|
||||||
|
node?: 1 | 2;
|
||||||
|
type: number;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,10 +2,16 @@ import logger from '../logger';
|
|||||||
import { MempoolBlock, TransactionExtended, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta } from '../mempool.interfaces';
|
import { MempoolBlock, TransactionExtended, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta } from '../mempool.interfaces';
|
||||||
import { Common } from './common';
|
import { Common } from './common';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
|
import { StaticPool } from 'node-worker-threads-pool';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
class MempoolBlocks {
|
class MempoolBlocks {
|
||||||
private mempoolBlocks: MempoolBlockWithTransactions[] = [];
|
private mempoolBlocks: MempoolBlockWithTransactions[] = [];
|
||||||
private mempoolBlockDeltas: MempoolBlockDelta[] = [];
|
private mempoolBlockDeltas: MempoolBlockDelta[] = [];
|
||||||
|
private makeTemplatesPool = new StaticPool({
|
||||||
|
size: 1,
|
||||||
|
task: path.resolve(__dirname, './tx-selection-worker.js'),
|
||||||
|
});
|
||||||
|
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
@@ -71,15 +77,15 @@ class MempoolBlocks {
|
|||||||
const time = end - start;
|
const time = end - start;
|
||||||
logger.debug('Mempool blocks calculated in ' + time / 1000 + ' seconds');
|
logger.debug('Mempool blocks calculated in ' + time / 1000 + ' seconds');
|
||||||
|
|
||||||
const { blocks, deltas } = this.calculateMempoolBlocks(memPoolArray, this.mempoolBlocks);
|
const blocks = this.calculateMempoolBlocks(memPoolArray, this.mempoolBlocks);
|
||||||
|
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, blocks);
|
||||||
|
|
||||||
this.mempoolBlocks = blocks;
|
this.mempoolBlocks = blocks;
|
||||||
this.mempoolBlockDeltas = deltas;
|
this.mempoolBlockDeltas = deltas;
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateMempoolBlocks(transactionsSorted: TransactionExtended[], prevBlocks: MempoolBlockWithTransactions[]):
|
private calculateMempoolBlocks(transactionsSorted: TransactionExtended[], prevBlocks: MempoolBlockWithTransactions[]): MempoolBlockWithTransactions[] {
|
||||||
{ blocks: MempoolBlockWithTransactions[], deltas: MempoolBlockDelta[] } {
|
|
||||||
const mempoolBlocks: MempoolBlockWithTransactions[] = [];
|
const mempoolBlocks: MempoolBlockWithTransactions[] = [];
|
||||||
const mempoolBlockDeltas: MempoolBlockDelta[] = [];
|
|
||||||
let blockWeight = 0;
|
let blockWeight = 0;
|
||||||
let blockSize = 0;
|
let blockSize = 0;
|
||||||
let transactions: TransactionExtended[] = [];
|
let transactions: TransactionExtended[] = [];
|
||||||
@@ -99,7 +105,12 @@ class MempoolBlocks {
|
|||||||
if (transactions.length) {
|
if (transactions.length) {
|
||||||
mempoolBlocks.push(this.dataToMempoolBlocks(transactions, blockSize, blockWeight, mempoolBlocks.length));
|
mempoolBlocks.push(this.dataToMempoolBlocks(transactions, blockSize, blockWeight, mempoolBlocks.length));
|
||||||
}
|
}
|
||||||
// Calculate change from previous block states
|
|
||||||
|
return mempoolBlocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateMempoolDeltas(prevBlocks: MempoolBlockWithTransactions[], mempoolBlocks: MempoolBlockWithTransactions[]): MempoolBlockDelta[] {
|
||||||
|
const mempoolBlockDeltas: MempoolBlockDelta[] = [];
|
||||||
for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) {
|
for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) {
|
||||||
let added: TransactionStripped[] = [];
|
let added: TransactionStripped[] = [];
|
||||||
let removed: string[] = [];
|
let removed: string[] = [];
|
||||||
@@ -132,10 +143,26 @@ class MempoolBlocks {
|
|||||||
removed
|
removed
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return {
|
return mempoolBlockDeltas;
|
||||||
blocks: mempoolBlocks,
|
}
|
||||||
deltas: mempoolBlockDeltas
|
|
||||||
};
|
public async makeBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, blockLimit: number, weightLimit: number | null = null, condenseRest = false): Promise<void> {
|
||||||
|
const { mempool, blocks } = await this.makeTemplatesPool.exec({ mempool: newMempool, blockLimit, weightLimit, condenseRest });
|
||||||
|
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, blocks);
|
||||||
|
|
||||||
|
// copy CPFP info across to main thread's mempool
|
||||||
|
Object.keys(newMempool).forEach((txid) => {
|
||||||
|
if (newMempool[txid] && mempool[txid]) {
|
||||||
|
newMempool[txid].effectiveFeePerVsize = mempool[txid].effectiveFeePerVsize;
|
||||||
|
newMempool[txid].ancestors = mempool[txid].ancestors;
|
||||||
|
newMempool[txid].descendants = mempool[txid].descendants;
|
||||||
|
newMempool[txid].bestDescendant = mempool[txid].bestDescendant;
|
||||||
|
newMempool[txid].cpfpChecked = mempool[txid].cpfpChecked;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.mempoolBlocks = blocks;
|
||||||
|
this.mempoolBlockDeltas = deltas;
|
||||||
}
|
}
|
||||||
|
|
||||||
private dataToMempoolBlocks(transactions: TransactionExtended[],
|
private dataToMempoolBlocks(transactions: TransactionExtended[],
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ class Mempool {
|
|||||||
maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 };
|
maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 };
|
||||||
private mempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
|
private mempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
|
||||||
deletedTransactions: TransactionExtended[]) => void) | undefined;
|
deletedTransactions: TransactionExtended[]) => void) | undefined;
|
||||||
|
private asyncMempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
|
||||||
|
deletedTransactions: TransactionExtended[]) => void) | undefined;
|
||||||
|
|
||||||
private txPerSecondArray: number[] = [];
|
private txPerSecondArray: number[] = [];
|
||||||
private txPerSecond: number = 0;
|
private txPerSecond: number = 0;
|
||||||
@@ -63,6 +65,11 @@ class Mempool {
|
|||||||
this.mempoolChangedCallback = fn;
|
this.mempoolChangedCallback = fn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: TransactionExtended; },
|
||||||
|
newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]) => Promise<void>) {
|
||||||
|
this.asyncMempoolChangedCallback = fn;
|
||||||
|
}
|
||||||
|
|
||||||
public getMempool(): { [txid: string]: TransactionExtended } {
|
public getMempool(): { [txid: string]: TransactionExtended } {
|
||||||
return this.mempoolCache;
|
return this.mempoolCache;
|
||||||
}
|
}
|
||||||
@@ -72,6 +79,9 @@ class Mempool {
|
|||||||
if (this.mempoolChangedCallback) {
|
if (this.mempoolChangedCallback) {
|
||||||
this.mempoolChangedCallback(this.mempoolCache, [], []);
|
this.mempoolChangedCallback(this.mempoolCache, [], []);
|
||||||
}
|
}
|
||||||
|
if (this.asyncMempoolChangedCallback) {
|
||||||
|
this.asyncMempoolChangedCallback(this.mempoolCache, [], []);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $updateMemPoolInfo() {
|
public async $updateMemPoolInfo() {
|
||||||
@@ -103,12 +113,11 @@ class Mempool {
|
|||||||
return txTimes;
|
return txTimes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $updateMempool() {
|
public async $updateMempool(): Promise<void> {
|
||||||
logger.debug('Updating mempool');
|
logger.debug(`Updating mempool...`);
|
||||||
const start = new Date().getTime();
|
const start = new Date().getTime();
|
||||||
let hasChange: boolean = false;
|
let hasChange: boolean = false;
|
||||||
const currentMempoolSize = Object.keys(this.mempoolCache).length;
|
const currentMempoolSize = Object.keys(this.mempoolCache).length;
|
||||||
let txCount = 0;
|
|
||||||
const transactions = await bitcoinApi.$getRawMempool();
|
const transactions = await bitcoinApi.$getRawMempool();
|
||||||
const diff = transactions.length - currentMempoolSize;
|
const diff = transactions.length - currentMempoolSize;
|
||||||
const newTransactions: TransactionExtended[] = [];
|
const newTransactions: TransactionExtended[] = [];
|
||||||
@@ -124,7 +133,6 @@ class Mempool {
|
|||||||
try {
|
try {
|
||||||
const transaction = await transactionUtils.$getTransactionExtended(txid);
|
const transaction = await transactionUtils.$getTransactionExtended(txid);
|
||||||
this.mempoolCache[txid] = transaction;
|
this.mempoolCache[txid] = transaction;
|
||||||
txCount++;
|
|
||||||
if (this.inSync) {
|
if (this.inSync) {
|
||||||
this.txPerSecondArray.push(new Date().getTime());
|
this.txPerSecondArray.push(new Date().getTime());
|
||||||
this.vBytesPerSecondArray.push({
|
this.vBytesPerSecondArray.push({
|
||||||
@@ -133,14 +141,9 @@ class Mempool {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
hasChange = true;
|
hasChange = true;
|
||||||
if (diff > 0) {
|
|
||||||
logger.debug('Fetched transaction ' + txCount + ' / ' + diff);
|
|
||||||
} else {
|
|
||||||
logger.debug('Fetched transaction ' + txCount);
|
|
||||||
}
|
|
||||||
newTransactions.push(transaction);
|
newTransactions.push(transaction);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
logger.debug(`Error finding transaction '${txid}' in the mempool: ` + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,11 +197,13 @@ class Mempool {
|
|||||||
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
||||||
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
|
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
|
||||||
}
|
}
|
||||||
|
if (this.asyncMempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
||||||
|
await this.asyncMempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
|
||||||
|
}
|
||||||
|
|
||||||
const end = new Date().getTime();
|
const end = new Date().getTime();
|
||||||
const time = end - start;
|
const time = end - start;
|
||||||
logger.debug(`New mempool size: ${Object.keys(this.mempoolCache).length} Change: ${diff}`);
|
logger.debug(`Mempool updated in ${time / 1000} seconds. New size: ${Object.keys(this.mempoolCache).length} (${diff > 0 ? '+' + diff : diff})`);
|
||||||
logger.debug('Mempool updated in ' + time / 1000 + ' seconds');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public handleRbfTransactions(rbfTransactions: { [txid: string]: TransactionExtended; }) {
|
public handleRbfTransactions(rbfTransactions: { [txid: string]: TransactionExtended; }) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Application, Request, Response } from 'express';
|
import { Application, Request, Response } from 'express';
|
||||||
import config from "../../config";
|
import config from "../../config";
|
||||||
import logger from '../../logger';
|
import logger from '../../logger';
|
||||||
|
import audits from '../audit';
|
||||||
import BlocksAuditsRepository from '../../repositories/BlocksAuditsRepository';
|
import BlocksAuditsRepository from '../../repositories/BlocksAuditsRepository';
|
||||||
import BlocksRepository from '../../repositories/BlocksRepository';
|
import BlocksRepository from '../../repositories/BlocksRepository';
|
||||||
import DifficultyAdjustmentsRepository from '../../repositories/DifficultyAdjustmentsRepository';
|
import DifficultyAdjustmentsRepository from '../../repositories/DifficultyAdjustmentsRepository';
|
||||||
@@ -26,7 +27,11 @@ class MiningRoutes {
|
|||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/sizes-weights/:interval', this.$getHistoricalBlockSizeAndWeight)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/sizes-weights/:interval', this.$getHistoricalBlockSizeAndWeight)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments/:interval', this.$getDifficultyAdjustments)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments/:interval', this.$getDifficultyAdjustments)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/predictions/:interval', this.$getHistoricalBlockPrediction)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/predictions/:interval', this.$getHistoricalBlockPrediction)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/scores', this.$getBlockAuditScores)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/scores/:height', this.$getBlockAuditScores)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/score/:hash', this.$getBlockAuditScore)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/:hash', this.$getBlockAudit)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/:hash', this.$getBlockAudit)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/timestamp/:timestamp', this.$getHeightFromTimestamp)
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,6 +243,12 @@ class MiningRoutes {
|
|||||||
public async $getBlockAudit(req: Request, res: Response) {
|
public async $getBlockAudit(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash);
|
const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash);
|
||||||
|
|
||||||
|
if (!audit) {
|
||||||
|
res.status(404).send(`This block has not been audited.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
res.header('Pragma', 'public');
|
res.header('Pragma', 'public');
|
||||||
res.header('Cache-control', 'public');
|
res.header('Cache-control', 'public');
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
|
||||||
@@ -246,6 +257,55 @@ class MiningRoutes {
|
|||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async $getHeightFromTimestamp(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const timestamp = parseInt(req.params.timestamp, 10);
|
||||||
|
// This will prevent people from entering milliseconds etc.
|
||||||
|
// Block timestamps are allowed to be up to 2 hours off, so 24 hours
|
||||||
|
// will never put the maximum value before the most recent block
|
||||||
|
const nowPlus1day = Math.floor(Date.now() / 1000) + 60 * 60 * 24;
|
||||||
|
// Prevent non-integers that are not seconds
|
||||||
|
if (!/^[1-9][0-9]*$/.test(req.params.timestamp) || timestamp > nowPlus1day) {
|
||||||
|
throw new Error(`Invalid timestamp, value must be Unix seconds`);
|
||||||
|
}
|
||||||
|
const result = await BlocksRepository.$getBlockHeightFromTimestamp(
|
||||||
|
timestamp,
|
||||||
|
);
|
||||||
|
res.header('Pragma', 'public');
|
||||||
|
res.header('Cache-control', 'public');
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
||||||
|
res.json(result);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $getBlockAuditScores(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
let height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
|
||||||
|
if (height == null) {
|
||||||
|
height = await BlocksRepository.$mostRecentBlockHeight();
|
||||||
|
}
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
|
res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15));
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getBlockAuditScore(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const audit = await BlocksAuditsRepository.$getBlockAuditScore(req.params.hash);
|
||||||
|
|
||||||
|
res.header('Pragma', 'public');
|
||||||
|
res.header('Cache-control', 'public');
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
|
||||||
|
res.json(audit || 'null');
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new MiningRoutes();
|
export default new MiningRoutes();
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ interface Pool {
|
|||||||
class PoolsParser {
|
class PoolsParser {
|
||||||
miningPools: any[] = [];
|
miningPools: any[] = [];
|
||||||
unknownPool: any = {
|
unknownPool: any = {
|
||||||
'name': "Unknown",
|
'name': 'Unknown',
|
||||||
'link': "https://learnmeabitcoin.com/technical/coinbase-transaction",
|
'link': 'https://learnmeabitcoin.com/technical/coinbase-transaction',
|
||||||
'regexes': "[]",
|
'regexes': '[]',
|
||||||
'addresses': "[]",
|
'addresses': '[]',
|
||||||
'slug': 'unknown'
|
'slug': 'unknown'
|
||||||
};
|
};
|
||||||
slugWarnFlag = false;
|
slugWarnFlag = false;
|
||||||
@@ -25,7 +25,7 @@ class PoolsParser {
|
|||||||
/**
|
/**
|
||||||
* Parse the pools.json file, consolidate the data and dump it into the database
|
* Parse the pools.json file, consolidate the data and dump it into the database
|
||||||
*/
|
*/
|
||||||
public async migratePoolsJson(poolsJson: object) {
|
public async migratePoolsJson(poolsJson: object): Promise<void> {
|
||||||
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) {
|
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -81,6 +81,7 @@ class PoolsParser {
|
|||||||
// Finally, we generate the final consolidated pools data
|
// Finally, we generate the final consolidated pools data
|
||||||
const finalPoolDataAdd: Pool[] = [];
|
const finalPoolDataAdd: Pool[] = [];
|
||||||
const finalPoolDataUpdate: Pool[] = [];
|
const finalPoolDataUpdate: Pool[] = [];
|
||||||
|
const finalPoolDataRename: Pool[] = [];
|
||||||
for (let i = 0; i < poolNames.length; ++i) {
|
for (let i = 0; i < poolNames.length; ++i) {
|
||||||
let allAddresses: string[] = [];
|
let allAddresses: string[] = [];
|
||||||
let allRegexes: string[] = [];
|
let allRegexes: string[] = [];
|
||||||
@@ -127,8 +128,26 @@ class PoolsParser {
|
|||||||
finalPoolDataUpdate.push(poolObj);
|
finalPoolDataUpdate.push(poolObj);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.debug(`Add '${finalPoolName}' mining pool`);
|
// Double check that if we're not just renaming a pool (same address same regex)
|
||||||
finalPoolDataAdd.push(poolObj);
|
const [poolToRename]: any[] = await DB.query(`
|
||||||
|
SELECT * FROM pools
|
||||||
|
WHERE addresses = ? OR regexes = ?`,
|
||||||
|
[JSON.stringify(poolObj.addresses), JSON.stringify(poolObj.regexes)]
|
||||||
|
);
|
||||||
|
if (poolToRename && poolToRename.length > 0) {
|
||||||
|
// We're actually renaming an existing pool
|
||||||
|
finalPoolDataRename.push({
|
||||||
|
'name': poolObj.name,
|
||||||
|
'link': poolObj.link,
|
||||||
|
'regexes': allRegexes,
|
||||||
|
'addresses': allAddresses,
|
||||||
|
'slug': slug
|
||||||
|
});
|
||||||
|
logger.debug(`Rename '${poolToRename[0].name}' mining pool to ${poolObj.name}`);
|
||||||
|
} else {
|
||||||
|
logger.debug(`Add '${finalPoolName}' mining pool`);
|
||||||
|
finalPoolDataAdd.push(poolObj);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.miningPools.push({
|
this.miningPools.push({
|
||||||
@@ -145,7 +164,9 @@ class PoolsParser {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (finalPoolDataAdd.length > 0 || finalPoolDataUpdate.length > 0) {
|
if (finalPoolDataAdd.length > 0 || finalPoolDataUpdate.length > 0 ||
|
||||||
|
finalPoolDataRename.length > 0
|
||||||
|
) {
|
||||||
logger.debug(`Update pools table now`);
|
logger.debug(`Update pools table now`);
|
||||||
|
|
||||||
// Add new mining pools into the database
|
// Add new mining pools into the database
|
||||||
@@ -169,8 +190,22 @@ class PoolsParser {
|
|||||||
;`);
|
;`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rename mining pools
|
||||||
|
const renameQueries: string[] = [];
|
||||||
|
for (let i = 0; i < finalPoolDataRename.length; ++i) {
|
||||||
|
renameQueries.push(`
|
||||||
|
UPDATE pools
|
||||||
|
SET name='${finalPoolDataRename[i].name}', link='${finalPoolDataRename[i].link}',
|
||||||
|
slug='${finalPoolDataRename[i].slug}'
|
||||||
|
WHERE regexes='${JSON.stringify(finalPoolDataRename[i].regexes)}'
|
||||||
|
AND addresses='${JSON.stringify(finalPoolDataRename[i].addresses)}'
|
||||||
|
;`);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.$deleteBlocskToReindex(finalPoolDataUpdate);
|
if (finalPoolDataAdd.length > 0 || updateQueries.length > 0) {
|
||||||
|
await this.$deleteBlocskToReindex(finalPoolDataUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
if (finalPoolDataAdd.length > 0) {
|
if (finalPoolDataAdd.length > 0) {
|
||||||
await DB.query({ sql: queryAdd, timeout: 120000 });
|
await DB.query({ sql: queryAdd, timeout: 120000 });
|
||||||
@@ -178,6 +213,9 @@ class PoolsParser {
|
|||||||
for (const query of updateQueries) {
|
for (const query of updateQueries) {
|
||||||
await DB.query({ sql: query, timeout: 120000 });
|
await DB.query({ sql: query, timeout: 120000 });
|
||||||
}
|
}
|
||||||
|
for (const query of renameQueries) {
|
||||||
|
await DB.query({ sql: query, timeout: 120000 });
|
||||||
|
}
|
||||||
await this.insertUnknownPool();
|
await this.insertUnknownPool();
|
||||||
logger.info('Mining pools.json import completed');
|
logger.info('Mining pools.json import completed');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
338
backend/src/api/tx-selection-worker.ts
Normal file
338
backend/src/api/tx-selection-worker.ts
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
import config from '../config';
|
||||||
|
import logger from '../logger';
|
||||||
|
import { TransactionExtended, MempoolBlockWithTransactions, AuditTransaction } from '../mempool.interfaces';
|
||||||
|
import { PairingHeap } from '../utils/pairing-heap';
|
||||||
|
import { Common } from './common';
|
||||||
|
import { parentPort } from 'worker_threads';
|
||||||
|
|
||||||
|
if (parentPort) {
|
||||||
|
parentPort.on('message', (params: { mempool: { [txid: string]: TransactionExtended }, blockLimit: number, weightLimit: number | null, condenseRest: boolean}) => {
|
||||||
|
const { mempool, blocks } = makeBlockTemplates(params);
|
||||||
|
|
||||||
|
// return the result to main thread.
|
||||||
|
if (parentPort) {
|
||||||
|
parentPort.postMessage({ mempool, blocks });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
*
|
||||||
|
* blockLimit: number of blocks to build in total.
|
||||||
|
* weightLimit: maximum weight of transactions to consider using the selection algorithm.
|
||||||
|
* if weightLimit is significantly lower than the mempool size, results may start to diverge from getBlockTemplate
|
||||||
|
* condenseRest: whether to ignore excess transactions or append them to the final block.
|
||||||
|
*/
|
||||||
|
function makeBlockTemplates({ mempool, blockLimit, weightLimit, condenseRest }: { mempool: { [txid: string]: TransactionExtended }, blockLimit: number, weightLimit?: number | null, condenseRest?: boolean | null })
|
||||||
|
: { mempool: { [txid: string]: TransactionExtended }, blocks: MempoolBlockWithTransactions[] } {
|
||||||
|
const start = Date.now();
|
||||||
|
const auditPool: { [txid: string]: AuditTransaction } = {};
|
||||||
|
const mempoolArray: AuditTransaction[] = [];
|
||||||
|
const restOfArray: TransactionExtended[] = [];
|
||||||
|
|
||||||
|
let weight = 0;
|
||||||
|
const maxWeight = weightLimit ? Math.max(4_000_000 * blockLimit, weightLimit) : Infinity;
|
||||||
|
// grab the top feerate txs up to maxWeight
|
||||||
|
Object.values(mempool).sort((a, b) => b.feePerVsize - a.feePerVsize).forEach(tx => {
|
||||||
|
weight += tx.weight;
|
||||||
|
if (weight >= maxWeight) {
|
||||||
|
restOfArray.push(tx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// initializing everything up front helps V8 optimize property access later
|
||||||
|
auditPool[tx.txid] = {
|
||||||
|
txid: tx.txid,
|
||||||
|
fee: tx.fee,
|
||||||
|
size: tx.size,
|
||||||
|
weight: tx.weight,
|
||||||
|
feePerVsize: tx.feePerVsize,
|
||||||
|
vin: tx.vin,
|
||||||
|
relativesSet: false,
|
||||||
|
ancestorMap: new Map<string, AuditTransaction>(),
|
||||||
|
children: new Set<AuditTransaction>(),
|
||||||
|
ancestorFee: 0,
|
||||||
|
ancestorWeight: 0,
|
||||||
|
score: 0,
|
||||||
|
used: false,
|
||||||
|
modified: false,
|
||||||
|
modifiedNode: null,
|
||||||
|
};
|
||||||
|
mempoolArray.push(auditPool[tx.txid]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build relatives graph & calculate ancestor scores
|
||||||
|
for (const tx of mempoolArray) {
|
||||||
|
if (!tx.relativesSet) {
|
||||||
|
setRelatives(tx, auditPool);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by descending ancestor score
|
||||||
|
mempoolArray.sort((a, b) => (b.score || 0) - (a.score || 0));
|
||||||
|
|
||||||
|
// Build blocks by greedily choosing the highest feerate package
|
||||||
|
// (i.e. the package rooted in the transaction with the best ancestor score)
|
||||||
|
const blocks: MempoolBlockWithTransactions[] = [];
|
||||||
|
let blockWeight = 4000;
|
||||||
|
let blockSize = 0;
|
||||||
|
let transactions: AuditTransaction[] = [];
|
||||||
|
const modified: PairingHeap<AuditTransaction> = new PairingHeap((a, b): boolean => (a.score || 0) > (b.score || 0));
|
||||||
|
let overflow: AuditTransaction[] = [];
|
||||||
|
let failures = 0;
|
||||||
|
let top = 0;
|
||||||
|
while ((top < mempoolArray.length || !modified.isEmpty()) && (condenseRest || blocks.length < blockLimit)) {
|
||||||
|
// skip invalid transactions
|
||||||
|
while (top < mempoolArray.length && (mempoolArray[top].used || mempoolArray[top].modified)) {
|
||||||
|
top++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select best next package
|
||||||
|
let nextTx;
|
||||||
|
const nextPoolTx = mempoolArray[top];
|
||||||
|
const nextModifiedTx = modified.peek();
|
||||||
|
if (nextPoolTx && (!nextModifiedTx || (nextPoolTx.score || 0) > (nextModifiedTx.score || 0))) {
|
||||||
|
nextTx = nextPoolTx;
|
||||||
|
top++;
|
||||||
|
} else {
|
||||||
|
modified.pop();
|
||||||
|
if (nextModifiedTx) {
|
||||||
|
nextTx = nextModifiedTx;
|
||||||
|
nextTx.modifiedNode = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextTx && !nextTx?.used) {
|
||||||
|
// Check if the package fits into this block
|
||||||
|
if (blockWeight + nextTx.ancestorWeight < config.MEMPOOL.BLOCK_WEIGHT_UNITS) {
|
||||||
|
const ancestors: AuditTransaction[] = Array.from(nextTx.ancestorMap.values());
|
||||||
|
const descendants: AuditTransaction[] = [];
|
||||||
|
// 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];
|
||||||
|
const effectiveFeeRate = nextTx.ancestorFee / (nextTx.ancestorWeight / 4);
|
||||||
|
const used: AuditTransaction[] = [];
|
||||||
|
while (sortedTxSet.length) {
|
||||||
|
const ancestor = sortedTxSet.pop();
|
||||||
|
const mempoolTx = mempool[ancestor.txid];
|
||||||
|
ancestor.used = true;
|
||||||
|
ancestor.usedBy = nextTx.txid;
|
||||||
|
// update original copy of this tx with effective fee rate & relatives data
|
||||||
|
mempoolTx.effectiveFeePerVsize = effectiveFeeRate;
|
||||||
|
mempoolTx.ancestors = sortedTxSet.map((a) => {
|
||||||
|
return {
|
||||||
|
txid: a.txid,
|
||||||
|
fee: a.fee,
|
||||||
|
weight: a.weight,
|
||||||
|
};
|
||||||
|
}).reverse();
|
||||||
|
mempoolTx.descendants = descendants.map((a) => {
|
||||||
|
return {
|
||||||
|
txid: a.txid,
|
||||||
|
fee: a.fee,
|
||||||
|
weight: a.weight,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
descendants.push(ancestor);
|
||||||
|
mempoolTx.cpfpChecked = true;
|
||||||
|
transactions.push(ancestor);
|
||||||
|
blockSize += ancestor.size;
|
||||||
|
blockWeight += ancestor.weight;
|
||||||
|
used.push(ancestor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove these as valid package ancestors for any descendants remaining in the mempool
|
||||||
|
if (used.length) {
|
||||||
|
used.forEach(tx => {
|
||||||
|
updateDescendants(tx, auditPool, modified);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
failures = 0;
|
||||||
|
} else {
|
||||||
|
// hold this package in an overflow list while we check for smaller options
|
||||||
|
overflow.push(nextTx);
|
||||||
|
failures++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this block is full
|
||||||
|
const exceededPackageTries = failures > 1000 && blockWeight > (config.MEMPOOL.BLOCK_WEIGHT_UNITS - 4000);
|
||||||
|
const queueEmpty = top >= mempoolArray.length && modified.isEmpty();
|
||||||
|
if ((exceededPackageTries || queueEmpty) && (!condenseRest || blocks.length < blockLimit - 1)) {
|
||||||
|
// construct this block
|
||||||
|
if (transactions.length) {
|
||||||
|
blocks.push(dataToMempoolBlocks(transactions.map(t => mempool[t.txid]), blockSize, blockWeight, blocks.length));
|
||||||
|
}
|
||||||
|
// 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
|
||||||
|
for (const overflowTx of overflow.reverse()) {
|
||||||
|
if (overflowTx.modified) {
|
||||||
|
overflowTx.modifiedNode = modified.add(overflowTx);
|
||||||
|
} else {
|
||||||
|
top--;
|
||||||
|
mempoolArray[top] = overflowTx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
overflow = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (condenseRest) {
|
||||||
|
// pack any leftover transactions into the last block
|
||||||
|
for (const tx of overflow) {
|
||||||
|
if (!tx || tx?.used) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
blockWeight += tx.weight;
|
||||||
|
blockSize += tx.size;
|
||||||
|
const mempoolTx = mempool[tx.txid];
|
||||||
|
// update original copy of this tx with effective fee rate & relatives data
|
||||||
|
mempoolTx.effectiveFeePerVsize = tx.score;
|
||||||
|
mempoolTx.ancestors = (Array.from(tx.ancestorMap?.values()) as AuditTransaction[]).map((a) => {
|
||||||
|
return {
|
||||||
|
txid: a.txid,
|
||||||
|
fee: a.fee,
|
||||||
|
weight: a.weight,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
mempoolTx.bestDescendant = null;
|
||||||
|
mempoolTx.cpfpChecked = true;
|
||||||
|
transactions.push(tx);
|
||||||
|
tx.used = true;
|
||||||
|
}
|
||||||
|
const blockTransactions = transactions.map(t => mempool[t.txid]);
|
||||||
|
restOfArray.forEach(tx => {
|
||||||
|
blockWeight += tx.weight;
|
||||||
|
blockSize += tx.size;
|
||||||
|
tx.effectiveFeePerVsize = tx.feePerVsize;
|
||||||
|
tx.cpfpChecked = false;
|
||||||
|
tx.ancestors = [];
|
||||||
|
tx.bestDescendant = null;
|
||||||
|
blockTransactions.push(tx);
|
||||||
|
});
|
||||||
|
if (blockTransactions.length) {
|
||||||
|
blocks.push(dataToMempoolBlocks(blockTransactions, blockSize, blockWeight, blocks.length));
|
||||||
|
}
|
||||||
|
transactions = [];
|
||||||
|
} else if (transactions.length) {
|
||||||
|
blocks.push(dataToMempoolBlocks(transactions.map(t => mempool[t.txid]), blockSize, blockWeight, blocks.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
const end = Date.now();
|
||||||
|
const time = end - start;
|
||||||
|
logger.debug('Mempool templates calculated in ' + time / 1000 + ' seconds');
|
||||||
|
|
||||||
|
return {
|
||||||
|
mempool,
|
||||||
|
blocks
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// traverse in-mempool ancestors
|
||||||
|
// recursion unavoidable, but should be limited to depth < 25 by mempool policy
|
||||||
|
function setRelatives(
|
||||||
|
tx: AuditTransaction,
|
||||||
|
mempool: { [txid: string]: AuditTransaction },
|
||||||
|
): void {
|
||||||
|
for (const parent of tx.vin) {
|
||||||
|
const parentTx = mempool[parent.txid];
|
||||||
|
if (parentTx && !tx.ancestorMap?.has(parent.txid)) {
|
||||||
|
tx.ancestorMap.set(parent.txid, parentTx);
|
||||||
|
parentTx.children.add(tx);
|
||||||
|
// visit each node only once
|
||||||
|
if (!parentTx.relativesSet) {
|
||||||
|
setRelatives(parentTx, mempool);
|
||||||
|
}
|
||||||
|
parentTx.ancestorMap.forEach((ancestor) => {
|
||||||
|
tx.ancestorMap.set(ancestor.txid, ancestor);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
tx.ancestorFee = tx.fee || 0;
|
||||||
|
tx.ancestorWeight = tx.weight || 0;
|
||||||
|
tx.ancestorMap.forEach((ancestor) => {
|
||||||
|
tx.ancestorFee += ancestor.fee;
|
||||||
|
tx.ancestorWeight += ancestor.weight;
|
||||||
|
});
|
||||||
|
tx.score = tx.ancestorFee / ((tx.ancestorWeight / 4) || 1);
|
||||||
|
tx.relativesSet = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// iterate over remaining descendants, removing the root as a valid ancestor & updating the ancestor score
|
||||||
|
// avoids recursion to limit call stack depth
|
||||||
|
function updateDescendants(
|
||||||
|
rootTx: AuditTransaction,
|
||||||
|
mempool: { [txid: string]: AuditTransaction },
|
||||||
|
modified: PairingHeap<AuditTransaction>,
|
||||||
|
): void {
|
||||||
|
const descendantSet: Set<AuditTransaction> = new Set();
|
||||||
|
// stack of nodes left to visit
|
||||||
|
const descendants: AuditTransaction[] = [];
|
||||||
|
let descendantTx;
|
||||||
|
let tmpScore;
|
||||||
|
rootTx.children.forEach(childTx => {
|
||||||
|
if (!descendantSet.has(childTx)) {
|
||||||
|
descendants.push(childTx);
|
||||||
|
descendantSet.add(childTx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
while (descendants.length) {
|
||||||
|
descendantTx = descendants.pop();
|
||||||
|
if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.txid)) {
|
||||||
|
// remove tx as ancestor
|
||||||
|
descendantTx.ancestorMap.delete(rootTx.txid);
|
||||||
|
descendantTx.ancestorFee -= rootTx.fee;
|
||||||
|
descendantTx.ancestorWeight -= rootTx.weight;
|
||||||
|
tmpScore = descendantTx.score;
|
||||||
|
descendantTx.score = descendantTx.ancestorFee / (descendantTx.ancestorWeight / 4);
|
||||||
|
|
||||||
|
if (!descendantTx.modifiedNode) {
|
||||||
|
descendantTx.modified = true;
|
||||||
|
descendantTx.modifiedNode = modified.add(descendantTx);
|
||||||
|
} else {
|
||||||
|
// rebalance modified heap if score has changed
|
||||||
|
if (descendantTx.score < tmpScore) {
|
||||||
|
modified.decreasePriority(descendantTx.modifiedNode);
|
||||||
|
} else if (descendantTx.score > tmpScore) {
|
||||||
|
modified.increasePriority(descendantTx.modifiedNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add this node's children to the stack
|
||||||
|
descendantTx.children.forEach(childTx => {
|
||||||
|
// visit each node only once
|
||||||
|
if (!descendantSet.has(childTx)) {
|
||||||
|
descendants.push(childTx);
|
||||||
|
descendantSet.add(childTx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dataToMempoolBlocks(transactions: TransactionExtended[],
|
||||||
|
blockSize: number, blockWeight: number, blocksIndex: number): MempoolBlockWithTransactions {
|
||||||
|
let rangeLength = 4;
|
||||||
|
if (blocksIndex === 0) {
|
||||||
|
rangeLength = 8;
|
||||||
|
}
|
||||||
|
if (transactions.length > 4000) {
|
||||||
|
rangeLength = 6;
|
||||||
|
} else if (transactions.length > 10000) {
|
||||||
|
rangeLength = 8;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
blockSize: blockSize,
|
||||||
|
blockVSize: blockWeight / 4,
|
||||||
|
nTx: transactions.length,
|
||||||
|
totalFees: transactions.reduce((acc, cur) => acc + cur.fee, 0),
|
||||||
|
medianFee: Common.percentile(transactions.map((tx) => tx.effectiveFeePerVsize), config.MEMPOOL.RECOMMENDED_FEE_PERCENTILE),
|
||||||
|
feeRange: Common.getFeesInRange(transactions, rangeLength),
|
||||||
|
transactionIds: transactions.map((tx) => tx.txid),
|
||||||
|
transactions: transactions.map((tx) => Common.stripTransaction(tx)),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ import difficultyAdjustment from './difficulty-adjustment';
|
|||||||
import feeApi from './fee-api';
|
import feeApi from './fee-api';
|
||||||
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
|
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
|
||||||
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
|
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
|
||||||
|
import Audit from './audit';
|
||||||
|
|
||||||
class WebsocketHandler {
|
class WebsocketHandler {
|
||||||
private wss: WebSocket.Server | undefined;
|
private wss: WebSocket.Server | undefined;
|
||||||
@@ -243,13 +244,18 @@ class WebsocketHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMempoolChange(newMempool: { [txid: string]: TransactionExtended },
|
async handleMempoolChange(newMempool: { [txid: string]: TransactionExtended },
|
||||||
newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]) {
|
newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]): Promise<void> {
|
||||||
if (!this.wss) {
|
if (!this.wss) {
|
||||||
throw new Error('WebSocket.Server is not set');
|
throw new Error('WebSocket.Server is not set');
|
||||||
}
|
}
|
||||||
|
|
||||||
mempoolBlocks.updateMempoolBlocks(newMempool);
|
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
|
||||||
|
await mempoolBlocks.makeBlockTemplates(newMempool, 8, null, true);
|
||||||
|
} else {
|
||||||
|
mempoolBlocks.updateMempoolBlocks(newMempool);
|
||||||
|
}
|
||||||
|
|
||||||
const mBlocks = mempoolBlocks.getMempoolBlocks();
|
const mBlocks = mempoolBlocks.getMempoolBlocks();
|
||||||
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
|
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
|
||||||
const mempoolInfo = memPool.getMempoolInfo();
|
const mempoolInfo = memPool.getMempoolInfo();
|
||||||
@@ -405,75 +411,70 @@ class WebsocketHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNewBlock(block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) {
|
async handleNewBlock(block: BlockExtended, txIds: string[], transactions: TransactionExtended[]): Promise<void> {
|
||||||
if (!this.wss) {
|
if (!this.wss) {
|
||||||
throw new Error('WebSocket.Server is not set');
|
throw new Error('WebSocket.Server is not set');
|
||||||
}
|
}
|
||||||
|
|
||||||
let mBlocks: undefined | MempoolBlock[];
|
|
||||||
let mBlockDeltas: undefined | MempoolBlockDelta[];
|
|
||||||
let matchRate = 0;
|
|
||||||
const _memPool = memPool.getMempool();
|
const _memPool = memPool.getMempool();
|
||||||
const _mempoolBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
|
|
||||||
|
|
||||||
if (_mempoolBlocks[0]) {
|
if (config.MEMPOOL.ADVANCED_GBT_AUDIT) {
|
||||||
const matches: string[] = [];
|
await mempoolBlocks.makeBlockTemplates(_memPool, 2);
|
||||||
const added: string[] = [];
|
} else {
|
||||||
const missing: string[] = [];
|
|
||||||
|
|
||||||
for (const txId of txIds) {
|
|
||||||
if (_mempoolBlocks[0].transactionIds.indexOf(txId) > -1) {
|
|
||||||
matches.push(txId);
|
|
||||||
} else {
|
|
||||||
added.push(txId);
|
|
||||||
}
|
|
||||||
delete _memPool[txId];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const txId of _mempoolBlocks[0].transactionIds) {
|
|
||||||
if (matches.includes(txId) || added.includes(txId)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
missing.push(txId);
|
|
||||||
}
|
|
||||||
|
|
||||||
matchRate = Math.round((Math.max(0, matches.length - missing.length - added.length) / txIds.length * 100) * 100) / 100;
|
|
||||||
mempoolBlocks.updateMempoolBlocks(_memPool);
|
mempoolBlocks.updateMempoolBlocks(_memPool);
|
||||||
mBlocks = mempoolBlocks.getMempoolBlocks();
|
}
|
||||||
mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
|
|
||||||
|
|
||||||
if (Common.indexingEnabled()) {
|
if (Common.indexingEnabled() && memPool.isInSync()) {
|
||||||
const stripped = _mempoolBlocks[0].transactions.map((tx) => {
|
const projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
|
||||||
return {
|
|
||||||
txid: tx.txid,
|
|
||||||
vsize: tx.vsize,
|
|
||||||
fee: tx.fee ? Math.round(tx.fee) : 0,
|
|
||||||
value: tx.value,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
BlocksSummariesRepository.$saveSummary({
|
|
||||||
height: block.height,
|
|
||||||
template: {
|
|
||||||
id: block.id,
|
|
||||||
transactions: stripped
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
BlocksAuditsRepository.$saveAudit({
|
const { censored, added, fresh, score } = Audit.auditBlock(transactions, projectedBlocks, _memPool);
|
||||||
time: block.timestamp,
|
const matchRate = Math.round(score * 100 * 100) / 100;
|
||||||
height: block.height,
|
|
||||||
hash: block.id,
|
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions.map((tx) => {
|
||||||
addedTxs: added,
|
return {
|
||||||
missingTxs: missing,
|
txid: tx.txid,
|
||||||
matchRate: matchRate,
|
vsize: tx.vsize,
|
||||||
});
|
fee: tx.fee ? Math.round(tx.fee) : 0,
|
||||||
|
value: tx.value,
|
||||||
|
};
|
||||||
|
}) : [];
|
||||||
|
|
||||||
|
BlocksSummariesRepository.$saveTemplate({
|
||||||
|
height: block.height,
|
||||||
|
template: {
|
||||||
|
id: block.id,
|
||||||
|
transactions: stripped
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
BlocksAuditsRepository.$saveAudit({
|
||||||
|
time: block.timestamp,
|
||||||
|
height: block.height,
|
||||||
|
hash: block.id,
|
||||||
|
addedTxs: added,
|
||||||
|
missingTxs: censored,
|
||||||
|
freshTxs: fresh,
|
||||||
|
matchRate: matchRate,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (block.extras) {
|
||||||
|
block.extras.matchRate = matchRate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (block.extras) {
|
// Update mempool to remove transactions included in the new block
|
||||||
block.extras.matchRate = matchRate;
|
for (const txId of txIds) {
|
||||||
|
delete _memPool[txId];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
|
||||||
|
await mempoolBlocks.makeBlockTemplates(_memPool, 8, null, true);
|
||||||
|
} else {
|
||||||
|
mempoolBlocks.updateMempoolBlocks(_memPool);
|
||||||
|
}
|
||||||
|
const mBlocks = mempoolBlocks.getMempoolBlocks();
|
||||||
|
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
|
||||||
|
|
||||||
const da = difficultyAdjustment.getDifficultyAdjustment();
|
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||||
const fees = feeApi.getRecommendedFee();
|
const fees = feeApi.getRecommendedFee();
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const configFromFile = require(
|
|||||||
|
|
||||||
interface IConfig {
|
interface IConfig {
|
||||||
MEMPOOL: {
|
MEMPOOL: {
|
||||||
|
ENABLED: boolean;
|
||||||
NETWORK: 'mainnet' | 'testnet' | 'signet' | 'liquid' | 'liquidtestnet';
|
NETWORK: 'mainnet' | 'testnet' | 'signet' | 'liquid' | 'liquidtestnet';
|
||||||
BACKEND: 'esplora' | 'electrum' | 'none';
|
BACKEND: 'esplora' | 'electrum' | 'none';
|
||||||
HTTP_PORT: number;
|
HTTP_PORT: number;
|
||||||
@@ -28,6 +29,9 @@ interface IConfig {
|
|||||||
AUTOMATIC_BLOCK_REINDEXING: boolean;
|
AUTOMATIC_BLOCK_REINDEXING: boolean;
|
||||||
POOLS_JSON_URL: string,
|
POOLS_JSON_URL: string,
|
||||||
POOLS_JSON_TREE_URL: string,
|
POOLS_JSON_TREE_URL: string,
|
||||||
|
ADVANCED_GBT_AUDIT: boolean;
|
||||||
|
ADVANCED_GBT_MEMPOOL: boolean;
|
||||||
|
TRANSACTION_INDEXING: boolean;
|
||||||
};
|
};
|
||||||
ESPLORA: {
|
ESPLORA: {
|
||||||
REST_API_URL: string;
|
REST_API_URL: string;
|
||||||
@@ -39,6 +43,8 @@ interface IConfig {
|
|||||||
STATS_REFRESH_INTERVAL: number;
|
STATS_REFRESH_INTERVAL: number;
|
||||||
GRAPH_REFRESH_INTERVAL: number;
|
GRAPH_REFRESH_INTERVAL: number;
|
||||||
LOGGER_UPDATE_INTERVAL: number;
|
LOGGER_UPDATE_INTERVAL: number;
|
||||||
|
FORENSICS_INTERVAL: number;
|
||||||
|
FORENSICS_RATE_LIMIT: number;
|
||||||
};
|
};
|
||||||
LND: {
|
LND: {
|
||||||
TLS_CERT_PATH: string;
|
TLS_CERT_PATH: string;
|
||||||
@@ -119,6 +125,7 @@ interface IConfig {
|
|||||||
|
|
||||||
const defaults: IConfig = {
|
const defaults: IConfig = {
|
||||||
'MEMPOOL': {
|
'MEMPOOL': {
|
||||||
|
'ENABLED': true,
|
||||||
'NETWORK': 'mainnet',
|
'NETWORK': 'mainnet',
|
||||||
'BACKEND': 'none',
|
'BACKEND': 'none',
|
||||||
'HTTP_PORT': 8999,
|
'HTTP_PORT': 8999,
|
||||||
@@ -143,6 +150,9 @@ const defaults: IConfig = {
|
|||||||
'AUTOMATIC_BLOCK_REINDEXING': false,
|
'AUTOMATIC_BLOCK_REINDEXING': false,
|
||||||
'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json',
|
'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json',
|
||||||
'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
|
'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
|
||||||
|
'ADVANCED_GBT_AUDIT': false,
|
||||||
|
'ADVANCED_GBT_MEMPOOL': false,
|
||||||
|
'TRANSACTION_INDEXING': false,
|
||||||
},
|
},
|
||||||
'ESPLORA': {
|
'ESPLORA': {
|
||||||
'REST_API_URL': 'http://127.0.0.1:3000',
|
'REST_API_URL': 'http://127.0.0.1:3000',
|
||||||
@@ -195,6 +205,8 @@ const defaults: IConfig = {
|
|||||||
'STATS_REFRESH_INTERVAL': 600,
|
'STATS_REFRESH_INTERVAL': 600,
|
||||||
'GRAPH_REFRESH_INTERVAL': 600,
|
'GRAPH_REFRESH_INTERVAL': 600,
|
||||||
'LOGGER_UPDATE_INTERVAL': 30,
|
'LOGGER_UPDATE_INTERVAL': 30,
|
||||||
|
'FORENSICS_INTERVAL': 43200,
|
||||||
|
'FORENSICS_RATE_LIMIT': 20,
|
||||||
},
|
},
|
||||||
'LND': {
|
'LND': {
|
||||||
'TLS_CERT_PATH': '',
|
'TLS_CERT_PATH': '',
|
||||||
@@ -224,11 +236,11 @@ const defaults: IConfig = {
|
|||||||
'BISQ_URL': 'https://bisq.markets/api',
|
'BISQ_URL': 'https://bisq.markets/api',
|
||||||
'BISQ_ONION': 'http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api'
|
'BISQ_ONION': 'http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api'
|
||||||
},
|
},
|
||||||
"MAXMIND": {
|
'MAXMIND': {
|
||||||
'ENABLED': false,
|
'ENABLED': false,
|
||||||
"GEOLITE2_CITY": "/usr/local/share/GeoIP/GeoLite2-City.mmdb",
|
'GEOLITE2_CITY': '/usr/local/share/GeoIP/GeoLite2-City.mmdb',
|
||||||
"GEOLITE2_ASN": "/usr/local/share/GeoIP/GeoLite2-ASN.mmdb",
|
'GEOLITE2_ASN': '/usr/local/share/GeoIP/GeoLite2-ASN.mmdb',
|
||||||
"GEOIP2_ISP": "/usr/local/share/GeoIP/GeoIP2-ISP.mmdb"
|
'GEOIP2_ISP': '/usr/local/share/GeoIP/GeoIP2-ISP.mmdb'
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import express from "express";
|
import express from 'express';
|
||||||
import { Application, Request, Response, NextFunction } from 'express';
|
import { Application, Request, Response, NextFunction } from 'express';
|
||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import * as WebSocket from 'ws';
|
import * as WebSocket from 'ws';
|
||||||
@@ -34,7 +34,8 @@ import miningRoutes from './api/mining/mining-routes';
|
|||||||
import bisqRoutes from './api/bisq/bisq.routes';
|
import bisqRoutes from './api/bisq/bisq.routes';
|
||||||
import liquidRoutes from './api/liquid/liquid.routes';
|
import liquidRoutes from './api/liquid/liquid.routes';
|
||||||
import bitcoinRoutes from './api/bitcoin/bitcoin.routes';
|
import bitcoinRoutes from './api/bitcoin/bitcoin.routes';
|
||||||
import fundingTxFetcher from "./tasks/lightning/sync-tasks/funding-tx-fetcher";
|
import fundingTxFetcher from './tasks/lightning/sync-tasks/funding-tx-fetcher';
|
||||||
|
import forensicsService from './tasks/lightning/forensics.service';
|
||||||
|
|
||||||
class Server {
|
class Server {
|
||||||
private wss: WebSocket.Server | undefined;
|
private wss: WebSocket.Server | undefined;
|
||||||
@@ -74,7 +75,7 @@ class Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async startServer(worker = false) {
|
async startServer(worker = false): Promise<void> {
|
||||||
logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`);
|
logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`);
|
||||||
|
|
||||||
this.app
|
this.app
|
||||||
@@ -83,7 +84,7 @@ class Server {
|
|||||||
next();
|
next();
|
||||||
})
|
})
|
||||||
.use(express.urlencoded({ extended: true }))
|
.use(express.urlencoded({ extended: true }))
|
||||||
.use(express.text())
|
.use(express.text({ type: ['text/plain', 'application/base64'] }))
|
||||||
;
|
;
|
||||||
|
|
||||||
this.server = http.createServer(this.app);
|
this.server = http.createServer(this.app);
|
||||||
@@ -92,7 +93,9 @@ class Server {
|
|||||||
this.setUpWebsocketHandling();
|
this.setUpWebsocketHandling();
|
||||||
|
|
||||||
await syncAssets.syncAssets$();
|
await syncAssets.syncAssets$();
|
||||||
diskCache.loadMempoolCache();
|
if (config.MEMPOOL.ENABLED) {
|
||||||
|
diskCache.loadMempoolCache();
|
||||||
|
}
|
||||||
|
|
||||||
if (config.DATABASE.ENABLED) {
|
if (config.DATABASE.ENABLED) {
|
||||||
await DB.checkDbConnection();
|
await DB.checkDbConnection();
|
||||||
@@ -127,7 +130,10 @@ class Server {
|
|||||||
fiatConversion.startService();
|
fiatConversion.startService();
|
||||||
|
|
||||||
this.setUpHttpApiRoutes();
|
this.setUpHttpApiRoutes();
|
||||||
this.runMainUpdateLoop();
|
|
||||||
|
if (config.MEMPOOL.ENABLED) {
|
||||||
|
this.runMainUpdateLoop();
|
||||||
|
}
|
||||||
|
|
||||||
if (config.BISQ.ENABLED) {
|
if (config.BISQ.ENABLED) {
|
||||||
bisq.startBisqService();
|
bisq.startBisqService();
|
||||||
@@ -149,7 +155,7 @@ class Server {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async runMainUpdateLoop() {
|
async runMainUpdateLoop(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
await memPool.$updateMemPoolInfo();
|
await memPool.$updateMemPoolInfo();
|
||||||
@@ -183,10 +189,11 @@ class Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async $runLightningBackend() {
|
async $runLightningBackend(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await fundingTxFetcher.$init();
|
await fundingTxFetcher.$init();
|
||||||
await networkSyncService.$startService();
|
await networkSyncService.$startService();
|
||||||
|
await forensicsService.$startService();
|
||||||
await lightningStatsUpdater.$startService();
|
await lightningStatsUpdater.$startService();
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
logger.err(`Nodejs lightning backend crashed. Restarting in 1 minute. Reason: ${(e instanceof Error ? e.message : e)}`);
|
logger.err(`Nodejs lightning backend crashed. Restarting in 1 minute. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||||
@@ -195,7 +202,7 @@ class Server {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
setUpWebsocketHandling() {
|
setUpWebsocketHandling(): void {
|
||||||
if (this.wss) {
|
if (this.wss) {
|
||||||
websocketHandler.setWebsocketServer(this.wss);
|
websocketHandler.setWebsocketServer(this.wss);
|
||||||
}
|
}
|
||||||
@@ -209,19 +216,21 @@ class Server {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
websocketHandler.setupConnectionHandling();
|
websocketHandler.setupConnectionHandling();
|
||||||
statistics.setNewStatisticsEntryCallback(websocketHandler.handleNewStatistic.bind(websocketHandler));
|
if (config.MEMPOOL.ENABLED) {
|
||||||
blocks.setNewBlockCallback(websocketHandler.handleNewBlock.bind(websocketHandler));
|
statistics.setNewStatisticsEntryCallback(websocketHandler.handleNewStatistic.bind(websocketHandler));
|
||||||
memPool.setMempoolChangedCallback(websocketHandler.handleMempoolChange.bind(websocketHandler));
|
memPool.setAsyncMempoolChangedCallback(websocketHandler.handleMempoolChange.bind(websocketHandler));
|
||||||
|
blocks.setNewAsyncBlockCallback(websocketHandler.handleNewBlock.bind(websocketHandler));
|
||||||
|
}
|
||||||
fiatConversion.setProgressChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
|
fiatConversion.setProgressChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
|
||||||
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
|
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
|
||||||
}
|
}
|
||||||
|
|
||||||
setUpHttpApiRoutes() {
|
setUpHttpApiRoutes(): void {
|
||||||
bitcoinRoutes.initRoutes(this.app);
|
bitcoinRoutes.initRoutes(this.app);
|
||||||
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED) {
|
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && config.MEMPOOL.ENABLED) {
|
||||||
statisticsRoutes.initRoutes(this.app);
|
statisticsRoutes.initRoutes(this.app);
|
||||||
}
|
}
|
||||||
if (Common.indexingEnabled()) {
|
if (Common.indexingEnabled() && config.MEMPOOL.ENABLED) {
|
||||||
miningRoutes.initRoutes(this.app);
|
miningRoutes.initRoutes(this.app);
|
||||||
}
|
}
|
||||||
if (config.BISQ.ENABLED) {
|
if (config.BISQ.ENABLED) {
|
||||||
@@ -238,4 +247,4 @@ class Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = new Server();
|
((): Server => new Server())();
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ class Indexer {
|
|||||||
await mining.$generateNetworkHashrateHistory();
|
await mining.$generateNetworkHashrateHistory();
|
||||||
await mining.$generatePoolHashrateHistory();
|
await mining.$generatePoolHashrateHistory();
|
||||||
await blocks.$generateBlocksSummariesDatabase();
|
await blocks.$generateBlocksSummariesDatabase();
|
||||||
|
await blocks.$generateCPFPDatabase();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.indexerRunning = false;
|
this.indexerRunning = false;
|
||||||
logger.err(`Indexer failed, trying again in 10 seconds. Reason: ` + (e instanceof Error ? e.message : e));
|
logger.err(`Indexer failed, trying again in 10 seconds. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { IEsploraApi } from './api/bitcoin/esplora-api.interface';
|
import { IEsploraApi } from './api/bitcoin/esplora-api.interface';
|
||||||
|
import { HeapNode } from "./utils/pairing-heap";
|
||||||
|
|
||||||
export interface PoolTag {
|
export interface PoolTag {
|
||||||
id: number; // mysql row id
|
id: number; // mysql row id
|
||||||
@@ -27,10 +28,16 @@ export interface BlockAudit {
|
|||||||
height: number,
|
height: number,
|
||||||
hash: string,
|
hash: string,
|
||||||
missingTxs: string[],
|
missingTxs: string[],
|
||||||
|
freshTxs: string[],
|
||||||
addedTxs: string[],
|
addedTxs: string[],
|
||||||
matchRate: number,
|
matchRate: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AuditScore {
|
||||||
|
hash: string,
|
||||||
|
matchRate?: number,
|
||||||
|
}
|
||||||
|
|
||||||
export interface MempoolBlock {
|
export interface MempoolBlock {
|
||||||
blockSize: number;
|
blockSize: number;
|
||||||
blockVSize: number;
|
blockVSize: number;
|
||||||
@@ -65,17 +72,46 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
|
|||||||
firstSeen?: number;
|
firstSeen?: number;
|
||||||
effectiveFeePerVsize: number;
|
effectiveFeePerVsize: number;
|
||||||
ancestors?: Ancestor[];
|
ancestors?: Ancestor[];
|
||||||
|
descendants?: Ancestor[];
|
||||||
bestDescendant?: BestDescendant | null;
|
bestDescendant?: BestDescendant | null;
|
||||||
cpfpChecked?: boolean;
|
cpfpChecked?: boolean;
|
||||||
deleteAfter?: number;
|
deleteAfter?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Ancestor {
|
export interface AuditTransaction {
|
||||||
|
txid: string;
|
||||||
|
fee: number;
|
||||||
|
size: number;
|
||||||
|
weight: number;
|
||||||
|
feePerVsize: number;
|
||||||
|
vin: IEsploraApi.Vin[];
|
||||||
|
relativesSet: boolean;
|
||||||
|
ancestorMap: Map<string, AuditTransaction>;
|
||||||
|
children: Set<AuditTransaction>;
|
||||||
|
ancestorFee: number;
|
||||||
|
ancestorWeight: number;
|
||||||
|
score: number;
|
||||||
|
used: boolean;
|
||||||
|
modified: boolean;
|
||||||
|
modifiedNode: HeapNode<AuditTransaction>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Ancestor {
|
||||||
txid: string;
|
txid: string;
|
||||||
weight: number;
|
weight: number;
|
||||||
fee: number;
|
fee: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TransactionSet {
|
||||||
|
fee: number;
|
||||||
|
weight: number;
|
||||||
|
score: number;
|
||||||
|
children?: Set<string>;
|
||||||
|
available?: boolean;
|
||||||
|
modified?: boolean;
|
||||||
|
modifiedNode?: HeapNode<string>;
|
||||||
|
}
|
||||||
|
|
||||||
interface BestDescendant {
|
interface BestDescendant {
|
||||||
txid: string;
|
txid: string;
|
||||||
weight: number;
|
weight: number;
|
||||||
@@ -84,7 +120,9 @@ interface BestDescendant {
|
|||||||
|
|
||||||
export interface CpfpInfo {
|
export interface CpfpInfo {
|
||||||
ancestors: Ancestor[];
|
ancestors: Ancestor[];
|
||||||
bestDescendant: BestDescendant | null;
|
bestDescendant?: BestDescendant | null;
|
||||||
|
descendants?: Ancestor[];
|
||||||
|
effectiveFeePerVsize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TransactionStripped {
|
export interface TransactionStripped {
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
|
import blocks from '../api/blocks';
|
||||||
import DB from '../database';
|
import DB from '../database';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import { BlockAudit } from '../mempool.interfaces';
|
import { BlockAudit, AuditScore } from '../mempool.interfaces';
|
||||||
|
|
||||||
class BlocksAuditRepositories {
|
class BlocksAuditRepositories {
|
||||||
public async $saveAudit(audit: BlockAudit): Promise<void> {
|
public async $saveAudit(audit: BlockAudit): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, match_rate)
|
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, fresh_txs, match_rate)
|
||||||
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
|
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
|
||||||
JSON.stringify(audit.addedTxs), audit.matchRate]);
|
JSON.stringify(audit.addedTxs), JSON.stringify(audit.freshTxs), audit.matchRate]);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
|
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
|
||||||
logger.debug(`Cannot save block audit for block ${audit.hash} because it has already been indexed, ignoring`);
|
logger.debug(`Cannot save block audit for block ${audit.hash} because it has already been indexed, ignoring`);
|
||||||
@@ -51,24 +52,58 @@ class BlocksAuditRepositories {
|
|||||||
const [rows]: any[] = await DB.query(
|
const [rows]: any[] = await DB.query(
|
||||||
`SELECT blocks.height, blocks.hash as id, UNIX_TIMESTAMP(blocks.blockTimestamp) as timestamp, blocks.size,
|
`SELECT blocks.height, blocks.hash as id, UNIX_TIMESTAMP(blocks.blockTimestamp) as timestamp, blocks.size,
|
||||||
blocks.weight, blocks.tx_count,
|
blocks.weight, blocks.tx_count,
|
||||||
transactions, template, missing_txs as missingTxs, added_txs as addedTxs, match_rate as matchRate
|
transactions, template, missing_txs as missingTxs, added_txs as addedTxs, fresh_txs as freshTxs, match_rate as matchRate
|
||||||
FROM blocks_audits
|
FROM blocks_audits
|
||||||
JOIN blocks ON blocks.hash = blocks_audits.hash
|
JOIN blocks ON blocks.hash = blocks_audits.hash
|
||||||
JOIN blocks_summaries ON blocks_summaries.id = blocks_audits.hash
|
JOIN blocks_summaries ON blocks_summaries.id = blocks_audits.hash
|
||||||
WHERE blocks_audits.hash = "${hash}"
|
WHERE blocks_audits.hash = "${hash}"
|
||||||
`);
|
`);
|
||||||
|
|
||||||
rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
|
if (rows.length) {
|
||||||
rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
|
rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
|
||||||
rows[0].transactions = JSON.parse(rows[0].transactions);
|
rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
|
||||||
rows[0].template = JSON.parse(rows[0].template);
|
rows[0].freshTxs = JSON.parse(rows[0].freshTxs);
|
||||||
|
rows[0].transactions = JSON.parse(rows[0].transactions);
|
||||||
|
rows[0].template = JSON.parse(rows[0].template);
|
||||||
|
|
||||||
|
if (rows[0].transactions.length) {
|
||||||
|
return rows[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Cannot fetch block audit from db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getBlockAuditScore(hash: string): Promise<AuditScore> {
|
||||||
|
try {
|
||||||
|
const [rows]: any[] = await DB.query(
|
||||||
|
`SELECT hash, match_rate as matchRate
|
||||||
|
FROM blocks_audits
|
||||||
|
WHERE blocks_audits.hash = "${hash}"
|
||||||
|
`);
|
||||||
return rows[0];
|
return rows[0];
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.err(`Cannot fetch block audit from db. Reason: ` + (e instanceof Error ? e.message : e));
|
logger.err(`Cannot fetch block audit from db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $getBlockAuditScores(maxHeight: number, minHeight: number): Promise<AuditScore[]> {
|
||||||
|
try {
|
||||||
|
const [rows]: any[] = await DB.query(
|
||||||
|
`SELECT hash, match_rate as matchRate
|
||||||
|
FROM blocks_audits
|
||||||
|
WHERE blocks_audits.height BETWEEN ? AND ?
|
||||||
|
`, [minHeight, maxHeight]);
|
||||||
|
return rows;
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Cannot fetch block audit from db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new BlocksAuditRepositories();
|
export default new BlocksAuditRepositories();
|
||||||
|
|||||||
@@ -392,6 +392,36 @@ class BlocksRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the first block at or directly after a given timestamp
|
||||||
|
* @param timestamp number unix time in seconds
|
||||||
|
* @returns The height and timestamp of a block (timestamp might vary from given timestamp)
|
||||||
|
*/
|
||||||
|
public async $getBlockHeightFromTimestamp(
|
||||||
|
timestamp: number,
|
||||||
|
): Promise<{ height: number; hash: string; timestamp: number }> {
|
||||||
|
try {
|
||||||
|
// Get first block at or after the given timestamp
|
||||||
|
const query = `SELECT height, hash, blockTimestamp as timestamp FROM blocks
|
||||||
|
WHERE blockTimestamp <= FROM_UNIXTIME(?)
|
||||||
|
ORDER BY blockTimestamp DESC
|
||||||
|
LIMIT 1`;
|
||||||
|
const params = [timestamp];
|
||||||
|
const [rows]: any[][] = await DB.query(query, params);
|
||||||
|
if (rows.length === 0) {
|
||||||
|
throw new Error(`No block was found before timestamp ${timestamp}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows[0];
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(
|
||||||
|
'Cannot get block height from timestamp from the db. Reason: ' +
|
||||||
|
(e instanceof Error ? e.message : e),
|
||||||
|
);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return blocks height
|
* Return blocks height
|
||||||
*/
|
*/
|
||||||
@@ -632,6 +662,23 @@ class BlocksRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of blocks that have not had CPFP data indexed
|
||||||
|
*/
|
||||||
|
public async $getCPFPUnindexedBlocks(): Promise<any[]> {
|
||||||
|
try {
|
||||||
|
const [rows]: any = await DB.query(`SELECT height, hash FROM blocks WHERE cpfp_indexed = 0 ORDER BY height DESC`);
|
||||||
|
return rows;
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('Cannot fetch CPFP unindexed blocks. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $setCPFPIndexed(hash: string): Promise<void> {
|
||||||
|
await DB.query(`UPDATE blocks SET cpfp_indexed = 1 WHERE hash = ?`, [hash]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the oldest block from a consecutive chain of block from the most recent one
|
* Return the oldest block from a consecutive chain of block from the most recent one
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -17,19 +17,16 @@ class BlocksSummariesRepository {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $saveSummary(params: { height: number, mined?: BlockSummary, template?: BlockSummary}) {
|
public async $saveSummary(params: { height: number, mined?: BlockSummary}) {
|
||||||
const blockId = params.mined?.id ?? params.template?.id;
|
const blockId = params.mined?.id;
|
||||||
try {
|
try {
|
||||||
const [dbSummary]: any[] = await DB.query(`SELECT * FROM blocks_summaries WHERE id = "${blockId}"`);
|
const transactions = JSON.stringify(params.mined?.transactions || []);
|
||||||
if (dbSummary.length === 0) { // First insertion
|
await DB.query(`
|
||||||
await DB.query(`INSERT INTO blocks_summaries VALUE (?, ?, ?, ?)`, [
|
INSERT INTO blocks_summaries (height, id, transactions, template)
|
||||||
params.height, blockId, JSON.stringify(params.mined?.transactions ?? []), JSON.stringify(params.template?.transactions ?? [])
|
VALUE (?, ?, ?, ?)
|
||||||
]);
|
ON DUPLICATE KEY UPDATE
|
||||||
} else if (params.mined !== undefined) { // Update mined block summary
|
transactions = ?
|
||||||
await DB.query(`UPDATE blocks_summaries SET transactions = ? WHERE id = "${params.mined.id}"`, [JSON.stringify(params.mined.transactions)]);
|
`, [params.height, blockId, transactions, '[]', transactions]);
|
||||||
} else if (params.template !== undefined) { // Update template block summary
|
|
||||||
await DB.query(`UPDATE blocks_summaries SET template = ? WHERE id = "${params.template.id}"`, [JSON.stringify(params.template?.transactions)]);
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
|
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
|
||||||
logger.debug(`Cannot save block summary for ${blockId} because it has already been indexed, ignoring`);
|
logger.debug(`Cannot save block summary for ${blockId} because it has already been indexed, ignoring`);
|
||||||
@@ -40,6 +37,26 @@ class BlocksSummariesRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $saveTemplate(params: { height: number, template: BlockSummary}) {
|
||||||
|
const blockId = params.template?.id;
|
||||||
|
try {
|
||||||
|
const transactions = JSON.stringify(params.template?.transactions || []);
|
||||||
|
await DB.query(`
|
||||||
|
INSERT INTO blocks_summaries (height, id, transactions, template)
|
||||||
|
VALUE (?, ?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
template = ?
|
||||||
|
`, [params.height, blockId, '[]', transactions, transactions]);
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
|
||||||
|
logger.debug(`Cannot save block template for ${blockId} because it has already been indexed, ignoring`);
|
||||||
|
} else {
|
||||||
|
logger.debug(`Cannot save block template for ${blockId}. Reason: ${e instanceof Error ? e.message : e}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async $getIndexedSummariesId(): Promise<string[]> {
|
public async $getIndexedSummariesId(): Promise<string[]> {
|
||||||
try {
|
try {
|
||||||
const [rows]: any[] = await DB.query(`SELECT id from blocks_summaries`);
|
const [rows]: any[] = await DB.query(`SELECT id from blocks_summaries`);
|
||||||
|
|||||||
43
backend/src/repositories/CpfpRepository.ts
Normal file
43
backend/src/repositories/CpfpRepository.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import DB from '../database';
|
||||||
|
import logger from '../logger';
|
||||||
|
import { Ancestor } from '../mempool.interfaces';
|
||||||
|
|
||||||
|
class CpfpRepository {
|
||||||
|
public async $saveCluster(height: number, txs: Ancestor[], effectiveFeePerVsize: number): Promise<void> {
|
||||||
|
try {
|
||||||
|
const txsJson = JSON.stringify(txs);
|
||||||
|
await DB.query(
|
||||||
|
`
|
||||||
|
INSERT INTO cpfp_clusters(root, height, txs, fee_rate)
|
||||||
|
VALUE (?, ?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
height = ?,
|
||||||
|
txs = ?,
|
||||||
|
fee_rate = ?
|
||||||
|
`,
|
||||||
|
[txs[0].txid, height, txsJson, effectiveFeePerVsize, height, txsJson, effectiveFeePerVsize, height]
|
||||||
|
);
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Cannot save cpfp cluster into db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $deleteClustersFrom(height: number): Promise<void> {
|
||||||
|
logger.info(`Delete newer cpfp clusters from height ${height} from the database`);
|
||||||
|
try {
|
||||||
|
await DB.query(
|
||||||
|
`
|
||||||
|
DELETE from cpfp_clusters
|
||||||
|
WHERE height >= ?
|
||||||
|
`,
|
||||||
|
[height]
|
||||||
|
);
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Cannot delete cpfp clusters from db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new CpfpRepository();
|
||||||
67
backend/src/repositories/NodeRecordsRepository.ts
Normal file
67
backend/src/repositories/NodeRecordsRepository.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { ResultSetHeader, RowDataPacket } from 'mysql2';
|
||||||
|
import DB from '../database';
|
||||||
|
import logger from '../logger';
|
||||||
|
|
||||||
|
export interface NodeRecord {
|
||||||
|
publicKey: string; // node public key
|
||||||
|
type: number; // TLV extension record type
|
||||||
|
payload: string; // base64 record payload
|
||||||
|
}
|
||||||
|
|
||||||
|
class NodesRecordsRepository {
|
||||||
|
public async $saveRecord(record: NodeRecord): Promise<void> {
|
||||||
|
try {
|
||||||
|
const payloadBytes = Buffer.from(record.payload, 'base64');
|
||||||
|
await DB.query(`
|
||||||
|
INSERT INTO nodes_records(public_key, type, payload)
|
||||||
|
VALUE (?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
payload = ?
|
||||||
|
`, [record.publicKey, record.type, payloadBytes, payloadBytes]);
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.errno !== 1062) { // ER_DUP_ENTRY - Not an issue, just ignore this
|
||||||
|
logger.err(`Cannot save node record (${[record.publicKey, record.type, record.payload]}) into db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
// We don't throw, not a critical issue if we miss some nodes records
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getRecordTypes(publicKey: string): Promise<any> {
|
||||||
|
try {
|
||||||
|
const query = `
|
||||||
|
SELECT type FROM nodes_records
|
||||||
|
WHERE public_key = ?
|
||||||
|
`;
|
||||||
|
const [rows] = await DB.query<RowDataPacket[][]>(query, [publicKey]);
|
||||||
|
return rows.map(row => row['type']);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Cannot retrieve custom records for ${publicKey} from db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $deleteUnusedRecords(publicKey: string, recordTypes: number[]): Promise<number> {
|
||||||
|
try {
|
||||||
|
let query;
|
||||||
|
if (recordTypes.length) {
|
||||||
|
query = `
|
||||||
|
DELETE FROM nodes_records
|
||||||
|
WHERE public_key = ?
|
||||||
|
AND type NOT IN (${recordTypes.map(type => `${type}`).join(',')})
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
query = `
|
||||||
|
DELETE FROM nodes_records
|
||||||
|
WHERE public_key = ?
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
const [result] = await DB.query<ResultSetHeader>(query, [publicKey]);
|
||||||
|
return result.affectedRows;
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Cannot delete unused custom records for ${publicKey} from db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new NodesRecordsRepository();
|
||||||
77
backend/src/repositories/TransactionRepository.ts
Normal file
77
backend/src/repositories/TransactionRepository.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import DB from '../database';
|
||||||
|
import logger from '../logger';
|
||||||
|
import { Ancestor, CpfpInfo } from '../mempool.interfaces';
|
||||||
|
|
||||||
|
interface CpfpSummary {
|
||||||
|
txid: string;
|
||||||
|
cluster: string;
|
||||||
|
root: string;
|
||||||
|
txs: Ancestor[];
|
||||||
|
height: number;
|
||||||
|
fee_rate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TransactionRepository {
|
||||||
|
public async $setCluster(txid: string, cluster: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await DB.query(
|
||||||
|
`
|
||||||
|
INSERT INTO transactions
|
||||||
|
(
|
||||||
|
txid,
|
||||||
|
cluster
|
||||||
|
)
|
||||||
|
VALUE (?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
cluster = ?
|
||||||
|
;`,
|
||||||
|
[txid, cluster, cluster]
|
||||||
|
);
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Cannot save transaction cpfp cluster into db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getCpfpInfo(txid: string): Promise<CpfpInfo | void> {
|
||||||
|
try {
|
||||||
|
let query = `
|
||||||
|
SELECT *
|
||||||
|
FROM transactions
|
||||||
|
LEFT JOIN cpfp_clusters AS cluster ON cluster.root = transactions.cluster
|
||||||
|
WHERE transactions.txid = ?
|
||||||
|
`;
|
||||||
|
const [rows]: any = await DB.query(query, [txid]);
|
||||||
|
if (rows.length) {
|
||||||
|
rows[0].txs = JSON.parse(rows[0].txs) as Ancestor[];
|
||||||
|
return this.convertCpfp(rows[0]);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('Cannot get transaction cpfp info from db. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private convertCpfp(cpfp: CpfpSummary): CpfpInfo {
|
||||||
|
const descendants: Ancestor[] = [];
|
||||||
|
const ancestors: Ancestor[] = [];
|
||||||
|
let matched = false;
|
||||||
|
for (const tx of cpfp.txs) {
|
||||||
|
if (tx.txid === cpfp.txid) {
|
||||||
|
matched = true;
|
||||||
|
} else if (!matched) {
|
||||||
|
descendants.push(tx);
|
||||||
|
} else {
|
||||||
|
ancestors.push(tx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
descendants,
|
||||||
|
ancestors,
|
||||||
|
effectiveFeePerVsize: cpfp.fee_rate
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new TransactionRepository();
|
||||||
|
|
||||||
457
backend/src/tasks/lightning/forensics.service.ts
Normal file
457
backend/src/tasks/lightning/forensics.service.ts
Normal file
@@ -0,0 +1,457 @@
|
|||||||
|
import DB from '../../database';
|
||||||
|
import logger from '../../logger';
|
||||||
|
import channelsApi from '../../api/explorer/channels.api';
|
||||||
|
import bitcoinApi from '../../api/bitcoin/bitcoin-api-factory';
|
||||||
|
import config from '../../config';
|
||||||
|
import { IEsploraApi } from '../../api/bitcoin/esplora-api.interface';
|
||||||
|
import { Common } from '../../api/common';
|
||||||
|
import { ILightningApi } from '../../api/lightning/lightning-api.interface';
|
||||||
|
|
||||||
|
const tempCacheSize = 10000;
|
||||||
|
|
||||||
|
class ForensicsService {
|
||||||
|
loggerTimer = 0;
|
||||||
|
closedChannelsScanBlock = 0;
|
||||||
|
txCache: { [txid: string]: IEsploraApi.Transaction } = {};
|
||||||
|
tempCached: string[] = [];
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
public async $startService(): Promise<void> {
|
||||||
|
logger.info('Starting lightning network forensics service');
|
||||||
|
|
||||||
|
this.loggerTimer = new Date().getTime() / 1000;
|
||||||
|
|
||||||
|
await this.$runTasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $runTasks(): Promise<void> {
|
||||||
|
try {
|
||||||
|
logger.info(`Running forensics scans`);
|
||||||
|
|
||||||
|
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||||
|
await this.$runClosedChannelsForensics(false);
|
||||||
|
await this.$runOpenedChannelsForensics();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('ForensicsService.$runTasks() error: ' + (e instanceof Error ? e.message : e));
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => { this.$runTasks(); }, 1000 * config.LIGHTNING.FORENSICS_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Mutually closed
|
||||||
|
2. Forced closed
|
||||||
|
3. Forced closed with penalty
|
||||||
|
|
||||||
|
┌────────────────────────────────────┐ ┌────────────────────────────┐
|
||||||
|
│ outputs contain revocation script? ├──yes──► force close w/ penalty = 3 │
|
||||||
|
└──────────────┬─────────────────────┘ └────────────────────────────┘
|
||||||
|
no
|
||||||
|
┌──────────────▼──────────────────────────┐
|
||||||
|
│ outputs contain other lightning script? ├──┐
|
||||||
|
└──────────────┬──────────────────────────┘ │
|
||||||
|
no yes
|
||||||
|
┌──────────────▼─────────────┐ │
|
||||||
|
│ sequence starts with 0x80 │ ┌────────▼────────┐
|
||||||
|
│ and ├──────► force close = 2 │
|
||||||
|
│ locktime starts with 0x20? │ └─────────────────┘
|
||||||
|
└──────────────┬─────────────┘
|
||||||
|
no
|
||||||
|
┌─────────▼────────┐
|
||||||
|
│ mutual close = 1 │
|
||||||
|
└──────────────────┘
|
||||||
|
*/
|
||||||
|
|
||||||
|
public async $runClosedChannelsForensics(onlyNewChannels: boolean = false): Promise<void> {
|
||||||
|
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let progress = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.info(`Started running closed channel forensics...`);
|
||||||
|
let channels;
|
||||||
|
if (onlyNewChannels) {
|
||||||
|
channels = await channelsApi.$getClosedChannelsWithoutReason();
|
||||||
|
} else {
|
||||||
|
channels = await channelsApi.$getUnresolvedClosedChannels();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const channel of channels) {
|
||||||
|
let reason = 0;
|
||||||
|
let resolvedForceClose = false;
|
||||||
|
// Only Esplora backend can retrieve spent transaction outputs
|
||||||
|
const cached: string[] = [];
|
||||||
|
try {
|
||||||
|
let outspends: IEsploraApi.Outspend[] | undefined;
|
||||||
|
try {
|
||||||
|
outspends = await bitcoinApi.$getOutspends(channel.closing_transaction_id);
|
||||||
|
await Common.sleep$(config.LIGHTNING.FORENSICS_RATE_LIMIT);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + channel.closing_transaction_id + '/outspends'}. Reason ${e instanceof Error ? e.message : e}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const lightningScriptReasons: number[] = [];
|
||||||
|
for (const outspend of outspends) {
|
||||||
|
if (outspend.spent && outspend.txid) {
|
||||||
|
let spendingTx = await this.fetchTransaction(outspend.txid);
|
||||||
|
if (!spendingTx) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cached.push(spendingTx.txid);
|
||||||
|
const lightningScript = this.findLightningScript(spendingTx.vin[outspend.vin || 0]);
|
||||||
|
lightningScriptReasons.push(lightningScript);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const filteredReasons = lightningScriptReasons.filter((r) => r !== 1);
|
||||||
|
if (filteredReasons.length) {
|
||||||
|
if (filteredReasons.some((r) => r === 2 || r === 4)) {
|
||||||
|
reason = 3;
|
||||||
|
} else {
|
||||||
|
reason = 2;
|
||||||
|
resolvedForceClose = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/*
|
||||||
|
We can detect a commitment transaction (force close) by reading Sequence and Locktime
|
||||||
|
https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction
|
||||||
|
*/
|
||||||
|
let closingTx = await this.fetchTransaction(channel.closing_transaction_id, true);
|
||||||
|
if (!closingTx) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cached.push(closingTx.txid);
|
||||||
|
const sequenceHex: string = closingTx.vin[0].sequence.toString(16);
|
||||||
|
const locktimeHex: string = closingTx.locktime.toString(16);
|
||||||
|
if (sequenceHex.substring(0, 2) === '80' && locktimeHex.substring(0, 2) === '20') {
|
||||||
|
reason = 2; // Here we can't be sure if it's a penalty or not
|
||||||
|
} else {
|
||||||
|
reason = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (reason) {
|
||||||
|
logger.debug('Setting closing reason ' + reason + ' for channel: ' + channel.id + '.');
|
||||||
|
await DB.query(`UPDATE channels SET closing_reason = ? WHERE id = ?`, [reason, channel.id]);
|
||||||
|
if (reason === 2 && resolvedForceClose) {
|
||||||
|
await DB.query(`UPDATE channels SET closing_resolved = ? WHERE id = ?`, [true, channel.id]);
|
||||||
|
}
|
||||||
|
if (reason !== 2 || resolvedForceClose) {
|
||||||
|
cached.forEach(txid => {
|
||||||
|
delete this.txCache[txid];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`$runClosedChannelsForensics() failed for channel ${channel.short_id}. Reason: ${e instanceof Error ? e.message : e}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
++progress;
|
||||||
|
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
|
||||||
|
if (elapsedSeconds > 10) {
|
||||||
|
logger.info(`Updating channel closed channel forensics ${progress}/${channels.length}`);
|
||||||
|
this.loggerTimer = new Date().getTime() / 1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.info(`Closed channels forensics scan complete.`);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('$runClosedChannelsForensics() error: ' + (e instanceof Error ? e.message : e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private findLightningScript(vin: IEsploraApi.Vin): number {
|
||||||
|
const topElement = vin.witness?.length > 2 ? vin.witness[vin.witness.length - 2] : null;
|
||||||
|
if (/^OP_IF OP_PUSHBYTES_33 \w{66} OP_ELSE OP_PUSH(NUM_\d+|BYTES_(1 \w{2}|2 \w{4})) OP_CSV OP_DROP OP_PUSHBYTES_33 \w{66} OP_ENDIF OP_CHECKSIG$/.test(vin.inner_witnessscript_asm)) {
|
||||||
|
// https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction-outputs
|
||||||
|
if (topElement === '01') {
|
||||||
|
// top element is '01' to get in the revocation path
|
||||||
|
// 'Revoked Lightning Force Close';
|
||||||
|
// Penalty force closed
|
||||||
|
return 2;
|
||||||
|
} else {
|
||||||
|
// top element is '', this is a delayed to_local output
|
||||||
|
// 'Lightning Force Close';
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
/^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_NOTIF OP_DROP OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CSV OP_DROP |)OP_ENDIF$/.test(vin.inner_witnessscript_asm) ||
|
||||||
|
/^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_IF OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_DROP OP_PUSHBYTES_3 \w{6} OP_CLTV OP_DROP OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CSV OP_DROP |)OP_ENDIF$/.test(vin.inner_witnessscript_asm)
|
||||||
|
) {
|
||||||
|
// https://github.com/lightning/bolts/blob/master/03-transactions.md#offered-htlc-outputs
|
||||||
|
// https://github.com/lightning/bolts/blob/master/03-transactions.md#received-htlc-outputs
|
||||||
|
if (topElement?.length === 66) {
|
||||||
|
// top element is a public key
|
||||||
|
// 'Revoked Lightning HTLC'; Penalty force closed
|
||||||
|
return 4;
|
||||||
|
} else if (topElement) {
|
||||||
|
// top element is a preimage
|
||||||
|
// 'Lightning HTLC';
|
||||||
|
return 5;
|
||||||
|
} else {
|
||||||
|
// top element is '' to get in the expiry of the script
|
||||||
|
// 'Expired Lightning HTLC';
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
} else if (/^OP_PUSHBYTES_33 \w{66} OP_CHECKSIG OP_IFDUP OP_NOTIF OP_PUSHNUM_16 OP_CSV OP_ENDIF$/.test(vin.inner_witnessscript_asm)) {
|
||||||
|
// https://github.com/lightning/bolts/blob/master/03-transactions.md#to_local_anchor-and-to_remote_anchor-output-option_anchors
|
||||||
|
if (topElement) {
|
||||||
|
// top element is a signature
|
||||||
|
// 'Lightning Anchor';
|
||||||
|
return 7;
|
||||||
|
} else {
|
||||||
|
// top element is '', it has been swept after 16 blocks
|
||||||
|
// 'Swept Lightning Anchor';
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a channel open tx spends funds from a another channel transaction,
|
||||||
|
// we can attribute that output to a specific counterparty
|
||||||
|
private async $runOpenedChannelsForensics(): Promise<void> {
|
||||||
|
const runTimer = Date.now();
|
||||||
|
let progress = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.info(`Started running open channel forensics...`);
|
||||||
|
const channels = await channelsApi.$getChannelsWithoutSourceChecked();
|
||||||
|
|
||||||
|
for (const openChannel of channels) {
|
||||||
|
let openTx = await this.fetchTransaction(openChannel.transaction_id, true);
|
||||||
|
if (!openTx) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const input of openTx.vin) {
|
||||||
|
const closeChannel = await channelsApi.$getChannelByClosingId(input.txid);
|
||||||
|
if (closeChannel) {
|
||||||
|
// this input directly spends a channel close output
|
||||||
|
await this.$attributeChannelBalances(closeChannel, openChannel, input);
|
||||||
|
} else {
|
||||||
|
const prevOpenChannels = await channelsApi.$getChannelsByOpeningId(input.txid);
|
||||||
|
if (prevOpenChannels?.length) {
|
||||||
|
// this input spends a channel open change output
|
||||||
|
for (const prevOpenChannel of prevOpenChannels) {
|
||||||
|
await this.$attributeChannelBalances(prevOpenChannel, openChannel, input, null, null, true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// check if this input spends any swept channel close outputs
|
||||||
|
await this.$attributeSweptChannelCloses(openChannel, input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// calculate how much of the total input value is attributable to the channel open output
|
||||||
|
openChannel.funding_ratio = openTx.vout[openChannel.transaction_vout].value / ((openTx.vout.reduce((sum, v) => sum + v.value, 0) || 1) + openTx.fee);
|
||||||
|
// save changes to the opening channel, and mark it as checked
|
||||||
|
if (openTx?.vin?.length === 1) {
|
||||||
|
openChannel.single_funded = true;
|
||||||
|
}
|
||||||
|
if (openChannel.node1_funding_balance || openChannel.node2_funding_balance || openChannel.node1_closing_balance || openChannel.node2_closing_balance || openChannel.closed_by) {
|
||||||
|
await channelsApi.$updateOpeningInfo(openChannel);
|
||||||
|
}
|
||||||
|
await channelsApi.$markChannelSourceChecked(openChannel.id);
|
||||||
|
|
||||||
|
++progress;
|
||||||
|
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
|
||||||
|
if (elapsedSeconds > 10) {
|
||||||
|
logger.info(`Updating opened channel forensics ${progress}/${channels?.length}`);
|
||||||
|
this.loggerTimer = new Date().getTime() / 1000;
|
||||||
|
this.truncateTempCache();
|
||||||
|
}
|
||||||
|
if (Date.now() - runTimer > (config.LIGHTNING.FORENSICS_INTERVAL * 1000)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Open channels forensics scan complete.`);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('$runOpenedChannelsForensics() error: ' + (e instanceof Error ? e.message : e));
|
||||||
|
} finally {
|
||||||
|
this.clearTempCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a channel open tx input spends the result of a swept channel close output
|
||||||
|
private async $attributeSweptChannelCloses(openChannel: ILightningApi.Channel, input: IEsploraApi.Vin): Promise<void> {
|
||||||
|
let sweepTx = await this.fetchTransaction(input.txid, true);
|
||||||
|
if (!sweepTx) {
|
||||||
|
logger.err(`couldn't find input transaction for channel forensics ${openChannel.channel_id} ${input.txid}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const openContribution = sweepTx.vout[input.vout].value;
|
||||||
|
for (const sweepInput of sweepTx.vin) {
|
||||||
|
const lnScriptType = this.findLightningScript(sweepInput);
|
||||||
|
if (lnScriptType > 1) {
|
||||||
|
const closeChannel = await channelsApi.$getChannelByClosingId(sweepInput.txid);
|
||||||
|
if (closeChannel) {
|
||||||
|
const initiator = (lnScriptType === 2 || lnScriptType === 4) ? 'remote' : (lnScriptType === 3 ? 'local' : null);
|
||||||
|
await this.$attributeChannelBalances(closeChannel, openChannel, sweepInput, openContribution, initiator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $attributeChannelBalances(
|
||||||
|
prevChannel, openChannel, input: IEsploraApi.Vin, openContribution: number | null = null,
|
||||||
|
initiator: 'remote' | 'local' | null = null, linkedOpenings: boolean = false
|
||||||
|
): Promise<void> {
|
||||||
|
// figure out which node controls the input/output
|
||||||
|
let openSide;
|
||||||
|
let prevLocal;
|
||||||
|
let prevRemote;
|
||||||
|
let matched = false;
|
||||||
|
let ambiguous = false; // if counterparties are the same in both channels, we can't tell them apart
|
||||||
|
if (openChannel.node1_public_key === prevChannel.node1_public_key) {
|
||||||
|
openSide = 1;
|
||||||
|
prevLocal = 1;
|
||||||
|
prevRemote = 2;
|
||||||
|
matched = true;
|
||||||
|
} else if (openChannel.node1_public_key === prevChannel.node2_public_key) {
|
||||||
|
openSide = 1;
|
||||||
|
prevLocal = 2;
|
||||||
|
prevRemote = 1;
|
||||||
|
matched = true;
|
||||||
|
}
|
||||||
|
if (openChannel.node2_public_key === prevChannel.node1_public_key) {
|
||||||
|
openSide = 2;
|
||||||
|
prevLocal = 1;
|
||||||
|
prevRemote = 2;
|
||||||
|
if (matched) {
|
||||||
|
ambiguous = true;
|
||||||
|
}
|
||||||
|
matched = true;
|
||||||
|
} else if (openChannel.node2_public_key === prevChannel.node2_public_key) {
|
||||||
|
openSide = 2;
|
||||||
|
prevLocal = 2;
|
||||||
|
prevRemote = 1;
|
||||||
|
if (matched) {
|
||||||
|
ambiguous = true;
|
||||||
|
}
|
||||||
|
matched = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matched && !ambiguous) {
|
||||||
|
// fetch closing channel transaction and perform forensics on the outputs
|
||||||
|
let prevChannelTx = await this.fetchTransaction(input.txid, true);
|
||||||
|
let outspends: IEsploraApi.Outspend[] | undefined;
|
||||||
|
try {
|
||||||
|
outspends = await bitcoinApi.$getOutspends(input.txid);
|
||||||
|
await Common.sleep$(config.LIGHTNING.FORENSICS_RATE_LIMIT);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + input.txid + '/outspends'}. Reason ${e instanceof Error ? e.message : e}`);
|
||||||
|
}
|
||||||
|
if (!outspends || !prevChannelTx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!linkedOpenings) {
|
||||||
|
if (!prevChannel.outputs || !prevChannel.outputs.length) {
|
||||||
|
prevChannel.outputs = prevChannelTx.vout.map(vout => {
|
||||||
|
return {
|
||||||
|
type: 0,
|
||||||
|
value: vout.value,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (let i = 0; i < outspends?.length; i++) {
|
||||||
|
const outspend = outspends[i];
|
||||||
|
const output = prevChannel.outputs[i];
|
||||||
|
if (outspend.spent && outspend.txid) {
|
||||||
|
try {
|
||||||
|
const spendingTx = await this.fetchTransaction(outspend.txid, true);
|
||||||
|
if (spendingTx) {
|
||||||
|
output.type = this.findLightningScript(spendingTx.vin[outspend.vin || 0]);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + outspend.txid}. Reason ${e instanceof Error ? e.message : e}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
output.type = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// attribute outputs to each counterparty, and sum up total known balances
|
||||||
|
prevChannel.outputs[input.vout].node = prevLocal;
|
||||||
|
const isPenalty = prevChannel.outputs.filter((out) => out.type === 2 || out.type === 4)?.length > 0;
|
||||||
|
const normalOutput = [1,3].includes(prevChannel.outputs[input.vout].type);
|
||||||
|
const mutualClose = ((prevChannel.status === 2 || prevChannel.status === 'closed') && prevChannel.closing_reason === 1);
|
||||||
|
let localClosingBalance = 0;
|
||||||
|
let remoteClosingBalance = 0;
|
||||||
|
for (const output of prevChannel.outputs) {
|
||||||
|
if (isPenalty) {
|
||||||
|
// penalty close, so local node takes everything
|
||||||
|
localClosingBalance += output.value;
|
||||||
|
} else if (output.node) {
|
||||||
|
// this output determinstically linked to one of the counterparties
|
||||||
|
if (output.node === prevLocal) {
|
||||||
|
localClosingBalance += output.value;
|
||||||
|
} else {
|
||||||
|
remoteClosingBalance += output.value;
|
||||||
|
}
|
||||||
|
} else if (normalOutput && (output.type === 1 || output.type === 3 || (mutualClose && prevChannel.outputs.length === 2))) {
|
||||||
|
// local node had one main output, therefore remote node takes the other
|
||||||
|
remoteClosingBalance += output.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prevChannel[`node${prevLocal}_closing_balance`] = localClosingBalance;
|
||||||
|
prevChannel[`node${prevRemote}_closing_balance`] = remoteClosingBalance;
|
||||||
|
prevChannel.closing_fee = prevChannelTx.fee;
|
||||||
|
|
||||||
|
if (initiator && !linkedOpenings) {
|
||||||
|
const initiatorSide = initiator === 'remote' ? prevRemote : prevLocal;
|
||||||
|
prevChannel.closed_by = prevChannel[`node${initiatorSide}_public_key`];
|
||||||
|
}
|
||||||
|
|
||||||
|
// save changes to the closing channel
|
||||||
|
await channelsApi.$updateClosingInfo(prevChannel);
|
||||||
|
} else {
|
||||||
|
if (prevChannelTx.vin.length <= 1) {
|
||||||
|
prevChannel[`node${prevLocal}_funding_balance`] = prevChannel.capacity;
|
||||||
|
prevChannel.single_funded = true;
|
||||||
|
prevChannel.funding_ratio = 1;
|
||||||
|
// save changes to the closing channel
|
||||||
|
await channelsApi.$updateOpeningInfo(prevChannel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
openChannel[`node${openSide}_funding_balance`] = openChannel[`node${openSide}_funding_balance`] + (openContribution || prevChannelTx?.vout[input.vout]?.value || 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchTransaction(txid: string, temp: boolean = false): Promise<IEsploraApi.Transaction | null> {
|
||||||
|
let tx = this.txCache[txid];
|
||||||
|
if (!tx) {
|
||||||
|
try {
|
||||||
|
tx = await bitcoinApi.$getRawTransaction(txid);
|
||||||
|
this.txCache[txid] = tx;
|
||||||
|
if (temp) {
|
||||||
|
this.tempCached.push(txid);
|
||||||
|
}
|
||||||
|
await Common.sleep$(config.LIGHTNING.FORENSICS_RATE_LIMIT);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + txid + '/outspends'}. Reason ${e instanceof Error ? e.message : e}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tx;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTempCache(): void {
|
||||||
|
for (const txid of this.tempCached) {
|
||||||
|
delete this.txCache[txid];
|
||||||
|
}
|
||||||
|
this.tempCached = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
truncateTempCache(): void {
|
||||||
|
if (this.tempCached.length > tempCacheSize) {
|
||||||
|
const removed = this.tempCached.splice(0, this.tempCached.length - tempCacheSize);
|
||||||
|
for (const txid of removed) {
|
||||||
|
delete this.txCache[txid];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new ForensicsService();
|
||||||
@@ -13,6 +13,8 @@ import fundingTxFetcher from './sync-tasks/funding-tx-fetcher';
|
|||||||
import NodesSocketsRepository from '../../repositories/NodesSocketsRepository';
|
import NodesSocketsRepository from '../../repositories/NodesSocketsRepository';
|
||||||
import { Common } from '../../api/common';
|
import { Common } from '../../api/common';
|
||||||
import blocks from '../../api/blocks';
|
import blocks from '../../api/blocks';
|
||||||
|
import NodeRecordsRepository from '../../repositories/NodeRecordsRepository';
|
||||||
|
import forensicsService from './forensics.service';
|
||||||
|
|
||||||
class NetworkSyncService {
|
class NetworkSyncService {
|
||||||
loggerTimer = 0;
|
loggerTimer = 0;
|
||||||
@@ -29,6 +31,7 @@ class NetworkSyncService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async $runTasks(): Promise<void> {
|
private async $runTasks(): Promise<void> {
|
||||||
|
const taskStartTime = Date.now();
|
||||||
try {
|
try {
|
||||||
logger.info(`Updating nodes and channels`);
|
logger.info(`Updating nodes and channels`);
|
||||||
|
|
||||||
@@ -45,15 +48,17 @@ class NetworkSyncService {
|
|||||||
await this.$lookUpCreationDateFromChain();
|
await this.$lookUpCreationDateFromChain();
|
||||||
await this.$updateNodeFirstSeen();
|
await this.$updateNodeFirstSeen();
|
||||||
await this.$scanForClosedChannels();
|
await this.$scanForClosedChannels();
|
||||||
|
|
||||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||||
await this.$runClosedChannelsForensics();
|
// run forensics on new channels only
|
||||||
|
await forensicsService.$runClosedChannelsForensics(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$runTasks() error: ' + (e instanceof Error ? e.message : e));
|
logger.err('$runTasks() error: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => { this.$runTasks(); }, 1000 * config.LIGHTNING.GRAPH_REFRESH_INTERVAL);
|
setTimeout(() => { this.$runTasks(); }, Math.max(1, (1000 * config.LIGHTNING.GRAPH_REFRESH_INTERVAL) - (Date.now() - taskStartTime)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,6 +68,7 @@ class NetworkSyncService {
|
|||||||
let progress = 0;
|
let progress = 0;
|
||||||
|
|
||||||
let deletedSockets = 0;
|
let deletedSockets = 0;
|
||||||
|
let deletedRecords = 0;
|
||||||
const graphNodesPubkeys: string[] = [];
|
const graphNodesPubkeys: string[] = [];
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
const latestUpdated = await channelsApi.$getLatestChannelUpdateForNode(node.pub_key);
|
const latestUpdated = await channelsApi.$getLatestChannelUpdateForNode(node.pub_key);
|
||||||
@@ -84,8 +90,23 @@ class NetworkSyncService {
|
|||||||
addresses.push(socket.addr);
|
addresses.push(socket.addr);
|
||||||
}
|
}
|
||||||
deletedSockets += await NodesSocketsRepository.$deleteUnusedSockets(node.pub_key, addresses);
|
deletedSockets += await NodesSocketsRepository.$deleteUnusedSockets(node.pub_key, addresses);
|
||||||
|
|
||||||
|
const oldRecordTypes = await NodeRecordsRepository.$getRecordTypes(node.pub_key);
|
||||||
|
const customRecordTypes: number[] = [];
|
||||||
|
for (const [type, payload] of Object.entries(node.custom_records || {})) {
|
||||||
|
const numericalType = parseInt(type);
|
||||||
|
await NodeRecordsRepository.$saveRecord({
|
||||||
|
publicKey: node.pub_key,
|
||||||
|
type: numericalType,
|
||||||
|
payload,
|
||||||
|
});
|
||||||
|
customRecordTypes.push(numericalType);
|
||||||
|
}
|
||||||
|
if (oldRecordTypes.reduce((changed, type) => changed || customRecordTypes.indexOf(type) === -1, false)) {
|
||||||
|
deletedRecords += await NodeRecordsRepository.$deleteUnusedRecords(node.pub_key, customRecordTypes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
logger.info(`${progress} nodes updated. ${deletedSockets} sockets deleted`);
|
logger.info(`${progress} nodes updated. ${deletedSockets} sockets deleted. ${deletedRecords} custom records deleted.`);
|
||||||
|
|
||||||
// If a channel if not present in the graph, mark it as inactive
|
// If a channel if not present in the graph, mark it as inactive
|
||||||
await nodesApi.$setNodesInactive(graphNodesPubkeys);
|
await nodesApi.$setNodesInactive(graphNodesPubkeys);
|
||||||
@@ -284,148 +305,6 @@ class NetworkSyncService {
|
|||||||
logger.err('$scanForClosedChannels() error: ' + (e instanceof Error ? e.message : e));
|
logger.err('$scanForClosedChannels() error: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
1. Mutually closed
|
|
||||||
2. Forced closed
|
|
||||||
3. Forced closed with penalty
|
|
||||||
*/
|
|
||||||
|
|
||||||
private async $runClosedChannelsForensics(): Promise<void> {
|
|
||||||
if (!config.ESPLORA.REST_API_URL) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let progress = 0;
|
|
||||||
|
|
||||||
try {
|
|
||||||
logger.info(`Started running closed channel forensics...`);
|
|
||||||
const channels = await channelsApi.$getClosedChannelsWithoutReason();
|
|
||||||
for (const channel of channels) {
|
|
||||||
let reason = 0;
|
|
||||||
// Only Esplora backend can retrieve spent transaction outputs
|
|
||||||
try {
|
|
||||||
let outspends: IEsploraApi.Outspend[] | undefined;
|
|
||||||
try {
|
|
||||||
outspends = await bitcoinApi.$getOutspends(channel.closing_transaction_id);
|
|
||||||
} catch (e) {
|
|
||||||
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + channel.closing_transaction_id + '/outspends'}. Reason ${e instanceof Error ? e.message : e}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const lightningScriptReasons: number[] = [];
|
|
||||||
for (const outspend of outspends) {
|
|
||||||
if (outspend.spent && outspend.txid) {
|
|
||||||
let spendingTx: IEsploraApi.Transaction | undefined;
|
|
||||||
try {
|
|
||||||
spendingTx = await bitcoinApi.$getRawTransaction(outspend.txid);
|
|
||||||
} catch (e) {
|
|
||||||
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + outspend.txid}. Reason ${e instanceof Error ? e.message : e}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const lightningScript = this.findLightningScript(spendingTx.vin[outspend.vin || 0]);
|
|
||||||
lightningScriptReasons.push(lightningScript);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (lightningScriptReasons.length === outspends.length
|
|
||||||
&& lightningScriptReasons.filter((r) => r === 1).length === outspends.length) {
|
|
||||||
reason = 1;
|
|
||||||
} else {
|
|
||||||
const filteredReasons = lightningScriptReasons.filter((r) => r !== 1);
|
|
||||||
if (filteredReasons.length) {
|
|
||||||
if (filteredReasons.some((r) => r === 2 || r === 4)) {
|
|
||||||
reason = 3;
|
|
||||||
} else {
|
|
||||||
reason = 2;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
/*
|
|
||||||
We can detect a commitment transaction (force close) by reading Sequence and Locktime
|
|
||||||
https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction
|
|
||||||
*/
|
|
||||||
let closingTx: IEsploraApi.Transaction | undefined;
|
|
||||||
try {
|
|
||||||
closingTx = await bitcoinApi.$getRawTransaction(channel.closing_transaction_id);
|
|
||||||
} catch (e) {
|
|
||||||
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + channel.closing_transaction_id}. Reason ${e instanceof Error ? e.message : e}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const sequenceHex: string = closingTx.vin[0].sequence.toString(16);
|
|
||||||
const locktimeHex: string = closingTx.locktime.toString(16);
|
|
||||||
if (sequenceHex.substring(0, 2) === '80' && locktimeHex.substring(0, 2) === '20') {
|
|
||||||
reason = 2; // Here we can't be sure if it's a penalty or not
|
|
||||||
} else {
|
|
||||||
reason = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (reason) {
|
|
||||||
logger.debug('Setting closing reason ' + reason + ' for channel: ' + channel.id + '.');
|
|
||||||
await DB.query(`UPDATE channels SET closing_reason = ? WHERE id = ?`, [reason, channel.id]);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.err(`$runClosedChannelsForensics() failed for channel ${channel.short_id}. Reason: ${e instanceof Error ? e.message : e}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
++progress;
|
|
||||||
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
|
|
||||||
if (elapsedSeconds > 10) {
|
|
||||||
logger.info(`Updating channel closed channel forensics ${progress}/${channels.length}`);
|
|
||||||
this.loggerTimer = new Date().getTime() / 1000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logger.info(`Closed channels forensics scan complete.`);
|
|
||||||
} catch (e) {
|
|
||||||
logger.err('$runClosedChannelsForensics() error: ' + (e instanceof Error ? e.message : e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private findLightningScript(vin: IEsploraApi.Vin): number {
|
|
||||||
const topElement = vin.witness[vin.witness.length - 2];
|
|
||||||
if (/^OP_IF OP_PUSHBYTES_33 \w{66} OP_ELSE OP_PUSH(NUM_\d+|BYTES_(1 \w{2}|2 \w{4})) OP_CSV OP_DROP OP_PUSHBYTES_33 \w{66} OP_ENDIF OP_CHECKSIG$/.test(vin.inner_witnessscript_asm)) {
|
|
||||||
// https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction-outputs
|
|
||||||
if (topElement === '01') {
|
|
||||||
// top element is '01' to get in the revocation path
|
|
||||||
// 'Revoked Lightning Force Close';
|
|
||||||
// Penalty force closed
|
|
||||||
return 2;
|
|
||||||
} else {
|
|
||||||
// top element is '', this is a delayed to_local output
|
|
||||||
// 'Lightning Force Close';
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
/^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_NOTIF OP_DROP OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CSV OP_DROP |)OP_ENDIF$/.test(vin.inner_witnessscript_asm) ||
|
|
||||||
/^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_IF OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_DROP OP_PUSHBYTES_3 \w{6} OP_CLTV OP_DROP OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CSV OP_DROP |)OP_ENDIF$/.test(vin.inner_witnessscript_asm)
|
|
||||||
) {
|
|
||||||
// https://github.com/lightning/bolts/blob/master/03-transactions.md#offered-htlc-outputs
|
|
||||||
// https://github.com/lightning/bolts/blob/master/03-transactions.md#received-htlc-outputs
|
|
||||||
if (topElement.length === 66) {
|
|
||||||
// top element is a public key
|
|
||||||
// 'Revoked Lightning HTLC'; Penalty force closed
|
|
||||||
return 4;
|
|
||||||
} else if (topElement) {
|
|
||||||
// top element is a preimage
|
|
||||||
// 'Lightning HTLC';
|
|
||||||
return 5;
|
|
||||||
} else {
|
|
||||||
// top element is '' to get in the expiry of the script
|
|
||||||
// 'Expired Lightning HTLC';
|
|
||||||
return 6;
|
|
||||||
}
|
|
||||||
} else if (/^OP_PUSHBYTES_33 \w{66} OP_CHECKSIG OP_IFDUP OP_NOTIF OP_PUSHNUM_16 OP_CSV OP_ENDIF$/.test(vin.inner_witnessscript_asm)) {
|
|
||||||
// https://github.com/lightning/bolts/blob/master/03-transactions.md#to_local_anchor-and-to_remote_anchor-output-option_anchors
|
|
||||||
if (topElement) {
|
|
||||||
// top element is a signature
|
|
||||||
// 'Lightning Anchor';
|
|
||||||
return 7;
|
|
||||||
} else {
|
|
||||||
// top element is '', it has been swept after 16 blocks
|
|
||||||
// 'Swept Lightning Anchor';
|
|
||||||
return 8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new NetworkSyncService();
|
export default new NetworkSyncService();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import DB from '../../../database';
|
|||||||
import logger from '../../../logger';
|
import logger from '../../../logger';
|
||||||
import { ResultSetHeader } from 'mysql2';
|
import { ResultSetHeader } from 'mysql2';
|
||||||
import * as IPCheck from '../../../utils/ipcheck.js';
|
import * as IPCheck from '../../../utils/ipcheck.js';
|
||||||
|
import { Reader } from 'mmdb-lib';
|
||||||
|
|
||||||
export async function $lookupNodeLocation(): Promise<void> {
|
export async function $lookupNodeLocation(): Promise<void> {
|
||||||
let loggerTimer = new Date().getTime() / 1000;
|
let loggerTimer = new Date().getTime() / 1000;
|
||||||
@@ -18,7 +19,10 @@ export async function $lookupNodeLocation(): Promise<void> {
|
|||||||
const nodes = await nodesApi.$getAllNodes();
|
const nodes = await nodesApi.$getAllNodes();
|
||||||
const lookupCity = await maxmind.open<CityResponse>(config.MAXMIND.GEOLITE2_CITY);
|
const lookupCity = await maxmind.open<CityResponse>(config.MAXMIND.GEOLITE2_CITY);
|
||||||
const lookupAsn = await maxmind.open<AsnResponse>(config.MAXMIND.GEOLITE2_ASN);
|
const lookupAsn = await maxmind.open<AsnResponse>(config.MAXMIND.GEOLITE2_ASN);
|
||||||
const lookupIsp = await maxmind.open<IspResponse>(config.MAXMIND.GEOIP2_ISP);
|
let lookupIsp: Reader<IspResponse> | null = null;
|
||||||
|
try {
|
||||||
|
lookupIsp = await maxmind.open<IspResponse>(config.MAXMIND.GEOIP2_ISP);
|
||||||
|
} catch (e) { }
|
||||||
|
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
const sockets: string[] = node.sockets.split(',');
|
const sockets: string[] = node.sockets.split(',');
|
||||||
@@ -29,7 +33,10 @@ export async function $lookupNodeLocation(): Promise<void> {
|
|||||||
if (hasClearnet && ip !== '127.0.1.1' && ip !== '127.0.0.1') {
|
if (hasClearnet && ip !== '127.0.1.1' && ip !== '127.0.0.1') {
|
||||||
const city = lookupCity.get(ip);
|
const city = lookupCity.get(ip);
|
||||||
const asn = lookupAsn.get(ip);
|
const asn = lookupAsn.get(ip);
|
||||||
const isp = lookupIsp.get(ip);
|
let isp: IspResponse | null = null;
|
||||||
|
if (lookupIsp) {
|
||||||
|
isp = lookupIsp.get(ip);
|
||||||
|
}
|
||||||
|
|
||||||
let asOverwrite: any | undefined;
|
let asOverwrite: any | undefined;
|
||||||
if (asn && (IPCheck.match(ip, '170.75.160.0/20') || IPCheck.match(ip, '172.81.176.0/21'))) {
|
if (asn && (IPCheck.match(ip, '170.75.160.0/20') || IPCheck.match(ip, '172.81.176.0/21'))) {
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
import { query } from '../../utils/axios-query';
|
|
||||||
import priceUpdater, { PriceFeed, PriceHistory } from '../price-updater';
|
|
||||||
|
|
||||||
class FtxApi implements PriceFeed {
|
|
||||||
public name: string = 'FTX';
|
|
||||||
public currencies: string[] = ['USD', 'BRZ', 'EUR', 'JPY', 'AUD'];
|
|
||||||
|
|
||||||
public url: string = 'https://ftx.com/api/markets/BTC/';
|
|
||||||
public urlHist: string = 'https://ftx.com/api/markets/BTC/{CURRENCY}/candles?resolution={GRANULARITY}';
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public async $fetchPrice(currency): Promise<number> {
|
|
||||||
const response = await query(this.url + currency);
|
|
||||||
return response ? parseInt(response['result']['last'], 10) : -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async $fetchRecentPrice(currencies: string[], type: 'hour' | 'day'): Promise<PriceHistory> {
|
|
||||||
const priceHistory: PriceHistory = {};
|
|
||||||
|
|
||||||
for (const currency of currencies) {
|
|
||||||
if (this.currencies.includes(currency) === false) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await query(this.urlHist.replace('{GRANULARITY}', type === 'hour' ? '3600' : '86400').replace('{CURRENCY}', currency));
|
|
||||||
const pricesRaw = response ? response['result'] : [];
|
|
||||||
|
|
||||||
for (const price of pricesRaw as any[]) {
|
|
||||||
const time = Math.round(price['time'] / 1000);
|
|
||||||
if (priceHistory[time] === undefined) {
|
|
||||||
priceHistory[time] = priceUpdater.getEmptyPricesObj();
|
|
||||||
}
|
|
||||||
priceHistory[time][currency] = price['close'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return priceHistory;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FtxApi;
|
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { Common } from '../api/common';
|
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import PricesRepository from '../repositories/PricesRepository';
|
import PricesRepository from '../repositories/PricesRepository';
|
||||||
import BitfinexApi from './price-feeds/bitfinex-api';
|
import BitfinexApi from './price-feeds/bitfinex-api';
|
||||||
import BitflyerApi from './price-feeds/bitflyer-api';
|
import BitflyerApi from './price-feeds/bitflyer-api';
|
||||||
import CoinbaseApi from './price-feeds/coinbase-api';
|
import CoinbaseApi from './price-feeds/coinbase-api';
|
||||||
import FtxApi from './price-feeds/ftx-api';
|
|
||||||
import GeminiApi from './price-feeds/gemini-api';
|
import GeminiApi from './price-feeds/gemini-api';
|
||||||
import KrakenApi from './price-feeds/kraken-api';
|
import KrakenApi from './price-feeds/kraken-api';
|
||||||
|
|
||||||
@@ -48,7 +46,6 @@ class PriceUpdater {
|
|||||||
this.latestPrices = this.getEmptyPricesObj();
|
this.latestPrices = this.getEmptyPricesObj();
|
||||||
|
|
||||||
this.feeds.push(new BitflyerApi()); // Does not have historical endpoint
|
this.feeds.push(new BitflyerApi()); // Does not have historical endpoint
|
||||||
this.feeds.push(new FtxApi());
|
|
||||||
this.feeds.push(new KrakenApi());
|
this.feeds.push(new KrakenApi());
|
||||||
this.feeds.push(new CoinbaseApi());
|
this.feeds.push(new CoinbaseApi());
|
||||||
this.feeds.push(new BitfinexApi());
|
this.feeds.push(new BitfinexApi());
|
||||||
|
|||||||
174
backend/src/utils/pairing-heap.ts
Normal file
174
backend/src/utils/pairing-heap.ts
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
export type HeapNode<T> = {
|
||||||
|
element: T
|
||||||
|
child?: HeapNode<T>
|
||||||
|
next?: HeapNode<T>
|
||||||
|
prev?: HeapNode<T>
|
||||||
|
} | null | undefined;
|
||||||
|
|
||||||
|
// minimal pairing heap priority queue implementation
|
||||||
|
export class PairingHeap<T> {
|
||||||
|
private root: HeapNode<T> = null;
|
||||||
|
private comparator: (a: T, b: T) => boolean;
|
||||||
|
|
||||||
|
// comparator function should return 'true' if a is higher priority than b
|
||||||
|
constructor(comparator: (a: T, b: T) => boolean) {
|
||||||
|
this.comparator = comparator;
|
||||||
|
}
|
||||||
|
|
||||||
|
isEmpty(): boolean {
|
||||||
|
return !this.root;
|
||||||
|
}
|
||||||
|
|
||||||
|
add(element: T): HeapNode<T> {
|
||||||
|
const node: HeapNode<T> = {
|
||||||
|
element
|
||||||
|
};
|
||||||
|
|
||||||
|
this.root = this.meld(this.root, node);
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the top priority element without modifying the queue
|
||||||
|
peek(): T | void {
|
||||||
|
return this.root?.element;
|
||||||
|
}
|
||||||
|
|
||||||
|
// removes and returns the top priority element
|
||||||
|
pop(): T | void {
|
||||||
|
let element;
|
||||||
|
if (this.root) {
|
||||||
|
const node = this.root;
|
||||||
|
element = node.element;
|
||||||
|
this.root = this.mergePairs(node.child);
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteNode(node: HeapNode<T>): void {
|
||||||
|
if (!node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node === this.root) {
|
||||||
|
this.root = this.mergePairs(node.child);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (node.prev) {
|
||||||
|
if (node.prev.child === node) {
|
||||||
|
node.prev.child = node.next;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
node.prev.next = node.next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (node.next) {
|
||||||
|
node.next.prev = node.prev;
|
||||||
|
}
|
||||||
|
this.root = this.meld(this.root, this.mergePairs(node.child));
|
||||||
|
}
|
||||||
|
|
||||||
|
node.child = null;
|
||||||
|
node.prev = null;
|
||||||
|
node.next = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fix the heap after increasing the priority of a given node
|
||||||
|
increasePriority(node: HeapNode<T>): void {
|
||||||
|
// already the top priority element
|
||||||
|
if (!node || node === this.root) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// extract from siblings
|
||||||
|
if (node.prev) {
|
||||||
|
if (node.prev?.child === node) {
|
||||||
|
if (this.comparator(node.prev.element, node.element)) {
|
||||||
|
// already in a valid position
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
node.prev.child = node.next;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
node.prev.next = node.next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (node.next) {
|
||||||
|
node.next.prev = node.prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.root = this.meld(this.root, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
decreasePriority(node: HeapNode<T>): void {
|
||||||
|
this.deleteNode(node);
|
||||||
|
this.root = this.meld(this.root, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
meld(a: HeapNode<T>, b: HeapNode<T>): HeapNode<T> {
|
||||||
|
if (!a) {
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
if (!b || a === b) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parent: HeapNode<T> = b;
|
||||||
|
let child: HeapNode<T> = a;
|
||||||
|
if (this.comparator(a.element, b.element)) {
|
||||||
|
parent = a;
|
||||||
|
child = b;
|
||||||
|
}
|
||||||
|
|
||||||
|
child.next = parent.child;
|
||||||
|
if (parent.child) {
|
||||||
|
parent.child.prev = child;
|
||||||
|
}
|
||||||
|
child.prev = parent;
|
||||||
|
parent.child = child;
|
||||||
|
|
||||||
|
parent.next = null;
|
||||||
|
parent.prev = null;
|
||||||
|
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
mergePairs(node: HeapNode<T>): HeapNode<T> {
|
||||||
|
if (!node) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let current: HeapNode<T> = node;
|
||||||
|
let next: HeapNode<T>;
|
||||||
|
let nextCurrent: HeapNode<T>;
|
||||||
|
let pairs: HeapNode<T>;
|
||||||
|
let melded: HeapNode<T>;
|
||||||
|
while (current) {
|
||||||
|
next = current.next;
|
||||||
|
if (next) {
|
||||||
|
nextCurrent = next.next;
|
||||||
|
melded = this.meld(current, next);
|
||||||
|
if (melded) {
|
||||||
|
melded.prev = pairs;
|
||||||
|
}
|
||||||
|
pairs = melded;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
nextCurrent = null;
|
||||||
|
current.prev = pairs;
|
||||||
|
pairs = current;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current = nextCurrent;
|
||||||
|
}
|
||||||
|
|
||||||
|
melded = null;
|
||||||
|
let prev: HeapNode<T>;
|
||||||
|
while (pairs) {
|
||||||
|
prev = pairs.prev;
|
||||||
|
melded = this.meld(melded, pairs);
|
||||||
|
pairs = prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
return melded;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -89,6 +89,7 @@ Below we list all settings from `mempool-config.json` and the corresponding over
|
|||||||
"MEMPOOL": {
|
"MEMPOOL": {
|
||||||
"NETWORK": "mainnet",
|
"NETWORK": "mainnet",
|
||||||
"BACKEND": "electrum",
|
"BACKEND": "electrum",
|
||||||
|
"ENABLED": true,
|
||||||
"HTTP_PORT": 8999,
|
"HTTP_PORT": 8999,
|
||||||
"SPAWN_CLUSTER_PROCS": 0,
|
"SPAWN_CLUSTER_PROCS": 0,
|
||||||
"API_URL_PREFIX": "/api/v1/",
|
"API_URL_PREFIX": "/api/v1/",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"MEMPOOL": {
|
"MEMPOOL": {
|
||||||
"NETWORK": "__MEMPOOL_NETWORK__",
|
"NETWORK": "__MEMPOOL_NETWORK__",
|
||||||
"BACKEND": "__MEMPOOL_BACKEND__",
|
"BACKEND": "__MEMPOOL_BACKEND__",
|
||||||
|
"ENABLED": __MEMPOOL_ENABLED__,
|
||||||
"HTTP_PORT": __MEMPOOL_HTTP_PORT__,
|
"HTTP_PORT": __MEMPOOL_HTTP_PORT__,
|
||||||
"SPAWN_CLUSTER_PROCS": __MEMPOOL_SPAWN_CLUSTER_PROCS__,
|
"SPAWN_CLUSTER_PROCS": __MEMPOOL_SPAWN_CLUSTER_PROCS__,
|
||||||
"API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
|
"API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
# MEMPOOL
|
# MEMPOOL
|
||||||
__MEMPOOL_NETWORK__=${MEMPOOL_NETWORK:=mainnet}
|
__MEMPOOL_NETWORK__=${MEMPOOL_NETWORK:=mainnet}
|
||||||
__MEMPOOL_BACKEND__=${MEMPOOL_BACKEND:=electrum}
|
__MEMPOOL_BACKEND__=${MEMPOOL_BACKEND:=electrum}
|
||||||
|
__MEMPOOL_ENABLED__=${MEMPOOL_ENABLED:=true}
|
||||||
__MEMPOOL_HTTP_PORT__=${BACKEND_HTTP_PORT:=8999}
|
__MEMPOOL_HTTP_PORT__=${BACKEND_HTTP_PORT:=8999}
|
||||||
__MEMPOOL_SPAWN_CLUSTER_PROCS__=${MEMPOOL_SPAWN_CLUSTER_PROCS:=0}
|
__MEMPOOL_SPAWN_CLUSTER_PROCS__=${MEMPOOL_SPAWN_CLUSTER_PROCS:=0}
|
||||||
__MEMPOOL_API_URL_PREFIX__=${MEMPOOL_API_URL_PREFIX:=/api/v1/}
|
__MEMPOOL_API_URL_PREFIX__=${MEMPOOL_API_URL_PREFIX:=/api/v1/}
|
||||||
@@ -111,6 +112,7 @@ mkdir -p "${__MEMPOOL_CACHE_DIR__}"
|
|||||||
|
|
||||||
sed -i "s/__MEMPOOL_NETWORK__/${__MEMPOOL_NETWORK__}/g" mempool-config.json
|
sed -i "s/__MEMPOOL_NETWORK__/${__MEMPOOL_NETWORK__}/g" mempool-config.json
|
||||||
sed -i "s/__MEMPOOL_BACKEND__/${__MEMPOOL_BACKEND__}/g" mempool-config.json
|
sed -i "s/__MEMPOOL_BACKEND__/${__MEMPOOL_BACKEND__}/g" mempool-config.json
|
||||||
|
sed -i "s/__MEMPOOL_ENABLED__/${__MEMPOOL_ENABLED__}/g" mempool-config.json
|
||||||
sed -i "s/__MEMPOOL_HTTP_PORT__/${__MEMPOOL_HTTP_PORT__}/g" mempool-config.json
|
sed -i "s/__MEMPOOL_HTTP_PORT__/${__MEMPOOL_HTTP_PORT__}/g" mempool-config.json
|
||||||
sed -i "s/__MEMPOOL_SPAWN_CLUSTER_PROCS__/${__MEMPOOL_SPAWN_CLUSTER_PROCS__}/g" mempool-config.json
|
sed -i "s/__MEMPOOL_SPAWN_CLUSTER_PROCS__/${__MEMPOOL_SPAWN_CLUSTER_PROCS__}/g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_API_URL_PREFIX__!${__MEMPOOL_API_URL_PREFIX__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_API_URL_PREFIX__!${__MEMPOOL_API_URL_PREFIX__}!g" mempool-config.json
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ WORKDIR /build
|
|||||||
COPY . .
|
COPY . .
|
||||||
RUN apt-get update
|
RUN apt-get update
|
||||||
RUN apt-get install -y build-essential rsync
|
RUN apt-get install -y build-essential rsync
|
||||||
|
RUN cp mempool-frontend-config.sample.json mempool-frontend-config.json
|
||||||
RUN npm install --omit=dev --omit=optional
|
RUN npm install --omit=dev --omit=optional
|
||||||
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM nginx:1.17.8-alpine
|
FROM nginx:1.17.8-alpine
|
||||||
@@ -28,7 +30,9 @@ RUN chown -R 1000:1000 /patch && chmod -R 755 /patch && \
|
|||||||
chown -R 1000:1000 /var/cache/nginx && \
|
chown -R 1000:1000 /var/cache/nginx && \
|
||||||
chown -R 1000:1000 /var/log/nginx && \
|
chown -R 1000:1000 /var/log/nginx && \
|
||||||
chown -R 1000:1000 /etc/nginx/nginx.conf && \
|
chown -R 1000:1000 /etc/nginx/nginx.conf && \
|
||||||
chown -R 1000:1000 /etc/nginx/conf.d
|
chown -R 1000:1000 /etc/nginx/conf.d && \
|
||||||
|
chown -R 1000:1000 /var/www/mempool
|
||||||
|
|
||||||
RUN touch /var/run/nginx.pid && \
|
RUN touch /var/run/nginx.pid && \
|
||||||
chown -R 1000:1000 /var/run/nginx.pid
|
chown -R 1000:1000 /var/run/nginx.pid
|
||||||
|
|
||||||
|
|||||||
@@ -10,4 +10,51 @@ cp /etc/nginx/nginx.conf /patch/nginx.conf
|
|||||||
sed -i "s/__MEMPOOL_FRONTEND_HTTP_PORT__/${__MEMPOOL_FRONTEND_HTTP_PORT__}/g" /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
|
cat /patch/nginx.conf > /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
# Runtime overrides - read env vars defined in docker compose
|
||||||
|
|
||||||
|
__TESTNET_ENABLED__=${TESTNET_ENABLED:=false}
|
||||||
|
__SIGNET_ENABLED__=${SIGNET_ENABLED:=false}
|
||||||
|
__LIQUID_ENABLED__=${LIQUID_EANBLED:=false}
|
||||||
|
__LIQUID_TESTNET_ENABLED__=${LIQUID_TESTNET_ENABLED:=false}
|
||||||
|
__BISQ_ENABLED__=${BISQ_ENABLED:=false}
|
||||||
|
__BISQ_SEPARATE_BACKEND__=${BISQ_SEPARATE_BACKEND:=false}
|
||||||
|
__ITEMS_PER_PAGE__=${ITEMS_PER_PAGE:=10}
|
||||||
|
__KEEP_BLOCKS_AMOUNT__=${KEEP_BLOCKS_AMOUNT:=8}
|
||||||
|
__NGINX_PROTOCOL__=${NGINX_PROTOCOL:=http}
|
||||||
|
__NGINX_HOSTNAME__=${NGINX_HOSTNAME:=localhost}
|
||||||
|
__NGINX_PORT__=${NGINX_PORT:=8999}
|
||||||
|
__BLOCK_WEIGHT_UNITS__=${BLOCK_WEIGHT_UNITS:=4000000}
|
||||||
|
__MEMPOOL_BLOCKS_AMOUNT__=${MEMPOOL_BLOCKS_AMOUNT:=8}
|
||||||
|
__BASE_MODULE__=${BASE_MODULE:=mempool}
|
||||||
|
__MEMPOOL_WEBSITE_URL__=${MEMPOOL_WEBSITE_URL:=https://mempool.space}
|
||||||
|
__LIQUID_WEBSITE_URL__=${LIQUID_WEBSITE_URL:=https://liquid.network}
|
||||||
|
__BISQ_WEBSITE_URL__=${BISQ_WEBSITE_URL:=https://bisq.markets}
|
||||||
|
__MINING_DASHBOARD__=${MINING_DASHBOARD:=true}
|
||||||
|
__LIGHTNING__=${LIGHTNING:=false}
|
||||||
|
|
||||||
|
# Export as environment variables to be used by envsubst
|
||||||
|
export __TESTNET_ENABLED__
|
||||||
|
export __SIGNET_ENABLED__
|
||||||
|
export __LIQUID_ENABLED__
|
||||||
|
export __LIQUID_TESTNET_ENABLED__
|
||||||
|
export __BISQ_ENABLED__
|
||||||
|
export __BISQ_SEPARATE_BACKEND__
|
||||||
|
export __ITEMS_PER_PAGE__
|
||||||
|
export __KEEP_BLOCKS_AMOUNT__
|
||||||
|
export __NGINX_PROTOCOL__
|
||||||
|
export __NGINX_HOSTNAME__
|
||||||
|
export __NGINX_PORT__
|
||||||
|
export __BLOCK_WEIGHT_UNITS__
|
||||||
|
export __MEMPOOL_BLOCKS_AMOUNT__
|
||||||
|
export __BASE_MODULE__
|
||||||
|
export __MEMPOOL_WEBSITE_URL__
|
||||||
|
export __LIQUID_WEBSITE_URL__
|
||||||
|
export __BISQ_WEBSITE_URL__
|
||||||
|
export __MINING_DASHBOARD__
|
||||||
|
export __LIGHTNING__
|
||||||
|
|
||||||
|
folder=$(find /var/www/mempool -name "config.js" | xargs dirname)
|
||||||
|
echo ${folder}
|
||||||
|
envsubst < ${folder}/config.template.js > ${folder}/config.js
|
||||||
|
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ https://www.transifex.com/mempool/mempool/dashboard/
|
|||||||
* French @Bayernatoor
|
* French @Bayernatoor
|
||||||
* Korean @kcalvinalvinn
|
* Korean @kcalvinalvinn
|
||||||
* Italian @HodlBits
|
* Italian @HodlBits
|
||||||
* Hebrew @Sh0ham
|
* Hebrew @rapidlab309
|
||||||
* Georgian @wyd_idk
|
* Georgian @wyd_idk
|
||||||
* Hungarian @btcdragonlord
|
* Hungarian @btcdragonlord
|
||||||
* Dutch @m__btc
|
* Dutch @m__btc
|
||||||
|
|||||||
@@ -152,15 +152,14 @@
|
|||||||
"assets": [
|
"assets": [
|
||||||
"src/favicon.ico",
|
"src/favicon.ico",
|
||||||
"src/resources",
|
"src/resources",
|
||||||
"src/robots.txt"
|
"src/robots.txt",
|
||||||
|
"src/config.js",
|
||||||
|
"src/config.template.js"
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"src/styles.scss",
|
"src/styles.scss",
|
||||||
"node_modules/@fortawesome/fontawesome-svg-core/styles.css"
|
"node_modules/@fortawesome/fontawesome-svg-core/styles.css"
|
||||||
],
|
],
|
||||||
"scripts": [
|
|
||||||
"generated-config.js"
|
|
||||||
],
|
|
||||||
"vendorChunk": true,
|
"vendorChunk": true,
|
||||||
"extractLicenses": false,
|
"extractLicenses": false,
|
||||||
"buildOptimizer": false,
|
"buildOptimizer": false,
|
||||||
@@ -222,6 +221,10 @@
|
|||||||
"proxyConfig": "proxy.conf.local.js",
|
"proxyConfig": "proxy.conf.local.js",
|
||||||
"verbose": true
|
"verbose": true
|
||||||
},
|
},
|
||||||
|
"local-esplora": {
|
||||||
|
"proxyConfig": "proxy.conf.local-esplora.js",
|
||||||
|
"verbose": true
|
||||||
|
},
|
||||||
"mixed": {
|
"mixed": {
|
||||||
"proxyConfig": "proxy.conf.mixed.js",
|
"proxyConfig": "proxy.conf.mixed.js",
|
||||||
"verbose": true
|
"verbose": true
|
||||||
@@ -265,57 +268,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"server": {
|
|
||||||
"builder": "@angular-devkit/build-angular:server",
|
|
||||||
"options": {
|
|
||||||
"outputPath": "dist/mempool/server",
|
|
||||||
"main": "server.ts",
|
|
||||||
"tsConfig": "tsconfig.server.json",
|
|
||||||
"sourceMap": true,
|
|
||||||
"optimization": false
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"production": {
|
|
||||||
"outputHashing": "media",
|
|
||||||
"fileReplacements": [
|
|
||||||
{
|
|
||||||
"replace": "src/environments/environment.ts",
|
|
||||||
"with": "src/environments/environment.prod.ts"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"sourceMap": false,
|
|
||||||
"localize": true,
|
|
||||||
"optimization": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"defaultConfiguration": ""
|
|
||||||
},
|
|
||||||
"serve-ssr": {
|
|
||||||
"builder": "@nguniversal/builders:ssr-dev-server",
|
|
||||||
"options": {
|
|
||||||
"browserTarget": "mempool:build",
|
|
||||||
"serverTarget": "mempool:server"
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"production": {
|
|
||||||
"browserTarget": "mempool:build:production",
|
|
||||||
"serverTarget": "mempool:server:production"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"prerender": {
|
|
||||||
"builder": "@nguniversal/builders:prerender",
|
|
||||||
"options": {
|
|
||||||
"browserTarget": "mempool:build:production",
|
|
||||||
"serverTarget": "mempool:server:production",
|
|
||||||
"routes": [
|
|
||||||
"/"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"production": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"cypress-run": {
|
"cypress-run": {
|
||||||
"builder": "@cypress/schematic:cypress",
|
"builder": "@cypress/schematic:cypress",
|
||||||
"options": {
|
"options": {
|
||||||
@@ -336,6 +288,5 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"defaultProject": "mempool"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ var fs = require('fs');
|
|||||||
const { spawnSync } = require('child_process');
|
const { spawnSync } = require('child_process');
|
||||||
|
|
||||||
const CONFIG_FILE_NAME = 'mempool-frontend-config.json';
|
const CONFIG_FILE_NAME = 'mempool-frontend-config.json';
|
||||||
const GENERATED_CONFIG_FILE_NAME = 'generated-config.js';
|
const GENERATED_CONFIG_FILE_NAME = 'src/resources/config.js';
|
||||||
|
const GENERATED_TEMPLATE_CONFIG_FILE_NAME = 'src/resources/config.template.js';
|
||||||
|
|
||||||
let settings = [];
|
let settings = [];
|
||||||
let configContent = {};
|
let configContent = {};
|
||||||
@@ -67,10 +68,17 @@ if (process.env.DOCKER_COMMIT_HASH) {
|
|||||||
|
|
||||||
const newConfig = `(function (window) {
|
const newConfig = `(function (window) {
|
||||||
window.__env = window.__env || {};${settings.reduce((str, obj) => `${str}
|
window.__env = window.__env || {};${settings.reduce((str, obj) => `${str}
|
||||||
window.__env.${obj.key} = ${ typeof obj.value === 'string' ? `'${obj.value}'` : obj.value };`, '')}
|
window.__env.${obj.key} = ${typeof obj.value === 'string' ? `'${obj.value}'` : obj.value};`, '')}
|
||||||
window.__env.GIT_COMMIT_HASH = '${gitCommitHash}';
|
window.__env.GIT_COMMIT_HASH = '${gitCommitHash}';
|
||||||
window.__env.PACKAGE_JSON_VERSION = '${packetJsonVersion}';
|
window.__env.PACKAGE_JSON_VERSION = '${packetJsonVersion}';
|
||||||
}(global || this));`;
|
}(this));`;
|
||||||
|
|
||||||
|
const newConfigTemplate = `(function (window) {
|
||||||
|
window.__env = window.__env || {};${settings.reduce((str, obj) => `${str}
|
||||||
|
window.__env.${obj.key} = ${typeof obj.value === 'string' ? `'\${__${obj.key}__}'` : `\${__${obj.key}__}`};`, '')}
|
||||||
|
window.__env.GIT_COMMIT_HASH = '${gitCommitHash}';
|
||||||
|
window.__env.PACKAGE_JSON_VERSION = '${packetJsonVersion}';
|
||||||
|
}(this));`;
|
||||||
|
|
||||||
function readConfig(path) {
|
function readConfig(path) {
|
||||||
try {
|
try {
|
||||||
@@ -89,6 +97,16 @@ function writeConfig(path, config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function writeConfigTemplate(path, config) {
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(path, config, 'utf8');
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeConfigTemplate(GENERATED_TEMPLATE_CONFIG_FILE_NAME, newConfigTemplate);
|
||||||
|
|
||||||
const currentConfig = readConfig(GENERATED_CONFIG_FILE_NAME);
|
const currentConfig = readConfig(GENERATED_CONFIG_FILE_NAME);
|
||||||
|
|
||||||
if (currentConfig && currentConfig === newConfig) {
|
if (currentConfig && currentConfig === newConfig) {
|
||||||
@@ -106,4 +124,4 @@ if (currentConfig && currentConfig === newConfig) {
|
|||||||
console.log('NEW CONFIG: ', newConfig);
|
console.log('NEW CONFIG: ', newConfig);
|
||||||
writeConfig(GENERATED_CONFIG_FILE_NAME, newConfig);
|
writeConfig(GENERATED_CONFIG_FILE_NAME, newConfig);
|
||||||
console.log(`${GENERATED_CONFIG_FILE_NAME} file updated`);
|
console.log(`${GENERATED_CONFIG_FILE_NAME} file updated`);
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -17,5 +17,8 @@
|
|||||||
"LIQUID_WEBSITE_URL": "https://liquid.network",
|
"LIQUID_WEBSITE_URL": "https://liquid.network",
|
||||||
"BISQ_WEBSITE_URL": "https://bisq.markets",
|
"BISQ_WEBSITE_URL": "https://bisq.markets",
|
||||||
"MINING_DASHBOARD": true,
|
"MINING_DASHBOARD": true,
|
||||||
|
"MAINNET_BLOCK_AUDIT_START_HEIGHT": 0,
|
||||||
|
"TESTNET_BLOCK_AUDIT_START_HEIGHT": 0,
|
||||||
|
"SIGNET_BLOCK_AUDIT_START_HEIGHT": 0,
|
||||||
"LIGHTNING": false
|
"LIGHTNING": false
|
||||||
}
|
}
|
||||||
|
|||||||
14552
frontend/package-lock.json
generated
14552
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -22,13 +22,14 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "./node_modules/@angular/cli/bin/ng.js",
|
"ng": "./node_modules/@angular/cli/bin/ng.js",
|
||||||
"tsc": "./node_modules/typescript/bin/tsc",
|
"tsc": "./node_modules/typescript/bin/tsc",
|
||||||
"i18n-extract-from-source": "./node_modules/@angular/cli/bin/ng extract-i18n --out-file ./src/locale/messages.xlf",
|
"i18n-extract-from-source": "npm run ng -- extract-i18n --out-file ./src/locale/messages.xlf",
|
||||||
"i18n-pull-from-transifex": "tx pull -a --parallel --minimum-perc 1 --force",
|
"i18n-pull-from-transifex": "tx pull -a --parallel --minimum-perc 1 --force",
|
||||||
"serve": "npm run generate-config && npm run ng -- serve -c local",
|
"serve": "npm run generate-config && npm run ng -- serve -c local",
|
||||||
"serve:stg": "npm run generate-config && npm run ng -- serve -c staging",
|
"serve:stg": "npm run generate-config && npm run ng -- serve -c staging",
|
||||||
"serve:local-prod": "npm run generate-config && npm run ng -- serve -c local-prod",
|
"serve:local-prod": "npm run generate-config && npm run ng -- serve -c local-prod",
|
||||||
"serve:local-staging": "npm run generate-config && npm run ng -- serve -c local-staging",
|
"serve:local-staging": "npm run generate-config && npm run ng -- serve -c local-staging",
|
||||||
"start": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local",
|
"start": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local",
|
||||||
|
"start:local-esplora": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-esplora",
|
||||||
"start:stg": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c staging",
|
"start:stg": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c staging",
|
||||||
"start:local-prod": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-prod",
|
"start:local-prod": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-prod",
|
||||||
"start:local-staging": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-staging",
|
"start:local-staging": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-staging",
|
||||||
@@ -50,9 +51,6 @@
|
|||||||
"config:defaults:mempool": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=mempool BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
|
"config:defaults:mempool": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=mempool BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
|
||||||
"config:defaults:liquid": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=liquid BLOCK_WEIGHT_UNITS=300000 && npm run generate-config",
|
"config:defaults:liquid": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=liquid BLOCK_WEIGHT_UNITS=300000 && npm run generate-config",
|
||||||
"config:defaults:bisq": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=bisq BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
|
"config:defaults:bisq": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=bisq BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
|
||||||
"dev:ssr": "npm run generate-config && npm run ng -- run mempool:serve-ssr",
|
|
||||||
"serve:ssr": "node server.run.js",
|
|
||||||
"build:ssr": "npm run build && npm run ng -- run mempool:server:production && npm run tsc -- server.run.ts",
|
|
||||||
"prerender": "npm run ng -- run mempool:prerender",
|
"prerender": "npm run ng -- run mempool:prerender",
|
||||||
"cypress:open": "cypress open",
|
"cypress:open": "cypress open",
|
||||||
"cypress:run": "cypress run",
|
"cypress:run": "cypress run",
|
||||||
@@ -63,63 +61,59 @@
|
|||||||
"cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record"
|
"cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular-devkit/build-angular": "~13.3.7",
|
"@angular-devkit/build-angular": "^14.2.10",
|
||||||
"@angular/animations": "~13.3.10",
|
"@angular/animations": "^14.2.12",
|
||||||
"@angular/cli": "~13.3.7",
|
"@angular/cli": "^14.2.10",
|
||||||
"@angular/common": "~13.3.10",
|
"@angular/common": "^14.2.12",
|
||||||
"@angular/compiler": "~13.3.10",
|
"@angular/compiler": "^14.2.12",
|
||||||
"@angular/core": "~13.3.10",
|
"@angular/core": "^14.2.12",
|
||||||
"@angular/forms": "~13.3.10",
|
"@angular/forms": "^14.2.12",
|
||||||
"@angular/localize": "~13.3.10",
|
"@angular/localize": "^14.2.12",
|
||||||
"@angular/platform-browser": "~13.3.10",
|
"@angular/platform-browser": "^14.2.12",
|
||||||
"@angular/platform-browser-dynamic": "~13.3.10",
|
"@angular/platform-browser-dynamic": "^14.2.12",
|
||||||
"@angular/platform-server": "~13.3.10",
|
"@angular/platform-server": "^14.2.12",
|
||||||
"@angular/router": "~13.3.10",
|
"@angular/router": "^14.2.12",
|
||||||
"@fortawesome/angular-fontawesome": "~0.10.2",
|
"@fortawesome/angular-fontawesome": "~0.11.1",
|
||||||
"@fortawesome/fontawesome-common-types": "~6.1.1",
|
"@fortawesome/fontawesome-common-types": "~6.2.1",
|
||||||
"@fortawesome/fontawesome-svg-core": "~6.1.1",
|
"@fortawesome/fontawesome-svg-core": "~6.2.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "~6.1.1",
|
"@fortawesome/free-solid-svg-icons": "~6.2.1",
|
||||||
"@mempool/mempool.js": "2.3.0",
|
"@mempool/mempool.js": "2.3.0",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^11.0.0",
|
"@ng-bootstrap/ng-bootstrap": "^13.1.1",
|
||||||
"@nguniversal/express-engine": "~13.1.1",
|
"@types/qrcode": "~1.5.0",
|
||||||
"@types/qrcode": "~1.4.2",
|
"bootstrap": "~4.6.1",
|
||||||
"bootstrap": "~4.5.0",
|
|
||||||
"browserify": "^17.0.0",
|
"browserify": "^17.0.0",
|
||||||
"clipboard": "^2.0.10",
|
"clipboard": "^2.0.11",
|
||||||
"domino": "^2.1.6",
|
"domino": "^2.1.6",
|
||||||
"echarts": "~5.3.2",
|
"echarts": "~5.4.0",
|
||||||
"echarts-gl": "^2.0.9",
|
"echarts-gl": "^2.0.9",
|
||||||
"express": "^4.17.1",
|
|
||||||
"lightweight-charts": "~3.8.0",
|
"lightweight-charts": "~3.8.0",
|
||||||
"ngx-echarts": "8.0.1",
|
"ngx-echarts": "~14.0.0",
|
||||||
"ngx-infinite-scroll": "^10.0.1",
|
"ngx-infinite-scroll": "^14.0.1",
|
||||||
"qrcode": "1.5.0",
|
"qrcode": "1.5.1",
|
||||||
"rxjs": "~7.5.5",
|
"rxjs": "~7.5.7",
|
||||||
"tinyify": "^3.0.0",
|
"tinyify": "^3.1.0",
|
||||||
"tlite": "^0.1.9",
|
"tlite": "^0.1.9",
|
||||||
"tslib": "~2.4.0",
|
"tslib": "~2.4.1",
|
||||||
"zone.js": "~0.11.5"
|
"zone.js": "~0.12.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular/compiler-cli": "~13.3.10",
|
"@angular/compiler-cli": "^14.2.12",
|
||||||
"@angular/language-service": "~13.3.10",
|
"@angular/language-service": "^14.2.12",
|
||||||
"@nguniversal/builders": "~13.1.1",
|
"@types/node": "^18.11.9",
|
||||||
"@types/express": "^4.17.0",
|
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||||
"@types/node": "^12.11.1",
|
"@typescript-eslint/parser": "^5.45.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.30.5",
|
"eslint": "^8.28.0",
|
||||||
"@typescript-eslint/parser": "^5.30.5",
|
|
||||||
"eslint": "^8.19.0",
|
|
||||||
"http-proxy-middleware": "~2.0.6",
|
"http-proxy-middleware": "~2.0.6",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.8.0",
|
||||||
"ts-node": "~10.8.1",
|
"ts-node": "~10.9.1",
|
||||||
"typescript": "~4.6.4"
|
"typescript": "~4.6.4"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@cypress/schematic": "~2.0.0",
|
"@cypress/schematic": "~2.3.0",
|
||||||
"cypress": "^10.3.0",
|
"cypress": "^11.2.0",
|
||||||
"cypress-fail-on-console-error": "~3.0.0",
|
"cypress-fail-on-console-error": "~4.0.2",
|
||||||
"cypress-wait-until": "^1.7.2",
|
"cypress-wait-until": "^1.7.2",
|
||||||
"mock-socket": "~9.1.4",
|
"mock-socket": "~9.1.5",
|
||||||
"start-server-and-test": "~1.14.0"
|
"start-server-and-test": "~1.14.0"
|
||||||
},
|
},
|
||||||
"scarfSettings": {
|
"scarfSettings": {
|
||||||
|
|||||||
137
frontend/proxy.conf.local-esplora.js
Normal file
137
frontend/proxy.conf.local-esplora.js
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const FRONTEND_CONFIG_FILE_NAME = 'mempool-frontend-config.json';
|
||||||
|
|
||||||
|
let configContent;
|
||||||
|
|
||||||
|
// Read frontend config
|
||||||
|
try {
|
||||||
|
const rawConfig = fs.readFileSync(FRONTEND_CONFIG_FILE_NAME);
|
||||||
|
configContent = JSON.parse(rawConfig);
|
||||||
|
console.log(`${FRONTEND_CONFIG_FILE_NAME} file found, using provided config`);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
if (e.code !== 'ENOENT') {
|
||||||
|
throw new Error(e);
|
||||||
|
} else {
|
||||||
|
console.log(`${FRONTEND_CONFIG_FILE_NAME} file not found, using default config`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let PROXY_CONFIG = [];
|
||||||
|
|
||||||
|
if (configContent && configContent.BASE_MODULE === 'liquid') {
|
||||||
|
PROXY_CONFIG.push(...[
|
||||||
|
{
|
||||||
|
context: ['/liquid/api/v1/**'],
|
||||||
|
target: `http://127.0.0.1:8999`,
|
||||||
|
secure: false,
|
||||||
|
ws: true,
|
||||||
|
changeOrigin: true,
|
||||||
|
proxyTimeout: 30000,
|
||||||
|
pathRewrite: {
|
||||||
|
"^/liquid": ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: ['/liquid/api/**'],
|
||||||
|
target: `http://127.0.0.1:3000`,
|
||||||
|
secure: false,
|
||||||
|
changeOrigin: true,
|
||||||
|
proxyTimeout: 30000,
|
||||||
|
pathRewrite: {
|
||||||
|
"^/liquid/api/": ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: ['/liquidtestnet/api/v1/**'],
|
||||||
|
target: `http://127.0.0.1:8999`,
|
||||||
|
secure: false,
|
||||||
|
ws: true,
|
||||||
|
changeOrigin: true,
|
||||||
|
proxyTimeout: 30000,
|
||||||
|
pathRewrite: {
|
||||||
|
"^/liquidtestnet": ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: ['/liquidtestnet/api/**'],
|
||||||
|
target: `http://127.0.0.1:3000`,
|
||||||
|
secure: false,
|
||||||
|
changeOrigin: true,
|
||||||
|
proxyTimeout: 30000,
|
||||||
|
pathRewrite: {
|
||||||
|
"^/liquidtestnet/api/": "/"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (configContent && configContent.BASE_MODULE === 'bisq') {
|
||||||
|
PROXY_CONFIG.push(...[
|
||||||
|
{
|
||||||
|
context: ['/bisq/api/v1/ws'],
|
||||||
|
target: `http://127.0.0.1:8999`,
|
||||||
|
secure: false,
|
||||||
|
ws: true,
|
||||||
|
changeOrigin: true,
|
||||||
|
proxyTimeout: 30000,
|
||||||
|
pathRewrite: {
|
||||||
|
"^/bisq": ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: ['/bisq/api/v1/**'],
|
||||||
|
target: `http://127.0.0.1:8999`,
|
||||||
|
secure: false,
|
||||||
|
changeOrigin: true,
|
||||||
|
proxyTimeout: 30000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: ['/bisq/api/**'],
|
||||||
|
target: `http://127.0.0.1:8999`,
|
||||||
|
secure: false,
|
||||||
|
changeOrigin: true,
|
||||||
|
proxyTimeout: 30000,
|
||||||
|
pathRewrite: {
|
||||||
|
"^/bisq/api/": "/api/v1/bisq/"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
PROXY_CONFIG.push(...[
|
||||||
|
{
|
||||||
|
context: ['/testnet/api/v1/lightning/**'],
|
||||||
|
target: `http://127.0.0.1:8999`,
|
||||||
|
secure: false,
|
||||||
|
changeOrigin: true,
|
||||||
|
proxyTimeout: 30000,
|
||||||
|
pathRewrite: {
|
||||||
|
"^/testnet": ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: ['/api/v1/**'],
|
||||||
|
target: `http://127.0.0.1:8999`,
|
||||||
|
secure: false,
|
||||||
|
ws: true,
|
||||||
|
changeOrigin: true,
|
||||||
|
proxyTimeout: 30000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: ['/api/**'],
|
||||||
|
target: `http://127.0.0.1:3000`,
|
||||||
|
secure: false,
|
||||||
|
changeOrigin: true,
|
||||||
|
proxyTimeout: 30000,
|
||||||
|
pathRewrite: {
|
||||||
|
"^/api": ""
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log(PROXY_CONFIG);
|
||||||
|
|
||||||
|
module.exports = PROXY_CONFIG;
|
||||||
@@ -3,9 +3,9 @@ const fs = require('fs');
|
|||||||
let PROXY_CONFIG = require('./proxy.conf');
|
let PROXY_CONFIG = require('./proxy.conf');
|
||||||
|
|
||||||
PROXY_CONFIG.forEach(entry => {
|
PROXY_CONFIG.forEach(entry => {
|
||||||
entry.target = entry.target.replace("mempool.space", "mempool.ninja");
|
entry.target = entry.target.replace("mempool.space", "mempool-staging.tk7.mempool.space");
|
||||||
entry.target = entry.target.replace("liquid.network", "liquid.place");
|
entry.target = entry.target.replace("liquid.network", "liquid-staging.tk7.mempool.space");
|
||||||
entry.target = entry.target.replace("bisq.markets", "bisq.ninja");
|
entry.target = entry.target.replace("bisq.markets", "bisq-staging.fra.mempool.space");
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = PROXY_CONFIG;
|
module.exports = PROXY_CONFIG;
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
import 'zone.js/node';
|
|
||||||
import './generated-config';
|
|
||||||
|
|
||||||
import * as domino from 'domino';
|
|
||||||
import * as express from 'express';
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
|
|
||||||
const {readFileSync, existsSync} = require('fs');
|
|
||||||
const {createProxyMiddleware} = require('http-proxy-middleware');
|
|
||||||
|
|
||||||
const template = fs.readFileSync(path.join(process.cwd(), 'dist/mempool/browser/en-US/', 'index.html')).toString();
|
|
||||||
const win = domino.createWindow(template);
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
win.__env = global.__env;
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
win.matchMedia = () => {
|
|
||||||
return {
|
|
||||||
matches: true
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
win.setTimeout = (fn) => { fn(); };
|
|
||||||
win.document.body.scrollTo = (() => {});
|
|
||||||
// @ts-ignore
|
|
||||||
global['window'] = win;
|
|
||||||
global['document'] = win.document;
|
|
||||||
// @ts-ignore
|
|
||||||
global['history'] = { state: { } };
|
|
||||||
|
|
||||||
global['localStorage'] = {
|
|
||||||
getItem: () => '',
|
|
||||||
setItem: () => {},
|
|
||||||
removeItem: () => {},
|
|
||||||
clear: () => {},
|
|
||||||
length: 0,
|
|
||||||
key: () => '',
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the list of supported and actually active locales
|
|
||||||
*/
|
|
||||||
function getActiveLocales() {
|
|
||||||
const angularConfig = JSON.parse(readFileSync('angular.json', 'utf8'));
|
|
||||||
|
|
||||||
const supportedLocales = [
|
|
||||||
angularConfig.projects.mempool.i18n.sourceLocale,
|
|
||||||
...Object.keys(angularConfig.projects.mempool.i18n.locales),
|
|
||||||
];
|
|
||||||
|
|
||||||
return supportedLocales.filter(locale => existsSync(`./dist/mempool/server/${locale}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
function app() {
|
|
||||||
const server = express();
|
|
||||||
|
|
||||||
// proxy API to nginx
|
|
||||||
server.get('/api/**', createProxyMiddleware({
|
|
||||||
// @ts-ignore
|
|
||||||
target: win.__env.NGINX_PROTOCOL + '://' + win.__env.NGINX_HOSTNAME + ':' + win.__env.NGINX_PORT,
|
|
||||||
changeOrigin: true,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// map / and /en to en-US
|
|
||||||
const defaultLocale = 'en-US';
|
|
||||||
console.log(`serving default locale: ${defaultLocale}`);
|
|
||||||
const appServerModule = require(`./dist/mempool/server/${defaultLocale}/main.js`);
|
|
||||||
server.use('/', appServerModule.app(defaultLocale));
|
|
||||||
server.use('/en', appServerModule.app(defaultLocale));
|
|
||||||
|
|
||||||
// map each locale to its localized main.js
|
|
||||||
getActiveLocales().forEach(locale => {
|
|
||||||
console.log('serving locale:', locale);
|
|
||||||
const appServerModule = require(`./dist/mempool/server/${locale}/main.js`);
|
|
||||||
|
|
||||||
// map everything to itself
|
|
||||||
server.use(`/${locale}`, appServerModule.app(locale));
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
return server;
|
|
||||||
}
|
|
||||||
|
|
||||||
function run() {
|
|
||||||
const port = process.env.PORT || 4000;
|
|
||||||
|
|
||||||
// Start up the Node server
|
|
||||||
app().listen(port, () => {
|
|
||||||
console.log(`Node Express server listening on port ${port}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
run();
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
import 'zone.js/node';
|
|
||||||
import './generated-config';
|
|
||||||
|
|
||||||
import { ngExpressEngine } from '@nguniversal/express-engine';
|
|
||||||
import * as express from 'express';
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
import * as domino from 'domino';
|
|
||||||
|
|
||||||
import { join } from 'path';
|
|
||||||
import { AppServerModule } from './src/main.server';
|
|
||||||
import { APP_BASE_HREF } from '@angular/common';
|
|
||||||
import { existsSync } from 'fs';
|
|
||||||
|
|
||||||
const template = fs.readFileSync(path.join(process.cwd(), 'dist/mempool/browser/en-US/', 'index.html')).toString();
|
|
||||||
const win = domino.createWindow(template);
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
win.__env = global.__env;
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
win.matchMedia = () => {
|
|
||||||
return {
|
|
||||||
matches: true
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
win.setTimeout = (fn) => { fn(); };
|
|
||||||
win.document.body.scrollTo = (() => {});
|
|
||||||
// @ts-ignore
|
|
||||||
global['window'] = win;
|
|
||||||
global['document'] = win.document;
|
|
||||||
// @ts-ignore
|
|
||||||
global['history'] = { state: { } };
|
|
||||||
|
|
||||||
global['localStorage'] = {
|
|
||||||
getItem: () => '',
|
|
||||||
setItem: () => {},
|
|
||||||
removeItem: () => {},
|
|
||||||
clear: () => {},
|
|
||||||
length: 0,
|
|
||||||
key: () => '',
|
|
||||||
};
|
|
||||||
|
|
||||||
// The Express app is exported so that it can be used by serverless Functions.
|
|
||||||
export function app(locale: string): express.Express {
|
|
||||||
const server = express();
|
|
||||||
const distFolder = join(process.cwd(), `dist/mempool/browser/${locale}`);
|
|
||||||
const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';
|
|
||||||
|
|
||||||
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
|
|
||||||
server.engine('html', ngExpressEngine({
|
|
||||||
bootstrap: AppServerModule,
|
|
||||||
}));
|
|
||||||
|
|
||||||
server.set('view engine', 'html');
|
|
||||||
server.set('views', distFolder);
|
|
||||||
|
|
||||||
// only handle URLs that actually exist
|
|
||||||
//server.get(locale, getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/tx/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/block/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/mempool-block/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/address/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/blocks', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/mining/pools', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/mining/pool/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/graphs', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/liquid', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/liquid/tx/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/liquid/block/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/liquid/mempool-block/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/liquid/address/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/liquid/asset/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/liquid/blocks', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/liquid/graphs', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/liquid/assets', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/liquid/api', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/liquid/tv', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/liquid/status', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/liquid/about', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/testnet', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/testnet/tx/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/testnet/block/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/testnet/mempool-block/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/testnet/address/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/testnet/blocks', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/testnet/mining/pools', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/testnet/graphs', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/testnet/api', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/testnet/tv', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/testnet/status', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/testnet/about', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/signet', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/signet/tx/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/signet/block/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/signet/mempool-block/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/signet/address/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/signet/blocks', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/signet/mining/pools', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/signet/graphs', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/signet/api', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/signet/tv', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/signet/status', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/signet/about', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/bisq', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/bisq/tx/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/bisq/blocks', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/bisq/block/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/bisq/address/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/bisq/stats', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/bisq/about', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/bisq/api', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/about', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/api', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/tv', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/status', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/terms-of-service', getLocalizedSSR(indexHtml));
|
|
||||||
|
|
||||||
// fallback to static file handler so we send HTTP 404 to nginx
|
|
||||||
server.get('/**', express.static(distFolder, { maxAge: '1y' }));
|
|
||||||
|
|
||||||
return server;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLocalizedSSR(indexHtml) {
|
|
||||||
return (req, res) => {
|
|
||||||
res.render(indexHtml, {
|
|
||||||
req,
|
|
||||||
providers: [
|
|
||||||
{ provide: APP_BASE_HREF, useValue: req.baseUrl }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// only used for development mode
|
|
||||||
function run(): void {
|
|
||||||
const port = process.env.PORT || 4000;
|
|
||||||
|
|
||||||
// Start up the Node server
|
|
||||||
const server = app('en-US');
|
|
||||||
server.listen(port, () => {
|
|
||||||
console.log(`Node Express server listening on port ${port}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Webpack will replace 'require' with '__webpack_require__'
|
|
||||||
// '__non_webpack_require__' is a proxy to Node 'require'
|
|
||||||
// The below code is to ensure that the server is run only when not requiring the bundle.
|
|
||||||
declare const __non_webpack_require__: NodeRequire;
|
|
||||||
const mainModule = __non_webpack_require__.main;
|
|
||||||
const moduleFilename = mainModule && mainModule.filename || '';
|
|
||||||
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
|
|
||||||
run();
|
|
||||||
}
|
|
||||||
|
|
||||||
export * from './src/main.server';
|
|
||||||
@@ -4,7 +4,6 @@ import { AppPreloadingStrategy } from './app.preloading-strategy'
|
|||||||
import { StartComponent } from './components/start/start.component';
|
import { StartComponent } from './components/start/start.component';
|
||||||
import { TransactionComponent } from './components/transaction/transaction.component';
|
import { TransactionComponent } from './components/transaction/transaction.component';
|
||||||
import { BlockComponent } from './components/block/block.component';
|
import { BlockComponent } from './components/block/block.component';
|
||||||
import { BlockAuditComponent } from './components/block-audit/block-audit.component';
|
|
||||||
import { AddressComponent } from './components/address/address.component';
|
import { AddressComponent } from './components/address/address.component';
|
||||||
import { MasterPageComponent } from './components/master-page/master-page.component';
|
import { MasterPageComponent } from './components/master-page/master-page.component';
|
||||||
import { AboutComponent } from './components/about/about.component';
|
import { AboutComponent } from './components/about/about.component';
|
||||||
@@ -74,12 +73,14 @@ let routes: Routes = [
|
|||||||
children: [],
|
children: [],
|
||||||
component: AddressComponent,
|
component: AddressComponent,
|
||||||
data: {
|
data: {
|
||||||
ogImage: true
|
ogImage: true,
|
||||||
|
networkSpecific: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'tx',
|
path: 'tx',
|
||||||
component: StartComponent,
|
component: StartComponent,
|
||||||
|
data: { networkSpecific: true },
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: ':id',
|
path: ':id',
|
||||||
@@ -90,6 +91,7 @@ let routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'block',
|
path: 'block',
|
||||||
component: StartComponent,
|
component: StartComponent,
|
||||||
|
data: { networkSpecific: true },
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: ':id',
|
path: ':id',
|
||||||
@@ -100,15 +102,6 @@ let routes: Routes = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'block-audit',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: ':id',
|
|
||||||
component: BlockAuditComponent,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'docs',
|
path: 'docs',
|
||||||
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule),
|
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule),
|
||||||
@@ -121,12 +114,13 @@ let routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'lightning',
|
path: 'lightning',
|
||||||
loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule),
|
loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule),
|
||||||
data: { preload: browserWindowEnv && browserWindowEnv.LIGHTNING === true },
|
data: { preload: browserWindowEnv && browserWindowEnv.LIGHTNING === true, networks: ['bitcoin'] },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'status',
|
path: 'status',
|
||||||
|
data: { networks: ['bitcoin', 'liquid'] },
|
||||||
component: StatusViewComponent
|
component: StatusViewComponent
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -185,11 +179,13 @@ let routes: Routes = [
|
|||||||
children: [],
|
children: [],
|
||||||
component: AddressComponent,
|
component: AddressComponent,
|
||||||
data: {
|
data: {
|
||||||
ogImage: true
|
ogImage: true,
|
||||||
|
networkSpecific: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'tx',
|
path: 'tx',
|
||||||
|
data: { networkSpecific: true },
|
||||||
component: StartComponent,
|
component: StartComponent,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
@@ -200,6 +196,7 @@ let routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'block',
|
path: 'block',
|
||||||
|
data: { networkSpecific: true },
|
||||||
component: StartComponent,
|
component: StartComponent,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
@@ -211,15 +208,6 @@ let routes: Routes = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'block-audit',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: ':id',
|
|
||||||
component: BlockAuditComponent,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'docs',
|
path: 'docs',
|
||||||
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
|
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
|
||||||
@@ -230,12 +218,14 @@ let routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'lightning',
|
path: 'lightning',
|
||||||
|
data: { networks: ['bitcoin'] },
|
||||||
loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule)
|
loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule)
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'status',
|
path: 'status',
|
||||||
|
data: { networks: ['bitcoin', 'liquid'] },
|
||||||
component: StatusViewComponent
|
component: StatusViewComponent
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -291,11 +281,13 @@ let routes: Routes = [
|
|||||||
children: [],
|
children: [],
|
||||||
component: AddressComponent,
|
component: AddressComponent,
|
||||||
data: {
|
data: {
|
||||||
ogImage: true
|
ogImage: true,
|
||||||
|
networkSpecific: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'tx',
|
path: 'tx',
|
||||||
|
data: { networkSpecific: true },
|
||||||
component: StartComponent,
|
component: StartComponent,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
@@ -306,6 +298,7 @@ let routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'block',
|
path: 'block',
|
||||||
|
data: { networkSpecific: true },
|
||||||
component: StartComponent,
|
component: StartComponent,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
@@ -317,15 +310,6 @@ let routes: Routes = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'block-audit',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: ':id',
|
|
||||||
component: BlockAuditComponent
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'docs',
|
path: 'docs',
|
||||||
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
|
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
|
||||||
@@ -336,6 +320,7 @@ let routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'lightning',
|
path: 'lightning',
|
||||||
|
data: { networks: ['bitcoin'] },
|
||||||
loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule)
|
loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule)
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -359,6 +344,7 @@ let routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'status',
|
path: 'status',
|
||||||
|
data: { networks: ['bitcoin', 'liquid'] },
|
||||||
component: StatusViewComponent
|
component: StatusViewComponent
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -422,11 +408,13 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
|||||||
children: [],
|
children: [],
|
||||||
component: AddressComponent,
|
component: AddressComponent,
|
||||||
data: {
|
data: {
|
||||||
ogImage: true
|
ogImage: true,
|
||||||
|
networkSpecific: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'tx',
|
path: 'tx',
|
||||||
|
data: { networkSpecific: true },
|
||||||
component: StartComponent,
|
component: StartComponent,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
@@ -437,6 +425,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'block',
|
path: 'block',
|
||||||
|
data: { networkSpecific: true },
|
||||||
component: StartComponent,
|
component: StartComponent,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
@@ -450,18 +439,22 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'assets',
|
path: 'assets',
|
||||||
|
data: { networks: ['liquid'] },
|
||||||
component: AssetsNavComponent,
|
component: AssetsNavComponent,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'all',
|
path: 'all',
|
||||||
|
data: { networks: ['liquid'] },
|
||||||
component: AssetsComponent,
|
component: AssetsComponent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'asset/:id',
|
path: 'asset/:id',
|
||||||
|
data: { networkSpecific: true },
|
||||||
component: AssetComponent
|
component: AssetComponent
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'group/:id',
|
path: 'group/:id',
|
||||||
|
data: { networkSpecific: true },
|
||||||
component: AssetGroupComponent
|
component: AssetGroupComponent
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -482,6 +475,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'status',
|
path: 'status',
|
||||||
|
data: { networks: ['bitcoin', 'liquid'] },
|
||||||
component: StatusViewComponent
|
component: StatusViewComponent
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -532,11 +526,13 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
|||||||
children: [],
|
children: [],
|
||||||
component: AddressComponent,
|
component: AddressComponent,
|
||||||
data: {
|
data: {
|
||||||
ogImage: true
|
ogImage: true,
|
||||||
|
networkSpecific: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'tx',
|
path: 'tx',
|
||||||
|
data: { networkSpecific: true },
|
||||||
component: StartComponent,
|
component: StartComponent,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
@@ -547,6 +543,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'block',
|
path: 'block',
|
||||||
|
data: { networkSpecific: true },
|
||||||
component: StartComponent,
|
component: StartComponent,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
@@ -560,22 +557,27 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'assets',
|
path: 'assets',
|
||||||
|
data: { networks: ['liquid'] },
|
||||||
component: AssetsNavComponent,
|
component: AssetsNavComponent,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'featured',
|
path: 'featured',
|
||||||
|
data: { networkSpecific: true },
|
||||||
component: AssetsFeaturedComponent,
|
component: AssetsFeaturedComponent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'all',
|
path: 'all',
|
||||||
|
data: { networks: ['liquid'] },
|
||||||
component: AssetsComponent,
|
component: AssetsComponent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'asset/:id',
|
path: 'asset/:id',
|
||||||
|
data: { networkSpecific: true },
|
||||||
component: AssetComponent
|
component: AssetComponent
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'group/:id',
|
path: 'group/:id',
|
||||||
|
data: { networkSpecific: true },
|
||||||
component: AssetGroupComponent
|
component: AssetGroupComponent
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -609,6 +611,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'status',
|
path: 'status',
|
||||||
|
data: { networks: ['bitcoin', 'liquid']},
|
||||||
component: StatusViewComponent
|
component: StatusViewComponent
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -624,7 +627,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
|||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [RouterModule.forRoot(routes, {
|
imports: [RouterModule.forRoot(routes, {
|
||||||
initialNavigation: 'enabled',
|
initialNavigation: 'enabledBlocking',
|
||||||
scrollPositionRestoration: 'enabled',
|
scrollPositionRestoration: 'enabled',
|
||||||
anchorScrolling: 'enabled',
|
anchorScrolling: 'enabled',
|
||||||
preloadingStrategy: AppPreloadingStrategy
|
preloadingStrategy: AppPreloadingStrategy
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const poolsColor = {
|
|||||||
'binancepool': '#1E88E5',
|
'binancepool': '#1E88E5',
|
||||||
'viabtc': '#039BE5',
|
'viabtc': '#039BE5',
|
||||||
'btccom': '#00897B',
|
'btccom': '#00897B',
|
||||||
'slushpool': '#00ACC1',
|
'braiinspool': '#00ACC1',
|
||||||
'sbicrypto': '#43A047',
|
'sbicrypto': '#43A047',
|
||||||
'marapool': '#7CB342',
|
'marapool': '#7CB342',
|
||||||
'luxor': '#C0CA33',
|
'luxor': '#C0CA33',
|
||||||
|
|||||||
@@ -10,27 +10,27 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form [formGroup]="radioGroupForm" class="mb-3 radio-form">
|
<form [formGroup]="radioGroupForm" class="mb-3 radio-form">
|
||||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="interval">
|
<div class="btn-group btn-group-toggle" name="radioBasic">
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'half_hour'">
|
||||||
<input ngbButton type="radio" [value]="'half_hour'" (click)="setFragment('half_hour')"> 30M
|
<input type="radio" [value]="'half_hour'" (click)="setFragment('half_hour')" formControlName="interval"> 30M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'hour'">
|
||||||
<input ngbButton type="radio" [value]="'hour'" (click)="setFragment('hour')"> 1H
|
<input type="radio" [value]="'hour'" (click)="setFragment('hour')" formControlName="interval"> 1H
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'half_day'">
|
||||||
<input ngbButton type="radio" [value]="'half_day'" (click)="setFragment('half_day')"> 12H
|
<input type="radio" [value]="'half_day'" (click)="setFragment('half_day')" formControlName="interval"> 12H
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'day'">
|
||||||
<input ngbButton type="radio" [value]="'day'" (click)="setFragment('day')"> 1D
|
<input type="radio" [value]="'day'" (click)="setFragment('day')" formControlName="interval"> 1D
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'week'">
|
||||||
<input ngbButton type="radio" [value]="'week'" (click)="setFragment('week')"> 1W
|
<input type="radio" [value]="'week'" (click)="setFragment('week')" formControlName="interval"> 1W
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'month'">
|
||||||
<input ngbButton type="radio" [value]="'month'" (click)="setFragment('month')"> 1M
|
<input type="radio" [value]="'month'" (click)="setFragment('month')" formControlName="interval"> 1M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'year'">
|
||||||
<input ngbButton type="radio" [value]="'year'" (click)="setFragment('year')"> 1Y
|
<input type="radio" [value]="'year'" (click)="setFragment('year')" formControlName="interval"> 1Y
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { combineLatest, merge, Observable, of } from 'rxjs';
|
import { combineLatest, merge, Observable, of } from 'rxjs';
|
||||||
import { map, switchMap } from 'rxjs/operators';
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
@@ -19,7 +19,7 @@ export class BisqMarketComponent implements OnInit, OnDestroy {
|
|||||||
currency$: Observable<any>;
|
currency$: Observable<any>;
|
||||||
offers$: Observable<OffersMarket>;
|
offers$: Observable<OffersMarket>;
|
||||||
trades$: Observable<Trade[]>;
|
trades$: Observable<Trade[]>;
|
||||||
radioGroupForm: FormGroup;
|
radioGroupForm: UntypedFormGroup;
|
||||||
defaultInterval = 'day';
|
defaultInterval = 'day';
|
||||||
|
|
||||||
isLoadingGraph = false;
|
isLoadingGraph = false;
|
||||||
@@ -28,7 +28,7 @@ export class BisqMarketComponent implements OnInit, OnDestroy {
|
|||||||
private websocketService: WebsocketService,
|
private websocketService: WebsocketService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private bisqApiService: BisqApiService,
|
private bisqApiService: BisqApiService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
) { }
|
) { }
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Observable, Subscription } from 'rxjs';
|
|||||||
import { switchMap, map, tap } from 'rxjs/operators';
|
import { switchMap, map, tap } from 'rxjs/operators';
|
||||||
import { BisqApiService } from '../bisq-api.service';
|
import { BisqApiService } from '../bisq-api.service';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '../../services/seo.service';
|
||||||
import { FormGroup, FormBuilder } from '@angular/forms';
|
import { UntypedFormGroup, UntypedFormBuilder } from '@angular/forms';
|
||||||
import { Router, ActivatedRoute } from '@angular/router';
|
import { Router, ActivatedRoute } from '@angular/router';
|
||||||
import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts } from '../../components/ngx-bootstrap-multiselect/types'
|
import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts } from '../../components/ngx-bootstrap-multiselect/types'
|
||||||
import { WebsocketService } from '../../services/websocket.service';
|
import { WebsocketService } from '../../services/websocket.service';
|
||||||
@@ -23,7 +23,7 @@ export class BisqTransactionsComponent implements OnInit, OnDestroy {
|
|||||||
fiveItemsPxSize = 250;
|
fiveItemsPxSize = 250;
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
loadingItems: number[];
|
loadingItems: number[];
|
||||||
radioGroupForm: FormGroup;
|
radioGroupForm: UntypedFormGroup;
|
||||||
types: string[] = [];
|
types: string[] = [];
|
||||||
radioGroupSubscription: Subscription;
|
radioGroupSubscription: Subscription;
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ export class BisqTransactionsComponent implements OnInit, OnDestroy {
|
|||||||
private websocketService: WebsocketService,
|
private websocketService: WebsocketService,
|
||||||
private bisqApiService: BisqApiService,
|
private bisqApiService: BisqApiService,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private cd: ChangeDetectorRef,
|
private cd: ChangeDetectorRef,
|
||||||
|
|||||||
@@ -20,14 +20,17 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'markets',
|
path: 'markets',
|
||||||
|
data: { networks: ['bisq'] },
|
||||||
component: BisqDashboardComponent,
|
component: BisqDashboardComponent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'transactions',
|
path: 'transactions',
|
||||||
|
data: { networks: ['bisq'] },
|
||||||
component: BisqTransactionsComponent
|
component: BisqTransactionsComponent
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'market/:pair',
|
path: 'market/:pair',
|
||||||
|
data: { networkSpecific: true },
|
||||||
component: BisqMarketComponent,
|
component: BisqMarketComponent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -36,6 +39,7 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'tx/:id',
|
path: 'tx/:id',
|
||||||
|
data: { networkSpecific: true },
|
||||||
component: BisqTransactionComponent
|
component: BisqTransactionComponent
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -45,14 +49,17 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'block/:id',
|
path: 'block/:id',
|
||||||
|
data: { networkSpecific: true },
|
||||||
component: BisqBlockComponent,
|
component: BisqBlockComponent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'address/:id',
|
path: 'address/:id',
|
||||||
|
data: { networkSpecific: true },
|
||||||
component: BisqAddressComponent,
|
component: BisqAddressComponent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'stats',
|
path: 'stats',
|
||||||
|
data: { networks: ['bisq'] },
|
||||||
component: BisqStatsComponent,
|
component: BisqStatsComponent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -274,6 +274,10 @@
|
|||||||
<img class="image" src="/resources/profile/schildbach.svg" />
|
<img class="image" src="/resources/profile/schildbach.svg" />
|
||||||
<span>Schildbach</span>
|
<span>Schildbach</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://github.com/nunchuk-io" target="_blank" title="Nunchuck">
|
||||||
|
<img class="image" src="/resources/profile/nunchuk.svg" />
|
||||||
|
<span>Nunchuk</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -191,6 +191,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.community-integrations-sponsor {
|
.community-integrations-sponsor {
|
||||||
max-width: 970px;
|
max-width: 965px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ export class AppComponent implements OnInit {
|
|||||||
if (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he')) {
|
if (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he')) {
|
||||||
this.dir = 'rtl';
|
this.dir = 'rtl';
|
||||||
this.class = 'rtl-layout';
|
this.class = 'rtl-layout';
|
||||||
|
} else {
|
||||||
|
this.class = 'ltr-layout';
|
||||||
}
|
}
|
||||||
|
|
||||||
tooltipConfig.animation = false;
|
tooltipConfig.animation = false;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { map } from 'rxjs/operators';
|
|||||||
import { moveDec } from '../../bitcoin.utils';
|
import { moveDec } from '../../bitcoin.utils';
|
||||||
import { AssetsService } from '../../services/assets.service';
|
import { AssetsService } from '../../services/assets.service';
|
||||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||||
import { environment } from 'src/environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-asset-circulation',
|
selector: 'app-asset-circulation',
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { AudioService } from '../../services/audio.service';
|
|||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '../../services/api.service';
|
||||||
import { of, merge, Subscription, combineLatest } from 'rxjs';
|
import { of, merge, Subscription, combineLatest } from 'rxjs';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '../../services/seo.service';
|
||||||
import { environment } from 'src/environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
import { AssetsService } from '../../services/assets.service';
|
import { AssetsService } from '../../services/assets.service';
|
||||||
import { moveDec } from '../../bitcoin.utils';
|
import { moveDec } from '../../bitcoin.utils';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { merge, Observable, of, Subject } from 'rxjs';
|
import { merge, Observable, of, Subject } from 'rxjs';
|
||||||
@@ -9,7 +9,7 @@ import { AssetsService } from '../../../services/assets.service';
|
|||||||
import { SeoService } from '../../../services/seo.service';
|
import { SeoService } from '../../../services/seo.service';
|
||||||
import { StateService } from '../../../services/state.service';
|
import { StateService } from '../../../services/state.service';
|
||||||
import { RelativeUrlPipe } from '../../../shared/pipes/relative-url/relative-url.pipe';
|
import { RelativeUrlPipe } from '../../../shared/pipes/relative-url/relative-url.pipe';
|
||||||
import { environment } from 'src/environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-assets-nav',
|
selector: 'app-assets-nav',
|
||||||
@@ -19,7 +19,7 @@ import { environment } from 'src/environments/environment';
|
|||||||
export class AssetsNavComponent implements OnInit {
|
export class AssetsNavComponent implements OnInit {
|
||||||
@ViewChild('instance', {static: true}) instance: NgbTypeahead;
|
@ViewChild('instance', {static: true}) instance: NgbTypeahead;
|
||||||
nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId;
|
nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId;
|
||||||
searchForm: FormGroup;
|
searchForm: UntypedFormGroup;
|
||||||
assetsCache: AssetExtended[];
|
assetsCache: AssetExtended[];
|
||||||
|
|
||||||
typeaheadSearchFn: ((text: Observable<string>) => Observable<readonly any[]>);
|
typeaheadSearchFn: ((text: Observable<string>) => Observable<readonly any[]>);
|
||||||
@@ -30,7 +30,7 @@ export class AssetsNavComponent implements OnInit {
|
|||||||
itemsPerPage = 15;
|
itemsPerPage = 15;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private assetsService: AssetsService,
|
private assetsService: AssetsService,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
|
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
|
||||||
import { AssetsService } from '../../services/assets.service';
|
import { AssetsService } from '../../services/assets.service';
|
||||||
import { environment } from 'src/environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
import { FormGroup } from '@angular/forms';
|
import { UntypedFormGroup } from '@angular/forms';
|
||||||
import { filter, map, switchMap, take } from 'rxjs/operators';
|
import { filter, map, switchMap, take } from 'rxjs/operators';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { combineLatest, Observable } from 'rxjs';
|
import { combineLatest, Observable } from 'rxjs';
|
||||||
@@ -22,7 +22,7 @@ export class AssetsComponent implements OnInit {
|
|||||||
|
|
||||||
assets: AssetExtended[];
|
assets: AssetExtended[];
|
||||||
assetsCache: AssetExtended[];
|
assetsCache: AssetExtended[];
|
||||||
searchForm: FormGroup;
|
searchForm: UntypedFormGroup;
|
||||||
assets$: Observable<AssetExtended[]>;
|
assets$: Observable<AssetExtended[]>;
|
||||||
|
|
||||||
page = 1;
|
page = 1;
|
||||||
|
|||||||
@@ -44,13 +44,13 @@
|
|||||||
<app-svg-images name="bisq" width="20" height="20" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images>
|
<app-svg-images name="bisq" width="20" height="20" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images>
|
||||||
</button>
|
</button>
|
||||||
<div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
|
<div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
|
||||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage" ngbDropdownItem class="mainnet"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
|
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['mainnet'] || '/')" ngbDropdownItem class="mainnet"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
|
||||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + '/signet'" ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
|
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['signet'] || '/signet')" ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
|
||||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + '/testnet'" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet</a>
|
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['testnet'] || '/testnet')" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet</a>
|
||||||
<h6 class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
|
<h6 class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
|
||||||
<a ngbDropdownItem class="mainnet active" routerLink="/"><app-svg-images name="bisq" width="20" height="20" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Bisq</a>
|
<a ngbDropdownItem class="mainnet active" [routerLink]="networkPaths['bisq'] || '/'"><app-svg-images name="bisq" width="20" height="20" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Bisq</a>
|
||||||
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage" ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
|
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + (networkPaths['liquid'] || '/')" ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
|
||||||
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + '/testnet'" ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a>
|
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + (networkPaths['liquidtestnet'] || '/testnet')" ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Env, StateService } from '../../services/state.service';
|
|||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { LanguageService } from '../../services/language.service';
|
import { LanguageService } from '../../services/language.service';
|
||||||
import { EnterpriseService } from '../../services/enterprise.service';
|
import { EnterpriseService } from '../../services/enterprise.service';
|
||||||
|
import { NavigationService } from '../../services/navigation.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-bisq-master-page',
|
selector: 'app-bisq-master-page',
|
||||||
@@ -15,17 +16,22 @@ export class BisqMasterPageComponent implements OnInit {
|
|||||||
env: Env;
|
env: Env;
|
||||||
isMobile = window.innerWidth <= 767.98;
|
isMobile = window.innerWidth <= 767.98;
|
||||||
urlLanguage: string;
|
urlLanguage: string;
|
||||||
|
networkPaths: { [network: string]: string };
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
private languageService: LanguageService,
|
private languageService: LanguageService,
|
||||||
private enterpriseService: EnterpriseService,
|
private enterpriseService: EnterpriseService,
|
||||||
|
private navigationService: NavigationService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.env = this.stateService.env;
|
this.env = this.stateService.env;
|
||||||
this.connectionState$ = this.stateService.connectionState$;
|
this.connectionState$ = this.stateService.connectionState$;
|
||||||
this.urlLanguage = this.languageService.getLanguageForUrl();
|
this.urlLanguage = this.languageService.getLanguageForUrl();
|
||||||
|
this.navigationService.subnetPaths.subscribe((paths) => {
|
||||||
|
this.networkPaths = paths;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
collapse(): void {
|
collapse(): void {
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
<div class="container-xl" (window:resize)="onResize($event)">
|
|
||||||
|
|
||||||
<div *ngIf="(auditObservable$ | async) as blockAudit; else skeleton">
|
|
||||||
<div class="title-block" id="block">
|
|
||||||
<h1>
|
|
||||||
<span class="next-previous-blocks">
|
|
||||||
<span i18n="shared.block-title">Block </span>
|
|
||||||
|
|
||||||
<a [routerLink]="['/block/' | relativeUrl, blockAudit.id]">{{ blockAudit.height }}</a>
|
|
||||||
|
|
||||||
<span i18n="shared.template-vs-mined">Template vs Mined</span>
|
|
||||||
</span>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div class="grow"></div>
|
|
||||||
|
|
||||||
<button [routerLink]="['/' | relativeUrl]" class="btn btn-sm">✕</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- OVERVIEW -->
|
|
||||||
<div class="box mb-3">
|
|
||||||
<div class="row">
|
|
||||||
<!-- LEFT COLUMN -->
|
|
||||||
<div class="col-sm">
|
|
||||||
<table class="table table-borderless table-striped">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td class="td-width" i18n="block.hash">Hash</td>
|
|
||||||
<td><a [routerLink]="['/block/' | relativeUrl, blockAudit.id]" title="{{ blockAudit.id }}">{{ blockAudit.id | shortenString : 13 }}</a>
|
|
||||||
<app-clipboard class="d-none d-sm-inline-block" [text]="blockAudit.id"></app-clipboard>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td i18n="blockAudit.timestamp">Timestamp</td>
|
|
||||||
<td>
|
|
||||||
‎{{ blockAudit.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
|
||||||
<div class="lg-inline">
|
|
||||||
<i class="symbol">(<app-time-since [time]="blockAudit.timestamp" [fastRender]="true">
|
|
||||||
</app-time-since>)</i>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td i18n="blockAudit.size">Size</td>
|
|
||||||
<td [innerHTML]="'‎' + (blockAudit.size | bytes: 2)"></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td i18n="block.weight">Weight</td>
|
|
||||||
<td [innerHTML]="'‎' + (blockAudit.weight | wuBytes: 2)"></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- RIGHT COLUMN -->
|
|
||||||
<div class="col-sm" *ngIf="blockAudit">
|
|
||||||
<table class="table table-borderless table-striped">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td class="td-width" i18n="shared.transaction-count">Transactions</td>
|
|
||||||
<td>{{ blockAudit.tx_count }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td i18n="block.match-rate">Match rate</td>
|
|
||||||
<td>{{ blockAudit.matchRate }}%</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td i18n="block.missing-txs">Missing txs</td>
|
|
||||||
<td>{{ blockAudit.missingTxs.length }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td i18n="block.added-txs">Added txs</td>
|
|
||||||
<td>{{ blockAudit.addedTxs.length }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div> <!-- row -->
|
|
||||||
</div> <!-- box -->
|
|
||||||
|
|
||||||
<!-- ADDED vs MISSING button -->
|
|
||||||
<div class="d-flex justify-content-center menu mt-3" *ngIf="isMobile">
|
|
||||||
<a routerLinkActive="active" class="btn btn-primary w-50 mr-1 ml-1 menu-button" i18n="block.missing-txs"
|
|
||||||
fragment="missing" (click)="changeMode('missing')">Missing</a>
|
|
||||||
<a routerLinkActive="active" class="btn btn-primary w-50 mr-1 ml-1 menu-button" i18n="block.added-txs"
|
|
||||||
fragment="added" (click)="changeMode('added')">Added</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- VISUALIZATIONS -->
|
|
||||||
<div class="box">
|
|
||||||
<div class="row">
|
|
||||||
<!-- MISSING TX RENDERING -->
|
|
||||||
<div class="col-sm" *ngIf="webGlEnabled">
|
|
||||||
<app-block-overview-graph #blockGraphTemplate [isLoading]="isLoading" [resolution]="75"
|
|
||||||
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false"
|
|
||||||
(txClickEvent)="onTxClick($event)"></app-block-overview-graph>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ADDED TX RENDERING -->
|
|
||||||
<div class="col-sm" *ngIf="webGlEnabled && !isMobile">
|
|
||||||
<app-block-overview-graph #blockGraphMined [isLoading]="isLoading" [resolution]="75"
|
|
||||||
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false"
|
|
||||||
(txClickEvent)="onTxClick($event)"></app-block-overview-graph>
|
|
||||||
</div>
|
|
||||||
</div> <!-- row -->
|
|
||||||
</div> <!-- box -->
|
|
||||||
|
|
||||||
<ng-template #skeleton></ng-template>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
.title-block {
|
|
||||||
border-top: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table {
|
|
||||||
tr td {
|
|
||||||
&:last-child {
|
|
||||||
text-align: right;
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.block-tx-title {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
flex-direction: column;
|
|
||||||
position: relative;
|
|
||||||
@media (min-width: 550px) {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
line-height: 1;
|
|
||||||
margin: 0;
|
|
||||||
position: relative;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
@media (min-width: 550px) {
|
|
||||||
padding-bottom: 0px;
|
|
||||||
align-self: end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-button {
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
max-width: 150px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
|
||||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { map, share, switchMap, tap } from 'rxjs/operators';
|
|
||||||
import { BlockAudit, TransactionStripped } from '../../interfaces/node-api.interface';
|
|
||||||
import { ApiService } from '../../services/api.service';
|
|
||||||
import { StateService } from '../../services/state.service';
|
|
||||||
import { detectWebGL } from '../../shared/graphs.utils';
|
|
||||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
|
||||||
import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overview-graph.component';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-block-audit',
|
|
||||||
templateUrl: './block-audit.component.html',
|
|
||||||
styleUrls: ['./block-audit.component.scss'],
|
|
||||||
styles: [`
|
|
||||||
.loadingGraphs {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: calc(50% - 15px);
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
`],
|
|
||||||
})
|
|
||||||
export class BlockAuditComponent implements OnInit, OnDestroy {
|
|
||||||
blockAudit: BlockAudit = undefined;
|
|
||||||
transactions: string[];
|
|
||||||
auditObservable$: Observable<BlockAudit>;
|
|
||||||
|
|
||||||
paginationMaxSize: number;
|
|
||||||
page = 1;
|
|
||||||
itemsPerPage: number;
|
|
||||||
|
|
||||||
mode: 'missing' | 'added' = 'missing';
|
|
||||||
isLoading = true;
|
|
||||||
webGlEnabled = true;
|
|
||||||
isMobile = window.innerWidth <= 767.98;
|
|
||||||
|
|
||||||
@ViewChild('blockGraphTemplate') blockGraphTemplate: BlockOverviewGraphComponent;
|
|
||||||
@ViewChild('blockGraphMined') blockGraphMined: BlockOverviewGraphComponent;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private route: ActivatedRoute,
|
|
||||||
public stateService: StateService,
|
|
||||||
private router: Router,
|
|
||||||
private apiService: ApiService
|
|
||||||
) {
|
|
||||||
this.webGlEnabled = detectWebGL();
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5;
|
|
||||||
this.itemsPerPage = this.stateService.env.ITEMS_PER_PAGE;
|
|
||||||
|
|
||||||
this.auditObservable$ = this.route.paramMap.pipe(
|
|
||||||
switchMap((params: ParamMap) => {
|
|
||||||
const blockHash: string = params.get('id') || '';
|
|
||||||
return this.apiService.getBlockAudit$(blockHash)
|
|
||||||
.pipe(
|
|
||||||
map((response) => {
|
|
||||||
const blockAudit = response.body;
|
|
||||||
for (let i = 0; i < blockAudit.template.length; ++i) {
|
|
||||||
if (blockAudit.missingTxs.includes(blockAudit.template[i].txid)) {
|
|
||||||
blockAudit.template[i].status = 'missing';
|
|
||||||
} else if (blockAudit.addedTxs.includes(blockAudit.template[i].txid)) {
|
|
||||||
blockAudit.template[i].status = 'added';
|
|
||||||
} else {
|
|
||||||
blockAudit.template[i].status = 'found';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let i = 0; i < blockAudit.transactions.length; ++i) {
|
|
||||||
if (blockAudit.missingTxs.includes(blockAudit.transactions[i].txid)) {
|
|
||||||
blockAudit.transactions[i].status = 'missing';
|
|
||||||
} else if (blockAudit.addedTxs.includes(blockAudit.transactions[i].txid)) {
|
|
||||||
blockAudit.transactions[i].status = 'added';
|
|
||||||
} else {
|
|
||||||
blockAudit.transactions[i].status = 'found';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return blockAudit;
|
|
||||||
}),
|
|
||||||
tap((blockAudit) => {
|
|
||||||
this.changeMode(this.mode);
|
|
||||||
if (this.blockGraphTemplate) {
|
|
||||||
this.blockGraphTemplate.destroy();
|
|
||||||
this.blockGraphTemplate.setup(blockAudit.template);
|
|
||||||
}
|
|
||||||
if (this.blockGraphMined) {
|
|
||||||
this.blockGraphMined.destroy();
|
|
||||||
this.blockGraphMined.setup(blockAudit.transactions);
|
|
||||||
}
|
|
||||||
this.isLoading = false;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
share()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
onResize(event: any) {
|
|
||||||
this.isMobile = event.target.innerWidth <= 767.98;
|
|
||||||
this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
changeMode(mode: 'missing' | 'added') {
|
|
||||||
this.router.navigate([], { fragment: mode });
|
|
||||||
this.mode = mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
onTxClick(event: TransactionStripped): void {
|
|
||||||
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.txid}`);
|
|
||||||
this.router.navigate([url]);
|
|
||||||
}
|
|
||||||
|
|
||||||
pageChange(page: number, target: HTMLElement) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,36 +10,36 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
|
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
|
||||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
<div class="btn-group btn-group-toggle" name="radioBasic">
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 144">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 144" [class.active]="radioGroupForm.get('dateSpan').value === '24h'">
|
||||||
<input ngbButton type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 24h
|
<input type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" formControlName="dateSpan"> 24h
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 432">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 432" [class.active]="radioGroupForm.get('dateSpan').value === '3d'">
|
||||||
<input ngbButton type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 3D
|
<input type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" formControlName="dateSpan"> 3D
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 1008">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 1008" [class.active]="radioGroupForm.get('dateSpan').value === '1w'">
|
||||||
<input ngbButton type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 1W
|
<input type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" formControlName="dateSpan"> 1W
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 4320">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 4320" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
|
||||||
<input ngbButton type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 1M
|
<input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" formControlName="dateSpan"> 1M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 12960">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 12960" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
|
||||||
<input ngbButton type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 3M
|
<input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" formControlName="dateSpan"> 3M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 25920">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 25920" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
|
||||||
<input ngbButton type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 6M
|
<input type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" formControlName="dateSpan"> 6M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 52560">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 52560" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
|
||||||
<input ngbButton type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 1Y
|
<input type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" formControlName="dateSpan"> 1Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 105120">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 105120" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
|
||||||
<input ngbButton type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 2Y
|
<input type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" formControlName="dateSpan"> 2Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 157680" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
|
||||||
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 3Y
|
<input type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" formControlName="dateSpan"> 3Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === 'all'">
|
||||||
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> ALL
|
<input type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" formControlName="dateSpan"> ALL
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
|||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '../../services/api.service';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '../../services/seo.service';
|
||||||
import { formatNumber } from '@angular/common';
|
import { formatNumber } from '@angular/common';
|
||||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
|
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
|
||||||
import { StorageService } from '../../services/storage.service';
|
import { StorageService } from '../../services/storage.service';
|
||||||
import { MiningService } from '../../services/mining.service';
|
import { MiningService } from '../../services/mining.service';
|
||||||
@@ -33,7 +33,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
|
|||||||
@Input() left: number | string = 75;
|
@Input() left: number | string = 75;
|
||||||
|
|
||||||
miningWindowPreference: string;
|
miningWindowPreference: string;
|
||||||
radioGroupForm: FormGroup;
|
radioGroupForm: UntypedFormGroup;
|
||||||
|
|
||||||
chartOptions: EChartsOption = {};
|
chartOptions: EChartsOption = {};
|
||||||
chartInitOptions = {
|
chartInitOptions = {
|
||||||
@@ -50,7 +50,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
|
|||||||
@Inject(LOCALE_ID) public locale: string,
|
@Inject(LOCALE_ID) public locale: string,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
private miningService: MiningService,
|
private miningService: MiningService,
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
|
|||||||
@@ -10,27 +10,27 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
|
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
|
||||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
<div class="btn-group btn-group-toggle" name="radioBasic">
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 4320">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 4320" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
|
||||||
<input ngbButton type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]"> 1M
|
<input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]" formControlName="dateSpan"> 1M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 12960">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 12960" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
|
||||||
<input ngbButton type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]"> 3M
|
<input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]" formControlName="dateSpan"> 3M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 25920">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 25920" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
|
||||||
<input ngbButton type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]"> 6M
|
<input type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]" formControlName="dateSpan"> 6M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 52560">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 52560" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
|
||||||
<input ngbButton type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]"> 1Y
|
<input type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]" formControlName="dateSpan"> 1Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 105120">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 105120" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
|
||||||
<input ngbButton type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]"> 2Y
|
<input type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]" formControlName="dateSpan"> 2Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 157680" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
|
||||||
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]"> 3Y
|
<input type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]" formControlName="dateSpan"> 3Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === 'all'">
|
||||||
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]"> ALL
|
<input type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]" formControlName="dateSpan"> ALL
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
|||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '../../services/api.service';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '../../services/seo.service';
|
||||||
import { formatCurrency, formatNumber, getCurrencySymbol } from '@angular/common';
|
import { formatCurrency, formatNumber, getCurrencySymbol } from '@angular/common';
|
||||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
|
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
|
||||||
import { StorageService } from '../../services/storage.service';
|
import { StorageService } from '../../services/storage.service';
|
||||||
import { MiningService } from '../../services/mining.service';
|
import { MiningService } from '../../services/mining.service';
|
||||||
@@ -31,7 +31,7 @@ export class BlockFeesGraphComponent implements OnInit {
|
|||||||
@Input() left: number | string = 75;
|
@Input() left: number | string = 75;
|
||||||
|
|
||||||
miningWindowPreference: string;
|
miningWindowPreference: string;
|
||||||
radioGroupForm: FormGroup;
|
radioGroupForm: UntypedFormGroup;
|
||||||
|
|
||||||
chartOptions: EChartsOption = {};
|
chartOptions: EChartsOption = {};
|
||||||
chartInitOptions = {
|
chartInitOptions = {
|
||||||
@@ -48,7 +48,7 @@ export class BlockFeesGraphComponent implements OnInit {
|
|||||||
@Inject(LOCALE_ID) public locale: string,
|
@Inject(LOCALE_ID) public locale: string,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
private miningService: MiningService,
|
private miningService: MiningService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<div class="block-overview-graph">
|
<div class="block-overview-graph">
|
||||||
<canvas class="block-overview-canvas" [class.clickable]="!!hoverTx" #blockCanvas></canvas>
|
<canvas class="block-overview-canvas" [class.clickable]="!!hoverTx" #blockCanvas></canvas>
|
||||||
<div class="loader-wrapper" [class.hidden]="!isLoading || disableSpinner">
|
<div class="loader-wrapper" [class.hidden]="(!isLoading || disableSpinner) && !unavailable">
|
||||||
<div class="spinner-border ml-3 loading" role="status"></div>
|
<div *ngIf="isLoading" class="spinner-border ml-3 loading" role="status"></div>
|
||||||
|
<div *ngIf="!isLoading && unavailable" class="ml-3" i18n="block.not-available">not available</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<app-block-overview-tooltip
|
<app-block-overview-tooltip
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, NgZone, AfterViewInit, OnDestroy } from '@angular/core';
|
import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, NgZone, AfterViewInit, OnDestroy, OnChanges } from '@angular/core';
|
||||||
import { TransactionStripped } from '../../interfaces/websocket.interface';
|
import { TransactionStripped } from '../../interfaces/websocket.interface';
|
||||||
import { FastVertexArray } from './fast-vertex-array';
|
import { FastVertexArray } from './fast-vertex-array';
|
||||||
import BlockScene from './block-scene';
|
import BlockScene from './block-scene';
|
||||||
@@ -11,14 +11,17 @@ import { Position } from './sprite-types';
|
|||||||
templateUrl: './block-overview-graph.component.html',
|
templateUrl: './block-overview-graph.component.html',
|
||||||
styleUrls: ['./block-overview-graph.component.scss'],
|
styleUrls: ['./block-overview-graph.component.scss'],
|
||||||
})
|
})
|
||||||
export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy {
|
export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, OnChanges {
|
||||||
@Input() isLoading: boolean;
|
@Input() isLoading: boolean;
|
||||||
@Input() resolution: number;
|
@Input() resolution: number;
|
||||||
@Input() blockLimit: number;
|
@Input() blockLimit: number;
|
||||||
@Input() orientation = 'left';
|
@Input() orientation = 'left';
|
||||||
@Input() flip = true;
|
@Input() flip = true;
|
||||||
@Input() disableSpinner = false;
|
@Input() disableSpinner = false;
|
||||||
|
@Input() mirrorTxid: string | void;
|
||||||
|
@Input() unavailable: boolean = false;
|
||||||
@Output() txClickEvent = new EventEmitter<TransactionStripped>();
|
@Output() txClickEvent = new EventEmitter<TransactionStripped>();
|
||||||
|
@Output() txHoverEvent = new EventEmitter<string>();
|
||||||
@Output() readyEvent = new EventEmitter();
|
@Output() readyEvent = new EventEmitter();
|
||||||
|
|
||||||
@ViewChild('blockCanvas')
|
@ViewChild('blockCanvas')
|
||||||
@@ -37,6 +40,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy {
|
|||||||
scene: BlockScene;
|
scene: BlockScene;
|
||||||
hoverTx: TxView | void;
|
hoverTx: TxView | void;
|
||||||
selectedTx: TxView | void;
|
selectedTx: TxView | void;
|
||||||
|
mirrorTx: TxView | void;
|
||||||
tooltipPosition: Position;
|
tooltipPosition: Position;
|
||||||
|
|
||||||
readyNextFrame = false;
|
readyNextFrame = false;
|
||||||
@@ -57,6 +61,17 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy {
|
|||||||
this.resizeCanvas();
|
this.resizeCanvas();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnChanges(changes): void {
|
||||||
|
if (changes.orientation || changes.flip) {
|
||||||
|
if (this.scene) {
|
||||||
|
this.scene.setOrientation(this.orientation, this.flip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changes.mirrorTxid) {
|
||||||
|
this.setMirror(this.mirrorTxid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
if (this.animationFrameRequest) {
|
if (this.animationFrameRequest) {
|
||||||
cancelAnimationFrame(this.animationFrameRequest);
|
cancelAnimationFrame(this.animationFrameRequest);
|
||||||
@@ -68,6 +83,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy {
|
|||||||
this.exit(direction);
|
this.exit(direction);
|
||||||
this.hoverTx = null;
|
this.hoverTx = null;
|
||||||
this.selectedTx = null;
|
this.selectedTx = null;
|
||||||
|
this.onTxHover(null);
|
||||||
this.start();
|
this.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +189,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy {
|
|||||||
this.gl.viewport(0, 0, this.displayWidth, this.displayHeight);
|
this.gl.viewport(0, 0, this.displayWidth, this.displayHeight);
|
||||||
}
|
}
|
||||||
if (this.scene) {
|
if (this.scene) {
|
||||||
this.scene.resize({ width: this.displayWidth, height: this.displayHeight });
|
this.scene.resize({ width: this.displayWidth, height: this.displayHeight, animate: false });
|
||||||
this.start();
|
this.start();
|
||||||
} else {
|
} else {
|
||||||
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
|
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
|
||||||
@@ -293,6 +309,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
this.hoverTx = null;
|
this.hoverTx = null;
|
||||||
this.selectedTx = null;
|
this.selectedTx = null;
|
||||||
|
this.onTxHover(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,17 +361,20 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy {
|
|||||||
this.selectedTx = selected;
|
this.selectedTx = selected;
|
||||||
} else {
|
} else {
|
||||||
this.hoverTx = selected;
|
this.hoverTx = selected;
|
||||||
|
this.onTxHover(this.hoverTx ? this.hoverTx.txid : null);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (clicked) {
|
if (clicked) {
|
||||||
this.selectedTx = null;
|
this.selectedTx = null;
|
||||||
}
|
}
|
||||||
this.hoverTx = null;
|
this.hoverTx = null;
|
||||||
|
this.onTxHover(null);
|
||||||
}
|
}
|
||||||
} else if (clicked) {
|
} else if (clicked) {
|
||||||
if (selected === this.selectedTx) {
|
if (selected === this.selectedTx) {
|
||||||
this.hoverTx = this.selectedTx;
|
this.hoverTx = this.selectedTx;
|
||||||
this.selectedTx = null;
|
this.selectedTx = null;
|
||||||
|
this.onTxHover(this.hoverTx ? this.hoverTx.txid : null);
|
||||||
} else {
|
} else {
|
||||||
this.selectedTx = selected;
|
this.selectedTx = selected;
|
||||||
}
|
}
|
||||||
@@ -362,6 +382,18 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setMirror(txid: string | void) {
|
||||||
|
if (this.mirrorTx) {
|
||||||
|
this.scene.setHover(this.mirrorTx, false);
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
if (txid && this.scene.txs[txid]) {
|
||||||
|
this.mirrorTx = this.scene.txs[txid];
|
||||||
|
this.scene.setHover(this.mirrorTx, true);
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onTxClick(cssX: number, cssY: number) {
|
onTxClick(cssX: number, cssY: number) {
|
||||||
const x = cssX * window.devicePixelRatio;
|
const x = cssX * window.devicePixelRatio;
|
||||||
const y = cssY * window.devicePixelRatio;
|
const y = cssY * window.devicePixelRatio;
|
||||||
@@ -370,6 +402,10 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy {
|
|||||||
this.txClickEvent.emit(selected);
|
this.txClickEvent.emit(selected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onTxHover(hoverId: string) {
|
||||||
|
this.txHoverEvent.emit(hoverId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebGL shader attributes
|
// WebGL shader attributes
|
||||||
|
|||||||
@@ -29,13 +29,22 @@ export default class BlockScene {
|
|||||||
this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray });
|
this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray });
|
||||||
}
|
}
|
||||||
|
|
||||||
resize({ width = this.width, height = this.height }: { width?: number, height?: number}): void {
|
resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void {
|
||||||
this.width = width;
|
this.width = width;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
this.gridSize = this.width / this.gridWidth;
|
this.gridSize = this.width / this.gridWidth;
|
||||||
this.unitPadding = width / 500;
|
this.unitPadding = width / 500;
|
||||||
this.unitWidth = this.gridSize - (this.unitPadding * 2);
|
this.unitWidth = this.gridSize - (this.unitPadding * 2);
|
||||||
|
|
||||||
|
this.dirty = true;
|
||||||
|
if (this.initialised && this.scene) {
|
||||||
|
this.updateAll(performance.now(), 50, 'left', animate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setOrientation(orientation: string, flip: boolean): void {
|
||||||
|
this.orientation = orientation;
|
||||||
|
this.flip = flip;
|
||||||
this.dirty = true;
|
this.dirty = true;
|
||||||
if (this.initialised && this.scene) {
|
if (this.initialised && this.scene) {
|
||||||
this.updateAll(performance.now(), 50);
|
this.updateAll(performance.now(), 50);
|
||||||
@@ -203,7 +212,7 @@ export default class BlockScene {
|
|||||||
this.vbytesPerUnit = blockLimit / Math.pow(resolution / 1.02, 2);
|
this.vbytesPerUnit = blockLimit / Math.pow(resolution / 1.02, 2);
|
||||||
this.gridWidth = resolution;
|
this.gridWidth = resolution;
|
||||||
this.gridHeight = resolution;
|
this.gridHeight = resolution;
|
||||||
this.resize({ width, height });
|
this.resize({ width, height, animate: true });
|
||||||
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
|
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
|
||||||
|
|
||||||
this.txs = {};
|
this.txs = {};
|
||||||
@@ -216,14 +225,14 @@ export default class BlockScene {
|
|||||||
this.animateUntil = Math.max(this.animateUntil, tx.update(update));
|
this.animateUntil = Math.max(this.animateUntil, tx.update(update));
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateTx(tx: TxView, startTime: number, delay: number, direction: string = 'left'): void {
|
private updateTx(tx: TxView, startTime: number, delay: number, direction: string = 'left', animate: boolean = true): void {
|
||||||
if (tx.dirty || this.dirty) {
|
if (tx.dirty || this.dirty) {
|
||||||
this.saveGridToScreenPosition(tx);
|
this.saveGridToScreenPosition(tx);
|
||||||
this.setTxOnScreen(tx, startTime, delay, direction);
|
this.setTxOnScreen(tx, startTime, delay, direction, animate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private setTxOnScreen(tx: TxView, startTime: number, delay: number = 50, direction: string = 'left'): void {
|
private setTxOnScreen(tx: TxView, startTime: number, delay: number = 50, direction: string = 'left', animate: boolean = true): void {
|
||||||
if (!tx.initialised) {
|
if (!tx.initialised) {
|
||||||
const txColor = tx.getColor();
|
const txColor = tx.getColor();
|
||||||
this.applyTxUpdate(tx, {
|
this.applyTxUpdate(tx, {
|
||||||
@@ -243,30 +252,42 @@ export default class BlockScene {
|
|||||||
position: tx.screenPosition,
|
position: tx.screenPosition,
|
||||||
color: txColor
|
color: txColor
|
||||||
},
|
},
|
||||||
duration: 1000,
|
duration: animate ? 1000 : 1,
|
||||||
start: startTime,
|
start: startTime,
|
||||||
delay,
|
delay: animate ? delay : 0,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.applyTxUpdate(tx, {
|
this.applyTxUpdate(tx, {
|
||||||
display: {
|
display: {
|
||||||
position: tx.screenPosition
|
position: tx.screenPosition
|
||||||
},
|
},
|
||||||
duration: 1000,
|
duration: animate ? 1000 : 0,
|
||||||
minDuration: 500,
|
minDuration: animate ? 500 : 0,
|
||||||
start: startTime,
|
start: startTime,
|
||||||
delay,
|
delay: animate ? delay : 0,
|
||||||
adjust: true
|
adjust: animate
|
||||||
});
|
});
|
||||||
|
if (!animate) {
|
||||||
|
this.applyTxUpdate(tx, {
|
||||||
|
display: {
|
||||||
|
position: tx.screenPosition
|
||||||
|
},
|
||||||
|
duration: 0,
|
||||||
|
minDuration: 0,
|
||||||
|
start: startTime,
|
||||||
|
delay: 0,
|
||||||
|
adjust: false
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateAll(startTime: number, delay: number = 50, direction: string = 'left'): void {
|
private updateAll(startTime: number, delay: number = 50, direction: string = 'left', animate: boolean = true): void {
|
||||||
this.scene.count = 0;
|
this.scene.count = 0;
|
||||||
const ids = this.getTxList();
|
const ids = this.getTxList();
|
||||||
startTime = startTime || performance.now();
|
startTime = startTime || performance.now();
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
this.updateTx(this.txs[id], startTime, delay, direction);
|
this.updateTx(this.txs[id], startTime, delay, direction, animate);
|
||||||
}
|
}
|
||||||
this.dirty = false;
|
this.dirty = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,20 @@ import { FastVertexArray } from './fast-vertex-array';
|
|||||||
import { TransactionStripped } from '../../interfaces/websocket.interface';
|
import { TransactionStripped } from '../../interfaces/websocket.interface';
|
||||||
import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from './sprite-types';
|
import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from './sprite-types';
|
||||||
import { feeLevels, mempoolFeeColors } from '../../app.constants';
|
import { feeLevels, mempoolFeeColors } from '../../app.constants';
|
||||||
|
import BlockScene from './block-scene';
|
||||||
|
|
||||||
const hoverTransitionTime = 300;
|
const hoverTransitionTime = 300;
|
||||||
const defaultHoverColor = hexToColor('1bd8f4');
|
const defaultHoverColor = hexToColor('1bd8f4');
|
||||||
|
|
||||||
|
const feeColors = mempoolFeeColors.map(hexToColor);
|
||||||
|
const auditFeeColors = feeColors.map((color) => darken(desaturate(color, 0.3), 0.9));
|
||||||
|
const auditColors = {
|
||||||
|
censored: hexToColor('f344df'),
|
||||||
|
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
|
||||||
|
added: hexToColor('0099ff'),
|
||||||
|
selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
|
||||||
|
}
|
||||||
|
|
||||||
// convert from this class's update format to TxSprite's update format
|
// convert from this class's update format to TxSprite's update format
|
||||||
function toSpriteUpdate(params: ViewUpdateParams): SpriteUpdateParams {
|
function toSpriteUpdate(params: ViewUpdateParams): SpriteUpdateParams {
|
||||||
return {
|
return {
|
||||||
@@ -25,7 +35,8 @@ export default class TxView implements TransactionStripped {
|
|||||||
vsize: number;
|
vsize: number;
|
||||||
value: number;
|
value: number;
|
||||||
feerate: number;
|
feerate: number;
|
||||||
status?: 'found' | 'missing' | 'added';
|
status?: 'found' | 'missing' | 'fresh' | 'added' | 'censored' | 'selected';
|
||||||
|
context?: 'projected' | 'actual';
|
||||||
|
|
||||||
initialised: boolean;
|
initialised: boolean;
|
||||||
vertexArray: FastVertexArray;
|
vertexArray: FastVertexArray;
|
||||||
@@ -39,6 +50,7 @@ export default class TxView implements TransactionStripped {
|
|||||||
dirty: boolean;
|
dirty: boolean;
|
||||||
|
|
||||||
constructor(tx: TransactionStripped, vertexArray: FastVertexArray) {
|
constructor(tx: TransactionStripped, vertexArray: FastVertexArray) {
|
||||||
|
this.context = tx.context;
|
||||||
this.txid = tx.txid;
|
this.txid = tx.txid;
|
||||||
this.fee = tx.fee;
|
this.fee = tx.fee;
|
||||||
this.vsize = tx.vsize;
|
this.vsize = tx.vsize;
|
||||||
@@ -142,16 +154,29 @@ export default class TxView implements TransactionStripped {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getColor(): Color {
|
getColor(): Color {
|
||||||
// Block audit
|
|
||||||
if (this.status === 'missing') {
|
|
||||||
return hexToColor('039BE5');
|
|
||||||
} else if (this.status === 'added') {
|
|
||||||
return hexToColor('D81B60');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Block component
|
|
||||||
const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, this.feerate) < feeLvl) - 1;
|
const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, this.feerate) < feeLvl) - 1;
|
||||||
return hexToColor(mempoolFeeColors[feeLevelIndex] || mempoolFeeColors[mempoolFeeColors.length - 1]);
|
const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1];
|
||||||
|
// Block audit
|
||||||
|
switch(this.status) {
|
||||||
|
case 'censored':
|
||||||
|
return auditColors.censored;
|
||||||
|
case 'missing':
|
||||||
|
return auditColors.missing;
|
||||||
|
case 'fresh':
|
||||||
|
return auditColors.missing;
|
||||||
|
case 'added':
|
||||||
|
return auditColors.added;
|
||||||
|
case 'selected':
|
||||||
|
return auditColors.selected;
|
||||||
|
case 'found':
|
||||||
|
if (this.context === 'projected') {
|
||||||
|
return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];
|
||||||
|
} else {
|
||||||
|
return feeLevelColor;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return feeLevelColor;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,3 +188,22 @@ function hexToColor(hex: string): Color {
|
|||||||
a: 1
|
a: 1
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function desaturate(color: Color, amount: number): Color {
|
||||||
|
const gray = (color.r + color.g + color.b) / 6;
|
||||||
|
return {
|
||||||
|
r: color.r + ((gray - color.r) * amount),
|
||||||
|
g: color.g + ((gray - color.g) * amount),
|
||||||
|
b: color.b + ((gray - color.b) * amount),
|
||||||
|
a: color.a,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function darken(color: Color, amount: number): Color {
|
||||||
|
return {
|
||||||
|
r: color.r * amount,
|
||||||
|
g: color.g * amount,
|
||||||
|
b: color.b * amount,
|
||||||
|
a: color.a,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,6 +32,17 @@
|
|||||||
<td class="td-width" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
|
<td class="td-width" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
|
||||||
<td [innerHTML]="'‎' + (vsize | vbytes: 2)"></td>
|
<td [innerHTML]="'‎' + (vsize | vbytes: 2)"></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr *ngIf="tx && tx.status && tx.status.length">
|
||||||
|
<td class="td-width" i18n="transaction.audit-status">Audit status</td>
|
||||||
|
<ng-container [ngSwitch]="tx?.status">
|
||||||
|
<td *ngSwitchCase="'found'" i18n="transaction.audit.match">match</td>
|
||||||
|
<td *ngSwitchCase="'censored'" i18n="transaction.audit.removed">removed</td>
|
||||||
|
<td *ngSwitchCase="'missing'" i18n="transaction.audit.marginal">marginal fee rate</td>
|
||||||
|
<td *ngSwitchCase="'fresh'" i18n="transaction.audit.recently-broadcast">recently broadcast</td>
|
||||||
|
<td *ngSwitchCase="'added'" i18n="transaction.audit.added">added</td>
|
||||||
|
<td *ngSwitchCase="'selected'" i18n="transaction.audit.marginal">marginal fee rate</td>
|
||||||
|
</ng-container>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,36 +10,36 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
|
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
|
||||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
<div class="btn-group btn-group-toggle" name="radioBasic">
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 144">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 144" [class.active]="radioGroupForm.get('dateSpan').value === '24h'">
|
||||||
<input ngbButton type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]"> 24h
|
<input type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 24h
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 432">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 432" [class.active]="radioGroupForm.get('dateSpan').value === '3d'">
|
||||||
<input ngbButton type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]"> 3D
|
<input type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 3D
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 1008">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 1008" [class.active]="radioGroupForm.get('dateSpan').value === '1w'">
|
||||||
<input ngbButton type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]"> 1W
|
<input type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 1W
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 4320">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 4320" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
|
||||||
<input ngbButton type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]"> 1M
|
<input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 1M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 12960">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 12960" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
|
||||||
<input ngbButton type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]"> 3M
|
<input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 3M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 25920">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 25920" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
|
||||||
<input ngbButton type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]"> 6M
|
<input type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 6M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 52560">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 52560" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
|
||||||
<input ngbButton type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]"> 1Y
|
<input type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 1Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 105120">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 105120" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
|
||||||
<input ngbButton type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]"> 2Y
|
<input type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 2Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 157680" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
|
||||||
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]"> 3Y
|
<input type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 3Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount > 157680">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount > 157680" [class.active]="radioGroupForm.get('dateSpan').value === 'all'">
|
||||||
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]"> ALL
|
<input type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> ALL
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
|||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '../../services/api.service';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '../../services/seo.service';
|
||||||
import { formatNumber } from '@angular/common';
|
import { formatNumber } from '@angular/common';
|
||||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
|
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
|
||||||
import { StorageService } from '../../services/storage.service';
|
import { StorageService } from '../../services/storage.service';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
@@ -31,7 +31,7 @@ export class BlockPredictionGraphComponent implements OnInit {
|
|||||||
@Input() left: number | string = 75;
|
@Input() left: number | string = 75;
|
||||||
|
|
||||||
miningWindowPreference: string;
|
miningWindowPreference: string;
|
||||||
radioGroupForm: FormGroup;
|
radioGroupForm: UntypedFormGroup;
|
||||||
|
|
||||||
chartOptions: EChartsOption = {};
|
chartOptions: EChartsOption = {};
|
||||||
chartInitOptions = {
|
chartInitOptions = {
|
||||||
@@ -48,7 +48,7 @@ export class BlockPredictionGraphComponent implements OnInit {
|
|||||||
@Inject(LOCALE_ID) public locale: string,
|
@Inject(LOCALE_ID) public locale: string,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
|
|||||||
@@ -11,27 +11,27 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
|
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
|
||||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
<div class="btn-group btn-group-toggle" name="radioBasic">
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 4320">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 4320" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
|
||||||
<input ngbButton type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]"> 1M
|
<input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]" formControlName="dateSpan"> 1M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 12960">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 12960" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
|
||||||
<input ngbButton type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]"> 3M
|
<input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]" formControlName="dateSpan"> 3M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 25920">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 25920" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
|
||||||
<input ngbButton type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]"> 6M
|
<input type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]" formControlName="dateSpan"> 6M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 52560">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 52560" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
|
||||||
<input ngbButton type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]"> 1Y
|
<input type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]" formControlName="dateSpan"> 1Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 105120">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 105120" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
|
||||||
<input ngbButton type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]"> 2Y
|
<input type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]" formControlName="dateSpan"> 2Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 157680" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
|
||||||
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]"> 3Y
|
<input type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]" formControlName="dateSpan"> 3Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === 'all'">
|
||||||
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]"> ALL
|
<input type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]"> ALL
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
|||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '../../services/api.service';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '../../services/seo.service';
|
||||||
import { formatCurrency, formatNumber, getCurrencySymbol } from '@angular/common';
|
import { formatCurrency, formatNumber, getCurrencySymbol } from '@angular/common';
|
||||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
|
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
|
||||||
import { MiningService } from '../../services/mining.service';
|
import { MiningService } from '../../services/mining.service';
|
||||||
import { StorageService } from '../../services/storage.service';
|
import { StorageService } from '../../services/storage.service';
|
||||||
@@ -31,7 +31,7 @@ export class BlockRewardsGraphComponent implements OnInit {
|
|||||||
@Input() left: number | string = 75;
|
@Input() left: number | string = 75;
|
||||||
|
|
||||||
miningWindowPreference: string;
|
miningWindowPreference: string;
|
||||||
radioGroupForm: FormGroup;
|
radioGroupForm: UntypedFormGroup;
|
||||||
|
|
||||||
chartOptions: EChartsOption = {};
|
chartOptions: EChartsOption = {};
|
||||||
chartInitOptions = {
|
chartInitOptions = {
|
||||||
@@ -48,7 +48,7 @@ export class BlockRewardsGraphComponent implements OnInit {
|
|||||||
@Inject(LOCALE_ID) public locale: string,
|
@Inject(LOCALE_ID) public locale: string,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
private miningService: MiningService,
|
private miningService: MiningService,
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
|
|||||||
@@ -9,36 +9,36 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(blockSizesWeightsObservable$ | async) as stats">
|
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(blockSizesWeightsObservable$ | async) as stats">
|
||||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
<div class="btn-group btn-group-toggle" name="radioBasic">
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 144">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 144" [class.active]="radioGroupForm.get('dateSpan').value === '24h'">
|
||||||
<input ngbButton type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> 24h
|
<input type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" formControlName="dateSpan"> 24h
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 432">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 432" [class.active]="radioGroupForm.get('dateSpan').value === '3d'">
|
||||||
<input ngbButton type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> 3D
|
<input type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" formControlName="dateSpan"> 3D
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 1008">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 1008" [class.active]="radioGroupForm.get('dateSpan').value === '1w'">
|
||||||
<input ngbButton type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> 1W
|
<input type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" formControlName="dateSpan"> 1W
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 4320">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 4320" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
|
||||||
<input ngbButton type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> 1M
|
<input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" formControlName="dateSpan"> 1M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 12960">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 12960" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
|
||||||
<input ngbButton type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> 3M
|
<input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" formControlName="dateSpan"> 3M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 25920">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 25920" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
|
||||||
<input ngbButton type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> 6M
|
<input type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" formControlName="dateSpan"> 6M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 52560">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 52560" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
|
||||||
<input ngbButton type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> 1Y
|
<input type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" formControlName="dateSpan"> 1Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 105120">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 105120" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
|
||||||
<input ngbButton type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> 2Y
|
<input type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" formControlName="dateSpan"> 2Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 157680" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
|
||||||
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> 3Y
|
<input type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" formControlName="dateSpan"> 3Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === 'all'">
|
||||||
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> ALL
|
<input type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" formControlName="dateSpan"> ALL
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
|||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '../../services/api.service';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '../../services/seo.service';
|
||||||
import { formatNumber } from '@angular/common';
|
import { formatNumber } from '@angular/common';
|
||||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { StorageService } from '../../services/storage.service';
|
import { StorageService } from '../../services/storage.service';
|
||||||
import { MiningService } from '../../services/mining.service';
|
import { MiningService } from '../../services/mining.service';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
@@ -30,7 +30,7 @@ export class BlockSizesWeightsGraphComponent implements OnInit {
|
|||||||
@Input() left: number | string = 75;
|
@Input() left: number | string = 75;
|
||||||
|
|
||||||
miningWindowPreference: string;
|
miningWindowPreference: string;
|
||||||
radioGroupForm: FormGroup;
|
radioGroupForm: UntypedFormGroup;
|
||||||
|
|
||||||
chartOptions: EChartsOption = {};
|
chartOptions: EChartsOption = {};
|
||||||
chartInitOptions = {
|
chartInitOptions = {
|
||||||
@@ -49,7 +49,7 @@ export class BlockSizesWeightsGraphComponent implements OnInit {
|
|||||||
@Inject(LOCALE_ID) public locale: string,
|
@Inject(LOCALE_ID) public locale: string,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
private miningService: MiningService,
|
private miningService: MiningService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
|
|||||||
@@ -1,36 +1,24 @@
|
|||||||
<div class="container-xl" (window:resize)="onResize($event)">
|
<div class="container-xl" (window:resize)="onResize($event)">
|
||||||
|
|
||||||
<div class="title-block" id="block">
|
<div class="title-block" [class.time-ltr]="timeLtr" id="block">
|
||||||
<h1>
|
<h1>
|
||||||
<ng-template [ngIf]="blockHeight === 0"><ng-container i18n="@@2303359202781425764">Genesis</ng-container>
|
<ng-container *ngIf="blockHeight == null || blockHeight > 0; else genesis" i18n="shared.block-title">Block</ng-container>
|
||||||
<span class="next-previous-blocks">
|
<ng-template #genesis i18n="@@2303359202781425764">Genesis</ng-template>
|
||||||
<a *ngIf="showNextBlocklink" [routerLink]="['/block/' | relativeUrl, nextBlockHeight]" (click)="navigateToNextBlock()" i18n-ngbTooltip="Next Block" ngbTooltip="Next Block" placement="bottom">
|
<span class="next-previous-blocks">
|
||||||
<fa-icon [icon]="['fas', 'angle-left']" [fixedWidth]="true"></fa-icon>
|
<a *ngIf="showNextBlocklink" class="nav-arrow next" [routerLink]="['/block/' | relativeUrl, nextBlockHeight]" (click)="navigateToNextBlock()" i18n-ngbTooltip="Next Block" ngbTooltip="Next Block" placement="bottom">
|
||||||
</a>
|
<fa-icon [icon]="['fas', 'angle-left']" [fixedWidth]="true"></fa-icon>
|
||||||
<a [routerLink]="['/block/' | relativeUrl, blockHash]">{{ blockHeight }}</a>
|
</a>
|
||||||
<span placement="bottom" class="disable">
|
<span *ngIf="!showNextBlocklink" placement="bottom" class="disable nav-arrow next">
|
||||||
<fa-icon [icon]="['fas', 'angle-right']" [fixedWidth]="true"></fa-icon>
|
<fa-icon [icon]="['fas', 'angle-left']" [fixedWidth]="true"></fa-icon>
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</ng-template>
|
<a [routerLink]="['/block/' | relativeUrl, blockHash]" class="block-link">{{ blockHeight }}</a>
|
||||||
<ng-template [ngIf]="blockHeight" i18n="shared.block-title">Block <ng-container *ngTemplateOutlet="blockTemplateContent"></ng-container></ng-template>
|
<a *ngIf="showPreviousBlocklink && block" class="nav-arrow prev" [routerLink]="['/block/' | relativeUrl, block.previousblockhash]" (click)="navigateToPreviousBlock()" i18n-ngbTooltip="Previous Block" ngbTooltip="Previous Block" placement="bottom">
|
||||||
<ng-template #blockTemplateContent>
|
<fa-icon [icon]="['fas', 'angle-right']" [fixedWidth]="true"></fa-icon>
|
||||||
<span class="next-previous-blocks">
|
</a>
|
||||||
<a *ngIf="showNextBlocklink" [routerLink]="['/block/' | relativeUrl, nextBlockHeight]" (click)="navigateToNextBlock()" i18n-ngbTooltip="Next Block" ngbTooltip="Next Block" placement="bottom">
|
<span *ngIf="!showPreviousBlocklink || !block" placement="bottom" class="disable nav-arrow prev">
|
||||||
<fa-icon [icon]="['fas', 'angle-left']" [fixedWidth]="true"></fa-icon>
|
<fa-icon [icon]="['fas', 'angle-right']" [fixedWidth]="true"></fa-icon>
|
||||||
</a>
|
|
||||||
<span *ngIf="!showNextBlocklink" placement="bottom" class="disable">
|
|
||||||
<fa-icon [icon]="['fas', 'angle-left']" [fixedWidth]="true"></fa-icon>
|
|
||||||
</span>
|
|
||||||
<a [routerLink]="['/block/' | relativeUrl, blockHash]">{{ blockHeight }}</a>
|
|
||||||
<a *ngIf="showPreviousBlocklink && block" [routerLink]="['/block/' | relativeUrl, block.previousblockhash]" (click)="navigateToPreviousBlock()" i18n-ngbTooltip="Previous Block" ngbTooltip="Previous Block" placement="bottom">
|
|
||||||
<fa-icon [icon]="['fas', 'angle-right']" [fixedWidth]="true"></fa-icon>
|
|
||||||
</a>
|
|
||||||
<span *ngIf="!showPreviousBlocklink" placement="bottom" class="disable">
|
|
||||||
<fa-icon [icon]="['fas', 'angle-right']" [fixedWidth]="true"></fa-icon>
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</ng-template>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div class="grow"></div>
|
<div class="grow"></div>
|
||||||
@@ -66,7 +54,19 @@
|
|||||||
<td i18n="block.weight">Weight</td>
|
<td i18n="block.weight">Weight</td>
|
||||||
<td [innerHTML]="'‎' + (block.weight | wuBytes: 2)"></td>
|
<td [innerHTML]="'‎' + (block.weight | wuBytes: 2)"></td>
|
||||||
</tr>
|
</tr>
|
||||||
<ng-template [ngIf]="webGlEnabled">
|
<tr *ngIf="auditEnabled">
|
||||||
|
<td i18n="block.health">Block health</td>
|
||||||
|
<td>
|
||||||
|
<span *ngIf="blockAudit?.matchRate != null">{{ blockAudit.matchRate }}%</span>
|
||||||
|
<span *ngIf="blockAudit?.matchRate === null" i18n="unknown">Unknown</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<ng-container *ngIf="webGlEnabled && (auditDataMissing || !indexingAvailable)">
|
||||||
|
<tr *ngIf="isMobile && auditEnabled"></tr>
|
||||||
|
<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>
|
||||||
|
</tr>
|
||||||
<tr *ngIf="block?.extras?.medianFee != undefined">
|
<tr *ngIf="block?.extras?.medianFee != undefined">
|
||||||
<td class="td-width" i18n="block.median-fee">Median fee</td>
|
<td class="td-width" i18n="block.median-fee">Median fee</td>
|
||||||
<td>~{{ block?.extras?.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="block?.extras?.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td>
|
<td>~{{ block?.extras?.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="block?.extras?.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td>
|
||||||
@@ -110,19 +110,19 @@
|
|||||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||||
<td i18n="block.miner">Miner</td>
|
<td i18n="block.miner">Miner</td>
|
||||||
<td *ngIf="stateService.env.MINING_DASHBOARD">
|
<td *ngIf="stateService.env.MINING_DASHBOARD">
|
||||||
<a [attr.data-cy]="'block-details-miner-badge'" placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge"
|
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge"
|
||||||
[class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
|
[class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
|
||||||
{{ block.extras.pool.name }}
|
{{ block.extras.pool.name }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">
|
<td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">
|
||||||
<span [attr.data-cy]="'block-details-miner-badge'" placement="bottom" class="badge"
|
<span placement="bottom" class="badge"
|
||||||
[class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
|
[class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
|
||||||
{{ block.extras.pool.name }}
|
{{ block.extras.pool.name }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-container>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -143,7 +143,11 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
</tr>
|
</tr>
|
||||||
<ng-template [ngIf]="webGlEnabled">
|
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||||
|
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
|
</tr>
|
||||||
|
<ng-container *ngIf="webGlEnabled && (!indexingAvailable || auditDataMissing)">
|
||||||
|
<tr *ngIf="isMobile && !auditEnabled"></tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="td-width" colspan="2"><span class="skeleton-loader"></span></td>
|
<td class="td-width" colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -153,17 +157,25 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||||
|
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
|
</tr>
|
||||||
|
</ng-container>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<div class="col-sm" *ngIf="!webGlEnabled">
|
<div class="col-sm">
|
||||||
<table class="table table-borderless table-striped" *ngIf="!isLoadingBlock">
|
<table class="table table-borderless table-striped" *ngIf="!isLoadingBlock && (!auditDataMissing || indexingAvailable && !webGlEnabled)">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<tr *ngIf="isMobile && auditEnabled"></tr>
|
||||||
|
<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>
|
||||||
|
</tr>
|
||||||
<tr *ngIf="block?.extras?.medianFee != undefined">
|
<tr *ngIf="block?.extras?.medianFee != undefined">
|
||||||
<td class="td-width" i18n="block.median-fee">Median fee</td>
|
<td class="td-width" i18n="block.median-fee">Median fee</td>
|
||||||
<td>~{{ block?.extras?.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="block?.extras?.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td>
|
<td>~{{ block?.extras?.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="block?.extras?.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td>
|
||||||
@@ -221,8 +233,9 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<table class="table table-borderless table-striped" *ngIf="isLoadingBlock">
|
<table class="table table-borderless table-striped" *ngIf="isLoadingBlock && !auditDataMissing && (indexingAvailable || !webGlEnabled)">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<tr *ngIf="isMobile && !auditEnabled"></tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="td-width" colspan="2"><span class="skeleton-loader"></span></td>
|
<td class="td-width" colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -235,22 +248,54 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||||
|
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
<div class="col-sm chart-container" *ngIf="webGlEnabled && (!indexingAvailable || auditDataMissing)">
|
||||||
<div class="col-sm chart-container" *ngIf="webGlEnabled">
|
<app-block-overview-graph
|
||||||
<app-block-overview-graph
|
#blockGraphActual
|
||||||
#blockGraph
|
[isLoading]="isLoadingOverview"
|
||||||
[isLoading]="isLoadingOverview"
|
[resolution]="75"
|
||||||
[resolution]="75"
|
[blockLimit]="stateService.blockVSize"
|
||||||
[blockLimit]="stateService.blockVSize"
|
[orientation]="'top'"
|
||||||
[orientation]="'top'"
|
[flip]="false"
|
||||||
[flip]="false"
|
(txClickEvent)="onTxClick($event)"
|
||||||
(txClickEvent)="onTxClick($event)"
|
></app-block-overview-graph>
|
||||||
></app-block-overview-graph>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<span id="overview"></span>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<!-- VISUALIZATIONS -->
|
||||||
|
<div class="box" *ngIf="!error && webGlEnabled && indexingAvailable && !auditDataMissing">
|
||||||
|
<div class="nav nav-tabs" *ngIf="isMobile && auditEnabled">
|
||||||
|
<a class="nav-link" [class.active]="mode === 'projected'" i18n="block.projected"
|
||||||
|
fragment="projected" (click)="changeMode('projected')">Projected</a>
|
||||||
|
<a class="nav-link" [class.active]="mode === 'actual'" i18n="block.actual"
|
||||||
|
fragment="actual" (click)="changeMode('actual')">Actual</a>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm">
|
||||||
|
<h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.projected-block">Projected Block</h3>
|
||||||
|
<app-block-overview-graph #blockGraphProjected [isLoading]="isLoadingOverview" [resolution]="75"
|
||||||
|
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx"
|
||||||
|
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !auditEnabled"></app-block-overview-graph>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm" *ngIf="!isMobile">
|
||||||
|
<h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.actual-block">Actual Block</h3>
|
||||||
|
<app-block-overview-graph #blockGraphActual [isLoading]="isLoadingOverview" [resolution]="75"
|
||||||
|
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" mode="mined"
|
||||||
|
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !auditEnabled"></app-block-overview-graph>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ng-template [ngIf]="!isLoadingBlock && !error">
|
<ng-template [ngIf]="!isLoadingBlock && !error">
|
||||||
<div [hidden]="!showDetails" id="details">
|
<div [hidden]="!showDetails" id="details">
|
||||||
<br>
|
<br>
|
||||||
@@ -278,6 +323,7 @@
|
|||||||
<div class="col-sm" *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
<div class="col-sm" *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||||
<table class="table table-borderless table-striped">
|
<table class="table table-borderless table-striped">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<tr *ngIf="isMobile"></tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="td-width" i18n="block.difficulty">Difficulty</td>
|
<td class="td-width" i18n="block.difficulty">Difficulty</td>
|
||||||
<td>{{ block.difficulty }}</td>
|
<td>{{ block.difficulty }}</td>
|
||||||
|
|||||||
@@ -111,7 +111,8 @@ h1 {
|
|||||||
|
|
||||||
.next-previous-blocks {
|
.next-previous-blocks {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
|
flex-direction: row;
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
font-size: 36px;
|
font-size: 36px;
|
||||||
}
|
}
|
||||||
@@ -125,6 +126,21 @@ h1 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.time-ltr .next-previous-blocks {
|
||||||
|
.nav-arrow {
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
|
.nav-arrow.next {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
.block-link {
|
||||||
|
order: 1;
|
||||||
|
}
|
||||||
|
.nav-arrow.prev {
|
||||||
|
order: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.disable {
|
.disable {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
color: #393e5c73;
|
color: #393e5c73;
|
||||||
@@ -155,3 +171,35 @@ h1 {
|
|||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-button {
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-subtitle {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs {
|
||||||
|
border-color: white;
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs .nav-link {
|
||||||
|
background: inherit;
|
||||||
|
border-width: 1px;
|
||||||
|
border-bottom: none;
|
||||||
|
border-color: transparent;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: #24273e;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active, &:hover {
|
||||||
|
border-color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
|
import { Component, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/core';
|
||||||
import { Location } from '@angular/common';
|
import { Location } from '@angular/common';
|
||||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||||
import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, pairwise } from 'rxjs/operators';
|
import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, pairwise, filter } from 'rxjs/operators';
|
||||||
import { Transaction, Vout } from '../../interfaces/electrs.interface';
|
import { Transaction, Vout } from '../../interfaces/electrs.interface';
|
||||||
import { Observable, of, Subscription, asyncScheduler, EMPTY } from 'rxjs';
|
import { Observable, of, Subscription, asyncScheduler, EMPTY, combineLatest } from 'rxjs';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '../../services/seo.service';
|
||||||
import { WebsocketService } from '../../services/websocket.service';
|
import { WebsocketService } from '../../services/websocket.service';
|
||||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||||
import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
|
import { BlockAudit, BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '../../services/api.service';
|
||||||
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
|
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
|
||||||
import { detectWebGL } from '../../shared/graphs.utils';
|
import { detectWebGL } from '../../shared/graphs.utils';
|
||||||
@@ -17,11 +17,20 @@ import { detectWebGL } from '../../shared/graphs.utils';
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-block',
|
selector: 'app-block',
|
||||||
templateUrl: './block.component.html',
|
templateUrl: './block.component.html',
|
||||||
styleUrls: ['./block.component.scss']
|
styleUrls: ['./block.component.scss'],
|
||||||
|
styles: [`
|
||||||
|
.loadingGraphs {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: calc(50% - 15px);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
`],
|
||||||
})
|
})
|
||||||
export class BlockComponent implements OnInit, OnDestroy {
|
export class BlockComponent implements OnInit, OnDestroy {
|
||||||
network = '';
|
network = '';
|
||||||
block: BlockExtended;
|
block: BlockExtended;
|
||||||
|
blockAudit: BlockAudit = undefined;
|
||||||
blockHeight: number;
|
blockHeight: number;
|
||||||
lastBlockHeight: number;
|
lastBlockHeight: number;
|
||||||
nextBlockHeight: number;
|
nextBlockHeight: number;
|
||||||
@@ -47,9 +56,18 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
transactionsError: any = null;
|
transactionsError: any = null;
|
||||||
overviewError: any = null;
|
overviewError: any = null;
|
||||||
webGlEnabled = true;
|
webGlEnabled = true;
|
||||||
|
indexingAvailable = false;
|
||||||
|
auditEnabled = true;
|
||||||
|
auditDataMissing: boolean;
|
||||||
|
isMobile = window.innerWidth <= 767.98;
|
||||||
|
hoverTx: string;
|
||||||
|
numMissing: number = 0;
|
||||||
|
numUnexpected: number = 0;
|
||||||
|
mode: 'projected' | 'actual' = 'projected';
|
||||||
|
|
||||||
transactionSubscription: Subscription;
|
transactionSubscription: Subscription;
|
||||||
overviewSubscription: Subscription;
|
overviewSubscription: Subscription;
|
||||||
|
auditSubscription: Subscription;
|
||||||
keyNavigationSubscription: Subscription;
|
keyNavigationSubscription: Subscription;
|
||||||
blocksSubscription: Subscription;
|
blocksSubscription: Subscription;
|
||||||
networkChangedSubscription: Subscription;
|
networkChangedSubscription: Subscription;
|
||||||
@@ -57,8 +75,12 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
nextBlockSubscription: Subscription = undefined;
|
nextBlockSubscription: Subscription = undefined;
|
||||||
nextBlockSummarySubscription: Subscription = undefined;
|
nextBlockSummarySubscription: Subscription = undefined;
|
||||||
nextBlockTxListSubscription: Subscription = undefined;
|
nextBlockTxListSubscription: Subscription = undefined;
|
||||||
|
timeLtrSubscription: Subscription;
|
||||||
|
timeLtr: boolean;
|
||||||
|
childChangeSubscription: Subscription;
|
||||||
|
|
||||||
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
|
@ViewChildren('blockGraphProjected') blockGraphProjected: QueryList<BlockOverviewGraphComponent>;
|
||||||
|
@ViewChildren('blockGraphActual') blockGraphActual: QueryList<BlockOverviewGraphComponent>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
@@ -80,6 +102,13 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
this.network = this.stateService.network;
|
this.network = this.stateService.network;
|
||||||
this.itemsPerPage = this.stateService.env.ITEMS_PER_PAGE;
|
this.itemsPerPage = this.stateService.env.ITEMS_PER_PAGE;
|
||||||
|
|
||||||
|
this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {
|
||||||
|
this.timeLtr = !!ltr;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.indexingAvailable = (this.stateService.env.BASE_MODULE === 'mempool' && this.stateService.env.MINING_DASHBOARD === true);
|
||||||
|
this.auditEnabled = this.indexingAvailable;
|
||||||
|
|
||||||
this.txsLoadingStatus$ = this.route.paramMap
|
this.txsLoadingStatus$ = this.route.paramMap
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap(() => this.stateService.loadingIndicators$),
|
switchMap(() => this.stateService.loadingIndicators$),
|
||||||
@@ -109,9 +138,11 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
this.error = undefined;
|
this.error = undefined;
|
||||||
this.fees = undefined;
|
this.fees = undefined;
|
||||||
this.stateService.markBlock$.next({});
|
this.stateService.markBlock$.next({});
|
||||||
|
this.auditDataMissing = false;
|
||||||
|
|
||||||
if (history.state.data && history.state.data.blockHeight) {
|
if (history.state.data && history.state.data.blockHeight) {
|
||||||
this.blockHeight = history.state.data.blockHeight;
|
this.blockHeight = history.state.data.blockHeight;
|
||||||
|
this.updateAuditDataMissingFromBlockHeight(this.blockHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
let isBlockHeight = false;
|
let isBlockHeight = false;
|
||||||
@@ -124,6 +155,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
if (history.state.data && history.state.data.block) {
|
if (history.state.data && history.state.data.block) {
|
||||||
this.blockHeight = history.state.data.block.height;
|
this.blockHeight = history.state.data.block.height;
|
||||||
|
this.updateAuditDataMissingFromBlockHeight(this.blockHeight);
|
||||||
return of(history.state.data.block);
|
return of(history.state.data.block);
|
||||||
} else {
|
} else {
|
||||||
this.isLoadingBlock = true;
|
this.isLoadingBlock = true;
|
||||||
@@ -182,10 +214,10 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.nextBlockSubscription = this.apiService.getBlock$(block.previousblockhash).subscribe();
|
this.nextBlockSubscription = this.apiService.getBlock$(block.previousblockhash).subscribe();
|
||||||
this.nextBlockTxListSubscription = this.electrsApiService.getBlockTransactions$(block.previousblockhash).subscribe();
|
this.nextBlockTxListSubscription = this.electrsApiService.getBlockTransactions$(block.previousblockhash).subscribe();
|
||||||
this.nextBlockSummarySubscription = this.apiService.getStrippedBlockTransactions$(block.previousblockhash).subscribe();
|
this.apiService.getBlockAudit$(block.previousblockhash);
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
this.updateAuditDataMissingFromBlockHeight(block.height);
|
||||||
this.block = block;
|
this.block = block;
|
||||||
this.blockHeight = block.height;
|
this.blockHeight = block.height;
|
||||||
this.lastBlockHeight = this.blockHeight;
|
this.lastBlockHeight = this.blockHeight;
|
||||||
@@ -230,40 +262,127 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
this.isLoadingOverview = false;
|
this.isLoadingOverview = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.overviewSubscription = block$.pipe(
|
if (!this.indexingAvailable) {
|
||||||
startWith(null),
|
this.overviewSubscription = block$.pipe(
|
||||||
pairwise(),
|
startWith(null),
|
||||||
switchMap(([prevBlock, block]) => this.apiService.getStrippedBlockTransactions$(block.id)
|
pairwise(),
|
||||||
.pipe(
|
switchMap(([prevBlock, block]) => this.apiService.getStrippedBlockTransactions$(block.id)
|
||||||
catchError((err) => {
|
.pipe(
|
||||||
this.overviewError = err;
|
catchError((err) => {
|
||||||
return of([]);
|
this.overviewError = err;
|
||||||
}),
|
return of([]);
|
||||||
switchMap((transactions) => {
|
}),
|
||||||
if (prevBlock) {
|
switchMap((transactions) => {
|
||||||
return of({ transactions, direction: (prevBlock.height < block.height) ? 'right' : 'left' });
|
if (prevBlock) {
|
||||||
} else {
|
return of({ transactions, direction: (prevBlock.height < block.height) ? 'right' : 'left' });
|
||||||
return of({ transactions, direction: 'down' });
|
} else {
|
||||||
|
return of({ transactions, direction: 'down' });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.subscribe(({transactions, direction}: {transactions: TransactionStripped[], direction: string}) => {
|
||||||
|
this.strippedTransactions = transactions;
|
||||||
|
this.isLoadingOverview = false;
|
||||||
|
this.setupBlockGraphs();
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
this.error = error;
|
||||||
|
this.isLoadingOverview = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.indexingAvailable) {
|
||||||
|
this.auditSubscription = block$.pipe(
|
||||||
|
startWith(null),
|
||||||
|
pairwise(),
|
||||||
|
switchMap(([prevBlock, block]) => this.apiService.getBlockAudit$(block.id)
|
||||||
|
.pipe(
|
||||||
|
catchError((err) => {
|
||||||
|
this.overviewError = err;
|
||||||
|
return of([]);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
filter((response) => response != null),
|
||||||
|
map((response) => {
|
||||||
|
const blockAudit = response.body;
|
||||||
|
const inTemplate = {};
|
||||||
|
const inBlock = {};
|
||||||
|
const isAdded = {};
|
||||||
|
const isCensored = {};
|
||||||
|
const isMissing = {};
|
||||||
|
const isSelected = {};
|
||||||
|
const isFresh = {};
|
||||||
|
this.numMissing = 0;
|
||||||
|
this.numUnexpected = 0;
|
||||||
|
|
||||||
|
if (blockAudit?.template) {
|
||||||
|
for (const tx of blockAudit.template) {
|
||||||
|
inTemplate[tx.txid] = true;
|
||||||
}
|
}
|
||||||
})
|
for (const tx of blockAudit.transactions) {
|
||||||
)
|
inBlock[tx.txid] = true;
|
||||||
),
|
}
|
||||||
)
|
for (const txid of blockAudit.addedTxs) {
|
||||||
.subscribe(({transactions, direction}: {transactions: TransactionStripped[], direction: string}) => {
|
isAdded[txid] = true;
|
||||||
this.strippedTransactions = transactions;
|
}
|
||||||
this.isLoadingOverview = false;
|
for (const txid of blockAudit.missingTxs) {
|
||||||
if (this.blockGraph) {
|
isCensored[txid] = true;
|
||||||
this.blockGraph.destroy();
|
}
|
||||||
this.blockGraph.setup(this.strippedTransactions);
|
for (const txid of blockAudit.freshTxs || []) {
|
||||||
}
|
isFresh[txid] = true;
|
||||||
},
|
}
|
||||||
(error) => {
|
// set transaction statuses
|
||||||
this.error = error;
|
for (const tx of blockAudit.template) {
|
||||||
this.isLoadingOverview = false;
|
tx.context = 'projected';
|
||||||
if (this.blockGraph) {
|
if (isCensored[tx.txid]) {
|
||||||
this.blockGraph.destroy();
|
tx.status = 'censored';
|
||||||
}
|
} else if (inBlock[tx.txid]) {
|
||||||
});
|
tx.status = 'found';
|
||||||
|
} else {
|
||||||
|
tx.status = isFresh[tx.txid] ? 'fresh' : 'missing';
|
||||||
|
isMissing[tx.txid] = true;
|
||||||
|
this.numMissing++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [index, tx] of blockAudit.transactions.entries()) {
|
||||||
|
tx.context = 'actual';
|
||||||
|
if (index === 0) {
|
||||||
|
tx.status = null;
|
||||||
|
} else if (isAdded[tx.txid]) {
|
||||||
|
tx.status = 'added';
|
||||||
|
} else if (inTemplate[tx.txid]) {
|
||||||
|
tx.status = 'found';
|
||||||
|
} else {
|
||||||
|
tx.status = 'selected';
|
||||||
|
isSelected[tx.txid] = true;
|
||||||
|
this.numUnexpected++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const tx of blockAudit.transactions) {
|
||||||
|
inBlock[tx.txid] = true;
|
||||||
|
}
|
||||||
|
this.auditEnabled = true;
|
||||||
|
} else {
|
||||||
|
this.auditEnabled = false;
|
||||||
|
this.auditDataMissing = true;
|
||||||
|
}
|
||||||
|
return blockAudit;
|
||||||
|
}),
|
||||||
|
catchError((err) => {
|
||||||
|
console.log(err);
|
||||||
|
this.error = err;
|
||||||
|
this.isLoadingOverview = false;
|
||||||
|
return of(null);
|
||||||
|
}),
|
||||||
|
).subscribe((blockAudit) => {
|
||||||
|
this.blockAudit = blockAudit;
|
||||||
|
this.setupBlockGraphs();
|
||||||
|
this.isLoadingOverview = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.networkChangedSubscription = this.stateService.networkChanged$
|
this.networkChangedSubscription = this.stateService.networkChanged$
|
||||||
.subscribe((network) => this.network = network);
|
.subscribe((network) => this.network = network);
|
||||||
@@ -274,13 +393,21 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
} else {
|
} else {
|
||||||
this.showDetails = false;
|
this.showDetails = false;
|
||||||
}
|
}
|
||||||
|
if (params.view === 'projected') {
|
||||||
|
this.mode = 'projected';
|
||||||
|
} else {
|
||||||
|
this.mode = 'actual';
|
||||||
|
}
|
||||||
|
this.setupBlockGraphs();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.keyNavigationSubscription = this.stateService.keyNavigation$.subscribe((event) => {
|
this.keyNavigationSubscription = this.stateService.keyNavigation$.subscribe((event) => {
|
||||||
if (this.showPreviousBlocklink && event.key === 'ArrowRight' && this.nextBlockHeight - 2 >= 0) {
|
const prevKey = this.timeLtr ? 'ArrowLeft' : 'ArrowRight';
|
||||||
|
const nextKey = this.timeLtr ? 'ArrowRight' : 'ArrowLeft';
|
||||||
|
if (this.showPreviousBlocklink && event.key === prevKey && this.nextBlockHeight - 2 >= 0) {
|
||||||
this.navigateToPreviousBlock();
|
this.navigateToPreviousBlock();
|
||||||
}
|
}
|
||||||
if (event.key === 'ArrowLeft') {
|
if (event.key === nextKey) {
|
||||||
if (this.showNextBlocklink) {
|
if (this.showNextBlocklink) {
|
||||||
this.navigateToNextBlock();
|
this.navigateToNextBlock();
|
||||||
} else {
|
} else {
|
||||||
@@ -290,15 +417,24 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
this.childChangeSubscription = combineLatest([this.blockGraphProjected.changes.pipe(startWith(null)), this.blockGraphActual.changes.pipe(startWith(null))]).subscribe(() => {
|
||||||
|
this.setupBlockGraphs();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
this.stateService.markBlock$.next({});
|
this.stateService.markBlock$.next({});
|
||||||
this.transactionSubscription.unsubscribe();
|
this.transactionSubscription.unsubscribe();
|
||||||
this.overviewSubscription.unsubscribe();
|
this.overviewSubscription?.unsubscribe();
|
||||||
|
this.auditSubscription?.unsubscribe();
|
||||||
this.keyNavigationSubscription.unsubscribe();
|
this.keyNavigationSubscription.unsubscribe();
|
||||||
this.blocksSubscription.unsubscribe();
|
this.blocksSubscription.unsubscribe();
|
||||||
this.networkChangedSubscription.unsubscribe();
|
this.networkChangedSubscription.unsubscribe();
|
||||||
this.queryParamsSubscription.unsubscribe();
|
this.queryParamsSubscription.unsubscribe();
|
||||||
|
this.timeLtrSubscription.unsubscribe();
|
||||||
this.unsubscribeNextBlockSubscriptions();
|
this.unsubscribeNextBlockSubscriptions();
|
||||||
|
this.childChangeSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
unsubscribeNextBlockSubscriptions() {
|
unsubscribeNextBlockSubscriptions() {
|
||||||
@@ -345,7 +481,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
this.showDetails = false;
|
this.showDetails = false;
|
||||||
this.router.navigate([], {
|
this.router.navigate([], {
|
||||||
relativeTo: this.route,
|
relativeTo: this.route,
|
||||||
queryParams: { showDetails: false },
|
queryParams: { showDetails: false, view: this.mode },
|
||||||
queryParamsHandling: 'merge',
|
queryParamsHandling: 'merge',
|
||||||
fragment: 'block'
|
fragment: 'block'
|
||||||
});
|
});
|
||||||
@@ -353,7 +489,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
this.showDetails = true;
|
this.showDetails = true;
|
||||||
this.router.navigate([], {
|
this.router.navigate([], {
|
||||||
relativeTo: this.route,
|
relativeTo: this.route,
|
||||||
queryParams: { showDetails: true },
|
queryParams: { showDetails: true, view: this.mode },
|
||||||
queryParamsHandling: 'merge',
|
queryParamsHandling: 'merge',
|
||||||
fragment: 'details'
|
fragment: 'details'
|
||||||
});
|
});
|
||||||
@@ -372,10 +508,6 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
return this.block && this.block.height > 681393 && (new Date().getTime() / 1000) < 1628640000;
|
return this.block && this.block.height > 681393 && (new Date().getTime() / 1000) < 1628640000;
|
||||||
}
|
}
|
||||||
|
|
||||||
onResize(event: any) {
|
|
||||||
this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
navigateToPreviousBlock() {
|
navigateToPreviousBlock() {
|
||||||
if (!this.block) {
|
if (!this.block) {
|
||||||
return;
|
return;
|
||||||
@@ -392,8 +524,8 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setNextAndPreviousBlockLink(){
|
setNextAndPreviousBlockLink(){
|
||||||
if (this.latestBlock && this.blockHeight) {
|
if (this.latestBlock) {
|
||||||
if (this.blockHeight === 0){
|
if (!this.blockHeight){
|
||||||
this.showPreviousBlocklink = false;
|
this.showPreviousBlocklink = false;
|
||||||
} else {
|
} else {
|
||||||
this.showPreviousBlocklink = true;
|
this.showPreviousBlocklink = true;
|
||||||
@@ -406,8 +538,72 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setupBlockGraphs(): void {
|
||||||
|
if (this.blockAudit || this.strippedTransactions) {
|
||||||
|
this.blockGraphProjected.forEach(graph => {
|
||||||
|
graph.destroy();
|
||||||
|
if (this.isMobile && this.mode === 'actual') {
|
||||||
|
graph.setup(this.blockAudit?.transactions || this.strippedTransactions || []);
|
||||||
|
} else {
|
||||||
|
graph.setup(this.blockAudit?.template || []);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.blockGraphActual.forEach(graph => {
|
||||||
|
graph.destroy();
|
||||||
|
graph.setup(this.blockAudit?.transactions || this.strippedTransactions || []);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onResize(event: any): void {
|
||||||
|
const isMobile = event.target.innerWidth <= 767.98;
|
||||||
|
const changed = isMobile !== this.isMobile;
|
||||||
|
this.isMobile = isMobile;
|
||||||
|
this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5;
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
this.changeMode(this.mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changeMode(mode: 'projected' | 'actual'): void {
|
||||||
|
this.router.navigate([], {
|
||||||
|
relativeTo: this.route,
|
||||||
|
queryParams: { showDetails: this.showDetails, view: mode },
|
||||||
|
queryParamsHandling: 'merge',
|
||||||
|
fragment: 'overview'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onTxClick(event: TransactionStripped): void {
|
onTxClick(event: TransactionStripped): void {
|
||||||
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.txid}`);
|
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.txid}`);
|
||||||
this.router.navigate([url]);
|
this.router.navigate([url]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onTxHover(txid: string): void {
|
||||||
|
if (txid && txid.length) {
|
||||||
|
this.hoverTx = txid;
|
||||||
|
} else {
|
||||||
|
this.hoverTx = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAuditDataMissingFromBlockHeight(blockHeight: number): void {
|
||||||
|
switch (this.stateService.network) {
|
||||||
|
case 'testnet':
|
||||||
|
if (blockHeight < this.stateService.env.TESTNET_BLOCK_AUDIT_START_HEIGHT) {
|
||||||
|
this.auditDataMissing = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'signet':
|
||||||
|
if (blockHeight < this.stateService.env.SIGNET_BLOCK_AUDIT_START_HEIGHT) {
|
||||||
|
this.auditDataMissing = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (blockHeight < this.stateService.env.MAINNET_BLOCK_AUDIT_START_HEIGHT) {
|
||||||
|
this.auditDataMissing = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<div class="blocks-container blockchain-blocks-container" *ngIf="(loadingBlocks$ | async) === false; else loadingBlocksTemplate">
|
<div class="blocks-container blockchain-blocks-container" [class.time-ltr]="timeLtr" *ngIf="(loadingBlocks$ | async) === false; else loadingBlocksTemplate">
|
||||||
<div *ngFor="let block of blocks; let i = index; trackBy: trackByBlocksFn" >
|
<div *ngFor="let block of blocks; let i = index; trackBy: trackByBlocksFn" >
|
||||||
<div [attr.data-cy]="'bitcoin-block-' + i" class="text-center bitcoin-block mined-block blockchain-blocks-{{ i }}" id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]" [class.blink-bg]="(specialBlocks[block.height] !== undefined)">
|
<div [attr.data-cy]="'bitcoin-block-' + i" class="text-center bitcoin-block mined-block blockchain-blocks-{{ i }}" id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]" [class.blink-bg]="(specialBlocks[block.height] !== undefined)">
|
||||||
<a draggable="false" [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }"
|
<a draggable="false" [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }"
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-template #loadingBlocksTemplate>
|
<ng-template #loadingBlocksTemplate>
|
||||||
<div class="blocks-container">
|
<div class="blocks-container" [class.time-ltr]="timeLtr">
|
||||||
<div class="flashing">
|
<div class="flashing">
|
||||||
<div *ngFor="let block of emptyBlocks; let i = index; trackBy: trackByBlocksFn" >
|
<div *ngFor="let block of emptyBlocks; let i = index; trackBy: trackByBlocksFn" >
|
||||||
<div class="text-center bitcoin-block mined-block" id="bitcoin-block-{{ block.height }}" [ngStyle]="emptyBlockStyles[i]"></div>
|
<div class="text-center bitcoin-block mined-block" id="bitcoin-block-{{ block.height }}" [ngStyle]="emptyBlockStyles[i]"></div>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
.mined-block {
|
.mined-block {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
transition: 2s;
|
transition: background 2s, left 2s, transform 1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-size {
|
.block-size {
|
||||||
@@ -145,3 +145,9 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events : none;
|
pointer-events : none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.time-ltr {
|
||||||
|
.bitcoin-block {
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,8 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
|||||||
blocksFilled = false;
|
blocksFilled = false;
|
||||||
transition = '1s';
|
transition = '1s';
|
||||||
showMiningInfo = false;
|
showMiningInfo = false;
|
||||||
|
timeLtrSubscription: Subscription;
|
||||||
|
timeLtr: boolean;
|
||||||
|
|
||||||
gradientColors = {
|
gradientColors = {
|
||||||
'': ['#9339f4', '#105fb0'],
|
'': ['#9339f4', '#105fb0'],
|
||||||
@@ -61,6 +63,11 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
|||||||
this.location.onUrlChange((url) => this.enabledMiningInfoIfNeeded(url));
|
this.location.onUrlChange((url) => this.enabledMiningInfoIfNeeded(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {
|
||||||
|
this.timeLtr = !!ltr;
|
||||||
|
this.cd.markForCheck();
|
||||||
|
});
|
||||||
|
|
||||||
if (this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') {
|
if (this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') {
|
||||||
this.feeRounding = '1.0-1';
|
this.feeRounding = '1.0-1';
|
||||||
}
|
}
|
||||||
@@ -123,6 +130,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
|||||||
this.networkSubscription.unsubscribe();
|
this.networkSubscription.unsubscribe();
|
||||||
this.tabHiddenSubscription.unsubscribe();
|
this.tabHiddenSubscription.unsubscribe();
|
||||||
this.markBlockSubscription.unsubscribe();
|
this.markBlockSubscription.unsubscribe();
|
||||||
|
this.timeLtrSubscription.unsubscribe();
|
||||||
clearInterval(this.interval);
|
clearInterval(this.interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
<div class="text-center" class="blockchain-wrapper" #container>
|
<div class="text-center" class="blockchain-wrapper" [class.time-ltr]="timeLtr" [class.ltr-transition]="ltrTransitionEnabled" #container>
|
||||||
<div class="position-container {{ network }}">
|
<div class="position-container" [ngClass]="network ? network : ''">
|
||||||
<span>
|
<span>
|
||||||
<app-mempool-blocks></app-mempool-blocks>
|
<div class="blocks-wrapper">
|
||||||
<app-blockchain-blocks></app-blockchain-blocks>
|
<app-mempool-blocks></app-mempool-blocks>
|
||||||
<div id="divider"></div>
|
<app-blockchain-blocks></app-blockchain-blocks>
|
||||||
|
</div>
|
||||||
|
<div id="divider">
|
||||||
|
<button class="time-toggle" (click)="toggleTimeDirection()"><fa-icon [icon]="['fas', 'exchange-alt']" [fixedWidth]="true"></fa-icon></button>
|
||||||
|
</div>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,23 +24,45 @@
|
|||||||
|
|
||||||
.position-container {
|
.position-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 0;
|
||||||
top: 75px;
|
top: 75px;
|
||||||
|
transform: translateX(50vw);
|
||||||
}
|
}
|
||||||
|
|
||||||
.position-container.liquid, .position-container.liquidtestnet {
|
.position-container.liquid, .position-container.liquidtestnet {
|
||||||
left: 420px;
|
transform: translateX(420px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.blockchain-wrapper.time-ltr {
|
||||||
|
.position-container.liquid, .position-container.liquidtestnet {
|
||||||
|
transform: translateX(calc(100vw - 420px));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
@media (max-width: 767.98px) {
|
||||||
.position-container {
|
.blockchain-wrapper {
|
||||||
left: 95%;
|
.position-container {
|
||||||
|
transform: translateX(95vw);
|
||||||
|
}
|
||||||
|
.position-container.liquid, .position-container.liquidtestnet {
|
||||||
|
transform: translateX(50vw);
|
||||||
|
}
|
||||||
|
.position-container.loading {
|
||||||
|
transform: translateX(50vw);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.position-container.liquid, .position-container.liquidtestnet {
|
.blockchain-wrapper.time-ltr {
|
||||||
left: 50%;
|
.position-container {
|
||||||
}
|
transform: translateX(5vw);
|
||||||
.position-container.loading {
|
}
|
||||||
left: 50%;
|
.position-container.liquid, .position-container.liquidtestnet {
|
||||||
|
transform: translateX(50vw);
|
||||||
|
}
|
||||||
|
.position-container.loading {
|
||||||
|
transform: translateX(50vw);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,3 +80,47 @@
|
|||||||
left: -150px;
|
left: -150px;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.time-toggle {
|
||||||
|
color: white;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
position: absolute;
|
||||||
|
bottom: -1.8em;
|
||||||
|
left: 1px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blockchain-wrapper.ltr-transition .blocks-wrapper,
|
||||||
|
.blockchain-wrapper.ltr-transition .position-container,
|
||||||
|
.blockchain-wrapper.ltr-transition .time-toggle {
|
||||||
|
transition: transform 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blockchain-wrapper.time-ltr {
|
||||||
|
.blocks-wrapper {
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-toggle {
|
||||||
|
transform: translateX(-50%) scaleX(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(.ltr-layout) {
|
||||||
|
.blockchain-wrapper.time-ltr .blocks-wrapper,
|
||||||
|
.blockchain-wrapper .blocks-wrapper {
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(.rtl-layout) {
|
||||||
|
.blockchain-wrapper.time-ltr .blocks-wrapper,
|
||||||
|
.blockchain-wrapper .blocks-wrapper {
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
|
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy } from '@angular/core';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -7,8 +8,11 @@ import { StateService } from '../../services/state.service';
|
|||||||
styleUrls: ['./blockchain.component.scss'],
|
styleUrls: ['./blockchain.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class BlockchainComponent implements OnInit {
|
export class BlockchainComponent implements OnInit, OnDestroy {
|
||||||
network: string;
|
network: string;
|
||||||
|
timeLtrSubscription: Subscription;
|
||||||
|
timeLtr: boolean = this.stateService.timeLtr.value;
|
||||||
|
ltrTransitionEnabled = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public stateService: StateService,
|
public stateService: StateService,
|
||||||
@@ -16,5 +20,17 @@ export class BlockchainComponent implements OnInit {
|
|||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.network = this.stateService.network;
|
this.network = this.stateService.network;
|
||||||
|
this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {
|
||||||
|
this.timeLtr = !!ltr;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.timeLtrSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleTimeDirection() {
|
||||||
|
this.ltrTransitionEnabled = true;
|
||||||
|
this.stateService.timeLtr.next(!this.timeLtr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user