Compare commits
386 Commits
mononaut/m
...
nymkappa/h
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20d0df9d5c | ||
|
|
c0ef01d4da | ||
|
|
c6711d8191 | ||
|
|
216bd5ad23 | ||
|
|
a7d59d6b2e | ||
|
|
a257bcc12a | ||
|
|
66c0ea7ca3 | ||
|
|
7692b2af66 | ||
|
|
397f53f42d | ||
|
|
2ea76d9c38 | ||
|
|
59ac27b104 | ||
|
|
d27bb7e156 | ||
|
|
4eadfc0a3b | ||
|
|
76cfa3ca47 | ||
|
|
3a4a4d9ffd | ||
|
|
8b01a83948 | ||
|
|
2ceb9001a1 | ||
|
|
d44b7926d2 | ||
|
|
57299e086e | ||
|
|
c1d17dac43 | ||
|
|
cb49f9d929 | ||
|
|
185be3d598 | ||
|
|
c950e3d0ae | ||
|
|
99cc47cf00 | ||
|
|
908b8b4352 | ||
|
|
1a7f475220 | ||
|
|
cb63d17a2f | ||
|
|
c8ce4631e2 | ||
|
|
96c2b0a2f7 | ||
|
|
23475c7a1b | ||
|
|
9ab3d3195e | ||
|
|
a22d07ae60 | ||
|
|
221658f6bf | ||
|
|
133df2e4be | ||
|
|
5fba9595af | ||
|
|
90ca77a46a | ||
|
|
f0c76c1349 | ||
|
|
6e5cfa9bf2 | ||
|
|
9ffcf2eca5 | ||
|
|
8514fb9bdc | ||
|
|
c1be7460c0 | ||
|
|
602aa4f948 | ||
|
|
2d2c55ce0e | ||
|
|
f0e207dff2 | ||
|
|
756e4356a5 | ||
|
|
9c303e8c23 | ||
|
|
a7ba4a0be8 | ||
|
|
54c2d7efe5 | ||
|
|
3f8eb3a2cd | ||
|
|
e095192968 | ||
|
|
862c9591a1 | ||
|
|
ca7221f8b7 | ||
|
|
8a579cc374 | ||
|
|
b454959acd | ||
|
|
4498e14be8 | ||
|
|
f27a9a3c50 | ||
|
|
1f0b597e2f | ||
|
|
a3884b95b8 | ||
|
|
f67687b573 | ||
|
|
7f4dc7eb3e | ||
|
|
1c4be164dd | ||
|
|
450d83461c | ||
|
|
5f222f59a7 | ||
|
|
8dac5cff9a | ||
|
|
83c7b3034b | ||
|
|
ce1babf67b | ||
|
|
7ea921a5cb | ||
|
|
26e3a2413d | ||
|
|
8ad6c93e92 | ||
|
|
198d79f149 | ||
|
|
8a72a5871d | ||
|
|
2c12f890bd | ||
|
|
f9300130fe | ||
|
|
5b557b2c12 | ||
|
|
071e9b6c2c | ||
|
|
f78971e640 | ||
|
|
b86c8f7976 | ||
|
|
2ce596a14b | ||
|
|
735ed87b78 | ||
|
|
d1741a51c9 | ||
|
|
9f0b3bd769 | ||
|
|
41088cca09 | ||
|
|
e92ffbd501 | ||
|
|
93d9538845 | ||
|
|
ae46fcafb9 | ||
|
|
69a994afd5 | ||
|
|
c6cc533baa | ||
|
|
dd0542bbe1 | ||
|
|
cdb4580c6d | ||
|
|
fe4b39df80 | ||
|
|
1a7519dd00 | ||
|
|
5116a27e8d | ||
|
|
73e8ba3e47 | ||
|
|
6805b673fa | ||
|
|
22236bdabe | ||
|
|
05f60cda56 | ||
|
|
c4004ba301 | ||
|
|
15b7e75b69 | ||
|
|
70384d8d9f | ||
|
|
2a27ee0c7c | ||
|
|
933a204462 | ||
|
|
6884830da6 | ||
|
|
24ec31acd9 | ||
|
|
1b2f1b38b4 | ||
|
|
3486c35f5e | ||
|
|
57a05c80a2 | ||
|
|
1ddb8a39c9 | ||
|
|
0a61429176 | ||
|
|
e440c3f235 | ||
|
|
177bbc83f3 | ||
|
|
040c067aac | ||
|
|
15b3c88a1f | ||
|
|
65f080d526 | ||
|
|
19347614bd | ||
|
|
3b9601a82e | ||
|
|
acae5a33b0 | ||
|
|
8b6db768cd | ||
|
|
4143a5f593 | ||
|
|
d31c2665ee | ||
|
|
2142ae55d5 | ||
|
|
0c87a4e7f6 | ||
|
|
e6980a832b | ||
|
|
b08b2ce44a | ||
|
|
d6b9e3118d | ||
|
|
9b4c93c8ee | ||
|
|
e59f5b8810 | ||
|
|
ddf1a300b6 | ||
|
|
8e223861d6 | ||
|
|
8808ff1a98 | ||
|
|
33a6ba04b6 | ||
|
|
d020858840 | ||
|
|
5e0160a039 | ||
|
|
2443bebae5 | ||
|
|
6fb68203bc | ||
|
|
d7acfad3d6 | ||
|
|
a700bd0ef1 | ||
|
|
ae2a849257 | ||
|
|
1a75e3e317 | ||
|
|
ba167c9cc2 | ||
|
|
3d27b7e7b4 | ||
|
|
c4f73b80da | ||
|
|
76a1eb12a6 | ||
|
|
fe16f0dddc | ||
|
|
67295c1b9b | ||
|
|
0bd760d4d6 | ||
|
|
0f2340600c | ||
|
|
72c9d02f88 | ||
|
|
43a42d356d | ||
|
|
60adad8db3 | ||
|
|
5b73362e44 | ||
|
|
517a37728c | ||
|
|
8876bb8f43 | ||
|
|
da2341dd00 | ||
|
|
146935efaf | ||
|
|
775fcbab31 | ||
|
|
cb12e66a3b | ||
|
|
ea08c0c950 | ||
|
|
b26d26b14c | ||
|
|
2d7316942f | ||
|
|
676abf58fd | ||
|
|
1d5843a112 | ||
|
|
9bfe1fb15e | ||
|
|
b29c4cf228 | ||
|
|
1f84e1722f | ||
|
|
2ad52e2c78 | ||
|
|
758122db5e | ||
|
|
83b6094174 | ||
|
|
7057b31c3c | ||
|
|
9091fc9210 | ||
|
|
d149c8bd24 | ||
|
|
9984621e5e | ||
|
|
54a27ef89f | ||
|
|
81ddce27df | ||
|
|
e6dbde952e | ||
|
|
be49f70b09 | ||
|
|
2a9346f695 | ||
|
|
05e88a25be | ||
|
|
574a800520 | ||
|
|
92de208414 | ||
|
|
1b4bbc24ba | ||
|
|
0e5698955f | ||
|
|
4220f99477 | ||
|
|
e144e139b7 | ||
|
|
06e699e52b | ||
|
|
07a0850f99 | ||
|
|
72a5f4a521 | ||
|
|
04605e10a5 | ||
|
|
407ce3c76d | ||
|
|
a99278320b | ||
|
|
367ee68fe0 | ||
|
|
1038b4f908 | ||
|
|
b90cd4c7e3 | ||
|
|
25482b9a06 | ||
|
|
e41829d5e0 | ||
|
|
156bf12034 | ||
|
|
fc1cdbac22 | ||
|
|
5c839aced3 | ||
|
|
3345a60863 | ||
|
|
36844f5b70 | ||
|
|
46d99db167 | ||
|
|
76dcb0830a | ||
|
|
0b29b61e93 | ||
|
|
f4425ed7fe | ||
|
|
7c02eab630 | ||
|
|
8867ef9680 | ||
|
|
1048a0ea83 | ||
|
|
7b216f7ec7 | ||
|
|
32cc2f0c63 | ||
|
|
c6b0e5ff0e | ||
|
|
68a580466f | ||
|
|
bb06a66a03 | ||
|
|
ec6372464f | ||
|
|
c13b8029d3 | ||
|
|
88e92b1b34 | ||
|
|
b0fa3efbbb | ||
|
|
0ccb5618f6 | ||
|
|
556313a676 | ||
|
|
1fe08a9ecc | ||
|
|
a11116ff3a | ||
|
|
ede0ccfd2e | ||
|
|
b64caf8f4b | ||
|
|
99290a7946 | ||
|
|
2d9709a427 | ||
|
|
a76d6c2949 | ||
|
|
d8cfc6e32d | ||
|
|
9457032ab1 | ||
|
|
74998e7f56 | ||
|
|
db0f968749 | ||
|
|
a1968e01e5 | ||
|
|
c7ab6b03fb | ||
|
|
2b206a7bcc | ||
|
|
c4b90c2a18 | ||
|
|
2a7d5760e0 | ||
|
|
c8719f1f1e | ||
|
|
d199c7746e | ||
|
|
67eb815992 | ||
|
|
4ccd3c8525 | ||
|
|
fc5af24b68 | ||
|
|
b17b66a52f | ||
|
|
819dedbc88 | ||
|
|
8717051a06 | ||
|
|
3e78b636d6 | ||
|
|
7865574bd4 | ||
|
|
b3ca8840e5 | ||
|
|
75316e60d0 | ||
|
|
31469ad361 | ||
|
|
a133ddf062 | ||
|
|
485a58e453 | ||
|
|
92090399cc | ||
|
|
893c3cd87d | ||
|
|
c93159414c | ||
|
|
b2d4f4078f | ||
|
|
be17e45785 | ||
|
|
dbe774cc64 | ||
|
|
cef6127e69 | ||
|
|
e513f05c09 | ||
|
|
64223c4744 | ||
|
|
07fd3d3409 | ||
|
|
f7360433a1 | ||
|
|
f6fac92180 | ||
|
|
82d1502bfa | ||
|
|
8ab104d191 | ||
|
|
263742132c | ||
|
|
e3c3f31ddb | ||
|
|
70d1f52268 | ||
|
|
e44f30d7a7 | ||
|
|
099d84a395 | ||
|
|
12285465d9 | ||
|
|
eab008c707 | ||
|
|
0f1def5822 | ||
|
|
fad39e0bea | ||
|
|
0a5a2c3c7e | ||
|
|
b526ee0877 | ||
|
|
98d98b2478 | ||
|
|
3bea10ea35 | ||
|
|
1ea45e9e96 | ||
|
|
555425d97e | ||
|
|
624b3473fc | ||
|
|
a3e61525fe | ||
|
|
9e05060af4 | ||
|
|
ee53597fe9 | ||
|
|
e362003746 | ||
|
|
185eae00e9 | ||
|
|
8c2d0e1d6c | ||
|
|
009fba3dd5 | ||
|
|
a0fc4861d4 | ||
|
|
62085581dd | ||
|
|
05efa8c300 | ||
|
|
eee99a6407 | ||
|
|
98cee4a6cd | ||
|
|
0302999806 | ||
|
|
1876d67e74 | ||
|
|
c0bb75e5b1 | ||
|
|
4059a902a1 | ||
|
|
4cc19a7235 | ||
|
|
c874d642c5 | ||
|
|
f0af1703da | ||
|
|
b47e148677 | ||
|
|
d22743c4b8 | ||
|
|
6db4afe878 | ||
|
|
4596394100 | ||
|
|
ae2ed8fdae | ||
|
|
5452d7f524 | ||
|
|
ff9e2456b9 | ||
|
|
4e581347c8 | ||
|
|
820777236e | ||
|
|
beeb5eb08c | ||
|
|
b78aca0282 | ||
|
|
9572f2d554 | ||
|
|
e59308c2f5 | ||
|
|
ef13596b59 | ||
|
|
c7f48b4390 | ||
|
|
80da024bbb | ||
|
|
f75f85f914 | ||
|
|
b3ac107b0b | ||
|
|
f8cedaa7a3 | ||
|
|
72bb92dd8b | ||
|
|
e3c4e219f3 | ||
|
|
aa3fa4478a | ||
|
|
c9171224e1 | ||
|
|
248cef7718 | ||
|
|
26c03eee88 | ||
|
|
db10ab9aae | ||
|
|
2ee7b9531a | ||
|
|
5f6af83944 | ||
|
|
8d2204a53f | ||
|
|
96bec279a9 | ||
|
|
5178ae43f6 | ||
|
|
ca26154426 | ||
|
|
021f0b32a1 | ||
|
|
b8cfeb579b | ||
|
|
fc5b99f93f | ||
|
|
ce4b0ed0f3 | ||
|
|
a31729b8b8 | ||
|
|
fbf27560b3 | ||
|
|
79e494150c | ||
|
|
b1a43abc0e | ||
|
|
3e50a3c9e7 | ||
|
|
104c7f4285 | ||
|
|
132d6204c3 | ||
|
|
77c6ad5576 | ||
|
|
4d35845c18 | ||
|
|
3d8a4a85f7 | ||
|
|
1545347a45 | ||
|
|
9facf28ba5 | ||
|
|
d6eb98561b | ||
|
|
0f688e8347 | ||
|
|
7f53741a7b | ||
|
|
d5672691e1 | ||
|
|
e6049c707b | ||
|
|
91e74e769c | ||
|
|
7f252f06b7 | ||
|
|
1b9d3f669d | ||
|
|
ef0ba9a77a | ||
|
|
15f10736e2 | ||
|
|
924399df46 | ||
|
|
cddff129b3 | ||
|
|
7c90e8ae06 | ||
|
|
34d996c7cb | ||
|
|
641a2ae3ae | ||
|
|
11a849ef28 | ||
|
|
7b0347e846 | ||
|
|
bb1352ed58 | ||
|
|
fa6456b92c | ||
|
|
bfea19238b | ||
|
|
36b91cfdfd | ||
|
|
7b56212064 | ||
|
|
d4be3c2c4c | ||
|
|
3a9f06f651 | ||
|
|
e652eb339d | ||
|
|
96435c329f | ||
|
|
1c69613d65 | ||
|
|
3707763e30 | ||
|
|
b6ce8229f0 | ||
|
|
b62ae9b6f6 | ||
|
|
f61ace2f92 | ||
|
|
2b572f2494 | ||
|
|
c0e4c1efe1 | ||
|
|
8078caaa89 | ||
|
|
6d945890f4 | ||
|
|
5922c616df | ||
|
|
14d8f67878 | ||
|
|
f80c0738b2 | ||
|
|
545b3e7325 | ||
|
|
3c23e3ff84 | ||
|
|
8a51f32e63 |
12
LICENSE
12
LICENSE
@@ -1,5 +1,5 @@
|
||||
The Mempool Open Source Project®
|
||||
Copyright (c) 2019-2023 Mempool Space K.K. and other shadowy super-coders
|
||||
Copyright (c) 2019-2024 Mempool Space K.K. and other shadowy super-coders
|
||||
|
||||
This program is free software; you can redistribute it and/or modify it under
|
||||
the terms of the GNU Affero General Public License as published by the Free
|
||||
@@ -12,10 +12,12 @@ or any other contributor to The Mempool Open Source Project.
|
||||
|
||||
The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®,
|
||||
Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full
|
||||
Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square logo,
|
||||
the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical
|
||||
Logo, and the mempool.space Horizontal logo are registered trademarks or trademarks
|
||||
of Mempool Space K.K in Japan, the United States, and/or other countries.
|
||||
Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square Logo,
|
||||
the mempool block visualization Logo, the mempool Blocks Logo, the mempool
|
||||
transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo,
|
||||
the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are
|
||||
registered trademarks or trademarks of Mempool Space K.K in Japan,
|
||||
the United States, and/or other countries.
|
||||
|
||||
See our full Trademark Policy and Guidelines for more details, published on
|
||||
<https://mempool.space/trademark-policy>.
|
||||
|
||||
@@ -77,7 +77,7 @@ Query OK, 0 rows affected (0.00 sec)
|
||||
|
||||
#### Build
|
||||
|
||||
_Make sure to use Node.js 16.10 and npm 7._
|
||||
_Make sure to use Node.js 20.x and npm 9.x or newer_
|
||||
|
||||
_The build process requires [Rust](https://www.rust-lang.org/tools/install) to be installed._
|
||||
|
||||
|
||||
@@ -27,8 +27,9 @@
|
||||
"AUTOMATIC_POOLS_UPDATE": false,
|
||||
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json",
|
||||
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
|
||||
"POOLS_UPDATE_DELAY": 604800,
|
||||
"AUDIT": false,
|
||||
"RUST_GBT": false,
|
||||
"RUST_GBT": true,
|
||||
"LIMIT_GBT": false,
|
||||
"CPFP_INDEXING": false,
|
||||
"DISK_CACHE_BLOCK_INTERVAL": 6,
|
||||
@@ -45,7 +46,8 @@
|
||||
"PASSWORD": "mempool",
|
||||
"TIMEOUT": 60000,
|
||||
"COOKIE": false,
|
||||
"COOKIE_PATH": "/path/to/bitcoin/.cookie"
|
||||
"COOKIE_PATH": "/path/to/bitcoin/.cookie",
|
||||
"DEBUG_LOG_PATH": "/path/to/bitcoin/debug.log"
|
||||
},
|
||||
"ELECTRUM": {
|
||||
"HOST": "127.0.0.1",
|
||||
|
||||
217
backend/package-lock.json
generated
217
backend/package-lock.json
generated
@@ -1,21 +1,22 @@
|
||||
{
|
||||
"name": "mempool-backend",
|
||||
"version": "3.0.0-dev",
|
||||
"version": "3.1.0-dev",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mempool-backend",
|
||||
"version": "3.0.0-dev",
|
||||
"version": "3.1.0-dev",
|
||||
"hasInstallScript": true,
|
||||
"license": "GNU Affero General Public License v3.0",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
"@mempool/electrum-client": "1.1.9",
|
||||
"@types/node": "^18.15.3",
|
||||
"axios": "~1.7.2",
|
||||
"axios": "1.7.2",
|
||||
"bitcoinjs-lib": "~6.1.3",
|
||||
"crypto-js": "~4.2.0",
|
||||
"express": "~4.19.2",
|
||||
"express": "~4.21.1",
|
||||
"maxmind": "~4.3.11",
|
||||
"mysql2": "~3.11.0",
|
||||
"redis": "^4.7.0",
|
||||
@@ -2280,6 +2281,7 @@
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
|
||||
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
@@ -2488,9 +2490,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
|
||||
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
|
||||
"version": "1.20.3",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"content-type": "~1.0.5",
|
||||
@@ -2500,7 +2502,7 @@
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "2.4.1",
|
||||
"qs": "6.11.0",
|
||||
"qs": "6.13.0",
|
||||
"raw-body": "2.5.2",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "1.0.0"
|
||||
@@ -2825,9 +2827,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
|
||||
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -3029,9 +3031,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -3459,36 +3461,36 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
|
||||
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
|
||||
"version": "4.21.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
|
||||
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.20.2",
|
||||
"body-parser": "1.20.3",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.6.0",
|
||||
"cookie": "0.7.1",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "1.2.0",
|
||||
"finalhandler": "1.3.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"merge-descriptors": "1.0.1",
|
||||
"merge-descriptors": "1.0.3",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.7",
|
||||
"path-to-regexp": "0.1.10",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "6.11.0",
|
||||
"qs": "6.13.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "0.18.0",
|
||||
"serve-static": "1.15.0",
|
||||
"send": "0.19.0",
|
||||
"serve-static": "1.16.2",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"type-is": "~1.6.18",
|
||||
@@ -3601,12 +3603,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
|
||||
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
||||
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
@@ -6050,9 +6052,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
||||
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-stream": {
|
||||
"version": "2.0.0",
|
||||
@@ -6266,9 +6271,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
|
||||
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
|
||||
"integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
@@ -6436,9 +6444,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
|
||||
"version": "0.1.10",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
|
||||
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
|
||||
},
|
||||
"node_modules/path-type": {
|
||||
"version": "4.0.0",
|
||||
@@ -6646,11 +6654,11 @@
|
||||
]
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.11.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.4"
|
||||
"side-channel": "^1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
@@ -6871,9 +6879,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
|
||||
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
|
||||
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
@@ -6906,6 +6914,14 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||
},
|
||||
"node_modules/send/node_modules/encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/send/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -6917,14 +6933,14 @@
|
||||
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
|
||||
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
|
||||
"version": "1.16.2",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
|
||||
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
|
||||
"dependencies": {
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "0.18.0"
|
||||
"send": "0.19.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
@@ -9603,9 +9619,9 @@
|
||||
}
|
||||
},
|
||||
"body-parser": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
|
||||
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
|
||||
"version": "1.20.3",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
|
||||
"requires": {
|
||||
"bytes": "3.1.2",
|
||||
"content-type": "~1.0.5",
|
||||
@@ -9615,7 +9631,7 @@
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "2.4.1",
|
||||
"qs": "6.11.0",
|
||||
"qs": "6.13.0",
|
||||
"raw-body": "2.5.2",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "1.0.0"
|
||||
@@ -9849,9 +9865,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
|
||||
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="
|
||||
},
|
||||
"cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
@@ -9996,9 +10012,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="
|
||||
},
|
||||
"error-ex": {
|
||||
"version": "1.3.2",
|
||||
@@ -10303,36 +10319,36 @@
|
||||
}
|
||||
},
|
||||
"express": {
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
|
||||
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
|
||||
"version": "4.21.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
|
||||
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
|
||||
"requires": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.20.2",
|
||||
"body-parser": "1.20.3",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.6.0",
|
||||
"cookie": "0.7.1",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "1.2.0",
|
||||
"finalhandler": "1.3.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"merge-descriptors": "1.0.1",
|
||||
"merge-descriptors": "1.0.3",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.7",
|
||||
"path-to-regexp": "0.1.10",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "6.11.0",
|
||||
"qs": "6.13.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "0.18.0",
|
||||
"serve-static": "1.15.0",
|
||||
"send": "0.19.0",
|
||||
"serve-static": "1.16.2",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"type-is": "~1.6.18",
|
||||
@@ -10434,12 +10450,12 @@
|
||||
}
|
||||
},
|
||||
"finalhandler": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
|
||||
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
||||
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
|
||||
"requires": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
@@ -12236,9 +12252,9 @@
|
||||
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="
|
||||
},
|
||||
"merge-descriptors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
||||
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="
|
||||
},
|
||||
"merge-stream": {
|
||||
"version": "2.0.0",
|
||||
@@ -12401,9 +12417,9 @@
|
||||
}
|
||||
},
|
||||
"object-inspect": {
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
|
||||
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ=="
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
|
||||
"integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g=="
|
||||
},
|
||||
"on-finished": {
|
||||
"version": "2.4.1",
|
||||
@@ -12520,9 +12536,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"path-to-regexp": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
|
||||
"version": "0.1.10",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
|
||||
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
|
||||
},
|
||||
"path-type": {
|
||||
"version": "4.0.0",
|
||||
@@ -12664,11 +12680,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.11.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||
"requires": {
|
||||
"side-channel": "^1.0.4"
|
||||
"side-channel": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"queue-microtask": {
|
||||
@@ -12802,9 +12818,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"send": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
|
||||
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
|
||||
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
|
||||
"requires": {
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
@@ -12836,6 +12852,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -12849,14 +12870,14 @@
|
||||
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
|
||||
},
|
||||
"serve-static": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
|
||||
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
|
||||
"version": "1.16.2",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
|
||||
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
|
||||
"requires": {
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "0.18.0"
|
||||
"send": "0.19.0"
|
||||
}
|
||||
},
|
||||
"set-function-length": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mempool-backend",
|
||||
"version": "3.0.0-dev",
|
||||
"version": "3.1.0-dev",
|
||||
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
||||
"license": "GNU Affero General Public License v3.0",
|
||||
"homepage": "https://mempool.space",
|
||||
@@ -42,10 +42,10 @@
|
||||
"@babel/core": "^7.25.2",
|
||||
"@mempool/electrum-client": "1.1.9",
|
||||
"@types/node": "^18.15.3",
|
||||
"axios": "~1.7.2",
|
||||
"axios": "1.7.2",
|
||||
"bitcoinjs-lib": "~6.1.3",
|
||||
"crypto-js": "~4.2.0",
|
||||
"express": "~4.19.2",
|
||||
"express": "~4.21.1",
|
||||
"maxmind": "~4.3.11",
|
||||
"mysql2": "~3.11.0",
|
||||
"rust-gbt": "file:./rust-gbt",
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"INDEXING_BLOCKS_AMOUNT": 14,
|
||||
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
|
||||
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
|
||||
"POOLS_UPDATE_DELAY": 604800,
|
||||
"AUDIT": true,
|
||||
"RUST_GBT": false,
|
||||
"LIMIT_GBT": false,
|
||||
@@ -46,7 +47,8 @@
|
||||
"PASSWORD": "__CORE_RPC_PASSWORD__",
|
||||
"TIMEOUT": 1000,
|
||||
"COOKIE": false,
|
||||
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__"
|
||||
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__",
|
||||
"DEBUG_LOG_PATH": "__CORE_RPC_DEBUG_LOG_PATH__"
|
||||
},
|
||||
"ELECTRUM": {
|
||||
"HOST": "__ELECTRUM_HOST__",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Common } from '../../api/common';
|
||||
import { MempoolTransactionExtended } from '../../mempool.interfaces';
|
||||
import { MempoolTransactionExtended, TransactionExtended } from '../../mempool.interfaces';
|
||||
|
||||
const randomTransactions = require('./test-data/transactions-random.json');
|
||||
const replacedTransactions = require('./test-data/transactions-replaced.json');
|
||||
@@ -10,14 +10,14 @@ describe('Common', () => {
|
||||
describe('RBF', () => {
|
||||
const newTransactions = rbfTransactions.concat(randomTransactions);
|
||||
test('should detect RBF transactions with fast method', () => {
|
||||
const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions);
|
||||
const result: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = Common.findRbfTransactions(newTransactions, replacedTransactions);
|
||||
expect(Object.values(result).length).toEqual(2);
|
||||
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
|
||||
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
|
||||
});
|
||||
|
||||
test('should detect RBF transactions with scalable method', () => {
|
||||
const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions, true);
|
||||
const result: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = Common.findRbfTransactions(newTransactions, replacedTransactions, true);
|
||||
expect(Object.values(result).length).toEqual(2);
|
||||
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
|
||||
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
|
||||
|
||||
@@ -41,8 +41,9 @@ describe('Mempool Backend Config', () => {
|
||||
STDOUT_LOG_MIN_PRIORITY: 'debug',
|
||||
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-v2.json',
|
||||
POOLS_UPDATE_DELAY: 604800,
|
||||
AUDIT: false,
|
||||
RUST_GBT: false,
|
||||
RUST_GBT: true,
|
||||
LIMIT_GBT: false,
|
||||
CPFP_INDEXING: false,
|
||||
MAX_BLOCKS_BULK_QUERY: 0,
|
||||
@@ -73,7 +74,8 @@ describe('Mempool Backend Config', () => {
|
||||
PASSWORD: 'mempool',
|
||||
TIMEOUT: 60000,
|
||||
COOKIE: false,
|
||||
COOKIE_PATH: '/bitcoin/.cookie'
|
||||
COOKIE_PATH: '/bitcoin/.cookie',
|
||||
DEBUG_LOG_PATH: '',
|
||||
});
|
||||
|
||||
expect(config.SECOND_CORE_RPC).toStrictEqual({
|
||||
|
||||
@@ -70,7 +70,7 @@ class AboutRoutes {
|
||||
res.status(500).end();
|
||||
}
|
||||
})
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'services/account/images/:username', async (req, res) => {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'services/account/images/:username/:md5', async (req, res) => {
|
||||
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
|
||||
try {
|
||||
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });
|
||||
|
||||
@@ -2,6 +2,7 @@ import config from '../config';
|
||||
import logger from '../logger';
|
||||
import { MempoolTransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
|
||||
import rbfCache from './rbf-cache';
|
||||
import transactionUtils from './transaction-utils';
|
||||
|
||||
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
|
||||
|
||||
@@ -15,7 +16,8 @@ class Audit {
|
||||
const matches: string[] = []; // present in both mined block and template
|
||||
const added: string[] = []; // present in mined block, not in template
|
||||
const unseen: string[] = []; // present in the mined block, not in our mempool
|
||||
const prioritized: string[] = []; // higher in the block than would be expected by in-band feerate alone
|
||||
let prioritized: string[] = []; // higher in the block than would be expected by in-band feerate alone
|
||||
let deprioritized: string[] = []; // lower in the block than would be expected by in-band feerate alone
|
||||
const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
|
||||
const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
|
||||
const accelerated: string[] = []; // prioritized by the mempool accelerator
|
||||
@@ -133,23 +135,7 @@ class Audit {
|
||||
totalWeight += tx.weight;
|
||||
}
|
||||
|
||||
|
||||
// identify "prioritized" transactions
|
||||
let lastEffectiveRate = 0;
|
||||
// Iterate over the mined template from bottom to top (excluding the coinbase)
|
||||
// Transactions should appear in ascending order of mining priority.
|
||||
for (let i = transactions.length - 1; i > 0; i--) {
|
||||
const blockTx = transactions[i];
|
||||
// If a tx has a lower in-band effective fee rate than the previous tx,
|
||||
// it must have been prioritized out-of-band (in order to have a higher mining priority)
|
||||
// so exclude from the analysis.
|
||||
if ((blockTx.effectiveFeePerVsize || 0) < lastEffectiveRate) {
|
||||
prioritized.push(blockTx.txid);
|
||||
// accelerated txs may or may not have their prioritized fee rate applied, so don't use them as a reference
|
||||
} else if (!isAccelerated[blockTx.txid]) {
|
||||
lastEffectiveRate = blockTx.effectiveFeePerVsize || 0;
|
||||
}
|
||||
}
|
||||
({ prioritized, deprioritized } = transactionUtils.identifyPrioritizedTransactions(transactions, 'effectiveFeePerVsize'));
|
||||
|
||||
// transactions missing from near the end of our template are probably not being censored
|
||||
let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface';
|
||||
import { IBitcoinApi, SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface';
|
||||
import { IEsploraApi } from './esplora-api.interface';
|
||||
|
||||
export interface AbstractBitcoinApi {
|
||||
@@ -23,12 +23,14 @@ export interface AbstractBitcoinApi {
|
||||
$getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
|
||||
$sendRawTransaction(rawTransaction: string): Promise<string>;
|
||||
$testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]>;
|
||||
$submitPackage(rawTransactions: string[], maxfeerate?: number, maxburnamount?: number): Promise<SubmitPackageResult>;
|
||||
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
|
||||
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
|
||||
$getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
|
||||
$getBatchedOutspendsInternal(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
|
||||
$getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]>;
|
||||
$getCoinbaseTx(blockhash: string): Promise<IEsploraApi.Transaction>;
|
||||
$getAddressTransactionSummary(address: string): Promise<IEsploraApi.AddressTxSummary[]>;
|
||||
|
||||
startHealthChecks(): void;
|
||||
getHealthStatus(): HealthCheckHost[];
|
||||
|
||||
@@ -218,3 +218,21 @@ export interface TestMempoolAcceptResult {
|
||||
},
|
||||
['reject-reason']?: string,
|
||||
}
|
||||
|
||||
export interface SubmitPackageResult {
|
||||
package_msg: string;
|
||||
"tx-results": { [wtxid: string]: TxResult };
|
||||
"replaced-transactions"?: string[];
|
||||
}
|
||||
|
||||
export interface TxResult {
|
||||
txid: string;
|
||||
"other-wtxid"?: string;
|
||||
vsize?: number;
|
||||
fees?: {
|
||||
base: number;
|
||||
"effective-feerate"?: number;
|
||||
"effective-includes"?: string[];
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
|
||||
import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface';
|
||||
import { IBitcoinApi, SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface';
|
||||
import { IEsploraApi } from './esplora-api.interface';
|
||||
import blocks from '../blocks';
|
||||
import mempool from '../mempool';
|
||||
@@ -196,6 +196,10 @@ class BitcoinApi implements AbstractBitcoinApi {
|
||||
}
|
||||
}
|
||||
|
||||
$submitPackage(rawTransactions: string[], maxfeerate?: number, maxburnamount?: number): Promise<SubmitPackageResult> {
|
||||
return this.bitcoindClient.submitPackage(rawTransactions, maxfeerate ?? undefined, maxburnamount ?? undefined);
|
||||
}
|
||||
|
||||
async $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
|
||||
const txOut = await this.bitcoindClient.getTxOut(txId, vout, false);
|
||||
return {
|
||||
@@ -251,6 +255,10 @@ class BitcoinApi implements AbstractBitcoinApi {
|
||||
return this.$getRawTransaction(txids[0]);
|
||||
}
|
||||
|
||||
async $getAddressTransactionSummary(address: string): Promise<IEsploraApi.AddressTxSummary[]> {
|
||||
throw new Error('Method getAddressTransactionSummary not supported by the Bitcoin RPC API.');
|
||||
}
|
||||
|
||||
$getEstimatedHashrate(blockHeight: number): Promise<number> {
|
||||
// 120 is the default block span in Core
|
||||
return this.bitcoindClient.getNetworkHashPs(120, blockHeight);
|
||||
@@ -323,6 +331,7 @@ class BitcoinApi implements AbstractBitcoinApi {
|
||||
'witness_v1_taproot': 'v1_p2tr',
|
||||
'nonstandard': 'nonstandard',
|
||||
'multisig': 'multisig',
|
||||
'anchor': 'anchor',
|
||||
'nulldata': 'op_return'
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Application, NextFunction, Request, Response } from 'express';
|
||||
import logger from '../../logger';
|
||||
import bitcoinClient from './bitcoin-client';
|
||||
import config from '../../config';
|
||||
|
||||
/**
|
||||
* Define a set of routes used by the accelerator server
|
||||
@@ -11,15 +12,15 @@ class BitcoinBackendRoutes {
|
||||
|
||||
public initRoutes(app: Application) {
|
||||
app
|
||||
.get('/api/internal/bitcoin-core/' + 'get-mempool-entry', this.disableCache, this.$getMempoolEntry)
|
||||
.post('/api/internal/bitcoin-core/' + 'decode-raw-transaction', this.disableCache, this.$decodeRawTransaction)
|
||||
.get('/api/internal/bitcoin-core/' + 'get-raw-transaction', this.disableCache, this.$getRawTransaction)
|
||||
.post('/api/internal/bitcoin-core/' + 'send-raw-transaction', this.disableCache, this.$sendRawTransaction)
|
||||
.post('/api/internal/bitcoin-core/' + 'test-mempool-accept', this.disableCache, this.$testMempoolAccept)
|
||||
.get('/api/internal/bitcoin-core/' + 'get-mempool-ancestors', this.disableCache, this.$getMempoolAncestors)
|
||||
.get('/api/internal/bitcoin-core/' + 'get-block', this.disableCache, this.$getBlock)
|
||||
.get('/api/internal/bitcoin-core/' + 'get-block-hash', this.disableCache, this.$getBlockHash)
|
||||
.get('/api/internal/bitcoin-core/' + 'get-block-count', this.disableCache, this.$getBlockCount)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-mempool-entry', this.disableCache, this.$getMempoolEntry)
|
||||
.post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'decode-raw-transaction', this.disableCache, this.$decodeRawTransaction)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-raw-transaction', this.disableCache, this.$getRawTransaction)
|
||||
.post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'send-raw-transaction', this.disableCache, this.$sendRawTransaction)
|
||||
.post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'test-mempool-accept', this.disableCache, this.$testMempoolAccept)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-mempool-ancestors', this.disableCache, this.$getMempoolAncestors)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block', this.disableCache, this.$getBlock)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block-hash', this.disableCache, this.$getBlockHash)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block-count', this.disableCache, this.$getBlockCount)
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import websocketHandler from '../websocket-handler';
|
||||
import mempool from '../mempool';
|
||||
import feeApi from '../fee-api';
|
||||
import mempoolBlocks from '../mempool-blocks';
|
||||
import bitcoinApi from './bitcoin-api-factory';
|
||||
import bitcoinApi, { bitcoinCoreApi } from './bitcoin-api-factory';
|
||||
import { Common } from '../common';
|
||||
import backendInfo from '../backend-info';
|
||||
import transactionUtils from '../transaction-utils';
|
||||
@@ -20,6 +20,8 @@ import difficultyAdjustment from '../difficulty-adjustment';
|
||||
import transactionRepository from '../../repositories/TransactionRepository';
|
||||
import rbfCache from '../rbf-cache';
|
||||
import { calculateMempoolTxCpfp } from '../cpfp';
|
||||
import { handleError } from '../../utils/api';
|
||||
import BlocksRepository from '../../repositories/BlocksRepository';
|
||||
|
||||
class BitcoinRoutes {
|
||||
public initRoutes(app: Application) {
|
||||
@@ -47,6 +49,8 @@ class BitcoinRoutes {
|
||||
.post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', this.getBlocksByBulk.bind(this))
|
||||
// Temporarily add txs/package endpoint for all backends until esplora supports it
|
||||
.post(config.MEMPOOL.API_URL_PREFIX + 'txs/package', this.$submitPackage)
|
||||
;
|
||||
|
||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||
@@ -77,8 +81,87 @@ class BitcoinRoutes {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'address-prefix/:prefix', this.getAddressPrefix)
|
||||
;
|
||||
}
|
||||
|
||||
app.get('/api/internal/health', this.generateHealthReport);
|
||||
}
|
||||
|
||||
private async generateHealthReport(req: Request, res: Response): Promise<void> {
|
||||
let response = {
|
||||
core: {
|
||||
height: -1
|
||||
},
|
||||
mempool: {
|
||||
height: -1,
|
||||
indexing: {
|
||||
enabled: Common.indexingEnabled(),
|
||||
blocks: {
|
||||
count: -1,
|
||||
progress: -1,
|
||||
withCpfp: {
|
||||
count: -1,
|
||||
progress: -1,
|
||||
},
|
||||
withCoinStats: {
|
||||
count: -1,
|
||||
progress: -1,
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
// Bitcoin Core
|
||||
let bitcoinCoreIndexes: number | string;
|
||||
try {
|
||||
bitcoinCoreIndexes = await bitcoinClient.getIndexInfo();
|
||||
for (const indexName in bitcoinCoreIndexes as any) {
|
||||
response.core[indexName.replace(/ /g,'_')] = bitcoinCoreIndexes[indexName];
|
||||
}
|
||||
} catch (e: any) {
|
||||
response.core['error'] = e.message;
|
||||
}
|
||||
try {
|
||||
response.core.height = await bitcoinCoreApi.$getBlockHeightTip();
|
||||
} catch (e: any) {
|
||||
response.core['error'] = e.message;
|
||||
}
|
||||
|
||||
// Mempool
|
||||
response.mempool.height = blocks.getCurrentBlockHeight();
|
||||
if (Common.indexingEnabled()) {
|
||||
const indexingBlockAmount = (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT === -1 ? response.core.height : config.MEMPOOL.INDEXING_BLOCKS_AMOUNT);
|
||||
const computeProgress = (count: number): number => Math.min(1.0, Math.round(count / indexingBlockAmount * 100) / 100);
|
||||
response.mempool.indexing.blocks.count = await BlocksRepository.$getIndexedBlockCount();
|
||||
response.mempool.indexing.blocks.progress = computeProgress(response.mempool.indexing.blocks.count);
|
||||
response.mempool.indexing.blocks.withCpfp.count = await BlocksRepository.$getIndexedCpfpBlockCount();
|
||||
response.mempool.indexing.blocks.withCpfp.progress = computeProgress(response.mempool.indexing.blocks.withCpfp.count);
|
||||
response.mempool.indexing.blocks.withCoinStats.count = await BlocksRepository.$getIndexedCoinStatsBlockCount();
|
||||
response.mempool.indexing.blocks.withCoinStats.progress = computeProgress(response.mempool.indexing.blocks.withCoinStats.count);
|
||||
}
|
||||
|
||||
// Esplora
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
try {
|
||||
response['esplora'] = {
|
||||
height: await bitcoinApi.$getBlockHeightTip()
|
||||
};
|
||||
} catch (e: any) {
|
||||
response['esplora'] = {
|
||||
height: -1,
|
||||
error: e.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
res.json(response);
|
||||
|
||||
} catch (e: any) {
|
||||
logger.err(`Unable to generate health report. Exception: ${JSON.stringify(e)}`);
|
||||
logger.err(e.stack);
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private getInitData(req: Request, res: Response) {
|
||||
try {
|
||||
@@ -86,7 +169,7 @@ class BitcoinRoutes {
|
||||
res.set('Content-Type', 'application/json');
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,13 +188,13 @@ class BitcoinRoutes {
|
||||
const result = mempoolBlocks.getMempoolBlocks();
|
||||
res.json(result);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private getTransactionTimes(req: Request, res: Response) {
|
||||
if (!Array.isArray(req.query.txId)) {
|
||||
res.status(500).send('Not an array');
|
||||
handleError(req, res, 500, 'Not an array');
|
||||
return;
|
||||
}
|
||||
const txIds: string[] = [];
|
||||
@@ -128,12 +211,12 @@ class BitcoinRoutes {
|
||||
private async $getBatchedOutspends(req: Request, res: Response): Promise<IEsploraApi.Outspend[][] | void> {
|
||||
const txids_csv = req.query.txids;
|
||||
if (!txids_csv || typeof txids_csv !== 'string') {
|
||||
res.status(500).send('Invalid txids format');
|
||||
handleError(req, res, 500, 'Invalid txids format');
|
||||
return;
|
||||
}
|
||||
const txids = txids_csv.split(',');
|
||||
if (txids.length > 50) {
|
||||
res.status(400).send('Too many txids requested');
|
||||
handleError(req, res, 400, 'Too many txids requested');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -141,13 +224,13 @@ class BitcoinRoutes {
|
||||
const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids);
|
||||
res.json(batchedOutspends);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async $getCpfpInfo(req: Request, res: Response) {
|
||||
if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
|
||||
res.status(501).send(`Invalid transaction ID.`);
|
||||
handleError(req, res, 501, `Invalid transaction ID.`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -180,7 +263,7 @@ class BitcoinRoutes {
|
||||
try {
|
||||
cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
|
||||
} catch (e) {
|
||||
res.status(500).send('failed to get CPFP info');
|
||||
handleError(req, res, 500, 'failed to get CPFP info');
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -209,7 +292,7 @@ class BitcoinRoutes {
|
||||
if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
|
||||
statusCode = 404;
|
||||
}
|
||||
res.status(statusCode).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, statusCode, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,7 +306,7 @@ class BitcoinRoutes {
|
||||
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
|
||||
statusCode = 404;
|
||||
}
|
||||
res.status(statusCode).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, statusCode, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,13 +367,13 @@ class BitcoinRoutes {
|
||||
// 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.`);
|
||||
handleError(req, res, 422, `Psbt had no missing nonWitnessUtxos.`);
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e instanceof Error && new RegExp(notFoundError).test(e.message)) {
|
||||
res.status(404).send(e.message);
|
||||
handleError(req, res, 404, e.message);
|
||||
} else {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -304,7 +387,7 @@ class BitcoinRoutes {
|
||||
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
|
||||
statusCode = 404;
|
||||
}
|
||||
res.status(statusCode).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, statusCode, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,7 +397,7 @@ class BitcoinRoutes {
|
||||
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);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,7 +419,7 @@ class BitcoinRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString());
|
||||
res.json(block);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,7 +429,7 @@ class BitcoinRoutes {
|
||||
res.setHeader('content-type', 'text/plain');
|
||||
res.send(blockHeader);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,10 +440,11 @@ class BitcoinRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
|
||||
res.json(auditSummary);
|
||||
} else {
|
||||
return res.status(404).send(`audit not available`);
|
||||
handleError(req, res, 404, `audit not available`);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,7 +455,8 @@ class BitcoinRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
|
||||
res.json(auditSummary);
|
||||
} else {
|
||||
return res.status(404).send(`transaction audit not available`);
|
||||
handleError(req, res, 404, `transaction audit not available`);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
@@ -388,42 +473,49 @@ class BitcoinRoutes {
|
||||
return await this.getLegacyBlocks(req, res);
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async getBlocksByBulk(req: Request, res: Response) {
|
||||
try {
|
||||
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { // Liquid - Not implemented
|
||||
return res.status(404).send(`This API is only available for Bitcoin networks`);
|
||||
handleError(req, res, 404, `This API is only available for Bitcoin networks`);
|
||||
return;
|
||||
}
|
||||
if (config.MEMPOOL.MAX_BLOCKS_BULK_QUERY <= 0) {
|
||||
return res.status(404).send(`This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`);
|
||||
handleError(req, res, 404, `This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`);
|
||||
return;
|
||||
}
|
||||
if (!Common.indexingEnabled()) {
|
||||
return res.status(404).send(`Indexing is required for this API`);
|
||||
handleError(req, res, 404, `Indexing is required for this API`);
|
||||
return;
|
||||
}
|
||||
|
||||
const from = parseInt(req.params.from, 10);
|
||||
if (!req.params.from || from < 0) {
|
||||
return res.status(400).send(`Parameter 'from' must be a block height (integer)`);
|
||||
handleError(req, res, 400, `Parameter 'from' must be a block height (integer)`);
|
||||
return;
|
||||
}
|
||||
const to = req.params.to === undefined ? await bitcoinApi.$getBlockHeightTip() : parseInt(req.params.to, 10);
|
||||
if (to < 0) {
|
||||
return res.status(400).send(`Parameter 'to' must be a block height (integer)`);
|
||||
handleError(req, res, 400, `Parameter 'to' must be a block height (integer)`);
|
||||
return;
|
||||
}
|
||||
if (from > to) {
|
||||
return res.status(400).send(`Parameter 'to' must be a higher block height than 'from'`);
|
||||
handleError(req, res, 400, `Parameter 'to' must be a higher block height than 'from'`);
|
||||
return;
|
||||
}
|
||||
if ((to - from + 1) > config.MEMPOOL.MAX_BLOCKS_BULK_QUERY) {
|
||||
return res.status(400).send(`You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`);
|
||||
handleError(req, res, 400, `You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`);
|
||||
return;
|
||||
}
|
||||
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(await blocks.$getBlocksBetweenHeight(from, to));
|
||||
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,10 +550,10 @@ class BitcoinRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(returnBlocks);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async getBlockTransactions(req: Request, res: Response) {
|
||||
try {
|
||||
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0);
|
||||
@@ -483,7 +575,7 @@ class BitcoinRoutes {
|
||||
res.json(transactions);
|
||||
} catch (e) {
|
||||
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100);
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,13 +584,13 @@ class BitcoinRoutes {
|
||||
const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10));
|
||||
res.send(blockHash);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async getAddress(req: Request, res: Response) {
|
||||
if (config.MEMPOOL.BACKEND === 'none') {
|
||||
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
|
||||
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -507,15 +599,16 @@ class BitcoinRoutes {
|
||||
res.json(addressData);
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
||||
return res.status(413).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 413, e instanceof Error ? e.message : e);
|
||||
return;
|
||||
}
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async getAddressTransactions(req: Request, res: Response): Promise<void> {
|
||||
if (config.MEMPOOL.BACKEND === 'none') {
|
||||
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
|
||||
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -528,23 +621,23 @@ class BitcoinRoutes {
|
||||
res.json(transactions);
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
||||
res.status(413).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 413, e instanceof Error ? e.message : e);
|
||||
return;
|
||||
}
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async getAddressTransactionSummary(req: Request, res: Response): Promise<void> {
|
||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||
res.status(405).send('Address summary lookups require mempool/electrs backend.');
|
||||
handleError(req, res, 405, 'Address summary lookups require mempool/electrs backend.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private async getScriptHash(req: Request, res: Response) {
|
||||
if (config.MEMPOOL.BACKEND === 'none') {
|
||||
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
|
||||
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -555,15 +648,16 @@ class BitcoinRoutes {
|
||||
res.json(addressData);
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
||||
return res.status(413).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 413, e instanceof Error ? e.message : e);
|
||||
return;
|
||||
}
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async getScriptHashTransactions(req: Request, res: Response): Promise<void> {
|
||||
if (config.MEMPOOL.BACKEND === 'none') {
|
||||
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
|
||||
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -578,16 +672,16 @@ class BitcoinRoutes {
|
||||
res.json(transactions);
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
||||
res.status(413).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 413, e instanceof Error ? e.message : e);
|
||||
return;
|
||||
}
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async getScriptHashTransactionSummary(req: Request, res: Response): Promise<void> {
|
||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||
res.status(405).send('Scripthash summary lookups require mempool/electrs backend.');
|
||||
handleError(req, res, 405, 'Scripthash summary lookups require mempool/electrs backend.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -597,7 +691,7 @@ class BitcoinRoutes {
|
||||
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
|
||||
res.send(blockHash);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -624,7 +718,7 @@ class BitcoinRoutes {
|
||||
const rawMempool = await bitcoinApi.$getRawMempool();
|
||||
res.send(rawMempool);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -632,12 +726,13 @@ class BitcoinRoutes {
|
||||
try {
|
||||
const result = blocks.getCurrentBlockHeight();
|
||||
if (!result) {
|
||||
return res.status(503).send(`Service Temporarily Unavailable`);
|
||||
handleError(req, res, 503, `Service Temporarily Unavailable`);
|
||||
return;
|
||||
}
|
||||
res.setHeader('content-type', 'text/plain');
|
||||
res.send(result.toString());
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -647,7 +742,7 @@ class BitcoinRoutes {
|
||||
res.setHeader('content-type', 'text/plain');
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -657,7 +752,7 @@ class BitcoinRoutes {
|
||||
res.setHeader('content-type', 'application/octet-stream');
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -666,7 +761,7 @@ class BitcoinRoutes {
|
||||
const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
|
||||
res.json(result);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -675,7 +770,7 @@ class BitcoinRoutes {
|
||||
const result = await bitcoinClient.validateAddress(req.params.address);
|
||||
res.json(result);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -688,7 +783,7 @@ class BitcoinRoutes {
|
||||
replaces
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -697,7 +792,7 @@ class BitcoinRoutes {
|
||||
const result = rbfCache.getRbfTrees(false);
|
||||
res.json(result);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -706,7 +801,7 @@ class BitcoinRoutes {
|
||||
const result = rbfCache.getRbfTrees(true);
|
||||
res.json(result);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -719,7 +814,7 @@ class BitcoinRoutes {
|
||||
res.status(204).send();
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -728,7 +823,7 @@ class BitcoinRoutes {
|
||||
const result = await bitcoinApi.$getOutspends(req.params.txId);
|
||||
res.json(result);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -738,10 +833,10 @@ class BitcoinRoutes {
|
||||
if (da) {
|
||||
res.json(da);
|
||||
} else {
|
||||
res.status(503).send(`Service Temporarily Unavailable`);
|
||||
handleError(req, res, 503, `Service Temporarily Unavailable`);
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -752,7 +847,7 @@ class BitcoinRoutes {
|
||||
const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx);
|
||||
res.send(txIdResult);
|
||||
} catch (e: any) {
|
||||
res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
|
||||
handleError(req, res, 400, e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
|
||||
: (e.message || 'Error'));
|
||||
}
|
||||
}
|
||||
@@ -764,7 +859,7 @@ class BitcoinRoutes {
|
||||
const txIdResult = await bitcoinClient.sendRawTransaction(txHex);
|
||||
res.send(txIdResult);
|
||||
} catch (e: any) {
|
||||
res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
|
||||
handleError(req, res, 400, e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
|
||||
: (e.message || 'Error'));
|
||||
}
|
||||
}
|
||||
@@ -776,8 +871,20 @@ class BitcoinRoutes {
|
||||
const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate);
|
||||
res.send(result);
|
||||
} catch (e: any) {
|
||||
res.setHeader('content-type', 'text/plain');
|
||||
res.status(400).send(e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
|
||||
handleError(req, res, 400, e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
|
||||
: (e.message || 'Error'));
|
||||
}
|
||||
}
|
||||
|
||||
private async $submitPackage(req: Request, res: Response) {
|
||||
try {
|
||||
const rawTxs = Common.getTransactionsFromRequest(req);
|
||||
const maxfeerate = parseFloat(req.query.maxfeerate as string);
|
||||
const maxburnamount = parseFloat(req.query.maxburnamount as string);
|
||||
const result = await bitcoinClient.submitPackage(rawTxs, maxfeerate ?? undefined, maxburnamount ?? undefined);
|
||||
res.send(result);
|
||||
} catch (e: any) {
|
||||
handleError(req, res, 400, e.message && e.code ? 'submitpackage RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
|
||||
: (e.message || 'Error'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,4 +179,11 @@ export namespace IEsploraApi {
|
||||
burn_count: number;
|
||||
}
|
||||
|
||||
export interface AddressTxSummary {
|
||||
txid: string;
|
||||
value: number;
|
||||
height: number;
|
||||
time: number;
|
||||
tx_position?: number;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-fact
|
||||
import { IEsploraApi } from './esplora-api.interface';
|
||||
import logger from '../../logger';
|
||||
import { Common } from '../common';
|
||||
import { TestMempoolAcceptResult } from './bitcoin-api.interface';
|
||||
import { SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface';
|
||||
|
||||
interface FailoverHost {
|
||||
host: string,
|
||||
@@ -305,7 +305,7 @@ class ElectrsApi implements AbstractBitcoinApi {
|
||||
}
|
||||
|
||||
$getAddress(address: string): Promise<IEsploraApi.Address> {
|
||||
throw new Error('Method getAddress not implemented.');
|
||||
return this.failoverRouter.$get<IEsploraApi.Address>('/address/' + address);
|
||||
}
|
||||
|
||||
$getAddressTransactions(address: string, txId?: string): Promise<IEsploraApi.Transaction[]> {
|
||||
@@ -332,6 +332,10 @@ class ElectrsApi implements AbstractBitcoinApi {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
$submitPackage(rawTransactions: string[]): Promise<SubmitPackageResult> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
|
||||
return this.failoverRouter.$get<IEsploraApi.Outspend>('/tx/' + txId + '/outspend/' + vout);
|
||||
}
|
||||
@@ -357,6 +361,10 @@ class ElectrsApi implements AbstractBitcoinApi {
|
||||
return this.failoverRouter.$get<IEsploraApi.Transaction>('/tx/' + txid);
|
||||
}
|
||||
|
||||
async $getAddressTransactionSummary(address: string): Promise<IEsploraApi.AddressTxSummary[]> {
|
||||
return this.failoverRouter.$get<IEsploraApi.AddressTxSummary[]>('/address/' + address + '/txs/summary');
|
||||
}
|
||||
|
||||
public startHealthChecks(): void {
|
||||
this.failoverRouter.startHealthChecks();
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp';
|
||||
import mempool from './mempool';
|
||||
import CpfpRepository from '../repositories/CpfpRepository';
|
||||
import accelerationApi from './services/acceleration';
|
||||
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
|
||||
|
||||
class Blocks {
|
||||
private blocks: BlockExtended[] = [];
|
||||
@@ -219,10 +220,10 @@ class Blocks {
|
||||
};
|
||||
}
|
||||
|
||||
public summarizeBlockTransactions(hash: string, transactions: TransactionExtended[]): BlockSummary {
|
||||
public summarizeBlockTransactions(hash: string, height: number, transactions: TransactionExtended[]): BlockSummary {
|
||||
return {
|
||||
id: hash,
|
||||
transactions: Common.classifyTransactions(transactions),
|
||||
transactions: Common.classifyTransactions(transactions, height),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -342,7 +343,12 @@ class Blocks {
|
||||
id: pool.uniqueId,
|
||||
name: pool.name,
|
||||
slug: pool.slug,
|
||||
minerNames: null,
|
||||
};
|
||||
|
||||
if (extras.pool.name === 'OCEAN') {
|
||||
extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw);
|
||||
}
|
||||
}
|
||||
|
||||
extras.matchRate = null;
|
||||
@@ -406,8 +412,16 @@ class Blocks {
|
||||
}
|
||||
|
||||
try {
|
||||
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
|
||||
const currentBlockHeight = blockchainInfo.blocks;
|
||||
let indexingBlockAmount = Math.min(config.MEMPOOL.INDEXING_BLOCKS_AMOUNT, currentBlockHeight);
|
||||
if (indexingBlockAmount <= -1) {
|
||||
indexingBlockAmount = currentBlockHeight + 1;
|
||||
}
|
||||
const lastBlockToIndex = Math.max(0, currentBlockHeight - indexingBlockAmount + 1);
|
||||
|
||||
// Get all indexed block hash
|
||||
const indexedBlocks = await blocksRepository.$getIndexedBlocks();
|
||||
const indexedBlocks = (await blocksRepository.$getIndexedBlocks()).filter(block => block.height >= lastBlockToIndex);
|
||||
const indexedBlockSummariesHashesArray = await BlocksSummariesRepository.$getIndexedSummariesId();
|
||||
|
||||
const indexedBlockSummariesHashes = {}; // Use a map for faster seek during the indexing loop
|
||||
@@ -616,7 +630,7 @@ class Blocks {
|
||||
// add CPFP
|
||||
const cpfpSummary = calculateGoodBlockCpfp(height, txs, []);
|
||||
// classify
|
||||
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
|
||||
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions);
|
||||
await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 2);
|
||||
if (unclassifiedBlocks[height].version < 2 && targetSummaryVersion === 2) {
|
||||
const cpfpClusters = await CpfpRepository.$getClustersAt(height);
|
||||
@@ -653,7 +667,7 @@ class Blocks {
|
||||
}
|
||||
const cpfpSummary = calculateGoodBlockCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as MempoolTransactionExtended[], []);
|
||||
// classify
|
||||
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
|
||||
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions);
|
||||
const classifiedTxMap: { [txid: string]: TransactionClassified } = {};
|
||||
for (const tx of classifiedTxs) {
|
||||
classifiedTxMap[tx.txid] = tx;
|
||||
@@ -912,7 +926,7 @@ class Blocks {
|
||||
}
|
||||
const cpfpSummary: CpfpSummary = calculateGoodBlockCpfp(block.height, transactions, accelerations.map(a => ({ txid: a.txid, max_bid: a.feeDelta })));
|
||||
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
|
||||
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions);
|
||||
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, block.height, cpfpSummary.transactions);
|
||||
this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`);
|
||||
|
||||
if (Common.indexingEnabled()) {
|
||||
@@ -1169,7 +1183,7 @@ class Blocks {
|
||||
transactions: cpfpSummary.transactions.map(tx => {
|
||||
let flags: number = 0;
|
||||
try {
|
||||
flags = Common.getTransactionFlags(tx);
|
||||
flags = Common.getTransactionFlags(tx, height);
|
||||
} catch (e) {
|
||||
logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
@@ -1188,7 +1202,7 @@ class Blocks {
|
||||
} else {
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||
summary = this.summarizeBlockTransactions(hash, txs);
|
||||
summary = this.summarizeBlockTransactions(hash, height || 0, txs);
|
||||
summaryVersion = 1;
|
||||
} else {
|
||||
// Call Core RPC
|
||||
@@ -1324,7 +1338,7 @@ class Blocks {
|
||||
let summaryVersion = 0;
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||
summary = this.summarizeBlockTransactions(cleanBlock.hash, txs);
|
||||
summary = this.summarizeBlockTransactions(cleanBlock.hash, cleanBlock.height, txs);
|
||||
summaryVersion = 1;
|
||||
} else {
|
||||
// Call Core RPC
|
||||
|
||||
@@ -10,7 +10,6 @@ import logger from '../logger';
|
||||
import { getVarIntLength, opcodes, parseMultisigScript } from '../utils/bitcoin-script';
|
||||
|
||||
// Bitcoin Core default policy settings
|
||||
const TX_MAX_STANDARD_VERSION = 2;
|
||||
const MAX_STANDARD_TX_WEIGHT = 400_000;
|
||||
const MAX_BLOCK_SIGOPS_COST = 80_000;
|
||||
const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5);
|
||||
@@ -80,8 +79,8 @@ export class Common {
|
||||
return arr;
|
||||
}
|
||||
|
||||
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: MempoolTransactionExtended[] } {
|
||||
const matches: { [txid: string]: MempoolTransactionExtended[] } = {};
|
||||
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} {
|
||||
const matches: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = {};
|
||||
|
||||
// For small N, a naive nested loop is extremely fast, but it doesn't scale
|
||||
if (added.length < 1000 && deleted.length < 50 && !forceScalable) {
|
||||
@@ -96,7 +95,7 @@ export class Common {
|
||||
addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
|
||||
});
|
||||
if (foundMatches?.length) {
|
||||
matches[addedTx.txid] = [...new Set(foundMatches)];
|
||||
matches[addedTx.txid] = { replaced: [...new Set(foundMatches)], replacedBy: addedTx };
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -124,7 +123,7 @@ export class Common {
|
||||
foundMatches.add(deletedTx);
|
||||
}
|
||||
if (foundMatches.size) {
|
||||
matches[addedTx.txid] = [...foundMatches];
|
||||
matches[addedTx.txid] = { replaced: [...foundMatches], replacedBy: addedTx };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,17 +138,17 @@ export class Common {
|
||||
const replaced: Set<MempoolTransactionExtended> = new Set();
|
||||
for (let i = 0; i < tx.vin.length; i++) {
|
||||
const vin = tx.vin[i];
|
||||
const match = spendMap.get(`${vin.txid}:${vin.vout}`);
|
||||
const key = `${vin.txid}:${vin.vout}`;
|
||||
const match = spendMap.get(key);
|
||||
if (match && match.txid !== tx.txid) {
|
||||
replaced.add(match);
|
||||
// remove this tx from the spendMap
|
||||
// prevents the same tx being replaced more than once
|
||||
for (const replacedVin of match.vin) {
|
||||
const key = `${replacedVin.txid}:${replacedVin.vout}`;
|
||||
spendMap.delete(key);
|
||||
const replacedKey = `${replacedVin.txid}:${replacedVin.vout}`;
|
||||
spendMap.delete(replacedKey);
|
||||
}
|
||||
}
|
||||
const key = `${vin.txid}:${vin.vout}`;
|
||||
spendMap.delete(key);
|
||||
}
|
||||
if (replaced.size) {
|
||||
@@ -200,10 +199,13 @@ export class Common {
|
||||
*
|
||||
* returns true early if any standardness rule is violated, otherwise false
|
||||
* (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced)
|
||||
*
|
||||
* As standardness rules change, we'll need to apply the rules in force *at the time* to older blocks.
|
||||
* For now, just pull out individual rules into versioned functions where necessary.
|
||||
*/
|
||||
static isNonStandard(tx: TransactionExtended): boolean {
|
||||
static isNonStandard(tx: TransactionExtended, height?: number): boolean {
|
||||
// version
|
||||
if (tx.version > TX_MAX_STANDARD_VERSION) {
|
||||
if (this.isNonStandardVersion(tx, height)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -250,6 +252,8 @@ export class Common {
|
||||
}
|
||||
} else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
|
||||
return true;
|
||||
} else if (this.isNonStandardAnchor(tx, height)) {
|
||||
return true;
|
||||
}
|
||||
// TODO: bad-witness-nonstandard
|
||||
}
|
||||
@@ -335,6 +339,49 @@ export class Common {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Individual versioned standardness rules
|
||||
|
||||
static V3_STANDARDNESS_ACTIVATION_HEIGHT = {
|
||||
'testnet4': 42_000,
|
||||
'testnet': 2_900_000,
|
||||
'signet': 211_000,
|
||||
'': 863_500,
|
||||
};
|
||||
static isNonStandardVersion(tx: TransactionExtended, height?: number): boolean {
|
||||
let TX_MAX_STANDARD_VERSION = 3;
|
||||
if (
|
||||
height != null
|
||||
&& this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
|
||||
&& height <= this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
|
||||
) {
|
||||
// V3 transactions were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
|
||||
TX_MAX_STANDARD_VERSION = 2;
|
||||
}
|
||||
|
||||
if (tx.version > TX_MAX_STANDARD_VERSION) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT = {
|
||||
'testnet4': 42_000,
|
||||
'testnet': 2_900_000,
|
||||
'signet': 211_000,
|
||||
'': 863_500,
|
||||
};
|
||||
static isNonStandardAnchor(tx: TransactionExtended, height?: number): boolean {
|
||||
if (
|
||||
height != null
|
||||
&& this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
|
||||
&& height <= this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
|
||||
) {
|
||||
// anchor outputs were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static getNonWitnessSize(tx: TransactionExtended): number {
|
||||
let weight = tx.weight;
|
||||
let hasWitness = false;
|
||||
@@ -415,7 +462,7 @@ export class Common {
|
||||
return flags;
|
||||
}
|
||||
|
||||
static getTransactionFlags(tx: TransactionExtended): number {
|
||||
static getTransactionFlags(tx: TransactionExtended, height?: number): number {
|
||||
let flags = tx.flags ? BigInt(tx.flags) : 0n;
|
||||
|
||||
// Update variable flags (CPFP, RBF)
|
||||
@@ -548,7 +595,7 @@ export class Common {
|
||||
if (hasFakePubkey) {
|
||||
flags |= TransactionFlags.fake_pubkey;
|
||||
}
|
||||
|
||||
|
||||
// fast but bad heuristic to detect possible coinjoins
|
||||
// (at least 5 inputs and 5 outputs, less than half of which are unique amounts, with no address reuse)
|
||||
const addressReuse = Object.keys(reusedOutputAddresses).reduce((acc, key) => Math.max(acc, (reusedInputAddresses[key] || 0) + (reusedOutputAddresses[key] || 0)), 0) > 1;
|
||||
@@ -564,17 +611,17 @@ export class Common {
|
||||
flags |= TransactionFlags.batch_payout;
|
||||
}
|
||||
|
||||
if (this.isNonStandard(tx)) {
|
||||
if (this.isNonStandard(tx, height)) {
|
||||
flags |= TransactionFlags.nonstandard;
|
||||
}
|
||||
|
||||
return Number(flags);
|
||||
}
|
||||
|
||||
static classifyTransaction(tx: TransactionExtended): TransactionClassified {
|
||||
static classifyTransaction(tx: TransactionExtended, height?: number): TransactionClassified {
|
||||
let flags = 0;
|
||||
try {
|
||||
flags = Common.getTransactionFlags(tx);
|
||||
flags = Common.getTransactionFlags(tx, height);
|
||||
} catch (e) {
|
||||
logger.warn('Failed to add classification flags to transaction: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
@@ -585,8 +632,8 @@ export class Common {
|
||||
};
|
||||
}
|
||||
|
||||
static classifyTransactions(txs: TransactionExtended[]): TransactionClassified[] {
|
||||
return txs.map(Common.classifyTransaction);
|
||||
static classifyTransactions(txs: TransactionExtended[], height?: number): TransactionClassified[] {
|
||||
return txs.map(tx => Common.classifyTransaction(tx, height));
|
||||
}
|
||||
|
||||
static stripTransaction(tx: TransactionExtended): TransactionStripped {
|
||||
|
||||
@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
|
||||
import { RowDataPacket } from 'mysql2';
|
||||
|
||||
class DatabaseMigration {
|
||||
private static currentVersion = 81;
|
||||
private static currentVersion = 83;
|
||||
private queryTimeout = 3600_000;
|
||||
private statisticsAddedIndexed = false;
|
||||
private uniqueLogs: string[] = [];
|
||||
@@ -653,9 +653,11 @@ class DatabaseMigration {
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `TRY` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `ZAR` float DEFAULT "-1"');
|
||||
|
||||
await this.$executeQuery('TRUNCATE hashrates');
|
||||
await this.$executeQuery('TRUNCATE difficulty_adjustments');
|
||||
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
|
||||
if (isBitcoin === true) {
|
||||
await this.$executeQuery('TRUNCATE hashrates');
|
||||
await this.$executeQuery('TRUNCATE difficulty_adjustments');
|
||||
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
|
||||
}
|
||||
|
||||
await this.updateToSchemaVersion(75);
|
||||
}
|
||||
@@ -692,12 +694,22 @@ class DatabaseMigration {
|
||||
await this.updateToSchemaVersion(80);
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 81) {
|
||||
if (databaseSchemaVersion < 81 && isBitcoin === true) {
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD version INT NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `version` (`version`)');
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"');
|
||||
await this.updateToSchemaVersion(81);
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 82 && isBitcoin === true && config.MEMPOOL.NETWORK === 'mainnet') {
|
||||
await this.$fixBadV1AuditBlocks();
|
||||
await this.updateToSchemaVersion(82);
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 83 && isBitcoin === true) {
|
||||
await this.$executeQuery('ALTER TABLE `blocks` ADD first_seen datetime(6) DEFAULT NULL');
|
||||
await this.updateToSchemaVersion(83);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1312,6 +1324,28 @@ class DatabaseMigration {
|
||||
logger.warn(`Failed to migrate cpfp transaction data`);
|
||||
}
|
||||
}
|
||||
|
||||
private async $fixBadV1AuditBlocks(): Promise<void> {
|
||||
const badBlocks = [
|
||||
'000000000000000000011ad49227fc8c9ba0ca96ad2ebce41a862f9a244478dc',
|
||||
'000000000000000000010ac1f68b3080153f2826ffddc87ceffdd68ed97d6960',
|
||||
'000000000000000000024cbdafeb2660ae8bd2947d166e7fe15d1689e86b2cf7',
|
||||
'00000000000000000002e1dbfbf6ae057f331992a058b822644b368034f87286',
|
||||
'0000000000000000000019973b2778f08ad6d21e083302ff0833d17066921ebb',
|
||||
];
|
||||
|
||||
for (const hash of badBlocks) {
|
||||
try {
|
||||
await this.$executeQuery(`
|
||||
UPDATE blocks_audits
|
||||
SET prioritized_txs = '[]'
|
||||
WHERE hash = '${hash}'
|
||||
`, true);
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new DatabaseMigration();
|
||||
|
||||
@@ -257,6 +257,7 @@ class DiskCache {
|
||||
trees: rbfData.rbf.trees,
|
||||
expiring: rbfData.rbf.expiring.map(([txid, value]) => ({ key: txid, value })),
|
||||
mempool: memPool.getMempool(),
|
||||
spendMap: memPool.getSpendMap(),
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import config from '../../config';
|
||||
import { Application, Request, Response } from 'express';
|
||||
import channelsApi from './channels.api';
|
||||
import { handleError } from '../../utils/api';
|
||||
|
||||
class ChannelsRoutes {
|
||||
constructor() { }
|
||||
@@ -22,7 +23,7 @@ class ChannelsRoutes {
|
||||
const channels = await channelsApi.$searchChannelsById(req.params.search);
|
||||
res.json(channels);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +39,7 @@ class ChannelsRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(channel);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,11 +54,11 @@ class ChannelsRoutes {
|
||||
const status: string = typeof req.query.status === 'string' ? req.query.status : '';
|
||||
|
||||
if (index < -1) {
|
||||
res.status(400).send('Invalid index');
|
||||
handleError(req, res, 400, 'Invalid index');
|
||||
return;
|
||||
}
|
||||
if (['open', 'active', 'closed'].includes(status) === false) {
|
||||
res.status(400).send('Invalid status');
|
||||
handleError(req, res, 400, 'Invalid status');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -69,14 +70,14 @@ class ChannelsRoutes {
|
||||
res.header('X-Total-Count', channelsCount.toString());
|
||||
res.json(channels);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async $getChannelsByTransactionIds(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
if (!Array.isArray(req.query.txId)) {
|
||||
res.status(400).send('Not an array');
|
||||
handleError(req, res, 400, 'Not an array');
|
||||
return;
|
||||
}
|
||||
const txIds: string[] = [];
|
||||
@@ -107,7 +108,7 @@ class ChannelsRoutes {
|
||||
|
||||
res.json(result);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +120,7 @@ class ChannelsRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(channels);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +133,7 @@ class ChannelsRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(channels);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Application, Request, Response } from 'express';
|
||||
import nodesApi from './nodes.api';
|
||||
import channelsApi from './channels.api';
|
||||
import statisticsApi from './statistics.api';
|
||||
import { handleError } from '../../utils/api';
|
||||
|
||||
class GeneralLightningRoutes {
|
||||
constructor() { }
|
||||
|
||||
@@ -27,7 +29,7 @@ class GeneralLightningRoutes {
|
||||
channels: channels,
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +43,7 @@ class GeneralLightningRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(statistics);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +52,7 @@ class GeneralLightningRoutes {
|
||||
const statistics = await statisticsApi.$getLatestStatistics();
|
||||
res.json(statistics);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Application, Request, Response } from 'express';
|
||||
import nodesApi from './nodes.api';
|
||||
import DB from '../../database';
|
||||
import { INodesRanking } from '../../mempool.interfaces';
|
||||
import { handleError } from '../../utils/api';
|
||||
|
||||
class NodesRoutes {
|
||||
constructor() { }
|
||||
@@ -31,7 +32,7 @@ class NodesRoutes {
|
||||
const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search);
|
||||
res.json(nodes);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,13 +182,13 @@ class NodesRoutes {
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(nodes);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +196,7 @@ class NodesRoutes {
|
||||
try {
|
||||
const node = await nodesApi.$getNode(req.params.public_key);
|
||||
if (!node) {
|
||||
res.status(404).send('Node not found');
|
||||
handleError(req, res, 404, 'Node not found');
|
||||
return;
|
||||
}
|
||||
res.header('Pragma', 'public');
|
||||
@@ -203,7 +204,7 @@ class NodesRoutes {
|
||||
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);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,7 +216,7 @@ class NodesRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(statistics);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,7 +224,7 @@ class NodesRoutes {
|
||||
try {
|
||||
const node = await nodesApi.$getFeeHistogram(req.params.public_key);
|
||||
if (!node) {
|
||||
res.status(404).send('Node not found');
|
||||
handleError(req, res, 404, 'Node not found');
|
||||
return;
|
||||
}
|
||||
res.header('Pragma', 'public');
|
||||
@@ -231,7 +232,7 @@ class NodesRoutes {
|
||||
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);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +248,7 @@ class NodesRoutes {
|
||||
topByChannels: topChannelsNodes,
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,7 +260,7 @@ class NodesRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(topCapacityNodes);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,7 +272,7 @@ class NodesRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(topCapacityNodes);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,7 +284,7 @@ class NodesRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(topCapacityNodes);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,7 +296,7 @@ class NodesRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
|
||||
res.json(nodesPerAs);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,7 +308,7 @@ class NodesRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
|
||||
res.json(worldNodes);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,7 +323,7 @@ class NodesRoutes {
|
||||
);
|
||||
|
||||
if (country.length === 0) {
|
||||
res.status(404).send(`This country does not exist or does not host any lightning nodes on clearnet`);
|
||||
handleError(req, res, 404, `This country does not exist or does not host any lightning nodes on clearnet`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -335,7 +336,7 @@ class NodesRoutes {
|
||||
nodes: nodes,
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,7 +350,7 @@ class NodesRoutes {
|
||||
);
|
||||
|
||||
if (isp.length === 0) {
|
||||
res.status(404).send(`This ISP does not exist or does not host any lightning nodes on clearnet`);
|
||||
handleError(req, res, 404, `This ISP does not exist or does not host any lightning nodes on clearnet`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -362,7 +363,7 @@ class NodesRoutes {
|
||||
nodes: nodes,
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,7 +375,7 @@ class NodesRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
|
||||
res.json(nodesPerAs);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Application, Request, Response } from 'express';
|
||||
import config from '../../config';
|
||||
import elementsParser from './elements-parser';
|
||||
import icons from './icons';
|
||||
import { handleError } from '../../utils/api';
|
||||
|
||||
class LiquidRoutes {
|
||||
public initRoutes(app: Application) {
|
||||
@@ -42,7 +43,7 @@ class LiquidRoutes {
|
||||
res.setHeader('content-length', result.length);
|
||||
res.send(result);
|
||||
} else {
|
||||
res.status(404).send('Asset icon not found');
|
||||
handleError(req, res, 404, 'Asset icon not found');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +52,7 @@ class LiquidRoutes {
|
||||
if (result) {
|
||||
res.json(result);
|
||||
} else {
|
||||
res.status(404).send('Asset icons not found');
|
||||
handleError(req, res, 404, 'Asset icons not found');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +83,7 @@ class LiquidRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
|
||||
res.json(pegs);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +95,7 @@ class LiquidRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
|
||||
res.json(reserves);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +107,7 @@ class LiquidRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||
res.json(currentSupply);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +119,7 @@ class LiquidRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||
res.json(currentReserves);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +131,7 @@ class LiquidRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||
res.json(auditStatus);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +143,7 @@ class LiquidRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||
res.json(federationAddresses);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +155,7 @@ class LiquidRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||
res.json(federationAddresses);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +167,7 @@ class LiquidRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||
res.json(federationUtxos);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +179,7 @@ class LiquidRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||
res.json(expiredUtxos);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +191,7 @@ class LiquidRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||
res.json(federationUtxos);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,7 +203,7 @@ class LiquidRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||
res.json(emergencySpentUtxos);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +215,7 @@ class LiquidRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||
res.json(emergencySpentUtxos);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,7 +227,7 @@ class LiquidRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||
res.json(recentPegs);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,7 +239,7 @@ class LiquidRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||
res.json(pegsVolume);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,7 +251,7 @@ class LiquidRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||
res.json(pegsCount);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -369,7 +369,7 @@ class MempoolBlocks {
|
||||
const lastBlockIndex = blocks.length - 1;
|
||||
let hasBlockStack = blocks.length >= 8;
|
||||
let stackWeight;
|
||||
let feeStatsCalculator: OnlineFeeStatsCalculator | void;
|
||||
let feeStatsCalculator: OnlineFeeStatsCalculator | null = null;
|
||||
if (hasBlockStack) {
|
||||
if (blockWeights && blockWeights[7] !== null) {
|
||||
stackWeight = blockWeights[7];
|
||||
@@ -380,28 +380,36 @@ class MempoolBlocks {
|
||||
feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5, [10, 20, 30, 40, 50, 60, 70, 80, 90]);
|
||||
}
|
||||
|
||||
const ancestors: Ancestor[] = [];
|
||||
const descendants: Ancestor[] = [];
|
||||
let ancestor: MempoolTransactionExtended
|
||||
for (const cluster of clusters) {
|
||||
for (const memberTxid of cluster) {
|
||||
const mempoolTx = mempool[memberTxid];
|
||||
if (mempoolTx) {
|
||||
const ancestors: Ancestor[] = [];
|
||||
const descendants: Ancestor[] = [];
|
||||
// ugly micro-optimization to avoid allocating new arrays
|
||||
ancestors.length = 0;
|
||||
descendants.length = 0;
|
||||
let matched = false;
|
||||
cluster.forEach(txid => {
|
||||
ancestor = mempool[txid];
|
||||
if (txid === memberTxid) {
|
||||
matched = true;
|
||||
} else {
|
||||
if (!mempool[txid]) {
|
||||
if (!ancestor) {
|
||||
console.log('txid missing from mempool! ', txid, candidates?.txs[txid]);
|
||||
return;
|
||||
}
|
||||
const relative = {
|
||||
txid: txid,
|
||||
fee: mempool[txid].fee,
|
||||
weight: (mempool[txid].adjustedVsize * 4),
|
||||
fee: ancestor.fee,
|
||||
weight: (ancestor.adjustedVsize * 4),
|
||||
};
|
||||
if (matched) {
|
||||
descendants.push(relative);
|
||||
mempoolTx.lastBoosted = Math.max(mempoolTx.lastBoosted || 0, mempool[txid].firstSeen || 0);
|
||||
if (!mempoolTx.lastBoosted || (ancestor.firstSeen && ancestor.firstSeen > mempoolTx.lastBoosted)) {
|
||||
mempoolTx.lastBoosted = ancestor.firstSeen;
|
||||
}
|
||||
} else {
|
||||
ancestors.push(relative);
|
||||
}
|
||||
@@ -410,7 +418,20 @@ class MempoolBlocks {
|
||||
if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) {
|
||||
mempoolTx.cpfpDirty = true;
|
||||
}
|
||||
Object.assign(mempoolTx, {ancestors, descendants, bestDescendant: null, cpfpChecked: true});
|
||||
// ugly micro-optimization to avoid allocating new arrays or objects
|
||||
if (mempoolTx.ancestors) {
|
||||
mempoolTx.ancestors.length = 0;
|
||||
} else {
|
||||
mempoolTx.ancestors = [];
|
||||
}
|
||||
if (mempoolTx.descendants) {
|
||||
mempoolTx.descendants.length = 0;
|
||||
} else {
|
||||
mempoolTx.descendants = [];
|
||||
}
|
||||
mempoolTx.ancestors.push(...ancestors);
|
||||
mempoolTx.descendants.push(...descendants);
|
||||
mempoolTx.cpfpChecked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -420,7 +441,10 @@ class MempoolBlocks {
|
||||
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
|
||||
// update this thread's mempool with the results
|
||||
let mempoolTx: MempoolTransactionExtended;
|
||||
const mempoolBlocks: MempoolBlockWithTransactions[] = blocks.map((block, blockIndex) => {
|
||||
let acceleration: Acceleration;
|
||||
const mempoolBlocks: MempoolBlockWithTransactions[] = [];
|
||||
for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
|
||||
const block = blocks[blockIndex];
|
||||
let totalSize = 0;
|
||||
let totalVsize = 0;
|
||||
let totalWeight = 0;
|
||||
@@ -436,7 +460,8 @@ class MempoolBlocks {
|
||||
}
|
||||
}
|
||||
|
||||
for (const txid of block) {
|
||||
for (let i = 0; i < block.length; i++) {
|
||||
const txid = block[i];
|
||||
if (txid) {
|
||||
mempoolTx = mempool[txid];
|
||||
// save position in projected blocks
|
||||
@@ -445,30 +470,37 @@ class MempoolBlocks {
|
||||
vsize: totalVsize + (mempoolTx.vsize / 2),
|
||||
};
|
||||
|
||||
const acceleration = accelerations[txid];
|
||||
if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
|
||||
if (!mempoolTx.acceleration) {
|
||||
mempoolTx.cpfpDirty = true;
|
||||
}
|
||||
mempoolTx.acceleration = true;
|
||||
mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools;
|
||||
mempoolTx.acceleratedAt = acceleration?.added;
|
||||
mempoolTx.feeDelta = acceleration?.feeDelta;
|
||||
for (const ancestor of mempoolTx.ancestors || []) {
|
||||
if (!mempool[ancestor.txid].acceleration) {
|
||||
mempool[ancestor.txid].cpfpDirty = true;
|
||||
if (txid in accelerations) {
|
||||
acceleration = accelerations[txid];
|
||||
if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
|
||||
if (!mempoolTx.acceleration) {
|
||||
mempoolTx.cpfpDirty = true;
|
||||
}
|
||||
mempoolTx.acceleration = true;
|
||||
mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools;
|
||||
mempoolTx.acceleratedAt = acceleration?.added;
|
||||
mempoolTx.feeDelta = acceleration?.feeDelta;
|
||||
for (const ancestor of mempoolTx.ancestors || []) {
|
||||
if (!mempool[ancestor.txid].acceleration) {
|
||||
mempool[ancestor.txid].cpfpDirty = true;
|
||||
}
|
||||
mempool[ancestor.txid].acceleration = true;
|
||||
mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy;
|
||||
mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt;
|
||||
mempool[ancestor.txid].feeDelta = mempoolTx.feeDelta;
|
||||
isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy;
|
||||
}
|
||||
} else {
|
||||
if (mempoolTx.acceleration) {
|
||||
mempoolTx.cpfpDirty = true;
|
||||
delete mempoolTx.acceleration;
|
||||
}
|
||||
mempool[ancestor.txid].acceleration = true;
|
||||
mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy;
|
||||
mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt;
|
||||
mempool[ancestor.txid].feeDelta = mempoolTx.feeDelta;
|
||||
isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy;
|
||||
}
|
||||
} else {
|
||||
if (mempoolTx.acceleration) {
|
||||
mempoolTx.cpfpDirty = true;
|
||||
delete mempoolTx.acceleration;
|
||||
}
|
||||
delete mempoolTx.acceleration;
|
||||
}
|
||||
|
||||
// online calculation of stack-of-blocks fee stats
|
||||
@@ -486,7 +518,7 @@ class MempoolBlocks {
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.dataToMempoolBlocks(
|
||||
mempoolBlocks[blockIndex] = this.dataToMempoolBlocks(
|
||||
block,
|
||||
transactions,
|
||||
totalSize,
|
||||
@@ -494,7 +526,7 @@ class MempoolBlocks {
|
||||
totalFees,
|
||||
(hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) ? feeStatsCalculator.getRawFeeStats() : undefined,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
if (saveResults) {
|
||||
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks);
|
||||
|
||||
@@ -10,6 +10,7 @@ import bitcoinClient from './bitcoin/bitcoin-client';
|
||||
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
||||
import rbfCache from './rbf-cache';
|
||||
import { Acceleration } from './services/acceleration';
|
||||
import accelerationApi from './services/acceleration';
|
||||
import redisCache from './redis-cache';
|
||||
import blocks from './blocks';
|
||||
|
||||
@@ -19,12 +20,13 @@ class Mempool {
|
||||
private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {};
|
||||
private mempoolCandidates: { [txid: string ]: boolean } = {};
|
||||
private spendMap = new Map<string, MempoolTransactionExtended>();
|
||||
private recentlyDeleted: MempoolTransactionExtended[][] = []; // buffer of transactions deleted in recent mempool updates
|
||||
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
|
||||
maxmempool: 300000000, mempoolminfee: Common.isLiquid() ? 0.00000100 : 0.00001000, minrelaytxfee: Common.isLiquid() ? 0.00000100 : 0.00001000 };
|
||||
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
|
||||
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void) | undefined;
|
||||
deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void) | undefined;
|
||||
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[],
|
||||
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], candidates?: GbtCandidates) => Promise<void>) | undefined;
|
||||
deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[], candidates?: GbtCandidates) => Promise<void>) | undefined;
|
||||
|
||||
private accelerations: { [txId: string]: Acceleration } = {};
|
||||
private accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {};
|
||||
@@ -74,12 +76,12 @@ class Mempool {
|
||||
}
|
||||
|
||||
public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
|
||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void): void {
|
||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void): void {
|
||||
this.mempoolChangedCallback = fn;
|
||||
}
|
||||
|
||||
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number,
|
||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[],
|
||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[],
|
||||
candidates?: GbtCandidates) => Promise<void>): void {
|
||||
this.$asyncMempoolChangedCallback = fn;
|
||||
}
|
||||
@@ -206,7 +208,7 @@ class Mempool {
|
||||
return txTimes;
|
||||
}
|
||||
|
||||
public async $updateMempool(transactions: string[], accelerations: Acceleration[] | null, minFeeMempool: string[], minFeeTip: number, pollRate: number): Promise<void> {
|
||||
public async $updateMempool(transactions: string[], accelerations: Record<string, Acceleration> | null, minFeeMempool: string[], minFeeTip: number, pollRate: number): Promise<void> {
|
||||
logger.debug(`Updating mempool...`);
|
||||
|
||||
// warn if this run stalls the main loop for more than 2 minutes
|
||||
@@ -353,7 +355,7 @@ class Mempool {
|
||||
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
|
||||
this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6);
|
||||
|
||||
const accelerationDelta = accelerations != null ? await this.$updateAccelerations(accelerations) : [];
|
||||
const accelerationDelta = accelerations != null ? await this.updateAccelerations(accelerations) : [];
|
||||
if (accelerationDelta.length) {
|
||||
hasChange = true;
|
||||
}
|
||||
@@ -362,12 +364,15 @@ class Mempool {
|
||||
|
||||
const candidatesChanged = candidates?.added?.length || candidates?.removed?.length;
|
||||
|
||||
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
||||
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta);
|
||||
this.recentlyDeleted.unshift(deletedTransactions);
|
||||
this.recentlyDeleted.length = Math.min(this.recentlyDeleted.length, 10); // truncate to the last 10 mempool updates
|
||||
|
||||
if (this.mempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length)) {
|
||||
this.mempoolChangedCallback(this.mempoolCache, newTransactions, this.recentlyDeleted, accelerationDelta);
|
||||
}
|
||||
if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length || candidatesChanged)) {
|
||||
if (this.$asyncMempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length || candidatesChanged)) {
|
||||
this.updateTimerProgress(timer, 'running async mempool callback');
|
||||
await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, deletedTransactions, accelerationDelta, candidates);
|
||||
await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, this.recentlyDeleted, accelerationDelta, candidates);
|
||||
this.updateTimerProgress(timer, 'completed async mempool callback');
|
||||
}
|
||||
|
||||
@@ -395,58 +400,11 @@ class Mempool {
|
||||
return this.accelerations;
|
||||
}
|
||||
|
||||
public $updateAccelerations(newAccelerations: Acceleration[]): string[] {
|
||||
public updateAccelerations(newAccelerationMap: Record<string, Acceleration>): string[] {
|
||||
try {
|
||||
const changed: string[] = [];
|
||||
|
||||
const newAccelerationMap: { [txid: string]: Acceleration } = {};
|
||||
for (const acceleration of newAccelerations) {
|
||||
// skip transactions we don't know about
|
||||
if (!this.mempoolCache[acceleration.txid]) {
|
||||
continue;
|
||||
}
|
||||
newAccelerationMap[acceleration.txid] = acceleration;
|
||||
if (this.accelerations[acceleration.txid] == null) {
|
||||
// new acceleration
|
||||
changed.push(acceleration.txid);
|
||||
} else {
|
||||
if (this.accelerations[acceleration.txid].feeDelta !== acceleration.feeDelta) {
|
||||
// feeDelta changed
|
||||
changed.push(acceleration.txid);
|
||||
} else if (this.accelerations[acceleration.txid].pools?.length) {
|
||||
let poolsChanged = false;
|
||||
const pools = new Set();
|
||||
this.accelerations[acceleration.txid].pools.forEach(pool => {
|
||||
pools.add(pool);
|
||||
});
|
||||
acceleration.pools.forEach(pool => {
|
||||
if (!pools.has(pool)) {
|
||||
poolsChanged = true;
|
||||
} else {
|
||||
pools.delete(pool);
|
||||
}
|
||||
});
|
||||
if (pools.size > 0) {
|
||||
poolsChanged = true;
|
||||
}
|
||||
if (poolsChanged) {
|
||||
// pools changed
|
||||
changed.push(acceleration.txid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const oldTxid of Object.keys(this.accelerations)) {
|
||||
if (!newAccelerationMap[oldTxid]) {
|
||||
// removed
|
||||
changed.push(oldTxid);
|
||||
}
|
||||
}
|
||||
|
||||
const accelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, newAccelerationMap);
|
||||
this.accelerations = newAccelerationMap;
|
||||
|
||||
return changed;
|
||||
return accelerationDelta;
|
||||
} catch (e: any) {
|
||||
logger.debug(`Failed to update accelerations: ` + (e instanceof Error ? e.message : e));
|
||||
return [];
|
||||
@@ -541,16 +499,7 @@ class Mempool {
|
||||
}
|
||||
}
|
||||
|
||||
public handleRbfTransactions(rbfTransactions: { [txid: string]: MempoolTransactionExtended[]; }): void {
|
||||
for (const rbfTransaction in rbfTransactions) {
|
||||
if (this.mempoolCache[rbfTransaction] && rbfTransactions[rbfTransaction]?.length) {
|
||||
// Store replaced transactions
|
||||
rbfCache.add(rbfTransactions[rbfTransaction], this.mempoolCache[rbfTransaction]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public handleMinedRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void {
|
||||
public handleRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void {
|
||||
for (const rbfTransaction in rbfTransactions) {
|
||||
if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) {
|
||||
// Store replaced transactions
|
||||
|
||||
@@ -10,6 +10,7 @@ import mining from "./mining";
|
||||
import PricesRepository from '../../repositories/PricesRepository';
|
||||
import AccelerationRepository from '../../repositories/AccelerationRepository';
|
||||
import accelerationApi from '../services/acceleration';
|
||||
import { handleError } from '../../utils/api';
|
||||
|
||||
class MiningRoutes {
|
||||
public initRoutes(app: Application) {
|
||||
@@ -53,12 +54,12 @@ class MiningRoutes {
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
||||
if (['testnet', 'signet', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) {
|
||||
res.status(400).send('Prices are not available on testnets.');
|
||||
handleError(req, res, 400, 'Prices are not available on testnets.');
|
||||
return;
|
||||
}
|
||||
const timestamp = parseInt(req.query.timestamp as string, 10) || 0;
|
||||
const currency = req.query.currency as string;
|
||||
|
||||
|
||||
let response;
|
||||
if (timestamp && currency) {
|
||||
response = await PricesRepository.$getNearestHistoricalPrice(timestamp, currency);
|
||||
@@ -71,7 +72,7 @@ class MiningRoutes {
|
||||
}
|
||||
res.status(200).send(response);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,9 +85,9 @@ class MiningRoutes {
|
||||
res.json(stats);
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
|
||||
res.status(404).send(e.message);
|
||||
handleError(req, res, 404, e.message);
|
||||
} else {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,9 +104,9 @@ class MiningRoutes {
|
||||
res.json(poolBlocks);
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
|
||||
res.status(404).send(e.message);
|
||||
handleError(req, res, 404, e.message);
|
||||
} else {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,7 +130,7 @@ class MiningRoutes {
|
||||
res.json(pools);
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +144,7 @@ class MiningRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(stats);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +158,7 @@ class MiningRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
||||
res.json(hashrates);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,9 +173,9 @@ class MiningRoutes {
|
||||
res.json(hashrates);
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
|
||||
res.status(404).send(e.message);
|
||||
handleError(req, res, 404, e.message);
|
||||
} else {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,7 +183,7 @@ class MiningRoutes {
|
||||
private async $getHistoricalHashrate(req: Request, res: Response) {
|
||||
let currentHashrate = 0, currentDifficulty = 0;
|
||||
try {
|
||||
currentHashrate = await bitcoinClient.getNetworkHashPs();
|
||||
currentHashrate = await bitcoinClient.getNetworkHashPs(1008);
|
||||
currentDifficulty = await bitcoinClient.getDifficulty();
|
||||
} catch (e) {
|
||||
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate and difficulty');
|
||||
@@ -203,7 +204,7 @@ class MiningRoutes {
|
||||
currentDifficulty: currentDifficulty,
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,7 +218,7 @@ class MiningRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(blockFees);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,7 +236,7 @@ class MiningRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(blockFees);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,7 +250,7 @@ class MiningRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(blockRewards);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,7 +264,7 @@ class MiningRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(blockFeeRates);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,7 +282,7 @@ class MiningRoutes {
|
||||
weights: blockWeights
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,7 +294,7 @@ class MiningRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
||||
res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment]));
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,7 +318,7 @@ class MiningRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate]));
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,7 +327,7 @@ class MiningRoutes {
|
||||
const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash);
|
||||
|
||||
if (!audit) {
|
||||
res.status(204).send(`This block has not been audited.`);
|
||||
handleError(req, res, 204, `This block has not been audited.`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -335,7 +336,7 @@ class MiningRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
|
||||
res.json(audit);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,7 +359,7 @@ class MiningRoutes {
|
||||
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);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,7 +372,7 @@ class MiningRoutes {
|
||||
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);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,7 +385,7 @@ class MiningRoutes {
|
||||
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);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,12 +395,12 @@ class MiningRoutes {
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
||||
res.status(400).send('Acceleration data is not available.');
|
||||
handleError(req, res, 400, 'Acceleration data is not available.');
|
||||
return;
|
||||
}
|
||||
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug));
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,13 +410,13 @@ class MiningRoutes {
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
|
||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
||||
res.status(400).send('Acceleration data is not available.');
|
||||
handleError(req, res, 400, 'Acceleration data is not available.');
|
||||
return;
|
||||
}
|
||||
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
|
||||
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height));
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,12 +426,12 @@ class MiningRoutes {
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
||||
res.status(400).send('Acceleration data is not available.');
|
||||
handleError(req, res, 400, 'Acceleration data is not available.');
|
||||
return;
|
||||
}
|
||||
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval));
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,12 +441,12 @@ class MiningRoutes {
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
||||
res.status(400).send('Acceleration data is not available.');
|
||||
handleError(req, res, 400, 'Acceleration data is not available.');
|
||||
return;
|
||||
}
|
||||
res.status(200).send(await AccelerationRepository.$getAccelerationTotals(<string>req.query.pool, <string>req.query.interval));
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,12 +456,12 @@ class MiningRoutes {
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
||||
res.status(400).send('Acceleration data is not available.');
|
||||
handleError(req, res, 400, 'Acceleration data is not available.');
|
||||
return;
|
||||
}
|
||||
res.status(200).send(accelerationApi.accelerations || []);
|
||||
res.status(200).send(Object.values(accelerationApi.getAccelerations() || {}));
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -472,7 +473,7 @@ class MiningRoutes {
|
||||
accelerationApi.accelerationRequested(req.params.txid);
|
||||
res.status(200).send();
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,9 +136,13 @@ class Mining {
|
||||
poolsStatistics['blockCount'] = blockCount;
|
||||
|
||||
const totalBlock24h: number = await BlocksRepository.$blockCount(null, '24h');
|
||||
const totalBlock3d: number = await BlocksRepository.$blockCount(null, '3d');
|
||||
const totalBlock1w: number = await BlocksRepository.$blockCount(null, '1w');
|
||||
|
||||
try {
|
||||
poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h);
|
||||
poolsStatistics['lastEstimatedHashrate3d'] = await bitcoinClient.getNetworkHashPs(totalBlock3d);
|
||||
poolsStatistics['lastEstimatedHashrate1w'] = await bitcoinClient.getNetworkHashPs(totalBlock1w);
|
||||
} catch (e) {
|
||||
poolsStatistics['lastEstimatedHashrate'] = 0;
|
||||
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate', logger.tags.mining);
|
||||
|
||||
@@ -44,6 +44,22 @@ interface CacheEvent {
|
||||
value?: any,
|
||||
}
|
||||
|
||||
/**
|
||||
* Singleton for tracking RBF trees
|
||||
*
|
||||
* Maintains a set of RBF trees, where each tree represents a sequence of
|
||||
* consecutive RBF replacements.
|
||||
*
|
||||
* Trees are identified by the txid of the root transaction.
|
||||
*
|
||||
* To maintain consistency, the following invariants must be upheld:
|
||||
* - Symmetry: replacedBy(A) = B <=> A in replaces(B)
|
||||
* - Unique id: treeMap(treeMap(X)) = treeMap(X)
|
||||
* - Unique tree: A in replaces(B) => treeMap(A) == treeMap(B)
|
||||
* - Existence: X in treeMap => treeMap(X) in rbfTrees
|
||||
* - Completeness: X in replacedBy => X in treeMap, Y in replaces => Y in treeMap
|
||||
*/
|
||||
|
||||
class RbfCache {
|
||||
private replacedBy: Map<string, string> = new Map();
|
||||
private replaces: Map<string, string[]> = new Map();
|
||||
@@ -61,6 +77,10 @@ class RbfCache {
|
||||
setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Low level cache operations
|
||||
*/
|
||||
|
||||
private addTx(txid: string, tx: MempoolTransactionExtended): void {
|
||||
this.txs.set(txid, tx);
|
||||
this.cacheQueue.push({ op: CacheOp.Add, type: 'tx', txid });
|
||||
@@ -92,6 +112,12 @@ class RbfCache {
|
||||
this.cacheQueue.push({ op: CacheOp.Remove, type: 'exp', txid });
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic data structure operations
|
||||
* must uphold tree invariants
|
||||
*/
|
||||
|
||||
|
||||
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
|
||||
if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
|
||||
return;
|
||||
@@ -114,6 +140,10 @@ class RbfCache {
|
||||
if (!replacedTx.rbf) {
|
||||
txFullRbf = true;
|
||||
}
|
||||
if (this.replacedBy.has(replacedTx.txid)) {
|
||||
// should never happen
|
||||
continue;
|
||||
}
|
||||
this.replacedBy.set(replacedTx.txid, newTx.txid);
|
||||
if (this.treeMap.has(replacedTx.txid)) {
|
||||
const treeId = this.treeMap.get(replacedTx.txid);
|
||||
@@ -140,18 +170,47 @@ class RbfCache {
|
||||
}
|
||||
}
|
||||
newTx.fullRbf = txFullRbf;
|
||||
const treeId = replacedTrees[0].tx.txid;
|
||||
const newTree = {
|
||||
tx: newTx,
|
||||
time: newTime,
|
||||
fullRbf: treeFullRbf,
|
||||
replaces: replacedTrees
|
||||
};
|
||||
this.addTree(treeId, newTree);
|
||||
this.updateTreeMap(treeId, newTree);
|
||||
this.addTree(newTree.tx.txid, newTree);
|
||||
this.updateTreeMap(newTree.tx.txid, newTree);
|
||||
this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid));
|
||||
}
|
||||
|
||||
public mined(txid): void {
|
||||
if (!this.txs.has(txid)) {
|
||||
return;
|
||||
}
|
||||
const treeId = this.treeMap.get(txid);
|
||||
if (treeId && this.rbfTrees.has(treeId)) {
|
||||
const tree = this.rbfTrees.get(treeId);
|
||||
if (tree) {
|
||||
this.setTreeMined(tree, txid);
|
||||
tree.mined = true;
|
||||
this.dirtyTrees.add(treeId);
|
||||
this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId });
|
||||
}
|
||||
}
|
||||
this.evict(txid);
|
||||
}
|
||||
|
||||
// flag a transaction as removed from the mempool
|
||||
public evict(txid: string, fast: boolean = false): void {
|
||||
this.evictionCount++;
|
||||
if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) {
|
||||
const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours
|
||||
this.addExpiration(txid, expiryTime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read-only public interface
|
||||
*/
|
||||
|
||||
public has(txId: string): boolean {
|
||||
return this.txs.has(txId);
|
||||
}
|
||||
@@ -232,32 +291,6 @@ class RbfCache {
|
||||
return changes;
|
||||
}
|
||||
|
||||
public mined(txid): void {
|
||||
if (!this.txs.has(txid)) {
|
||||
return;
|
||||
}
|
||||
const treeId = this.treeMap.get(txid);
|
||||
if (treeId && this.rbfTrees.has(treeId)) {
|
||||
const tree = this.rbfTrees.get(treeId);
|
||||
if (tree) {
|
||||
this.setTreeMined(tree, txid);
|
||||
tree.mined = true;
|
||||
this.dirtyTrees.add(treeId);
|
||||
this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId });
|
||||
}
|
||||
}
|
||||
this.evict(txid);
|
||||
}
|
||||
|
||||
// flag a transaction as removed from the mempool
|
||||
public evict(txid: string, fast: boolean = false): void {
|
||||
this.evictionCount++;
|
||||
if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) {
|
||||
const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours
|
||||
this.addExpiration(txid, expiryTime);
|
||||
}
|
||||
}
|
||||
|
||||
// is the transaction involved in a full rbf replacement?
|
||||
public isFullRbf(txid: string): boolean {
|
||||
const treeId = this.treeMap.get(txid);
|
||||
@@ -271,6 +304,10 @@ class RbfCache {
|
||||
return tree?.fullRbf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache maintenance & utility functions
|
||||
*/
|
||||
|
||||
private cleanup(): void {
|
||||
const now = Date.now();
|
||||
for (const txid of this.expiring.keys()) {
|
||||
@@ -299,10 +336,6 @@ class RbfCache {
|
||||
for (const tx of (replaces || [])) {
|
||||
// recursively remove prior versions from the cache
|
||||
this.replacedBy.delete(tx);
|
||||
// if this is the id of a tree, remove that too
|
||||
if (this.treeMap.get(tx) === tx) {
|
||||
this.removeTree(tx);
|
||||
}
|
||||
this.remove(tx);
|
||||
}
|
||||
}
|
||||
@@ -370,14 +403,21 @@ class RbfCache {
|
||||
};
|
||||
}
|
||||
|
||||
public async load({ txs, trees, expiring, mempool }): Promise<void> {
|
||||
public async load({ txs, trees, expiring, mempool, spendMap }): Promise<void> {
|
||||
try {
|
||||
txs.forEach(txEntry => {
|
||||
this.txs.set(txEntry.value.txid, txEntry.value);
|
||||
});
|
||||
this.staleCount = 0;
|
||||
for (const deflatedTree of trees) {
|
||||
await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
|
||||
for (const deflatedTree of trees.sort((a, b) => Object.keys(b).length - Object.keys(a).length)) {
|
||||
const tree = await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
|
||||
if (tree) {
|
||||
this.addTree(tree.tx.txid, tree);
|
||||
this.updateTreeMap(tree.tx.txid, tree);
|
||||
if (tree.mined) {
|
||||
this.evict(tree.tx.txid);
|
||||
}
|
||||
}
|
||||
}
|
||||
expiring.forEach(expiringEntry => {
|
||||
if (this.txs.has(expiringEntry.key)) {
|
||||
@@ -385,6 +425,31 @@ class RbfCache {
|
||||
}
|
||||
});
|
||||
this.staleCount = 0;
|
||||
|
||||
// connect cached trees to current mempool transactions
|
||||
const conflicts: Record<string, { replacedBy: MempoolTransactionExtended, replaces: Set<MempoolTransactionExtended> }> = {};
|
||||
for (const tree of this.rbfTrees.values()) {
|
||||
const tx = this.getTx(tree.tx.txid);
|
||||
if (!tx || tree.mined) {
|
||||
continue;
|
||||
}
|
||||
for (const vin of tx.vin) {
|
||||
const conflict = spendMap.get(`${vin.txid}:${vin.vout}`);
|
||||
if (conflict && conflict.txid !== tx.txid) {
|
||||
if (!conflicts[conflict.txid]) {
|
||||
conflicts[conflict.txid] = {
|
||||
replacedBy: conflict,
|
||||
replaces: new Set(),
|
||||
};
|
||||
}
|
||||
conflicts[conflict.txid].replaces.add(tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const { replacedBy, replaces } of Object.values(conflicts)) {
|
||||
this.add([...replaces.values()], replacedBy);
|
||||
}
|
||||
|
||||
await this.checkTrees();
|
||||
logger.debug(`loaded ${txs.length} txs, ${trees.length} trees into rbf cache, ${expiring.length} due to expire, ${this.staleCount} were stale`);
|
||||
this.cleanup();
|
||||
@@ -426,6 +491,12 @@ class RbfCache {
|
||||
return;
|
||||
}
|
||||
|
||||
// if this tx is already in the cache, return early
|
||||
if (this.treeMap.has(txid)) {
|
||||
this.removeTree(deflated.key);
|
||||
return;
|
||||
}
|
||||
|
||||
// recursively reconstruct child trees
|
||||
for (const childId of treeInfo.replaces) {
|
||||
const replaced = await this.importTree(mempool, root, childId, deflated, txs, mined);
|
||||
@@ -457,10 +528,6 @@ class RbfCache {
|
||||
fullRbf: treeInfo.fullRbf,
|
||||
replaces,
|
||||
};
|
||||
this.treeMap.set(txid, root);
|
||||
if (root === txid) {
|
||||
this.addTree(root, tree);
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
@@ -511,6 +578,7 @@ class RbfCache {
|
||||
processTxs(txs);
|
||||
}
|
||||
|
||||
// evict missing transactions
|
||||
for (const txid of txids) {
|
||||
if (!found[txid]) {
|
||||
this.evict(txid, false);
|
||||
|
||||
@@ -365,6 +365,7 @@ class RedisCache {
|
||||
trees: rbfTrees.map(loadedTree => { loadedTree.value.key = loadedTree.key; return loadedTree.value; }),
|
||||
expiring: rbfExpirations,
|
||||
mempool: memPool.getMempool(),
|
||||
spendMap: memPool.getSpendMap(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { WebSocket } from 'ws';
|
||||
import config from '../../config';
|
||||
import logger from '../../logger';
|
||||
import { BlockExtended } from '../../mempool.interfaces';
|
||||
import axios from 'axios';
|
||||
import mempool from '../mempool';
|
||||
import websocketHandler from '../websocket-handler';
|
||||
|
||||
type MyAccelerationStatus = 'requested' | 'accelerating' | 'done';
|
||||
|
||||
@@ -37,14 +40,20 @@ export interface AccelerationHistory {
|
||||
};
|
||||
|
||||
class AccelerationApi {
|
||||
private ws: WebSocket | null = null;
|
||||
private useWebsocket: boolean = config.MEMPOOL.OFFICIAL && config.MEMPOOL_SERVICES.ACCELERATIONS;
|
||||
private startedWebsocketLoop: boolean = false;
|
||||
private websocketConnected: boolean = false;
|
||||
private onDemandPollingEnabled = !config.MEMPOOL_SERVICES.ACCELERATIONS;
|
||||
private apiPath = config.MEMPOOL.OFFICIAL ? (config.MEMPOOL_SERVICES.API + '/accelerator/accelerations') : (config.EXTERNAL_DATA_SERVER.MEMPOOL_API + '/accelerations');
|
||||
private _accelerations: Acceleration[] | null = null;
|
||||
private _accelerations: Record<string, Acceleration> = {};
|
||||
private lastPoll = 0;
|
||||
private forcePoll = false;
|
||||
private myAccelerations: Record<string, { status: MyAccelerationStatus, added: number, acceleration?: Acceleration }> = {};
|
||||
|
||||
public get accelerations(): Acceleration[] | null {
|
||||
public constructor() {}
|
||||
|
||||
public getAccelerations(): Record<string, Acceleration> {
|
||||
return this._accelerations;
|
||||
}
|
||||
|
||||
@@ -72,11 +81,18 @@ class AccelerationApi {
|
||||
}
|
||||
}
|
||||
|
||||
public async $updateAccelerations(): Promise<Acceleration[] | null> {
|
||||
public async $updateAccelerations(): Promise<Record<string, Acceleration> | null> {
|
||||
if (this.useWebsocket && this.websocketConnected) {
|
||||
return this._accelerations;
|
||||
}
|
||||
if (!this.onDemandPollingEnabled) {
|
||||
const accelerations = await this.$fetchAccelerations();
|
||||
if (accelerations) {
|
||||
this._accelerations = accelerations;
|
||||
const latestAccelerations = {};
|
||||
for (const acc of accelerations) {
|
||||
latestAccelerations[acc.txid] = acc;
|
||||
}
|
||||
this._accelerations = latestAccelerations;
|
||||
return this._accelerations;
|
||||
}
|
||||
} else {
|
||||
@@ -85,7 +101,7 @@ class AccelerationApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
private async $updateAccelerationsOnDemand(): Promise<Acceleration[] | null> {
|
||||
private async $updateAccelerationsOnDemand(): Promise<Record<string, Acceleration> | null> {
|
||||
const shouldUpdate = this.forcePoll
|
||||
|| this.countMyAccelerationsWithStatus('requested') > 0
|
||||
|| (this.countMyAccelerationsWithStatus('accelerating') > 0 && this.lastPoll < (Date.now() - (10 * 60 * 1000)));
|
||||
@@ -120,7 +136,11 @@ class AccelerationApi {
|
||||
}
|
||||
}
|
||||
|
||||
this._accelerations = Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[];
|
||||
const latestAccelerations = {};
|
||||
for (const acc of Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[]) {
|
||||
latestAccelerations[acc.txid] = acc;
|
||||
}
|
||||
this._accelerations = latestAccelerations;
|
||||
return this._accelerations;
|
||||
}
|
||||
|
||||
@@ -152,6 +172,110 @@ class AccelerationApi {
|
||||
}
|
||||
return anyAccelerated;
|
||||
}
|
||||
|
||||
// get a list of accelerations that have changed between two sets of accelerations
|
||||
public getAccelerationDelta(oldAccelerationMap: Record<string, Acceleration>, newAccelerationMap: Record<string, Acceleration>): string[] {
|
||||
const changed: string[] = [];
|
||||
const mempoolCache = mempool.getMempool();
|
||||
|
||||
for (const acceleration of Object.values(newAccelerationMap)) {
|
||||
// skip transactions we don't know about
|
||||
if (!mempoolCache[acceleration.txid]) {
|
||||
continue;
|
||||
}
|
||||
if (oldAccelerationMap[acceleration.txid] == null) {
|
||||
// new acceleration
|
||||
changed.push(acceleration.txid);
|
||||
} else {
|
||||
if (oldAccelerationMap[acceleration.txid].feeDelta !== acceleration.feeDelta) {
|
||||
// feeDelta changed
|
||||
changed.push(acceleration.txid);
|
||||
} else if (oldAccelerationMap[acceleration.txid].pools?.length) {
|
||||
let poolsChanged = false;
|
||||
const pools = new Set();
|
||||
oldAccelerationMap[acceleration.txid].pools.forEach(pool => {
|
||||
pools.add(pool);
|
||||
});
|
||||
acceleration.pools.forEach(pool => {
|
||||
if (!pools.has(pool)) {
|
||||
poolsChanged = true;
|
||||
} else {
|
||||
pools.delete(pool);
|
||||
}
|
||||
});
|
||||
if (pools.size > 0) {
|
||||
poolsChanged = true;
|
||||
}
|
||||
if (poolsChanged) {
|
||||
// pools changed
|
||||
changed.push(acceleration.txid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const oldTxid of Object.keys(oldAccelerationMap)) {
|
||||
if (!newAccelerationMap[oldTxid]) {
|
||||
// removed
|
||||
changed.push(oldTxid);
|
||||
}
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
private handleWebsocketMessage(msg: any): void {
|
||||
if (msg?.accelerations !== null) {
|
||||
const latestAccelerations = {};
|
||||
for (const acc of msg?.accelerations || []) {
|
||||
latestAccelerations[acc.txid] = acc;
|
||||
}
|
||||
this._accelerations = latestAccelerations;
|
||||
websocketHandler.handleAccelerationsChanged(this._accelerations);
|
||||
}
|
||||
}
|
||||
|
||||
public async connectWebsocket(): Promise<void> {
|
||||
if (this.startedWebsocketLoop) {
|
||||
return;
|
||||
}
|
||||
while (this.useWebsocket) {
|
||||
this.startedWebsocketLoop = true;
|
||||
if (!this.ws) {
|
||||
this.ws = new WebSocket(`${config.MEMPOOL_SERVICES.API.replace('https://', 'ws://').replace('http://', 'ws://')}/accelerator/ws`);
|
||||
this.websocketConnected = true;
|
||||
|
||||
this.ws.on('open', () => {
|
||||
logger.info('Acceleration websocket opened');
|
||||
this.ws?.send(JSON.stringify({
|
||||
'watch-accelerations': true
|
||||
}));
|
||||
});
|
||||
|
||||
this.ws.on('error', (error) => {
|
||||
logger.err('Acceleration websocket error: ' + error);
|
||||
this.ws = null;
|
||||
this.websocketConnected = false;
|
||||
});
|
||||
|
||||
this.ws.on('close', () => {
|
||||
logger.info('Acceleration websocket closed');
|
||||
this.ws = null;
|
||||
this.websocketConnected = false;
|
||||
});
|
||||
|
||||
this.ws.on('message', (data, isBinary) => {
|
||||
try {
|
||||
const parsedMsg = JSON.parse((isBinary ? data : data.toString()) as string);
|
||||
this.handleWebsocketMessage(parsedMsg);
|
||||
} catch (e) {
|
||||
logger.warn('Failed to parse acceleration websocket message: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
});
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new AccelerationApi();
|
||||
26
backend/src/api/services/services-routes.ts
Normal file
26
backend/src/api/services/services-routes.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Application, Request, Response } from 'express';
|
||||
import config from '../../config';
|
||||
import WalletApi from './wallets';
|
||||
|
||||
class ServicesRoutes {
|
||||
public initRoutes(app: Application): void {
|
||||
app
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'wallet/:walletId', this.$getWallet)
|
||||
;
|
||||
}
|
||||
|
||||
private async $getWallet(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 5).toUTCString());
|
||||
const walletId = req.params.walletId;
|
||||
const wallet = await WalletApi.getWallet(walletId);
|
||||
res.status(200).send(wallet);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new ServicesRoutes();
|
||||
153
backend/src/api/services/wallets.ts
Normal file
153
backend/src/api/services/wallets.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import config from '../../config';
|
||||
import logger from '../../logger';
|
||||
import { IEsploraApi } from '../bitcoin/esplora-api.interface';
|
||||
import bitcoinApi from '../bitcoin/bitcoin-api-factory';
|
||||
import axios from 'axios';
|
||||
import { TransactionExtended } from '../../mempool.interfaces';
|
||||
|
||||
interface WalletAddress {
|
||||
address: string;
|
||||
active: boolean;
|
||||
stats: {
|
||||
funded_txo_count: number;
|
||||
funded_txo_sum: number;
|
||||
spent_txo_count: number;
|
||||
spent_txo_sum: number;
|
||||
tx_count: number;
|
||||
};
|
||||
transactions: IEsploraApi.AddressTxSummary[];
|
||||
lastSync: number;
|
||||
}
|
||||
|
||||
interface Wallet {
|
||||
name: string;
|
||||
addresses: Record<string, WalletAddress>;
|
||||
lastPoll: number;
|
||||
}
|
||||
|
||||
const POLL_FREQUENCY = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
class WalletApi {
|
||||
private wallets: Record<string, Wallet> = {};
|
||||
private syncing = false;
|
||||
|
||||
constructor() {
|
||||
this.wallets = config.WALLETS.ENABLED ? (config.WALLETS.WALLETS as string[]).reduce((acc, wallet) => {
|
||||
acc[wallet] = { name: wallet, addresses: {}, lastPoll: 0 };
|
||||
return acc;
|
||||
}, {} as Record<string, Wallet>) : {};
|
||||
}
|
||||
|
||||
public getWallet(wallet: string): Record<string, WalletAddress> {
|
||||
return this.wallets?.[wallet]?.addresses || {};
|
||||
}
|
||||
|
||||
// resync wallet addresses from the services backend
|
||||
async $syncWallets(): Promise<void> {
|
||||
if (!config.WALLETS.ENABLED || this.syncing) {
|
||||
return;
|
||||
}
|
||||
this.syncing = true;
|
||||
for (const walletKey of Object.keys(this.wallets)) {
|
||||
const wallet = this.wallets[walletKey];
|
||||
if (wallet.lastPoll < (Date.now() - POLL_FREQUENCY)) {
|
||||
try {
|
||||
const response = await axios.get(config.MEMPOOL_SERVICES.API + `/wallets/${wallet.name}`);
|
||||
const addresses: Record<string, WalletAddress> = response.data;
|
||||
const addressList: WalletAddress[] = Object.values(addresses);
|
||||
// sync all current addresses
|
||||
for (const address of addressList) {
|
||||
await this.$syncWalletAddress(wallet, address);
|
||||
}
|
||||
// remove old addresses
|
||||
for (const address of Object.keys(wallet.addresses)) {
|
||||
if (!addresses[address]) {
|
||||
delete wallet.addresses[address];
|
||||
}
|
||||
}
|
||||
wallet.lastPoll = Date.now();
|
||||
logger.debug(`Synced ${Object.keys(wallet.addresses).length} addresses for wallet ${wallet.name}`);
|
||||
} catch (e) {
|
||||
logger.err(`Error syncing wallet ${wallet.name}: ${(e instanceof Error ? e.message : e)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.syncing = false;
|
||||
}
|
||||
|
||||
// resync address transactions from esplora
|
||||
async $syncWalletAddress(wallet: Wallet, address: WalletAddress): Promise<void> {
|
||||
// fetch full transaction data if the address is new or still active and hasn't been synced in the last hour
|
||||
const refreshTransactions = !wallet.addresses[address.address] || (address.active && (Date.now() - wallet.addresses[address.address].lastSync) > 60 * 60 * 1000);
|
||||
if (refreshTransactions) {
|
||||
try {
|
||||
const summary = await bitcoinApi.$getAddressTransactionSummary(address.address);
|
||||
const addressInfo = await bitcoinApi.$getAddress(address.address);
|
||||
const walletAddress: WalletAddress = {
|
||||
address: address.address,
|
||||
active: address.active,
|
||||
transactions: summary,
|
||||
stats: addressInfo.chain_stats,
|
||||
lastSync: Date.now(),
|
||||
};
|
||||
wallet.addresses[address.address] = walletAddress;
|
||||
} catch (e) {
|
||||
logger.err(`Error syncing wallet address ${address.address}: ${(e instanceof Error ? e.message : e)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check a new block for transactions that affect wallet address balances, and add relevant transactions to wallets
|
||||
processBlock(block: IEsploraApi.Block, blockTxs: TransactionExtended[]): Record<string, IEsploraApi.Transaction[]> {
|
||||
const walletTransactions: Record<string, IEsploraApi.Transaction[]> = {};
|
||||
for (const walletKey of Object.keys(this.wallets)) {
|
||||
const wallet = this.wallets[walletKey];
|
||||
walletTransactions[walletKey] = [];
|
||||
for (const tx of blockTxs) {
|
||||
const funded: Record<string, number> = {};
|
||||
const spent: Record<string, number> = {};
|
||||
const fundedCount: Record<string, number> = {};
|
||||
const spentCount: Record<string, number> = {};
|
||||
let anyMatch = false;
|
||||
for (const vin of tx.vin) {
|
||||
const address = vin.prevout?.scriptpubkey_address;
|
||||
if (address && wallet.addresses[address]) {
|
||||
anyMatch = true;
|
||||
spent[address] = (spent[address] ?? 0) + (vin.prevout?.value ?? 0);
|
||||
spentCount[address] = (spentCount[address] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
for (const vout of tx.vout) {
|
||||
const address = vout.scriptpubkey_address;
|
||||
if (address && wallet.addresses[address]) {
|
||||
anyMatch = true;
|
||||
funded[address] = (funded[address] ?? 0) + (vout.value ?? 0);
|
||||
fundedCount[address] = (fundedCount[address] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
for (const address of Object.keys({ ...funded, ...spent })) {
|
||||
// update address stats
|
||||
wallet.addresses[address].stats.tx_count++;
|
||||
wallet.addresses[address].stats.funded_txo_count += fundedCount[address] || 0;
|
||||
wallet.addresses[address].stats.spent_txo_count += spentCount[address] || 0;
|
||||
wallet.addresses[address].stats.funded_txo_sum += funded[address] || 0;
|
||||
wallet.addresses[address].stats.spent_txo_sum += spent[address] || 0;
|
||||
// add tx to summary
|
||||
const txSummary: IEsploraApi.AddressTxSummary = {
|
||||
txid: tx.txid,
|
||||
value: (funded[address] ?? 0) - (spent[address] ?? 0),
|
||||
height: block.height,
|
||||
time: block.timestamp,
|
||||
};
|
||||
wallet.addresses[address].transactions?.push(txSummary);
|
||||
}
|
||||
if (anyMatch) {
|
||||
walletTransactions[walletKey].push(tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
return walletTransactions;
|
||||
}
|
||||
}
|
||||
|
||||
export default new WalletApi();
|
||||
@@ -121,6 +121,7 @@ class TransactionUtils {
|
||||
const adjustedVsize = Math.max(fractionalVsize, sigops * 5); // adjusted vsize = Max(weight, sigops * bytes_per_sigop) / witness_scale_factor
|
||||
const feePerVbytes = (transaction.fee || 0) / fractionalVsize;
|
||||
const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize;
|
||||
const effectiveFeePerVsize = transaction['effectiveFeePerVsize'] || adjustedFeePerVsize || feePerVbytes;
|
||||
const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, {
|
||||
order: this.txidToOrdering(transaction.txid),
|
||||
vsize,
|
||||
@@ -128,7 +129,7 @@ class TransactionUtils {
|
||||
sigops,
|
||||
feePerVsize: feePerVbytes,
|
||||
adjustedFeePerVsize: adjustedFeePerVsize,
|
||||
effectiveFeePerVsize: adjustedFeePerVsize,
|
||||
effectiveFeePerVsize: effectiveFeePerVsize,
|
||||
});
|
||||
if (!transactionExtended?.status?.confirmed && !transactionExtended.firstSeen) {
|
||||
transactionExtended.firstSeen = Math.round((Date.now() / 1000));
|
||||
@@ -338,6 +339,87 @@ class TransactionUtils {
|
||||
const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2;
|
||||
return witness[positionOfScript];
|
||||
}
|
||||
|
||||
// calculate the most parsimonious set of prioritizations given a list of block transactions
|
||||
// (i.e. the most likely prioritizations and deprioritizations)
|
||||
public identifyPrioritizedTransactions(transactions: any[], rateKey: string): { prioritized: string[], deprioritized: string[] } {
|
||||
// find the longest increasing subsequence of transactions
|
||||
// (adapted from https://en.wikipedia.org/wiki/Longest_increasing_subsequence#Efficient_algorithms)
|
||||
// should be O(n log n)
|
||||
const X = transactions.slice(1).reverse().map((tx) => ({ txid: tx.txid, rate: tx[rateKey] })); // standard block order is by *decreasing* effective fee rate, but we want to iterate in increasing order (and skip the coinbase)
|
||||
if (X.length < 2) {
|
||||
return { prioritized: [], deprioritized: [] };
|
||||
}
|
||||
const N = X.length;
|
||||
const P: number[] = new Array(N);
|
||||
const M: number[] = new Array(N + 1);
|
||||
M[0] = -1; // undefined so can be set to any value
|
||||
|
||||
let L = 0;
|
||||
for (let i = 0; i < N; i++) {
|
||||
// Binary search for the smallest positive l ≤ L
|
||||
// such that X[M[l]].effectiveFeePerVsize > X[i].effectiveFeePerVsize
|
||||
let lo = 1;
|
||||
let hi = L + 1;
|
||||
while (lo < hi) {
|
||||
const mid = lo + Math.floor((hi - lo) / 2); // lo <= mid < hi
|
||||
if (X[M[mid]].rate > X[i].rate) {
|
||||
hi = mid;
|
||||
} else { // if X[M[mid]].effectiveFeePerVsize < X[i].effectiveFeePerVsize
|
||||
lo = mid + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// After searching, lo == hi is 1 greater than the
|
||||
// length of the longest prefix of X[i]
|
||||
const newL = lo;
|
||||
|
||||
// The predecessor of X[i] is the last index of
|
||||
// the subsequence of length newL-1
|
||||
P[i] = M[newL - 1];
|
||||
M[newL] = i;
|
||||
|
||||
if (newL > L) {
|
||||
// If we found a subsequence longer than any we've
|
||||
// found yet, update L
|
||||
L = newL;
|
||||
}
|
||||
}
|
||||
|
||||
// Reconstruct the longest increasing subsequence
|
||||
// It consists of the values of X at the L indices:
|
||||
// ..., P[P[M[L]]], P[M[L]], M[L]
|
||||
const LIS: any[] = new Array(L);
|
||||
let k = M[L];
|
||||
for (let j = L - 1; j >= 0; j--) {
|
||||
LIS[j] = X[k];
|
||||
k = P[k];
|
||||
}
|
||||
|
||||
const lisMap = new Map<string, number>();
|
||||
LIS.forEach((tx, index) => lisMap.set(tx.txid, index));
|
||||
|
||||
const prioritized: string[] = [];
|
||||
const deprioritized: string[] = [];
|
||||
|
||||
let lastRate = X[0].rate;
|
||||
|
||||
for (const tx of X) {
|
||||
if (lisMap.has(tx.txid)) {
|
||||
lastRate = tx.rate;
|
||||
} else {
|
||||
if (Math.abs(tx.rate - lastRate) < 0.1) {
|
||||
// skip if the rate is almost the same as the previous transaction
|
||||
} else if (tx.rate <= lastRate) {
|
||||
prioritized.push(tx.txid);
|
||||
} else {
|
||||
deprioritized.push(tx.txid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { prioritized, deprioritized };
|
||||
}
|
||||
}
|
||||
|
||||
export default new TransactionUtils();
|
||||
|
||||
@@ -16,16 +16,19 @@ import transactionUtils from './transaction-utils';
|
||||
import rbfCache, { ReplacementInfo } from './rbf-cache';
|
||||
import difficultyAdjustment from './difficulty-adjustment';
|
||||
import feeApi from './fee-api';
|
||||
import BlocksRepository from '../repositories/BlocksRepository';
|
||||
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
|
||||
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
|
||||
import Audit from './audit';
|
||||
import priceUpdater from '../tasks/price-updater';
|
||||
import { ApiPrice } from '../repositories/PricesRepository';
|
||||
import { Acceleration } from './services/acceleration';
|
||||
import accelerationApi from './services/acceleration';
|
||||
import mempool from './mempool';
|
||||
import statistics from './statistics/statistics';
|
||||
import accelerationRepository from '../repositories/AccelerationRepository';
|
||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||
import walletApi from './services/wallets';
|
||||
|
||||
interface AddressTransactions {
|
||||
mempool: MempoolTransactionExtended[],
|
||||
@@ -34,6 +37,7 @@ interface AddressTransactions {
|
||||
}
|
||||
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
||||
import { calculateMempoolTxCpfp } from './cpfp';
|
||||
import { getRecentFirstSeen } from '../utils/file-read';
|
||||
|
||||
// valid 'want' subscriptions
|
||||
const wantable = [
|
||||
@@ -57,6 +61,8 @@ class WebsocketHandler {
|
||||
private lastRbfSummary: ReplacementInfo[] | null = null;
|
||||
private mempoolSequence: number = 0;
|
||||
|
||||
private accelerations: Record<string, Acceleration> = {};
|
||||
|
||||
constructor() { }
|
||||
|
||||
addWebsocketServer(wss: WebSocket.Server) {
|
||||
@@ -305,6 +311,14 @@ class WebsocketHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedMessage && parsedMessage['track-wallet']) {
|
||||
if (parsedMessage['track-wallet'] === 'stop') {
|
||||
client['track-wallet'] = null;
|
||||
} else {
|
||||
client['track-wallet'] = parsedMessage['track-wallet'];
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedMessage && parsedMessage['track-asset']) {
|
||||
if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-asset'])) {
|
||||
client['track-asset'] = parsedMessage['track-asset'];
|
||||
@@ -484,6 +498,42 @@ class WebsocketHandler {
|
||||
}
|
||||
}
|
||||
|
||||
handleAccelerationsChanged(accelerations: Record<string, Acceleration>): void {
|
||||
if (!this.webSocketServers.length) {
|
||||
throw new Error('No WebSocket.Server has been set');
|
||||
}
|
||||
|
||||
const websocketAccelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, accelerations);
|
||||
this.accelerations = accelerations;
|
||||
|
||||
if (!websocketAccelerationDelta.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// pre-compute acceleration delta
|
||||
const accelerationUpdate = {
|
||||
added: websocketAccelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
|
||||
removed: websocketAccelerationDelta.filter(txid => !accelerations[txid]),
|
||||
};
|
||||
|
||||
try {
|
||||
const response = JSON.stringify({
|
||||
accelerations: accelerationUpdate,
|
||||
});
|
||||
|
||||
for (const server of this.webSocketServers) {
|
||||
server.clients.forEach((client) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
client.send(response);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
logger.debug(`Error sending acceleration update to websocket clients: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
handleReorg(): void {
|
||||
if (!this.webSocketServers.length) {
|
||||
throw new Error('No WebSocket.Server have been set');
|
||||
@@ -520,8 +570,17 @@ class WebsocketHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param newMempool
|
||||
* @param mempoolSize
|
||||
* @param newTransactions array of transactions added this mempool update.
|
||||
* @param recentlyDeletedTransactions array of arrays of transactions removed in the last N mempool updates, most recent first.
|
||||
* @param accelerationDelta
|
||||
* @param candidates
|
||||
*/
|
||||
async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number,
|
||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[],
|
||||
newTransactions: MempoolTransactionExtended[], recentlyDeletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[],
|
||||
candidates?: GbtCandidates): Promise<void> {
|
||||
if (!this.webSocketServers.length) {
|
||||
throw new Error('No WebSocket.Server have been set');
|
||||
@@ -529,6 +588,8 @@ class WebsocketHandler {
|
||||
|
||||
this.printLogs();
|
||||
|
||||
const deletedTransactions = recentlyDeletedTransactions.length ? recentlyDeletedTransactions[0] : [];
|
||||
|
||||
const transactionIds = (memPool.limitGBT && candidates) ? Object.keys(candidates?.txs || {}) : Object.keys(newMempool);
|
||||
let added = newTransactions;
|
||||
let removed = deletedTransactions;
|
||||
@@ -547,9 +608,9 @@ class WebsocketHandler {
|
||||
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
|
||||
const mempoolInfo = memPool.getMempoolInfo();
|
||||
const vBytesPerSecond = memPool.getVBytesPerSecond();
|
||||
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
|
||||
const rbfTransactions = Common.findRbfTransactions(newTransactions, recentlyDeletedTransactions.flat());
|
||||
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||
const accelerations = memPool.getAccelerations();
|
||||
const accelerations = accelerationApi.getAccelerations();
|
||||
memPool.handleRbfTransactions(rbfTransactions);
|
||||
const rbfChanges = rbfCache.getRbfChanges();
|
||||
let rbfReplacements;
|
||||
@@ -578,7 +639,7 @@ class WebsocketHandler {
|
||||
const replacedTransactions: { replaced: string, by: TransactionExtended }[] = [];
|
||||
for (const tx of newTransactions) {
|
||||
if (rbfTransactions[tx.txid]) {
|
||||
for (const replaced of rbfTransactions[tx.txid]) {
|
||||
for (const replaced of rbfTransactions[tx.txid].replaced) {
|
||||
replacedTransactions.push({ replaced: replaced.txid, by: tx });
|
||||
}
|
||||
}
|
||||
@@ -657,10 +718,13 @@ class WebsocketHandler {
|
||||
const addressCache = this.makeAddressCache(newTransactions);
|
||||
const removedAddressCache = this.makeAddressCache(deletedTransactions);
|
||||
|
||||
const websocketAccelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, accelerations);
|
||||
this.accelerations = accelerations;
|
||||
|
||||
// pre-compute acceleration delta
|
||||
const accelerationUpdate = {
|
||||
added: accelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
|
||||
removed: accelerationDelta.filter(txid => !accelerations[txid]),
|
||||
added: websocketAccelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
|
||||
removed: websocketAccelerationDelta.filter(txid => !accelerations[txid]),
|
||||
};
|
||||
|
||||
// TODO - Fix indentation after PR is merged
|
||||
@@ -947,7 +1011,7 @@ class WebsocketHandler {
|
||||
await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions));
|
||||
|
||||
const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
|
||||
memPool.handleMinedRbfTransactions(rbfTransactions);
|
||||
memPool.handleRbfTransactions(rbfTransactions);
|
||||
memPool.removeFromSpendMap(transactions);
|
||||
|
||||
if (config.MEMPOOL.AUDIT && memPool.isInSync()) {
|
||||
@@ -1017,6 +1081,14 @@ class WebsocketHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (config.CORE_RPC.DEBUG_LOG_PATH && block.extras) {
|
||||
const firstSeen = getRecentFirstSeen(block.id);
|
||||
if (firstSeen) {
|
||||
BlocksRepository.$saveFirstSeenTime(block.id, firstSeen);
|
||||
block.extras.firstSeen = firstSeen;
|
||||
}
|
||||
}
|
||||
|
||||
const confirmedTxids: { [txid: string]: boolean } = {};
|
||||
|
||||
// Update mempool to remove transactions included in the new block
|
||||
@@ -1091,6 +1163,9 @@ class WebsocketHandler {
|
||||
replaced: replacedTransactions,
|
||||
};
|
||||
|
||||
// check for wallet transactions
|
||||
const walletTransactions = config.WALLETS.ENABLED ? walletApi.processBlock(block, transactions) : [];
|
||||
|
||||
const responseCache = { ...this.socketData };
|
||||
function getCachedResponse(key, data): string {
|
||||
if (!responseCache[key]) {
|
||||
@@ -1295,6 +1370,11 @@ class WebsocketHandler {
|
||||
response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta);
|
||||
}
|
||||
|
||||
if (client['track-wallet']) {
|
||||
const trackedWallet = client['track-wallet'];
|
||||
response['wallet-transactions'] = getCachedResponse(`wallet-transactions-${trackedWallet}`, walletTransactions[trackedWallet] ?? {});
|
||||
}
|
||||
|
||||
if (Object.keys(response).length) {
|
||||
client.send(this.serializeResponse(response));
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ interface IConfig {
|
||||
AUTOMATIC_POOLS_UPDATE: boolean;
|
||||
POOLS_JSON_URL: string,
|
||||
POOLS_JSON_TREE_URL: string,
|
||||
POOLS_UPDATE_DELAY: number,
|
||||
AUDIT: boolean;
|
||||
RUST_GBT: boolean;
|
||||
LIMIT_GBT: boolean;
|
||||
@@ -85,6 +86,7 @@ interface IConfig {
|
||||
TIMEOUT: number;
|
||||
COOKIE: boolean;
|
||||
COOKIE_PATH: string;
|
||||
DEBUG_LOG_PATH: string;
|
||||
};
|
||||
SECOND_CORE_RPC: {
|
||||
HOST: string;
|
||||
@@ -160,6 +162,10 @@ interface IConfig {
|
||||
PAID: boolean;
|
||||
API_KEY: string;
|
||||
},
|
||||
WALLETS: {
|
||||
ENABLED: boolean;
|
||||
WALLETS: string[];
|
||||
}
|
||||
}
|
||||
|
||||
const defaults: IConfig = {
|
||||
@@ -192,8 +198,9 @@ const defaults: IConfig = {
|
||||
'AUTOMATIC_POOLS_UPDATE': false,
|
||||
'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json',
|
||||
'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
|
||||
'POOLS_UPDATE_DELAY': 604800, // in seconds, default is one week
|
||||
'AUDIT': false,
|
||||
'RUST_GBT': false,
|
||||
'RUST_GBT': true,
|
||||
'LIMIT_GBT': false,
|
||||
'CPFP_INDEXING': false,
|
||||
'MAX_BLOCKS_BULK_QUERY': 0,
|
||||
@@ -225,7 +232,8 @@ const defaults: IConfig = {
|
||||
'PASSWORD': 'mempool',
|
||||
'TIMEOUT': 60000,
|
||||
'COOKIE': false,
|
||||
'COOKIE_PATH': '/bitcoin/.cookie'
|
||||
'COOKIE_PATH': '/bitcoin/.cookie',
|
||||
'DEBUG_LOG_PATH': '',
|
||||
},
|
||||
'SECOND_CORE_RPC': {
|
||||
'HOST': '127.0.0.1',
|
||||
@@ -320,6 +328,10 @@ const defaults: IConfig = {
|
||||
'PAID': false,
|
||||
'API_KEY': '',
|
||||
},
|
||||
'WALLETS': {
|
||||
'ENABLED': false,
|
||||
'WALLETS': [],
|
||||
},
|
||||
};
|
||||
|
||||
class Config implements IConfig {
|
||||
@@ -341,6 +353,7 @@ class Config implements IConfig {
|
||||
MEMPOOL_SERVICES: IConfig['MEMPOOL_SERVICES'];
|
||||
REDIS: IConfig['REDIS'];
|
||||
FIAT_PRICE: IConfig['FIAT_PRICE'];
|
||||
WALLETS: IConfig['WALLETS'];
|
||||
|
||||
constructor() {
|
||||
const configs = this.merge(configFromFile, defaults);
|
||||
@@ -362,6 +375,7 @@ class Config implements IConfig {
|
||||
this.MEMPOOL_SERVICES = configs.MEMPOOL_SERVICES;
|
||||
this.REDIS = configs.REDIS;
|
||||
this.FIAT_PRICE = configs.FIAT_PRICE;
|
||||
this.WALLETS = configs.WALLETS;
|
||||
}
|
||||
|
||||
merge = (...objects: object[]): IConfig => {
|
||||
|
||||
@@ -32,6 +32,7 @@ import pricesRoutes from './api/prices/prices.routes';
|
||||
import miningRoutes from './api/mining/mining-routes';
|
||||
import liquidRoutes from './api/liquid/liquid.routes';
|
||||
import bitcoinRoutes from './api/bitcoin/bitcoin.routes';
|
||||
import servicesRoutes from './api/services/services-routes';
|
||||
import fundingTxFetcher from './tasks/lightning/sync-tasks/funding-tx-fetcher';
|
||||
import forensicsService from './tasks/lightning/forensics.service';
|
||||
import priceUpdater from './tasks/price-updater';
|
||||
@@ -46,6 +47,7 @@ import bitcoinSecondClient from './api/bitcoin/bitcoin-second-client';
|
||||
import accelerationRoutes from './api/acceleration/acceleration.routes';
|
||||
import aboutRoutes from './api/about.routes';
|
||||
import mempoolBlocks from './api/mempool-blocks';
|
||||
import walletApi from './api/services/wallets';
|
||||
|
||||
class Server {
|
||||
private wss: WebSocket.Server | undefined;
|
||||
@@ -211,6 +213,8 @@ class Server {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
poolsUpdater.$startService();
|
||||
}
|
||||
|
||||
async runMainUpdateLoop(): Promise<void> {
|
||||
@@ -229,13 +233,17 @@ class Server {
|
||||
const newMempool = await bitcoinApi.$getRawMempool();
|
||||
const minFeeMempool = memPool.limitGBT ? await bitcoinSecondClient.getRawMemPool() : null;
|
||||
const minFeeTip = memPool.limitGBT ? await bitcoinSecondClient.getBlockCount() : -1;
|
||||
const newAccelerations = await accelerationApi.$updateAccelerations();
|
||||
const latestAccelerations = await accelerationApi.$updateAccelerations();
|
||||
const numHandledBlocks = await blocks.$updateBlocks();
|
||||
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1);
|
||||
if (numHandledBlocks === 0) {
|
||||
await memPool.$updateMempool(newMempool, newAccelerations, minFeeMempool, minFeeTip, pollRate);
|
||||
await memPool.$updateMempool(newMempool, latestAccelerations, minFeeMempool, minFeeTip, pollRate);
|
||||
}
|
||||
indexer.$run();
|
||||
if (config.WALLETS.ENABLED) {
|
||||
// might take a while, so run in the background
|
||||
walletApi.$syncWallets();
|
||||
}
|
||||
if (config.FIAT_PRICE.ENABLED) {
|
||||
priceUpdater.$run();
|
||||
}
|
||||
@@ -310,8 +318,10 @@ class Server {
|
||||
priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
|
||||
}
|
||||
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
|
||||
|
||||
accelerationApi.connectWebsocket();
|
||||
}
|
||||
|
||||
|
||||
setUpHttpApiRoutes(): void {
|
||||
bitcoinRoutes.initRoutes(this.app);
|
||||
bitcoinCoreRoutes.initRoutes(this.app);
|
||||
@@ -333,6 +343,9 @@ class Server {
|
||||
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
|
||||
accelerationRoutes.initRoutes(this.app);
|
||||
}
|
||||
if (config.WALLETS.ENABLED) {
|
||||
servicesRoutes.initRoutes(this.app);
|
||||
}
|
||||
if (!config.MEMPOOL.OFFICIAL) {
|
||||
aboutRoutes.initRoutes(this.app);
|
||||
}
|
||||
|
||||
@@ -299,6 +299,7 @@ export interface BlockExtension {
|
||||
id: number; // Note - This is the `unique_id`, not to mix with the auto increment `id`
|
||||
name: string;
|
||||
slug: string;
|
||||
minerNames: string[] | null;
|
||||
};
|
||||
avgFee: number;
|
||||
avgFeeRate: number;
|
||||
@@ -319,6 +320,7 @@ export interface BlockExtension {
|
||||
segwitTotalSize: number;
|
||||
segwitTotalWeight: number;
|
||||
header: string;
|
||||
firstSeen: number | null;
|
||||
utxoSetChange: number;
|
||||
// Requires coinstatsindex, will be set to NULL otherwise
|
||||
utxoSetSize: number | null;
|
||||
|
||||
@@ -132,11 +132,12 @@ class BlocksAuditRepositories {
|
||||
firstSeen = tx.time;
|
||||
}
|
||||
});
|
||||
const wasSeen = blockAudit.version === 1 ? !blockAudit.unseenTxs.includes(txid) : (isExpected || isPrioritized || isAccelerated);
|
||||
|
||||
return {
|
||||
seen: isExpected || isPrioritized || isAccelerated,
|
||||
seen: wasSeen,
|
||||
expected: isExpected,
|
||||
added: isAdded,
|
||||
added: isAdded && (blockAudit.version === 0 || !wasSeen),
|
||||
prioritized: isPrioritized,
|
||||
conflict: isConflict,
|
||||
accelerated: isAccelerated,
|
||||
|
||||
@@ -14,6 +14,7 @@ import chainTips from '../api/chain-tips';
|
||||
import blocks from '../api/blocks';
|
||||
import BlocksAuditsRepository from './BlocksAuditsRepository';
|
||||
import transactionUtils from '../api/transaction-utils';
|
||||
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
|
||||
|
||||
interface DatabaseBlock {
|
||||
id: string;
|
||||
@@ -56,6 +57,7 @@ interface DatabaseBlock {
|
||||
utxoSetChange: number;
|
||||
utxoSetSize: number;
|
||||
totalInputAmt: number;
|
||||
firstSeen: number;
|
||||
}
|
||||
|
||||
const BLOCK_DB_FIELDS = `
|
||||
@@ -98,7 +100,8 @@ const BLOCK_DB_FIELDS = `
|
||||
blocks.header,
|
||||
blocks.utxoset_change AS utxoSetChange,
|
||||
blocks.utxoset_size AS utxoSetSize,
|
||||
blocks.total_input_amt AS totalInputAmt
|
||||
blocks.total_input_amt AS totalInputAmt,
|
||||
UNIX_TIMESTAMP(blocks.first_seen) AS firstSeen
|
||||
`;
|
||||
|
||||
class BlocksRepository {
|
||||
@@ -388,7 +391,7 @@ class BlocksRepository {
|
||||
/**
|
||||
* Get blocks count for a period
|
||||
*/
|
||||
public async $blockCountBetweenHeight(startHeight: number, endHeight: number): Promise<number> {
|
||||
public async $blockCountBetweenHeight(startHeight: number, endHeight: number): Promise<number> {
|
||||
const params: any[] = [];
|
||||
let query = `SELECT count(height) as blockCount
|
||||
FROM blocks
|
||||
@@ -498,7 +501,7 @@ class BlocksRepository {
|
||||
}
|
||||
|
||||
query += ` ORDER BY height DESC
|
||||
LIMIT 10`;
|
||||
LIMIT 100`;
|
||||
|
||||
try {
|
||||
const [rows]: any[] = await DB.query(query, params);
|
||||
@@ -726,7 +729,7 @@ class BlocksRepository {
|
||||
/**
|
||||
* Get the historical averaged block fee rate percentiles
|
||||
*/
|
||||
public async $getHistoricalBlockFeeRates(div: number, interval: string | null): Promise<any> {
|
||||
public async $getHistoricalBlockFeeRates(div: number, interval: string | null): Promise<any> {
|
||||
try {
|
||||
let query = `SELECT
|
||||
CAST(AVG(height) as INT) as avgHeight,
|
||||
@@ -757,7 +760,7 @@ class BlocksRepository {
|
||||
/**
|
||||
* Get the historical averaged block sizes
|
||||
*/
|
||||
public async $getHistoricalBlockSizes(div: number, interval: string | null): Promise<any> {
|
||||
public async $getHistoricalBlockSizes(div: number, interval: string | null): Promise<any> {
|
||||
try {
|
||||
let query = `SELECT
|
||||
CAST(AVG(height) as INT) as avgHeight,
|
||||
@@ -782,7 +785,7 @@ class BlocksRepository {
|
||||
/**
|
||||
* Get the historical averaged block weights
|
||||
*/
|
||||
public async $getHistoricalBlockWeights(div: number, interval: string | null): Promise<any> {
|
||||
public async $getHistoricalBlockWeights(div: number, interval: string | null): Promise<any> {
|
||||
try {
|
||||
let query = `SELECT
|
||||
CAST(AVG(height) as INT) as avgHeight,
|
||||
@@ -820,7 +823,7 @@ class BlocksRepository {
|
||||
/**
|
||||
* Get a list of blocks that have not had CPFP data indexed
|
||||
*/
|
||||
public async $getCPFPUnindexedBlocks(): Promise<number[]> {
|
||||
public async $getCPFPUnindexedBlocks(): Promise<number[]> {
|
||||
try {
|
||||
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
|
||||
const currentBlockHeight = blockchainInfo.blocks;
|
||||
@@ -890,7 +893,7 @@ class BlocksRepository {
|
||||
/**
|
||||
* Save block price by batch
|
||||
*/
|
||||
public async $saveBlockPrices(blockPrices: BlockPrice[]): Promise<void> {
|
||||
public async $saveBlockPrices(blockPrices: BlockPrice[]): Promise<void> {
|
||||
try {
|
||||
let query = `INSERT INTO blocks_prices(height, price_id) VALUES`;
|
||||
for (const price of blockPrices) {
|
||||
@@ -1020,6 +1023,24 @@ class BlocksRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save block first seen time
|
||||
*
|
||||
* @param id
|
||||
*/
|
||||
public async $saveFirstSeenTime(id: string, firstSeen: number): Promise<void> {
|
||||
try {
|
||||
await DB.query(`
|
||||
UPDATE blocks SET first_seen = FROM_UNIXTIME(?)
|
||||
WHERE hash = ?`,
|
||||
[firstSeen, id]
|
||||
);
|
||||
} catch (e) {
|
||||
logger.err(`Cannot update block first seen time. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a mysql row block into a BlockExtended. Note that you
|
||||
* must provide the correct field into dbBlk object param
|
||||
@@ -1054,6 +1075,7 @@ class BlocksRepository {
|
||||
id: dbBlk.poolId,
|
||||
name: dbBlk.poolName,
|
||||
slug: dbBlk.poolSlug,
|
||||
minerNames: null,
|
||||
};
|
||||
extras.avgFee = dbBlk.avgFee;
|
||||
extras.avgFeeRate = dbBlk.avgFeeRate;
|
||||
@@ -1076,6 +1098,7 @@ class BlocksRepository {
|
||||
extras.utxoSetSize = dbBlk.utxoSetSize;
|
||||
extras.totalInputAmt = dbBlk.totalInputAmt;
|
||||
extras.virtualSize = dbBlk.weight / 4.0;
|
||||
extras.firstSeen = dbBlk.firstSeen;
|
||||
|
||||
// Re-org can happen after indexing so we need to always get the
|
||||
// latest state from core
|
||||
@@ -1106,7 +1129,7 @@ class BlocksRepository {
|
||||
let summaryVersion = 0;
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
const txs = (await bitcoinApi.$getTxsForBlock(dbBlk.id)).map(tx => transactionUtils.extendTransaction(tx));
|
||||
summary = blocks.summarizeBlockTransactions(dbBlk.id, txs);
|
||||
summary = blocks.summarizeBlockTransactions(dbBlk.id, dbBlk.height, txs);
|
||||
summaryVersion = 1;
|
||||
} else {
|
||||
// Call Core RPC
|
||||
@@ -1123,9 +1146,64 @@ class BlocksRepository {
|
||||
}
|
||||
}
|
||||
|
||||
if (extras.pool.name === 'OCEAN') {
|
||||
extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw);
|
||||
}
|
||||
|
||||
blk.extras = <BlockExtension>extras;
|
||||
return <BlockExtended>blk;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count how many blocks are indexed
|
||||
*/
|
||||
public async $getIndexedBlockCount(): Promise<number> {
|
||||
try {
|
||||
const [res]: any[] = await DB.query(`SELECT COUNT(hash) as count FROM blocks`);
|
||||
if (!res || !res.length) {
|
||||
logger.err(`Unable to count indexed blocks in our db`);
|
||||
return -1;
|
||||
}
|
||||
return res[0].count;
|
||||
} catch (e) {
|
||||
logger.err(`Unable to count indexed blocks in our db. Exception: ${JSON.stringify(e)}`);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Count how many blocks are indexed with CPFP data
|
||||
*/
|
||||
public async $getIndexedCpfpBlockCount(): Promise<number> {
|
||||
try {
|
||||
const [res]: any[] = await DB.query(`SELECT COUNT(DISTINCT height) as count FROM compact_cpfp_clusters`);
|
||||
if (!res || !res.length) {
|
||||
logger.err(`Unable to count indexed blocks with CPFP data in our db`);
|
||||
return -1;
|
||||
}
|
||||
return res[0].count;
|
||||
} catch (e) {
|
||||
logger.err(`Unable to count indexed blocks with CPFP data in our db. Exception: ${JSON.stringify(e)}`);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Count how many blocks are indexed with coin stats data
|
||||
*/
|
||||
public async $getIndexedCoinStatsBlockCount(): Promise<number> {
|
||||
try {
|
||||
const [res]: any[] = await DB.query(`SELECT COUNT(hash) as count FROM blocks WHERE utxoset_size IS NOT NULL && total_input_amt IS NOT NULL`);
|
||||
if (!res || !res.length) {
|
||||
logger.err(`Unable to count indexed blocks with coin stats data in our db`);
|
||||
return -1;
|
||||
}
|
||||
return res[0].count;
|
||||
} catch (e) {
|
||||
logger.err(`Unable to count indexed blocks with coin stats data in our db. Exception: ${JSON.stringify(e)}`);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new BlocksRepository();
|
||||
|
||||
@@ -83,6 +83,7 @@ module.exports = {
|
||||
signRawTransaction: 'signrawtransaction', // bitcoind v0.7.0+
|
||||
stop: 'stop',
|
||||
submitBlock: 'submitblock', // bitcoind v0.7.0+
|
||||
submitPackage: 'submitpackage',
|
||||
validateAddress: 'validateaddress',
|
||||
verifyChain: 'verifychain', // bitcoind v0.9.0+
|
||||
verifyMessage: 'verifymessage',
|
||||
|
||||
@@ -6,16 +6,30 @@ import backendInfo from '../api/backend-info';
|
||||
import logger from '../logger';
|
||||
import { SocksProxyAgent } from 'socks-proxy-agent';
|
||||
import * as https from 'https';
|
||||
import { Common } from '../api/common';
|
||||
|
||||
/**
|
||||
* Maintain the most recent version of pools-v2.json
|
||||
*/
|
||||
class PoolsUpdater {
|
||||
tag = 'PoolsUpdater';
|
||||
|
||||
lastRun: number = 0;
|
||||
currentSha: string | null = null;
|
||||
poolsUrl: string = config.MEMPOOL.POOLS_JSON_URL;
|
||||
treeUrl: string = config.MEMPOOL.POOLS_JSON_TREE_URL;
|
||||
|
||||
public async $startService(): Promise<void> {
|
||||
while ('Bitcoin is still alive') {
|
||||
try {
|
||||
await this.updatePoolsJson();
|
||||
} catch (e: any) {
|
||||
logger.info(`Exception ${e} in PoolsUpdater::$startService. Code: ${e.code}. Message: ${e.message}`, this.tag);
|
||||
}
|
||||
await Common.sleep$(10000);
|
||||
}
|
||||
}
|
||||
|
||||
public async updatePoolsJson(): Promise<void> {
|
||||
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false ||
|
||||
config.MEMPOOL.ENABLED === false
|
||||
@@ -23,11 +37,8 @@ class PoolsUpdater {
|
||||
return;
|
||||
}
|
||||
|
||||
const oneWeek = 604800;
|
||||
const oneDay = 86400;
|
||||
|
||||
const now = new Date().getTime() / 1000;
|
||||
if (now - this.lastRun < oneWeek) { // Execute the PoolsUpdate only once a week, or upon restart
|
||||
if (now - this.lastRun < config.MEMPOOL.POOLS_UPDATE_DELAY) { // Execute the PoolsUpdate only once a week, or upon restart
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -43,7 +54,7 @@ class PoolsUpdater {
|
||||
this.currentSha = await this.getShaFromDb();
|
||||
}
|
||||
|
||||
logger.debug(`pools-v2.json sha | Current: ${this.currentSha} | Github: ${githubSha}`);
|
||||
logger.debug(`pools-v2.json sha | Current: ${this.currentSha} | Github: ${githubSha}`, this.tag);
|
||||
if (this.currentSha !== null && this.currentSha === githubSha) {
|
||||
return;
|
||||
}
|
||||
@@ -53,16 +64,16 @@ class PoolsUpdater {
|
||||
config.MEMPOOL.AUTOMATIC_POOLS_UPDATE !== true && // Automatic pools update is disabled
|
||||
!process.env.npm_config_update_pools // We're not manually updating mining pool
|
||||
) {
|
||||
logger.warn(`Updated mining pools data is available (${githubSha}) but AUTOMATIC_POOLS_UPDATE is disabled`);
|
||||
logger.info(`You can update your mining pools using the --update-pools command flag. You may want to clear your nginx cache as well if applicable`);
|
||||
logger.warn(`Updated mining pools data is available (${githubSha}) but AUTOMATIC_POOLS_UPDATE is disabled`, this.tag);
|
||||
logger.info(`You can update your mining pools using the --update-pools command flag. You may want to clear your nginx cache as well if applicable`, this.tag);
|
||||
return;
|
||||
}
|
||||
|
||||
const network = config.SOCKS5PROXY.ENABLED ? 'tor' : 'clearnet';
|
||||
if (this.currentSha === null) {
|
||||
logger.info(`Downloading pools-v2.json for the first time from ${this.poolsUrl} over ${network}`, logger.tags.mining);
|
||||
logger.info(`Downloading pools-v2.json for the first time from ${this.poolsUrl} over ${network}`, this.tag);
|
||||
} else {
|
||||
logger.warn(`pools-v2.json is outdated, fetching latest from ${this.poolsUrl} over ${network}`, logger.tags.mining);
|
||||
logger.warn(`pools-v2.json is outdated, fetching latest from ${this.poolsUrl} over ${network}`, this.tag);
|
||||
}
|
||||
const poolsJson = await this.query(this.poolsUrl);
|
||||
if (poolsJson === undefined) {
|
||||
@@ -71,7 +82,7 @@ class PoolsUpdater {
|
||||
poolsParser.setMiningPools(poolsJson);
|
||||
|
||||
if (config.DATABASE.ENABLED === false) { // Don't run db operations
|
||||
logger.info(`Mining pools-v2.json (${githubSha}) import completed (no database)`);
|
||||
logger.info(`Mining pools-v2.json (${githubSha}) import completed (no database)`, this.tag);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -81,14 +92,14 @@ class PoolsUpdater {
|
||||
await this.updateDBSha(githubSha);
|
||||
await DB.query('COMMIT;');
|
||||
} catch (e) {
|
||||
logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, logger.tags.mining);
|
||||
logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, this.tag);
|
||||
await DB.query('ROLLBACK;');
|
||||
}
|
||||
logger.info(`Mining pools-v2.json (${githubSha}) import completed`);
|
||||
logger.info(`Mining pools-v2.json (${githubSha}) import completed`, this.tag);
|
||||
|
||||
} catch (e) {
|
||||
this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week
|
||||
logger.err(`PoolsUpdater failed. Will try again in 24h. Exception: ${JSON.stringify(e)}`, logger.tags.mining);
|
||||
this.lastRun = now - 600; // Try again in 10 minutes
|
||||
logger.err(`PoolsUpdater failed. Will try again in 10 minutes. Exception: ${JSON.stringify(e)}`, this.tag);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +113,7 @@ class PoolsUpdater {
|
||||
await DB.query('DELETE FROM state where name="pools_json_sha"');
|
||||
await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`);
|
||||
} catch (e) {
|
||||
logger.err('Cannot save github pools-v2.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
|
||||
logger.err('Cannot save github pools-v2.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e), this.tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,7 +126,7 @@ class PoolsUpdater {
|
||||
const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"');
|
||||
return (rows.length > 0 ? rows[0].string : null);
|
||||
} catch (e) {
|
||||
logger.err('Cannot fetch pools-v2.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
|
||||
logger.err('Cannot fetch pools-v2.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), this.tag);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -134,7 +145,7 @@ class PoolsUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
logger.err(`Cannot find "pools-v2.json" in git tree (${this.treeUrl})`, logger.tags.mining);
|
||||
logger.err(`Cannot find "pools-v2.json" in git tree (${this.treeUrl})`, this.tag);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -186,7 +197,7 @@ class PoolsUpdater {
|
||||
}
|
||||
return data.data;
|
||||
} catch (e) {
|
||||
logger.err('Could not connect to Github. Reason: ' + (e instanceof Error ? e.message : e));
|
||||
logger.err('Could not connect to Github. Reason: ' + (e instanceof Error ? e.message : e), this.tag);
|
||||
retry++;
|
||||
}
|
||||
await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL);
|
||||
|
||||
9
backend/src/utils/api.ts
Normal file
9
backend/src/utils/api.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
export function handleError(req: Request, res: Response, statusCode: number, errorMessage: string | unknown): void {
|
||||
if (req.accepts('json')) {
|
||||
res.status(statusCode).json({ error: errorMessage });
|
||||
} else {
|
||||
res.status(statusCode).send(errorMessage);
|
||||
}
|
||||
}
|
||||
@@ -158,7 +158,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
|
||||
if (!opN) {
|
||||
return;
|
||||
}
|
||||
if (!opN.startsWith('OP_PUSHNUM_')) {
|
||||
if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
|
||||
return;
|
||||
}
|
||||
const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10);
|
||||
@@ -178,7 +178,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
|
||||
if (!opM) {
|
||||
return;
|
||||
}
|
||||
if (!opM.startsWith('OP_PUSHNUM_')) {
|
||||
if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
|
||||
return;
|
||||
}
|
||||
const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10);
|
||||
@@ -200,4 +200,28 @@ export function getVarIntLength(n: number): number {
|
||||
} else {
|
||||
return 9;
|
||||
}
|
||||
}
|
||||
|
||||
/** Extracts miner names from a DATUM coinbase transaction */
|
||||
export function parseDATUMTemplateCreator(coinbaseRaw: string): string[] | null {
|
||||
let bytes: number[] = [];
|
||||
for (let c = 0; c < coinbaseRaw.length; c += 2) {
|
||||
bytes.push(parseInt(coinbaseRaw.slice(c, c + 2), 16));
|
||||
}
|
||||
|
||||
// Skip block height
|
||||
let tagLengthByte = 1 + bytes[0];
|
||||
|
||||
let tagsLength = bytes[tagLengthByte];
|
||||
if (tagsLength == 0x4c) {
|
||||
tagLengthByte += 1;
|
||||
tagsLength = bytes[tagLengthByte];
|
||||
}
|
||||
|
||||
const tagStart = tagLengthByte + 1;
|
||||
const tags = bytes.slice(tagStart, tagStart + tagsLength);
|
||||
let tagString = String.fromCharCode(...tags);
|
||||
tagString = tagString.replace('\x00', '');
|
||||
|
||||
return tagString.split('\x0f').map((name) => name.replace(/[^a-zA-Z0-9 ]/g, ''));
|
||||
}
|
||||
58
backend/src/utils/file-read.ts
Normal file
58
backend/src/utils/file-read.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import * as fs from 'fs';
|
||||
import logger from '../logger';
|
||||
import config from '../config';
|
||||
|
||||
function readFile(filePath: string, bufferSize?: number): string[] {
|
||||
const fileSize = fs.statSync(filePath).size;
|
||||
const chunkSize = bufferSize || fileSize;
|
||||
const fileDescriptor = fs.openSync(filePath, 'r');
|
||||
const buffer = Buffer.alloc(chunkSize);
|
||||
|
||||
fs.readSync(fileDescriptor, buffer, 0, chunkSize, fileSize - chunkSize);
|
||||
fs.closeSync(fileDescriptor);
|
||||
|
||||
const lines = buffer.toString('utf8', 0, chunkSize).split('\n');
|
||||
return lines;
|
||||
}
|
||||
|
||||
function extractDateFromLogLine(line: string): number | undefined {
|
||||
// Extract time from log: "2021-08-31T12:34:56Z" or "2021-08-31T12:34:56.123456Z"
|
||||
const dateMatch = line.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{6})?Z/);
|
||||
if (!dateMatch) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const dateStr = dateMatch[0];
|
||||
const date = new Date(dateStr);
|
||||
let timestamp = Math.floor(date.getTime() / 1000); // Remove decimal (microseconds are added later)
|
||||
|
||||
const timePart = dateStr.split('T')[1];
|
||||
const microseconds = timePart.split('.')[1] || '';
|
||||
|
||||
if (!microseconds) {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
return parseFloat(timestamp + '.' + microseconds);
|
||||
}
|
||||
|
||||
export function getRecentFirstSeen(hash: string): number | undefined {
|
||||
const debugLogPath = config.CORE_RPC.DEBUG_LOG_PATH;
|
||||
if (debugLogPath) {
|
||||
try {
|
||||
// Read the last few lines of debug.log
|
||||
const lines = readFile(debugLogPath, 2048);
|
||||
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
const line = lines[i];
|
||||
if (line && line.includes(`Saw new header hash=${hash}`)) {
|
||||
return extractDateFromLogLine(line);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.err(`Cannot parse block first seen time from Core logs. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
@@ -109,6 +109,7 @@ Below we list all settings from `mempool-config.json` and the corresponding over
|
||||
"AUTOMATIC_POOLS_UPDATE": false,
|
||||
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json",
|
||||
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
|
||||
"POOLS_UPDATE_DELAY": 604800,
|
||||
"CPFP_INDEXING": false,
|
||||
"MAX_BLOCKS_BULK_QUERY": 0,
|
||||
"DISK_CACHE_BLOCK_INTERVAL": 6,
|
||||
@@ -140,6 +141,7 @@ Corresponding `docker-compose.yml` overrides:
|
||||
MEMPOOL_AUTOMATIC_POOLS_UPDATE: ""
|
||||
MEMPOOL_POOLS_JSON_URL: ""
|
||||
MEMPOOL_POOLS_JSON_TREE_URL: ""
|
||||
MEMPOOL_POOLS_UPDATE_DELAY: ""
|
||||
MEMPOOL_CPFP_INDEXING: ""
|
||||
MEMPOOL_MAX_BLOCKS_BULK_QUERY: ""
|
||||
MEMPOOL_DISK_CACHE_BLOCK_INTERVAL: ""
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"ALLOW_UNREACHABLE": __MEMPOOL_ALLOW_UNREACHABLE__,
|
||||
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
|
||||
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
|
||||
"POOLS_UPDATE_DELAY": __MEMPOOL_POOLS_UPDATE_DELAY__,
|
||||
"PRICE_UPDATES_PER_HOUR": __MEMPOOL_PRICE_UPDATES_PER_HOUR__,
|
||||
"MAX_TRACKED_ADDRESSES": __MEMPOOL_MAX_TRACKED_ADDRESSES__
|
||||
},
|
||||
@@ -46,7 +47,8 @@
|
||||
"PASSWORD": "__CORE_RPC_PASSWORD__",
|
||||
"TIMEOUT": __CORE_RPC_TIMEOUT__,
|
||||
"COOKIE": __CORE_RPC_COOKIE__,
|
||||
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__"
|
||||
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__",
|
||||
"DEBUG_LOG_PATH": "__CORE_RPC_DEBUG_LOG_PATH__"
|
||||
},
|
||||
"ELECTRUM": {
|
||||
"HOST": "__ELECTRUM_HOST__",
|
||||
|
||||
@@ -29,8 +29,9 @@ __MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info}
|
||||
__MEMPOOL_AUTOMATIC_POOLS_UPDATE__=${MEMPOOL_AUTOMATIC_POOLS_UPDATE:=false}
|
||||
__MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json}
|
||||
__MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master}
|
||||
__MEMPOOL_POOLS_UPDATE_DELAY__=${MEMPOOL_POOLS_UPDATE_DELAY:=604800}
|
||||
__MEMPOOL_AUDIT__=${MEMPOOL_AUDIT:=false}
|
||||
__MEMPOOL_RUST_GBT__=${MEMPOOL_RUST_GBT:=false}
|
||||
__MEMPOOL_RUST_GBT__=${MEMPOOL_RUST_GBT:=true}
|
||||
__MEMPOOL_LIMIT_GBT__=${MEMPOOL_LIMIT_GBT:=false}
|
||||
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
|
||||
__MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0}
|
||||
@@ -48,6 +49,7 @@ __CORE_RPC_PASSWORD__=${CORE_RPC_PASSWORD:=mempool}
|
||||
__CORE_RPC_TIMEOUT__=${CORE_RPC_TIMEOUT:=60000}
|
||||
__CORE_RPC_COOKIE__=${CORE_RPC_COOKIE:=false}
|
||||
__CORE_RPC_COOKIE_PATH__=${CORE_RPC_COOKIE_PATH:=""}
|
||||
__CORE_RPC_DEBUG_LOG_PATH__=${CORE_RPC_DEBUG_LOG_PATH:=""}
|
||||
|
||||
# ELECTRUM
|
||||
__ELECTRUM_HOST__=${ELECTRUM_HOST:=127.0.0.1}
|
||||
@@ -187,6 +189,7 @@ sed -i "s!__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__!${__MEMPOOL_STDOUT_LOG_MIN_PRIORIT
|
||||
sed -i "s!__MEMPOOL_AUTOMATIC_POOLS_UPDATE__!${__MEMPOOL_AUTOMATIC_POOLS_UPDATE__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_POOLS_UPDATE_DELAY__!${__MEMPOOL_POOLS_UPDATE_DELAY__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_AUDIT__!${__MEMPOOL_AUDIT__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_RUST_GBT__!${__MEMPOOL_RUST_GBT__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_LIMIT_GBT__!${__MEMPOOL_LIMIT_GBT__}!g" mempool-config.json
|
||||
@@ -205,6 +208,7 @@ sed -i "s!__CORE_RPC_PASSWORD__!${__CORE_RPC_PASSWORD__}!g" mempool-config.json
|
||||
sed -i "s!__CORE_RPC_TIMEOUT__!${__CORE_RPC_TIMEOUT__}!g" mempool-config.json
|
||||
sed -i "s!__CORE_RPC_COOKIE__!${__CORE_RPC_COOKIE__}!g" mempool-config.json
|
||||
sed -i "s!__CORE_RPC_COOKIE_PATH__!${__CORE_RPC_COOKIE_PATH__}!g" mempool-config.json
|
||||
sed -i "s!__CORE_RPC_DEBUG_LOG_PATH__!${__CORE_RPC_DEBUG_LOG_PATH__}!g" mempool-config.json
|
||||
|
||||
sed -i "s!__ELECTRUM_HOST__!${__ELECTRUM_HOST__}!g" mempool-config.json
|
||||
sed -i "s!__ELECTRUM_PORT__!${__ELECTRUM_PORT__}!g" mempool-config.json
|
||||
|
||||
@@ -33,7 +33,7 @@ $ npm run config:defaults:liquid
|
||||
|
||||
### 3. Run the Frontend
|
||||
|
||||
_Make sure to use Node.js 16.10 and npm 7._
|
||||
_Make sure to use Node.js 20.x and npm 9.x or newer._
|
||||
|
||||
Install project dependencies and run the frontend server:
|
||||
|
||||
@@ -70,7 +70,7 @@ Set up the [Mempool backend](../backend/) first, if you haven't already.
|
||||
|
||||
### 1. Build the Frontend
|
||||
|
||||
_Make sure to use Node.js 16.10 and npm 7._
|
||||
_Make sure to use Node.js 20.x and npm 9.x or newer._
|
||||
|
||||
Build the frontend:
|
||||
|
||||
|
||||
@@ -54,6 +54,10 @@
|
||||
"translation": "src/locale/messages.fr.xlf",
|
||||
"baseHref": "/fr/"
|
||||
},
|
||||
"hr": {
|
||||
"translation": "src/locale/messages.hr.xlf",
|
||||
"baseHref": "/hr/"
|
||||
},
|
||||
"ja": {
|
||||
"translation": "src/locale/messages.ja.xlf",
|
||||
"baseHref": "/ja/"
|
||||
|
||||
48
frontend/custom-bitb-config.json
Normal file
48
frontend/custom-bitb-config.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"theme": "wiz",
|
||||
"enterprise": "bitb",
|
||||
"branding": {
|
||||
"name": "bitb",
|
||||
"title": "BITB",
|
||||
"site_id": 20,
|
||||
"header_img": "/resources/bitblogo.svg",
|
||||
"footer_img": "/resources/bitblogo.svg"
|
||||
},
|
||||
"dashboard": {
|
||||
"widgets": [
|
||||
{
|
||||
"component": "fees",
|
||||
"mobileOrder": 4
|
||||
},
|
||||
{
|
||||
"component": "walletBalance",
|
||||
"mobileOrder": 1,
|
||||
"props": {
|
||||
"wallet": "BITB"
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": "goggles",
|
||||
"mobileOrder": 5
|
||||
},
|
||||
{
|
||||
"component": "wallet",
|
||||
"mobileOrder": 2,
|
||||
"props": {
|
||||
"wallet": "BITB",
|
||||
"period": "all"
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": "blocks"
|
||||
},
|
||||
{
|
||||
"component": "walletTransactions",
|
||||
"mobileOrder": 3,
|
||||
"props": {
|
||||
"wallet": "BITB"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -750,7 +750,7 @@
|
||||
},
|
||||
"backendInfo": {
|
||||
"hostname": "node205.tk7.mempool.space",
|
||||
"version": "3.0.0-dev",
|
||||
"version": "3.1.0-dev",
|
||||
"gitCommit": "abbc8a134",
|
||||
"lightning": false
|
||||
},
|
||||
|
||||
1750
frontend/package-lock.json
generated
1750
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mempool-frontend",
|
||||
"version": "3.0.0-dev",
|
||||
"version": "3.1.0-dev",
|
||||
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
||||
"license": "GNU Affero General Public License v3.0",
|
||||
"homepage": "https://mempool.space",
|
||||
@@ -92,10 +92,10 @@
|
||||
"ngx-infinite-scroll": "^17.0.0",
|
||||
"qrcode": "1.5.1",
|
||||
"rxjs": "~7.8.1",
|
||||
"esbuild": "^0.23.0",
|
||||
"esbuild": "^0.24.0",
|
||||
"tinyify": "^4.0.0",
|
||||
"tlite": "^0.1.9",
|
||||
"tslib": "~2.6.0",
|
||||
"tslib": "~2.8.0",
|
||||
"zone.js": "~0.14.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -105,7 +105,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "^7.4.0",
|
||||
"@typescript-eslint/parser": "^7.4.0",
|
||||
"eslint": "^8.57.0",
|
||||
"browser-sync": "^3.0.0",
|
||||
"browser-sync": "^3.0.3",
|
||||
"http-proxy-middleware": "~2.0.6",
|
||||
"prettier": "^3.0.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
@@ -115,7 +115,7 @@
|
||||
"optionalDependencies": {
|
||||
"@cypress/schematic": "^2.5.0",
|
||||
"@types/cypress": "^1.1.3",
|
||||
"cypress": "^13.13.0",
|
||||
"cypress": "^13.15.0",
|
||||
"cypress-fail-on-console-error": "~5.1.0",
|
||||
"cypress-wait-until": "^2.0.1",
|
||||
"mock-socket": "~9.3.1",
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { AppPreloadingStrategy } from './app.preloading-strategy'
|
||||
import { BlockViewComponent } from './components/block-view/block-view.component';
|
||||
import { EightBlocksComponent } from './components/eight-blocks/eight-blocks.component';
|
||||
import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component';
|
||||
import { ClockComponent } from './components/clock/clock.component';
|
||||
import { StatusViewComponent } from './components/status-view/status-view.component';
|
||||
import { AddressGroupComponent } from './components/address-group/address-group.component';
|
||||
import { TrackerComponent } from './components/tracker/tracker.component';
|
||||
import { AccelerateCheckout } from './components/accelerate-checkout/accelerate-checkout.component';
|
||||
import { TrackerGuard } from './route-guards';
|
||||
import { AppPreloadingStrategy } from '@app/app.preloading-strategy'
|
||||
import { BlockViewComponent } from '@components/block-view/block-view.component';
|
||||
import { EightBlocksComponent } from '@components/eight-blocks/eight-blocks.component';
|
||||
import { MempoolBlockViewComponent } from '@components/mempool-block-view/mempool-block-view.component';
|
||||
import { ClockComponent } from '@components/clock/clock.component';
|
||||
import { StatusViewComponent } from '@components/status-view/status-view.component';
|
||||
import { AddressGroupComponent } from '@components/address-group/address-group.component';
|
||||
import { TrackerComponent } from '@components/tracker/tracker.component';
|
||||
import { AccelerateCheckout } from '@components/accelerate-checkout/accelerate-checkout.component';
|
||||
import { TrackerGuard } from '@app/route-guards';
|
||||
|
||||
const browserWindow = window || {};
|
||||
// @ts-ignore
|
||||
@@ -22,16 +22,16 @@ let routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
||||
loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'wallet',
|
||||
path: 'widget/wallet',
|
||||
children: [],
|
||||
component: AddressGroupComponent,
|
||||
data: {
|
||||
@@ -45,7 +45,7 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
@@ -60,12 +60,12 @@ let routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
||||
loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
@@ -83,7 +83,7 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
@@ -103,16 +103,16 @@ let routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
||||
loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'wallet',
|
||||
path: 'widget/wallet',
|
||||
children: [],
|
||||
component: AddressGroupComponent,
|
||||
data: {
|
||||
@@ -126,7 +126,7 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
@@ -138,22 +138,22 @@ let routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'tx',
|
||||
canMatch: [TrackerGuard],
|
||||
runGuardsAndResolvers: 'always',
|
||||
loadChildren: () => import('./components/tracker/tracker.module').then(m => m.TrackerModule),
|
||||
loadChildren: () => import('@components/tracker/tracker.module').then(m => m.TrackerModule),
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
||||
loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'wallet',
|
||||
path: 'widget/wallet',
|
||||
children: [],
|
||||
component: AddressGroupComponent,
|
||||
data: {
|
||||
@@ -165,19 +165,19 @@ let routes: Routes = [
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
|
||||
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
|
||||
},
|
||||
{
|
||||
path: 'testnet',
|
||||
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
|
||||
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
|
||||
},
|
||||
{
|
||||
path: 'testnet4',
|
||||
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
|
||||
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
|
||||
},
|
||||
{
|
||||
path: 'signet',
|
||||
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
|
||||
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -212,7 +212,7 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
];
|
||||
@@ -225,16 +225,16 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
|
||||
loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
|
||||
loadChildren: () => import ('@app/liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'wallet',
|
||||
path: 'widget/wallet',
|
||||
children: [],
|
||||
component: AddressGroupComponent,
|
||||
data: {
|
||||
@@ -248,7 +248,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
|
||||
loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
@@ -260,16 +260,16 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
|
||||
loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
|
||||
loadChildren: () => import ('@app/liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'wallet',
|
||||
path: 'widget/wallet',
|
||||
children: [],
|
||||
component: AddressGroupComponent,
|
||||
data: {
|
||||
@@ -281,11 +281,11 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
|
||||
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
|
||||
},
|
||||
{
|
||||
path: 'testnet',
|
||||
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
|
||||
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -296,7 +296,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
|
||||
loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
];
|
||||
|
||||
@@ -151,7 +151,7 @@ export const languages: Language[] = [
|
||||
{ code: 'fr', name: 'Français' }, // French
|
||||
// { code: 'gl', name: 'Galego' }, // Galician
|
||||
{ code: 'ko', name: '한국어' }, // Korean
|
||||
// { code: 'hr', name: 'Hrvatski' }, // Croatian
|
||||
{ code: 'hr', name: 'Hrvatski' }, // Croatian
|
||||
// { code: 'id', name: 'Bahasa Indonesia' },// Indonesian
|
||||
{ code: 'hi', name: 'हिन्दी' }, // Hindi
|
||||
{ code: 'ne', name: 'नेपाली' }, // Nepalese
|
||||
|
||||
@@ -2,11 +2,11 @@ import { HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { ServerModule } from '@angular/platform-server';
|
||||
|
||||
import { ZONE_SERVICE } from './injection-tokens';
|
||||
import { ZONE_SERVICE } from '@app/injection-tokens';
|
||||
import { AppModule } from './app.module';
|
||||
import { AppComponent } from './components/app/app.component';
|
||||
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
|
||||
import { ZoneService } from './services/zone.service';
|
||||
import { AppComponent } from '@components/app/app.component';
|
||||
import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor';
|
||||
import { ZoneService } from '@app/services/zone.service';
|
||||
|
||||
|
||||
@NgModule({
|
||||
@@ -20,4 +20,4 @@ import { ZoneService } from './services/zone.service';
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppServerModule {}
|
||||
export class AppServerModule {}
|
||||
|
||||
@@ -2,35 +2,38 @@ import { BrowserModule } from '@angular/platform-browser';
|
||||
import { ModuleWithProviders, NgModule } from '@angular/core';
|
||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ZONE_SERVICE } from './injection-tokens';
|
||||
import { ZONE_SERVICE } from '@app/injection-tokens';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './components/app/app.component';
|
||||
import { ElectrsApiService } from './services/electrs-api.service';
|
||||
import { StateService } from './services/state.service';
|
||||
import { CacheService } from './services/cache.service';
|
||||
import { PriceService } from './services/price.service';
|
||||
import { EnterpriseService } from './services/enterprise.service';
|
||||
import { WebsocketService } from './services/websocket.service';
|
||||
import { AudioService } from './services/audio.service';
|
||||
import { PreloadService } from './services/preload.service';
|
||||
import { SeoService } from './services/seo.service';
|
||||
import { OpenGraphService } from './services/opengraph.service';
|
||||
import { ZoneService } from './services/zone-shim.service';
|
||||
import { SharedModule } from './shared/shared.module';
|
||||
import { StorageService } from './services/storage.service';
|
||||
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
|
||||
import { LanguageService } from './services/language.service';
|
||||
import { ThemeService } from './services/theme.service';
|
||||
import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe';
|
||||
import { FiatCurrencyPipe } from './shared/pipes/fiat-currency.pipe';
|
||||
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
|
||||
import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe';
|
||||
import { AppPreloadingStrategy } from './app.preloading-strategy';
|
||||
import { ServicesApiServices } from './services/services-api.service';
|
||||
import { AppComponent } from '@components/app/app.component';
|
||||
import { ElectrsApiService } from '@app/services/electrs-api.service';
|
||||
import { OrdApiService } from '@app/services/ord-api.service';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { CacheService } from '@app/services/cache.service';
|
||||
import { PriceService } from '@app/services/price.service';
|
||||
import { EnterpriseService } from '@app/services/enterprise.service';
|
||||
import { WebsocketService } from '@app/services/websocket.service';
|
||||
import { AudioService } from '@app/services/audio.service';
|
||||
import { PreloadService } from '@app/services/preload.service';
|
||||
import { SeoService } from '@app/services/seo.service';
|
||||
import { OpenGraphService } from '@app/services/opengraph.service';
|
||||
import { ZoneService } from '@app/services/zone-shim.service';
|
||||
import { SharedModule } from '@app/shared/shared.module';
|
||||
import { StorageService } from '@app/services/storage.service';
|
||||
import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor';
|
||||
import { LanguageService } from '@app/services/language.service';
|
||||
import { ThemeService } from '@app/services/theme.service';
|
||||
import { TimeService } from '@app/services/time.service';
|
||||
import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe';
|
||||
import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe';
|
||||
import { ShortenStringPipe } from '@app/shared/pipes/shorten-string-pipe/shorten-string.pipe';
|
||||
import { CapAddressPipe } from '@app/shared/pipes/cap-address-pipe/cap-address-pipe';
|
||||
import { AppPreloadingStrategy } from '@app/app.preloading-strategy';
|
||||
import { ServicesApiServices } from '@app/services/services-api.service';
|
||||
import { DatePipe } from '@angular/common';
|
||||
|
||||
const providers = [
|
||||
ElectrsApiService,
|
||||
OrdApiService,
|
||||
StateService,
|
||||
CacheService,
|
||||
PriceService,
|
||||
@@ -42,6 +45,7 @@ const providers = [
|
||||
EnterpriseService,
|
||||
LanguageService,
|
||||
ThemeService,
|
||||
TimeService,
|
||||
ShortenStringPipe,
|
||||
FiatShortenerPipe,
|
||||
FiatCurrencyPipe,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { MasterPageComponent } from './components/master-page/master-page.component';
|
||||
import { MasterPageComponent } from '@components/master-page/master-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: MasterPageComponent,
|
||||
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule),
|
||||
loadChildren: () => import('@app/graphs/graphs.module').then(m => m.GraphsModule),
|
||||
data: { preload: true },
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Transaction, Vin } from './interfaces/electrs.interface';
|
||||
import { Hash } from './shared/sha256';
|
||||
import { Transaction, Vin } from '@interfaces/electrs.interface';
|
||||
import { Hash } from '@app/shared/sha256';
|
||||
|
||||
const P2SH_P2WPKH_COST = 21 * 4; // the WU cost for the non-witness part of P2SH-P2WPKH
|
||||
const P2SH_P2WSH_COST = 35 * 4; // the WU cost for the non-witness part of P2SH-P2WSH
|
||||
@@ -135,7 +135,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
|
||||
return;
|
||||
}
|
||||
const opN = ops.pop();
|
||||
if (!opN.startsWith('OP_PUSHNUM_')) {
|
||||
if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
|
||||
return;
|
||||
}
|
||||
const n = parseInt(opN.match(/[0-9]+/)[0], 10);
|
||||
@@ -152,7 +152,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
|
||||
}
|
||||
}
|
||||
const opM = ops.pop();
|
||||
if (!opM.startsWith('OP_PUSHNUM_')) {
|
||||
if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
|
||||
return;
|
||||
}
|
||||
const m = parseInt(opM.match(/[0-9]+/)[0], 10);
|
||||
@@ -303,4 +303,4 @@ export async function calcScriptHash$(script: string): Promise<string> {
|
||||
return hashArray
|
||||
.map((bytes) => bytes.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { EnterpriseService } from '../../services/enterprise.service';
|
||||
import { EnterpriseService } from '@app/services/enterprise.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-about-sponsors',
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<span>Spiral</span>
|
||||
</a>
|
||||
<a href="https://foundrydigital.com/" target="_blank" title="Foundry">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" style="zoom: 1;" width="32" height="76" viewBox="0 0 32 76">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" style="zoom: 1;" width="32" height="90" viewBox="0 -5 32 90" class="image">
|
||||
<defs>
|
||||
<style>
|
||||
.d {
|
||||
@@ -125,17 +125,14 @@
|
||||
<span>Blockstream</span>
|
||||
</a>
|
||||
<a href="https://unchained.com/" target="_blank" title="Unchained">
|
||||
<svg id="Layer_1" width="78" height="78" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 156.68 156.68"><defs><style>.cls-unchained-1{fill:#fff;}</style></defs><path class="cls-unchained-1" d="m78.34,0C35.07,0,0,35.07,0,78.34s35.07,78.34,78.34,78.34,78.34-35.07,78.34-78.34S121.6,0,78.34,0ZM20.23,109.5c-4.99-9.28-7.81-19.89-7.81-31.16C12.42,41.93,41.93,12.42,78.34,12.42c33.15,0,60.58,24.46,65.23,56.32h-37.48c-45.29,0-71.19,20.05-85.85,40.76Zm58.11,34.76c-12.42,0-24.04-3.44-33.96-9.41,3.94-8.85,9.11-18.7,15.84-28.9,20.99-31.8,52.2-31.19,76.49-31.19h7.45c.06,1.18.1,2.38.1,3.58,0,36.41-29.51,65.92-65.92,65.92Z"/><path class="cls-unchained-1" d="m91.98,42.4l-3.62-1.18c-3.94-1.29-7.03-4.38-8.32-8.32l-1.18-3.63c-.13-.39-.68-.39-.81,0l-1.18,3.63c-1.29,3.94-4.38,7.03-8.32,8.32l-3.62,1.18c-.39.13-.39.68,0,.81l3.62,1.18c3.94,1.29,7.03,4.38,8.32,8.32l1.18,3.63c.13.39.68.39.81,0l1.18-3.63c1.29-3.94,4.38-7.03,8.32-8.32l3.62-1.18c.39-.13.39-.68,0-.81Z"/></svg>
|
||||
<svg id="Layer_1" width="78" height="78" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 156.68 156.68" class="image">
|
||||
<defs><style>.cls-unchained-1{fill:#fff;}</style></defs><path class="cls-unchained-1" d="m78.34,0C35.07,0,0,35.07,0,78.34s35.07,78.34,78.34,78.34,78.34-35.07,78.34-78.34S121.6,0,78.34,0ZM20.23,109.5c-4.99-9.28-7.81-19.89-7.81-31.16C12.42,41.93,41.93,12.42,78.34,12.42c33.15,0,60.58,24.46,65.23,56.32h-37.48c-45.29,0-71.19,20.05-85.85,40.76Zm58.11,34.76c-12.42,0-24.04-3.44-33.96-9.41,3.94-8.85,9.11-18.7,15.84-28.9,20.99-31.8,52.2-31.19,76.49-31.19h7.45c.06,1.18.1,2.38.1,3.58,0,36.41-29.51,65.92-65.92,65.92Z"/><path class="cls-unchained-1" d="m91.98,42.4l-3.62-1.18c-3.94-1.29-7.03-4.38-8.32-8.32l-1.18-3.63c-.13-.39-.68-.39-.81,0l-1.18,3.63c-1.29,3.94-4.38,7.03-8.32,8.32l-3.62,1.18c-.39.13-.39.68,0,.81l3.62,1.18c3.94,1.29,7.03,4.38,8.32,8.32l1.18,3.63c.13.39.68.39.81,0l1.18-3.63c1.29-3.94,4.38-7.03,8.32-8.32l3.62-1.18c.39-.13.39-.68,0-.81Z"/>
|
||||
</svg>
|
||||
<span>Unchained</span>
|
||||
</a>
|
||||
<a href="https://gemini.com/" target="_blank" title="Gemini">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360" class="image">
|
||||
<rect style="fill: black" width="360" height="360" />
|
||||
<g transform="matrix(0.62 0 0 0.62 180 180)">
|
||||
<path style="fill: rgb(0,220,250)" transform=" translate(-162, -162)" d="M 211.74 0 C 154.74 0 106.35 43.84 100.25 100.25 C 43.84 106.35 1.4210854715202004e-14 154.76 1.4210854715202004e-14 211.74 C 0.044122601308501076 273.7212006364817 50.27879936351834 323.95587739869154 112.26 324 C 169.26 324 217.84 280.15999999999997 223.75 223.75 C 280.15999999999997 217.65 324 169.24 324 112.26 C 323.95587739869154 50.278799363518324 273.72120063648174 0.04412260130848722 211.74 -1.4210854715202004e-14 z M 297.74 124.84 C 291.9644950552469 162.621439649343 262.2969457716857 192.26062994820046 224.51 198 L 224.51 124.84 z M 26.3 199.16 C 31.986912917108594 161.30935034910615 61.653433460549415 131.56986937804106 99.48999999999998 125.78999999999999 L 99.49 199 L 26.3 199 z M 198.21 224.51 C 191.87736076583954 267.0991541201681 155.312384597087 298.62923417787493 112.255 298.62923417787493 C 69.19761540291302 298.62923417787493 32.63263923416048 267.0991541201682 26.3 224.51 z M 199.16 124.83999999999999 L 199.16 199 L 124.84 199 L 124.84 124.84 z M 297.7 99.48999999999998 L 125.78999999999999 99.48999999999998 C 132.12263923416046 56.90084587983182 168.687615402913 25.37076582212505 211.745 25.37076582212505 C 254.80238459708698 25.37076582212505 291.3673607658395 56.900845879831834 297.7 99.49 z" stroke-linecap="round" />
|
||||
</g>
|
||||
</svg>
|
||||
<span>Gemini</span>
|
||||
<a href="https://bitkey.world/" target="_blank" title="Bitkey">
|
||||
<img class="image" src="/resources/profile/bitkey.svg" />
|
||||
<span>Bitkey</span>
|
||||
</a>
|
||||
<a href="https://bullbitcoin.com/" target="_blank" title="Bull Bitcoin">
|
||||
<svg aria-hidden="true" class="image" viewBox="0 -5 40 40" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -150,7 +147,7 @@
|
||||
<span>Bull Bitcoin</span>
|
||||
</a>
|
||||
<a href="https://exodus.com/" target="_blank" title="Exodus">
|
||||
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg" class="image">
|
||||
<circle cx="250" cy="250" r="250" fill="#1F2033"/>
|
||||
<g clip-path="url(#clip0_2_14)">
|
||||
<path d="M411.042 178.303L271.79 87V138.048L361.121 196.097L350.612 229.351H271.79V271.648H350.612L361.121 304.903L271.79 362.952V414L411.042 322.989L388.271 250.646L411.042 178.303Z" fill="url(#paint0_linear_2_14)"/>
|
||||
@@ -191,12 +188,30 @@
|
||||
</svg>
|
||||
<span>Exodus</span>
|
||||
</a>
|
||||
<a href="https://gemini.com/" target="_blank" title="Gemini">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360" class="image">
|
||||
<rect style="fill: black" width="360" height="360" />
|
||||
<g transform="matrix(0.62 0 0 0.62 180 180)">
|
||||
<path style="fill: rgb(0,220,250)" transform=" translate(-162, -162)" d="M 211.74 0 C 154.74 0 106.35 43.84 100.25 100.25 C 43.84 106.35 1.4210854715202004e-14 154.76 1.4210854715202004e-14 211.74 C 0.044122601308501076 273.7212006364817 50.27879936351834 323.95587739869154 112.26 324 C 169.26 324 217.84 280.15999999999997 223.75 223.75 C 280.15999999999997 217.65 324 169.24 324 112.26 C 323.95587739869154 50.278799363518324 273.72120063648174 0.04412260130848722 211.74 -1.4210854715202004e-14 z M 297.74 124.84 C 291.9644950552469 162.621439649343 262.2969457716857 192.26062994820046 224.51 198 L 224.51 124.84 z M 26.3 199.16 C 31.986912917108594 161.30935034910615 61.653433460549415 131.56986937804106 99.48999999999998 125.78999999999999 L 99.49 199 L 26.3 199 z M 198.21 224.51 C 191.87736076583954 267.0991541201681 155.312384597087 298.62923417787493 112.255 298.62923417787493 C 69.19761540291302 298.62923417787493 32.63263923416048 267.0991541201682 26.3 224.51 z M 199.16 124.83999999999999 L 199.16 199 L 124.84 199 L 124.84 124.84 z M 297.7 99.48999999999998 L 125.78999999999999 99.48999999999998 C 132.12263923416046 56.90084587983182 168.687615402913 25.37076582212505 211.745 25.37076582212505 C 254.80238459708698 25.37076582212505 291.3673607658395 56.900845879831834 297.7 99.49 z" stroke-linecap="round" />
|
||||
</g>
|
||||
</svg>
|
||||
<span>Gemini</span>
|
||||
</a>
|
||||
<a href="https://leather.io/" target="_blank" title="Leather">
|
||||
<img class="image" src="/resources/profile/leather.svg" />
|
||||
<span>Leather</span>
|
||||
</a>
|
||||
|
||||
<a href="https://taprootwizards.com/" target="_blank" title="Taproot Wizards">
|
||||
<img class="image" src="/resources/profile/wizardhat.png" />
|
||||
<span>Taproot Wizards</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container>
|
||||
<div *ngIf="profiles$ | async as profiles" id="community-sponsors-anchor">
|
||||
<div class="community-sponsor" style="margin-bottom: 68px" *ngIf="profiles.whales.length > 0">
|
||||
<div class="community-sponsor whale-sponsor" style="margin-bottom: 68px" *ngIf="profiles.whales.length > 0">
|
||||
<h3 i18n="about.sponsors.withHeart">Whale Sponsors</h3>
|
||||
<div class="wrapper">
|
||||
<ng-container>
|
||||
@@ -435,7 +450,7 @@
|
||||
Trademark Notice<br>
|
||||
</div>
|
||||
<p>
|
||||
The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem®, Mempool Goggles™, the mempool logo, the mempool Square logo, the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical Logo, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
|
||||
The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem®, Mempool Goggles™, the mempool Logo, the mempool Square Logo, the mempool block visualization Logo, the mempool Blocks Logo, the mempool transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo, the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
|
||||
</p>
|
||||
<p>
|
||||
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on <https://mempool.space/trademark-policy>.
|
||||
|
||||
@@ -13,8 +13,6 @@
|
||||
|
||||
.image.not-rounded {
|
||||
border-radius: 0;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.intro {
|
||||
@@ -94,6 +92,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.whale-sponsor {
|
||||
img {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
.alliances {
|
||||
margin-bottom: 100px;
|
||||
a {
|
||||
@@ -158,9 +163,8 @@
|
||||
margin: 40px 29px 10px;
|
||||
&.image.coldcard {
|
||||
border-radius: 0;
|
||||
width: auto;
|
||||
max-height: 50px;
|
||||
margin: 40px 29px 14px 29px;
|
||||
height: auto;
|
||||
margin: 20px 29px 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,3 +258,12 @@
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.enterprise-sponsor {
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
max-width: 800px;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
import { ChangeDetectionStrategy, Component, ElementRef, Inject, LOCALE_ID, OnInit, ViewChild } from '@angular/core';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { OpenGraphService } from '../../services/opengraph.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { WebsocketService } from '@app/services/websocket.service';
|
||||
import { SeoService } from '@app/services/seo.service';
|
||||
import { OpenGraphService } from '@app/services/opengraph.service';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { IBackendInfo } from '../../interfaces/websocket.interface';
|
||||
import { ApiService } from '@app/services/api.service';
|
||||
import { IBackendInfo } from '@interfaces/websocket.interface';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { map, share, tap } from 'rxjs/operators';
|
||||
import { ITranslators } from '../../interfaces/node-api.interface';
|
||||
import { ITranslators } from '@interfaces/node-api.interface';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { EnterpriseService } from '../../services/enterprise.service';
|
||||
import { EnterpriseService } from '@app/services/enterprise.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-about',
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { AboutComponent } from './about.component';
|
||||
import { AboutSponsorsComponent } from './about-sponsors.component';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { AboutComponent } from '@components/about/about.component';
|
||||
import { AboutSponsorsComponent } from '@components/about/about-sponsors.component';
|
||||
import { SharedModule } from '@app/shared/shared.module';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
|
||||
@@ -403,14 +403,14 @@
|
||||
}
|
||||
@if (canPayWithApplePay) {
|
||||
@if (canPayWithCashapp) { <span class="mt-1 mb-1"></span> }
|
||||
<div class="paymentMethod mx-2" style="width: 200px; height: 55px">
|
||||
<img src="/resources/apple-pay.png" height=37 (click)="moveToStep('applepay')">
|
||||
<div class="paymentMethod mx-2" style="width: 200px; height: 55px" (click)="moveToStep('applepay')">
|
||||
<img src="/resources/apple-pay.png" height=37>
|
||||
</div>
|
||||
}
|
||||
@if (canPayWithGooglePay) {
|
||||
@if (canPayWithCashapp || canPayWithApplePay) { <span class="mt-1 mb-1"></span> }
|
||||
<div class="paymentMethod mx-2" style="width: 200px; height: 55px">
|
||||
<img src="/resources/google-pay.png" height=37 (click)="moveToStep('googlepay')">
|
||||
<div class="paymentMethod mx-2" style="width: 200px; height: 55px" (click)="moveToStep('googlepay')">
|
||||
<img src="/resources/google-pay.png" height=37>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -525,7 +525,7 @@
|
||||
<div class="col-sm">
|
||||
<div class="d-flex flex-row flex-column justify-content-center align-items-center">
|
||||
<span i18n="accelerator.confirming-acceleration-with-miners">Confirming your acceleration with our mining pool partners...</span>
|
||||
@if (timeSincePaid > 20000) {
|
||||
@if (timeSincePaid > 30000) {
|
||||
<span i18n="accelerator.confirming-acceleration-with-miners">...sorry, this is taking longer than expected...</span>
|
||||
}
|
||||
<div class="m-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
/* eslint-disable no-console */
|
||||
import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core';
|
||||
import { Subscription, tap, of, catchError, Observable, switchMap } from 'rxjs';
|
||||
import { ServicesApiServices } from '../../services/services-api.service';
|
||||
import { md5, insecureRandomUUID } from '../../shared/common.utils';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { AudioService } from '../../services/audio.service';
|
||||
import { ETA, EtaService } from '../../services/eta.service';
|
||||
import { Transaction } from '../../interfaces/electrs.interface';
|
||||
import { MiningStats } from '../../services/mining.service';
|
||||
import { IAuth, AuthServiceMempool } from '../../services/auth.service';
|
||||
import { EnterpriseService } from '../../services/enterprise.service';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { ServicesApiServices } from '@app/services/services-api.service';
|
||||
import { md5, insecureRandomUUID } from '@app/shared/common.utils';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { AudioService } from '@app/services/audio.service';
|
||||
import { ETA, EtaService } from '@app/services/eta.service';
|
||||
import { Transaction } from '@interfaces/electrs.interface';
|
||||
import { MiningStats } from '@app/services/mining.service';
|
||||
import { IAuth, AuthServiceMempool } from '@app/services/auth.service';
|
||||
import { EnterpriseService } from '@app/services/enterprise.service';
|
||||
import { ApiService } from '@app/services/api.service';
|
||||
import { isDevMode } from '@angular/core';
|
||||
|
||||
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay';
|
||||
@@ -75,6 +75,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
@Output() changeMode = new EventEmitter<boolean>();
|
||||
|
||||
calculating = true;
|
||||
processing = false;
|
||||
selectedOption: 'wait' | 'accel';
|
||||
cantPayReason = '';
|
||||
quoteError = ''; // error fetching estimate or initial data
|
||||
@@ -83,13 +84,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
timePaid: number = 0; // time acceleration requested
|
||||
math = Math;
|
||||
isMobile: boolean = window.innerWidth <= 767.98;
|
||||
isProdDomain = ['mempool.space',
|
||||
'mempool-staging.va1.mempool.space',
|
||||
'mempool-staging.fmt.mempool.space',
|
||||
'mempool-staging.fra.mempool.space',
|
||||
'mempool-staging.tk7.mempool.space',
|
||||
'mempool-staging.sg1.mempool.space'
|
||||
].indexOf(document.location.hostname) > -1;
|
||||
isProdDomain = false;
|
||||
|
||||
private _step: CheckoutStep = 'summary';
|
||||
simpleMode: boolean = true;
|
||||
@@ -142,6 +137,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
private authService: AuthServiceMempool,
|
||||
private enterpriseService: EnterpriseService,
|
||||
) {
|
||||
this.isProdDomain = this.stateService.env.PROD_DOMAINS.indexOf(document.location.hostname) > -1;
|
||||
this.accelerationUUID = insecureRandomUUID();
|
||||
|
||||
// Check if Apple Pay available
|
||||
@@ -196,9 +192,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
if (changes.scrollEvent && this.scrollEvent) {
|
||||
this.scrollToElement('acceleratePreviewAnchor', 'start');
|
||||
}
|
||||
if (changes.accelerating) {
|
||||
if ((this.step === 'processing' || this.step === 'paid') && this.accelerating) {
|
||||
if (changes.accelerating && this.accelerating) {
|
||||
if (this.step === 'processing' || this.step === 'paid') {
|
||||
this.moveToStep('success');
|
||||
} else { // Edge case where the transaction gets accelerated by someone else or on another session
|
||||
this.closeModal();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -371,6 +369,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
this.selectFeeRateIndex = index;
|
||||
this.userBid = Math.max(0, fee);
|
||||
this.cost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
|
||||
this.validateChoice();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,9 +377,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
* Account-based acceleration request
|
||||
*/
|
||||
accelerateWithMempoolAccount(): void {
|
||||
if (!this.canPay || this.calculating) {
|
||||
if (!this.canPay || this.calculating || this.processing) {
|
||||
return;
|
||||
}
|
||||
this.processing = true;
|
||||
if (this.accelerationSubscription) {
|
||||
this.accelerationSubscription.unsubscribe();
|
||||
}
|
||||
@@ -390,6 +390,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
this.accelerationUUID
|
||||
).subscribe({
|
||||
next: () => {
|
||||
this.processing = false;
|
||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||
this.audioService.playSound('ascend-chime-cartoon');
|
||||
this.showSuccess = true;
|
||||
@@ -397,6 +398,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
this.moveToStep('paid');
|
||||
},
|
||||
error: (response) => {
|
||||
this.processing = false;
|
||||
this.accelerateError = response.error;
|
||||
}
|
||||
});
|
||||
@@ -466,10 +468,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
* APPLE PAY
|
||||
*/
|
||||
async requestApplePayPayment(): Promise<void> {
|
||||
if (this.processing) {
|
||||
return;
|
||||
}
|
||||
if (this.conversionsSubscription) {
|
||||
this.conversionsSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
this.processing = true;
|
||||
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||
async (conversions) => {
|
||||
this.conversions = conversions;
|
||||
@@ -494,6 +500,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
console.error(`Unable to find apple pay button id='apple-pay-button'`);
|
||||
// Try again
|
||||
setTimeout(this.requestApplePayPayment.bind(this), 500);
|
||||
this.processing = false;
|
||||
return;
|
||||
}
|
||||
this.loadingApplePay = false;
|
||||
@@ -505,6 +512,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
||||
console.error(`Cannot retreive payment card details`);
|
||||
this.accelerateError = 'apple_pay_no_card_details';
|
||||
this.processing = false;
|
||||
return;
|
||||
}
|
||||
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
|
||||
@@ -513,9 +521,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
tokenResult.token,
|
||||
cardTag,
|
||||
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
||||
this.accelerationUUID
|
||||
this.accelerationUUID,
|
||||
costUSD
|
||||
).subscribe({
|
||||
next: () => {
|
||||
this.processing = false;
|
||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||
this.audioService.playSound('ascend-chime-cartoon');
|
||||
if (this.applePay) {
|
||||
@@ -526,6 +536,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
}, 1000);
|
||||
},
|
||||
error: (response) => {
|
||||
this.processing = false;
|
||||
this.accelerateError = response.error;
|
||||
if (!(response.status === 403 && response.error === 'not_available')) {
|
||||
setTimeout(() => {
|
||||
@@ -537,6 +548,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.processing = false;
|
||||
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
|
||||
if (tokenResult.errors) {
|
||||
errorMessage += ` and errors: ${JSON.stringify(
|
||||
@@ -547,6 +559,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
this.processing = false;
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
@@ -557,10 +570,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
* GOOGLE PAY
|
||||
*/
|
||||
async requestGooglePayPayment(): Promise<void> {
|
||||
if (this.processing) {
|
||||
return;
|
||||
}
|
||||
if (this.conversionsSubscription) {
|
||||
this.conversionsSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
|
||||
this.processing = true;
|
||||
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||
async (conversions) => {
|
||||
this.conversions = conversions;
|
||||
@@ -595,6 +612,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
||||
console.error(`Cannot retreive payment card details`);
|
||||
this.accelerateError = 'apple_pay_no_card_details';
|
||||
this.processing = false;
|
||||
return;
|
||||
}
|
||||
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
|
||||
@@ -603,9 +621,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
tokenResult.token,
|
||||
cardTag,
|
||||
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
||||
this.accelerationUUID
|
||||
this.accelerationUUID,
|
||||
costUSD
|
||||
).subscribe({
|
||||
next: () => {
|
||||
this.processing = false;
|
||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||
this.audioService.playSound('ascend-chime-cartoon');
|
||||
if (this.googlePay) {
|
||||
@@ -616,6 +636,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
}, 1000);
|
||||
},
|
||||
error: (response) => {
|
||||
this.processing = false;
|
||||
this.accelerateError = response.error;
|
||||
if (!(response.status === 403 && response.error === 'not_available')) {
|
||||
setTimeout(() => {
|
||||
@@ -627,6 +648,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.processing = false;
|
||||
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
|
||||
if (tokenResult.errors) {
|
||||
errorMessage += ` and errors: ${JSON.stringify(
|
||||
@@ -644,10 +666,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
* CASHAPP
|
||||
*/
|
||||
async requestCashAppPayment(): Promise<void> {
|
||||
if (this.processing) {
|
||||
return;
|
||||
}
|
||||
if (this.conversionsSubscription) {
|
||||
this.conversionsSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
this.processing = true;
|
||||
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||
async (conversions) => {
|
||||
this.conversions = conversions;
|
||||
@@ -678,6 +704,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
this.cashAppPay.addEventListener('ontokenization', event => {
|
||||
const { tokenResult, error } = event.detail;
|
||||
if (error) {
|
||||
this.processing = false;
|
||||
this.accelerateError = error;
|
||||
} else if (tokenResult.status === 'OK') {
|
||||
this.servicesApiService.accelerateWithCashApp$(
|
||||
@@ -685,9 +712,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
tokenResult.token,
|
||||
tokenResult.details.cashAppPay.cashtag,
|
||||
tokenResult.details.cashAppPay.referenceId,
|
||||
this.accelerationUUID
|
||||
this.accelerationUUID,
|
||||
costUSD
|
||||
).subscribe({
|
||||
next: () => {
|
||||
this.processing = false;
|
||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||
this.audioService.playSound('ascend-chime-cartoon');
|
||||
if (this.cashAppPay) {
|
||||
@@ -702,6 +731,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
}, 1000);
|
||||
},
|
||||
error: (response) => {
|
||||
this.processing = false;
|
||||
this.accelerateError = response.error;
|
||||
if (!(response.status === 403 && response.error === 'not_available')) {
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<span class="fee">{{ bar.class === 'tx' ? '' : '+' }}{{ bar.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></span>
|
||||
<span class="fee">{{ bar.class === 'tx' ? '' : '+' }}{{ bar.fee | number }} <span class="symbol" i18n="shared.sats">sats</span></span>
|
||||
<div class="spacer"></div>
|
||||
<div class="spacer"></div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, Input, Output, OnChanges, EventEmitter, HostListener, OnInit, ViewChild, ElementRef, AfterViewInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
|
||||
import { Transaction } from '../../interfaces/electrs.interface';
|
||||
import { AccelerationEstimate, RateOption } from './accelerate-checkout.component';
|
||||
import { Transaction } from '@interfaces/electrs.interface';
|
||||
import { AccelerationEstimate, RateOption } from '@components/accelerate-checkout/accelerate-checkout.component';
|
||||
|
||||
interface GraphBar {
|
||||
rate: number;
|
||||
|
||||
@@ -21,14 +21,14 @@
|
||||
</tr>
|
||||
<tr *ngIf="accelerationInfo.fee">
|
||||
<td class="label" i18n="transaction.fee|Transaction fee">Fee</td>
|
||||
<td class="value">{{ accelerationInfo.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
|
||||
<td class="value">{{ accelerationInfo.fee | number }} <span class="symbol" i18n="shared.sats">sats</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="accelerationInfo.bidBoost >= 0 || accelerationInfo.feeDelta">
|
||||
<td class="label" i18n="transaction.out-of-band-fees">Out-of-band fees</td>
|
||||
@if (accelerationInfo.status === 'accelerated') {
|
||||
<td style="color: #905cf4;" class="value">{{ accelerationInfo.feeDelta | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
|
||||
<td class="value oobFees">{{ accelerationInfo.feeDelta | number }} <span class="symbol" i18n="shared.sats">sats</span></td>
|
||||
} @else {
|
||||
<td style="color: #905cf4;" class="value">{{ accelerationInfo.bidBoost | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
|
||||
<td class="value oobFees">{{ accelerationInfo.bidBoost | number }} <span class="symbol" i18n="shared.sats">sats</span></td>
|
||||
}
|
||||
</tr>
|
||||
<tr *ngIf="accelerationInfo.fee && accelerationInfo.weight">
|
||||
@@ -38,22 +38,23 @@
|
||||
} @else if (accelerationInfo.status === 'accelerated' || accelerationInfo.status === 'mined') {
|
||||
<td class="label" i18n="transaction.accelerated-fee-rate|Accelerated transaction fee rate">Accelerated fee rate</td>
|
||||
@if (accelerationInfo.status === 'accelerated') {
|
||||
<td class="value"><app-fee-rate [fee]="accelerationInfo.fee + (accelerationInfo.feeDelta || 0)" [weight]="accelerationInfo.weight"></app-fee-rate></td>
|
||||
<td class="value oobFees"><app-fee-rate [fee]="accelerationInfo.fee + (accelerationInfo.feeDelta || 0)" [weight]="accelerationInfo.weight"></app-fee-rate></td>
|
||||
} @else {
|
||||
<td class="value"><app-fee-rate [fee]="accelerationInfo.fee + (accelerationInfo.bidBoost || 0)" [weight]="accelerationInfo.weight"></app-fee-rate></td>
|
||||
<td class="value oobFees"><app-fee-rate [fee]="accelerationInfo.fee + (accelerationInfo.bidBoost || 0)" [weight]="accelerationInfo.weight"></app-fee-rate></td>
|
||||
}
|
||||
}
|
||||
</tr>
|
||||
<tr *ngIf="['accelerated', 'mined'].includes(accelerationInfo.status) && hasPoolsData()">
|
||||
<td class="label" i18n="transaction.accelerated-by-hashrate|Accelerated to hashrate">Accelerated by</td>
|
||||
<td class="value" *ngIf="accelerationInfo.pools">
|
||||
<ng-container *ngFor="let pool of accelerationInfo.pools">
|
||||
<ng-container *ngFor="let pool of accelerationInfo.pools; let i = index;">
|
||||
<img *ngIf="accelerationInfo.poolsData[pool]"
|
||||
class="pool-logo"
|
||||
[style.opacity]="accelerationInfo?.minedByPoolUniqueId && pool !== accelerationInfo?.minedByPoolUniqueId ? '0.3' : '1'"
|
||||
[src]="'/resources/mining-pools/' + accelerationInfo.poolsData[pool].slug + '.svg'"
|
||||
onError="this.src = '/resources/mining-pools/default.svg'"
|
||||
[alt]="'Logo of ' + pool.name + ' mining pool'">
|
||||
<br *ngIf="i % 6 === 5">
|
||||
</ng-container>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
.label {
|
||||
padding-right: 30px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.pool-logo {
|
||||
@@ -30,6 +31,11 @@
|
||||
height: 22px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
margin-right: 3px;
|
||||
margin-right: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.oobFees {
|
||||
color: #905cf4;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="interval">
|
||||
<div class="interval-time">
|
||||
@if (eta) {
|
||||
~<app-time [time]="eta?.wait / 1000"></app-time> <!-- <span *ngIf="accelerateRatio > 1" class="compare"> ({{ accelerateRatio }}x faster)</span> -->
|
||||
~<app-time [time]="eta?.wait / 1000"></app-time>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -38,7 +38,7 @@
|
||||
<div class="node-spacer"></div>
|
||||
<div class="interval">
|
||||
<div class="interval-time">
|
||||
<app-time [time]="acceleratedAt - transactionTime"></app-time>
|
||||
<app-time [time]="firstSeenToAccelerated"></app-time>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-spacer"></div>
|
||||
@@ -46,10 +46,8 @@
|
||||
<div class="interval-time">
|
||||
@if (tx.status.confirmed) {
|
||||
<div class="interval-time">
|
||||
<app-time [time]="tx.status.block_time - acceleratedAt"></app-time>
|
||||
<app-time [time]="acceleratedToMined"></app-time>
|
||||
</div>
|
||||
} @else if (standardETA && !tx.status.confirmed) {
|
||||
<!-- ~<app-time [time]="standardETA / 1000 - now"></app-time> -->
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, Input, OnInit, OnChanges, HostListener } from '@angular/core';
|
||||
import { ETA } from '../../services/eta.service';
|
||||
import { Transaction } from '../../interfaces/electrs.interface';
|
||||
import { Acceleration, SinglePoolStats } from '../../interfaces/node-api.interface';
|
||||
import { MiningService } from '../../services/mining.service';
|
||||
import { ETA } from '@app/services/eta.service';
|
||||
import { Transaction } from '@interfaces/electrs.interface';
|
||||
import { Acceleration, SinglePoolStats } from '@interfaces/node-api.interface';
|
||||
import { MiningService } from '@app/services/mining.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-acceleration-timeline',
|
||||
@@ -11,19 +11,16 @@ import { MiningService } from '../../services/mining.service';
|
||||
})
|
||||
export class AccelerationTimelineComponent implements OnInit, OnChanges {
|
||||
@Input() transactionTime: number;
|
||||
@Input() acceleratedAt: number;
|
||||
@Input() tx: Transaction;
|
||||
@Input() accelerationInfo: Acceleration;
|
||||
@Input() eta: ETA;
|
||||
// A mined transaction has standard ETA and accelerated ETA undefined
|
||||
// A transaction in mempool has either standardETA defined (if accelerated) or acceleratedETA defined (if not accelerated yet)
|
||||
@Input() standardETA: number;
|
||||
@Input() acceleratedETA: number;
|
||||
|
||||
acceleratedAt: number;
|
||||
now: number;
|
||||
accelerateRatio: number;
|
||||
useAbsoluteTime: boolean = false;
|
||||
interval: number;
|
||||
firstSeenToAccelerated: number;
|
||||
acceleratedToMined: number;
|
||||
|
||||
tooltipPosition = null;
|
||||
hoverInfo: any = null;
|
||||
@@ -34,38 +31,24 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.acceleratedAt = this.tx.acceleratedAt ?? new Date().getTime() / 1000;
|
||||
this.now = Math.floor(new Date().getTime() / 1000);
|
||||
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
|
||||
this.updateTimes();
|
||||
|
||||
this.miningService.getPools().subscribe(pools => {
|
||||
for (const pool of pools) {
|
||||
this.poolsData[pool.unique_id] = pool;
|
||||
}
|
||||
});
|
||||
|
||||
this.interval = window.setInterval(() => {
|
||||
this.now = Math.floor(new Date().getTime() / 1000);
|
||||
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
ngOnChanges(changes): void {
|
||||
// Hide standard ETA while we don't have a proper standard ETA calculation, see https://github.com/mempool/mempool/issues/65
|
||||
|
||||
// if (changes?.eta?.currentValue || changes?.standardETA?.currentValue || changes?.acceleratedETA?.currentValue) {
|
||||
// if (changes?.eta?.currentValue) {
|
||||
// if (changes?.acceleratedETA?.currentValue) {
|
||||
// this.accelerateRatio = Math.floor((Math.floor(changes.eta.currentValue.time / 1000) - this.now) / (Math.floor(changes.acceleratedETA.currentValue / 1000) - this.now));
|
||||
// } else if (changes?.standardETA?.currentValue) {
|
||||
// this.accelerateRatio = Math.floor((Math.floor(changes.standardETA.currentValue / 1000) - this.now) / (Math.floor(changes.eta.currentValue.time / 1000) - this.now));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
this.updateTimes();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
clearInterval(this.interval);
|
||||
updateTimes(): void {
|
||||
this.now = Math.floor(new Date().getTime() / 1000);
|
||||
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
|
||||
this.firstSeenToAccelerated = Math.max(0, this.acceleratedAt - this.transactionTime);
|
||||
this.acceleratedToMined = Math.max(0, this.tx.status.block_time - this.acceleratedAt);
|
||||
}
|
||||
|
||||
onHover(event, status: string): void {
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
|
||||
import { EChartsOption } from '../../../graphs/echarts';
|
||||
import { EChartsOption } from '@app/graphs/echarts';
|
||||
import { Observable, Subject, Subscription, combineLatest, fromEvent, merge, share } from 'rxjs';
|
||||
import { startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { SeoService } from '../../../services/seo.service';
|
||||
import { SeoService } from '@app/services/seo.service';
|
||||
import { formatNumber } from '@angular/common';
|
||||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../../shared/graphs.utils';
|
||||
import { StorageService } from '../../../services/storage.service';
|
||||
import { MiningService } from '../../../services/mining.service';
|
||||
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '@app/shared/graphs.utils';
|
||||
import { StorageService } from '@app/services/storage.service';
|
||||
import { MiningService } from '@app/services/mining.service';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Acceleration } from '../../../interfaces/node-api.interface';
|
||||
import { ServicesApiServices } from '../../../services/services-api.service';
|
||||
import { StateService } from '../../../services/state.service';
|
||||
import { RelativeUrlPipe } from '../../../shared/pipes/relative-url/relative-url.pipe';
|
||||
import { Acceleration } from '@interfaces/node-api.interface';
|
||||
import { ServicesApiServices } from '@app/services/services-api.service';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-acceleration-fees-graph',
|
||||
@@ -264,7 +264,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
|
||||
type: 'bar',
|
||||
barWidth: '90%',
|
||||
large: true,
|
||||
barMinHeight: 1,
|
||||
barMinHeight: 3,
|
||||
},
|
||||
],
|
||||
dataZoom: (this.widget || data.length === 0 )? undefined : [{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ServicesApiServices } from '../../../services/services-api.service';
|
||||
import { ServicesApiServices } from '@app/services/services-api.service';
|
||||
|
||||
export type AccelerationStats = {
|
||||
totalRequested: number;
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<app-fee-rate [fee]="acceleration.effectiveFee" [weight]="acceleration.effectiveVsize * 4"></app-fee-rate>
|
||||
</td>
|
||||
<td class="bid text-right">
|
||||
{{ (acceleration.feeDelta) | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
|
||||
{{ (acceleration.feeDelta) | number }} <span class="symbol" i18n="shared.sats">sats</span>
|
||||
</td>
|
||||
<td class="time text-right">
|
||||
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
|
||||
@@ -41,7 +41,7 @@
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!pending">
|
||||
<td *ngIf="acceleration.boost != null" class="fee text-right">
|
||||
{{ acceleration.boost | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
|
||||
{{ acceleration.boost | number }} <span class="symbol" i18n="shared.sats">sats</span>
|
||||
</td>
|
||||
<td *ngIf="acceleration.boost == null" class="fee text-right">
|
||||
~
|
||||
@@ -64,7 +64,7 @@
|
||||
<span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="accelerator.pending">Pending</span>
|
||||
<span *ngIf="acceleration.status.includes('completed') && acceleration.minedByPoolUniqueId && pools[acceleration.minedByPoolUniqueId]" class="badge badge-success"><ng-container i18n="accelerator.completed">Completed</ng-container><span *ngIf="acceleration.status === 'completed_provisional'"> ⌛</span></span>
|
||||
<span *ngIf="acceleration.status.includes('completed') && (!acceleration.minedByPoolUniqueId || !pools[acceleration.minedByPoolUniqueId])" class="badge badge-success"><ng-container i18n="transaction.rbf.mined">Mined</ng-container><span *ngIf="acceleration.status === 'completed_provisional'"> ⌛</span></span>
|
||||
<span *ngIf="acceleration.status.includes('failed')" class="badge badge-danger"><ng-container i18n="accelerator.canceled">Failed</ng-container><span *ngIf="acceleration.status === 'failed_provisional'"> ⌛</span></span>
|
||||
<span *ngIf="acceleration.status.includes('failed')" class="badge badge-danger"><ng-container i18n="accelerator.canceled">Canceled</ng-container><span *ngIf="acceleration.status === 'failed_provisional'"> ⌛</span></span>
|
||||
</td>
|
||||
<td class="date text-right" *ngIf="!this.widget">
|
||||
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy, Inject, LOCALE_ID } from '@angular/core';
|
||||
import { BehaviorSubject, Observable, Subscription, catchError, filter, of, switchMap, tap, throttleTime } from 'rxjs';
|
||||
import { Acceleration, BlockExtended, SinglePoolStats } from '../../../interfaces/node-api.interface';
|
||||
import { StateService } from '../../../services/state.service';
|
||||
import { WebsocketService } from '../../../services/websocket.service';
|
||||
import { ServicesApiServices } from '../../../services/services-api.service';
|
||||
import { SeoService } from '../../../services/seo.service';
|
||||
import { BehaviorSubject, Observable, Subscription, catchError, combineLatest, filter, of, switchMap, tap, throttleTime, timer } from 'rxjs';
|
||||
import { Acceleration, BlockExtended, SinglePoolStats } from '@interfaces/node-api.interface';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { WebsocketService } from '@app/services/websocket.service';
|
||||
import { ServicesApiServices } from '@app/services/services-api.service';
|
||||
import { SeoService } from '@app/services/seo.service';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { MiningService } from '../../../services/mining.service';
|
||||
import { MiningService } from '@app/services/mining.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-accelerations-list',
|
||||
@@ -61,8 +61,11 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
|
||||
this.websocketService.want(['blocks']);
|
||||
this.seoService.setTitle($localize`:@@02573b6980a2d611b4361a2595a4447e390058cd:Accelerations`);
|
||||
|
||||
this.paramSubscription = this.route.params.pipe(
|
||||
tap(params => {
|
||||
this.paramSubscription = combineLatest([
|
||||
this.route.params,
|
||||
timer(0),
|
||||
]).pipe(
|
||||
tap(([params]) => {
|
||||
this.page = +params['page'] || 1;
|
||||
this.pageSubject.next(this.page);
|
||||
})
|
||||
@@ -148,4 +151,4 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
|
||||
this.paramSubscription?.unsubscribe();
|
||||
this.keyNavigationSubscription?.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
|
||||
import { SeoService } from '../../../services/seo.service';
|
||||
import { OpenGraphService } from '../../../services/opengraph.service';
|
||||
import { WebsocketService } from '../../../services/websocket.service';
|
||||
import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface';
|
||||
import { StateService } from '../../../services/state.service';
|
||||
import { SeoService } from '@app/services/seo.service';
|
||||
import { OpenGraphService } from '@app/services/opengraph.service';
|
||||
import { WebsocketService } from '@app/services/websocket.service';
|
||||
import { Acceleration, BlockExtended } from '@interfaces/node-api.interface';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { Observable, Subscription, catchError, combineLatest, distinctUntilChanged, map, of, share, switchMap, tap } from 'rxjs';
|
||||
import { Color } from '../../block-overview-graph/sprite-types';
|
||||
import { hexToColor } from '../../block-overview-graph/utils';
|
||||
import TxView from '../../block-overview-graph/tx-view';
|
||||
import { feeLevels, defaultMempoolFeeColors, contrastMempoolFeeColors } from '../../../app.constants';
|
||||
import { ServicesApiServices } from '../../../services/services-api.service';
|
||||
import { detectWebGL } from '../../../shared/graphs.utils';
|
||||
import { AudioService } from '../../../services/audio.service';
|
||||
import { ThemeService } from '../../../services/theme.service';
|
||||
import { Color } from '@components/block-overview-graph/sprite-types';
|
||||
import { hexToColor } from '@components/block-overview-graph/utils';
|
||||
import TxView from '@components/block-overview-graph/tx-view';
|
||||
import { feeLevels, defaultMempoolFeeColors, contrastMempoolFeeColors } from '@app/app.constants';
|
||||
import { ServicesApiServices } from '@app/services/services-api.service';
|
||||
import { detectWebGL } from '@app/shared/graphs.utils';
|
||||
import { AudioService } from '@app/services/audio.service';
|
||||
import { ThemeService } from '@app/services/theme.service';
|
||||
|
||||
const acceleratedColor: Color = hexToColor('8F5FF6');
|
||||
const normalColors = defaultMempoolFeeColors.map(hex => hexToColor(hex + '5F'));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@if (chartOnly) {
|
||||
<ng-container *ngTemplateOutlet="pieChart"></ng-container>
|
||||
} @else {
|
||||
<table>
|
||||
<table style="width: 100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td-width field-label" [class]="chartPositionLeft ? 'chart-left' : ''" i18n="transaction.accelerated-to-feerate|Accelerated to feerate">Accelerated to</td>
|
||||
@@ -10,10 +10,10 @@
|
||||
</td>
|
||||
<td class="field-value" [class]="chartPositionLeft ? 'chart-left' : ''">
|
||||
<div class="effective-fee-container">
|
||||
@if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize)) {
|
||||
<app-fee-rate [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
|
||||
@if (accelerationInfo?.acceleratedFeeRate && (!effectiveFeeRate || accelerationInfo.acceleratedFeeRate >= effectiveFeeRate)) {
|
||||
<app-fee-rate class="oobFees" [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
|
||||
} @else {
|
||||
<app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
|
||||
<app-fee-rate class="oobFees" [fee]="effectiveFeeRate"></app-fee-rate>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -61,4 +61,8 @@
|
||||
& > div, & > div > svg {
|
||||
overflow: visible !important;
|
||||
}
|
||||
}
|
||||
|
||||
.oobFees {
|
||||
color: #905cf4;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, ChangeDetectionStrategy, Input, Output, OnChanges, SimpleChanges, EventEmitter } from '@angular/core';
|
||||
import { Transaction } from '../../../interfaces/electrs.interface';
|
||||
import { Acceleration, SinglePoolStats } from '../../../interfaces/node-api.interface';
|
||||
import { EChartsOption, PieSeriesOption } from '../../../graphs/echarts';
|
||||
import { MiningStats } from '../../../services/mining.service';
|
||||
import { Component, ChangeDetectionStrategy, Input, Output, OnChanges, SimpleChanges, EventEmitter, ChangeDetectorRef } from '@angular/core';
|
||||
import { Transaction } from '@interfaces/electrs.interface';
|
||||
import { Acceleration, SinglePoolStats } from '@interfaces/node-api.interface';
|
||||
import { EChartsOption, PieSeriesOption } from '@app/graphs/echarts';
|
||||
import { MiningStats } from '@app/services/mining.service';
|
||||
|
||||
function lighten(color, p): { r, g, b } {
|
||||
return {
|
||||
@@ -23,7 +23,8 @@ function toRGB({r,g,b}): string {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ActiveAccelerationBox implements OnChanges {
|
||||
@Input() tx: Transaction;
|
||||
@Input() acceleratedBy?: number[];
|
||||
@Input() effectiveFeeRate?: number;
|
||||
@Input() accelerationInfo: Acceleration;
|
||||
@Input() miningStats: MiningStats;
|
||||
@Input() pools: number[];
|
||||
@@ -41,10 +42,12 @@ export class ActiveAccelerationBox implements OnChanges {
|
||||
timespan = '';
|
||||
chartInstance: any = undefined;
|
||||
|
||||
constructor() {}
|
||||
constructor(
|
||||
private cd: ChangeDetectorRef,
|
||||
) {}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
const pools = this.pools || this.accelerationInfo?.pools || this.tx.acceleratedBy;
|
||||
const pools = this.pools || this.accelerationInfo?.pools || this.acceleratedBy;
|
||||
if (pools && this.miningStats) {
|
||||
this.prepareChartOptions(pools);
|
||||
}
|
||||
@@ -67,17 +70,27 @@ export class ActiveAccelerationBox implements OnChanges {
|
||||
|
||||
const acceleratingPools = (poolList || []).filter(id => pools[id]).sort((a,b) => pools[a].lastEstimatedHashrate - pools[b].lastEstimatedHashrate);
|
||||
const totalAcceleratedHashrate = acceleratingPools.reduce((total, pool) => total + pools[pool].lastEstimatedHashrate, 0);
|
||||
const lightenStep = acceleratingPools.length ? (0.48 / acceleratingPools.length) : 0;
|
||||
// Find the first pool with at least 1% of the total network hashrate
|
||||
const firstSignificantPool = acceleratingPools.findIndex(pool => pools[pool].lastEstimatedHashrate > this.miningStats.lastEstimatedHashrate / 100);
|
||||
const numSignificantPools = acceleratingPools.length - firstSignificantPool;
|
||||
acceleratingPools.forEach((poolId, index) => {
|
||||
const pool = pools[poolId];
|
||||
const poolShare = ((pool.lastEstimatedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1);
|
||||
let color = 'white';
|
||||
if (index >= firstSignificantPool) {
|
||||
if (numSignificantPools > 1) {
|
||||
color = toRGB(lighten({ r: 147, g: 57, b: 244 }, 1 - (index - firstSignificantPool) / Math.max((numSignificantPools - 1), 1)));
|
||||
} else {
|
||||
color = toRGB({ r: 147, g: 57, b: 244 });
|
||||
}
|
||||
}
|
||||
data.push(getDataItem(
|
||||
pool.lastEstimatedHashrate,
|
||||
toRGB(lighten({ r: 147, g: 57, b: 244 }, index * lightenStep)),
|
||||
color,
|
||||
`<b style="color: white">${pool.name} (${poolShare}%)</b>`,
|
||||
true,
|
||||
) as PieSeriesOption);
|
||||
})
|
||||
});
|
||||
this.acceleratedByPercentage = ((totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1) + '%';
|
||||
const notAcceleratedByPercentage = ((1 - (totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate)) * 100).toFixed(1) + '%';
|
||||
data.push(getDataItem(
|
||||
@@ -128,6 +141,7 @@ export class ActiveAccelerationBox implements OnChanges {
|
||||
}
|
||||
]
|
||||
};
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
onChartInit(ec) {
|
||||
@@ -140,4 +154,4 @@ export class ActiveAccelerationBox implements OnChanges {
|
||||
onToggleCpfp(): void {
|
||||
this.toggleCpfp.emit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
import { Acceleration } from '../../../interfaces/node-api.interface';
|
||||
import { StateService } from '../../../services/state.service';
|
||||
import { WebsocketService } from '../../../services/websocket.service';
|
||||
import { Acceleration } from '@interfaces/node-api.interface';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { WebsocketService } from '@app/services/websocket.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-pending-stats',
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<div class="sparkles" #sparkleAnchor>
|
||||
<div *ngFor="let sparkle of sparkles" class="sparkle" [style]="sparkle.style">
|
||||
<span class="inner-sparkle" [style]="sparkle.rotation">+</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,45 @@
|
||||
.sparkles {
|
||||
position: absolute;
|
||||
top: var(--block-size);
|
||||
height: 50px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.sparkle {
|
||||
position: absolute;
|
||||
color: rgba(152, 88, 255, 0.75);
|
||||
opacity: 0;
|
||||
transform: scale(0.8) rotate(0deg);
|
||||
animation: pop ease 2000ms forwards, sparkle ease 500ms infinite;
|
||||
}
|
||||
|
||||
.inner-sparkle {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@keyframes pop {
|
||||
0% {
|
||||
transform: scale(0.8) rotate(0deg);
|
||||
opacity: 0;
|
||||
}
|
||||
20% {
|
||||
transform: scale(1) rotate(72deg);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(0) rotate(360deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sparkle {
|
||||
0% {
|
||||
color: rgba(152, 88, 255, 0.75);
|
||||
}
|
||||
50% {
|
||||
color: rgba(198, 162, 255, 0.75);
|
||||
}
|
||||
100% {
|
||||
color: rgba(152, 88, 255, 0.75);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-acceleration-sparkles',
|
||||
templateUrl: './acceleration-sparkles.component.html',
|
||||
styleUrls: ['./acceleration-sparkles.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AccelerationSparklesComponent implements OnChanges {
|
||||
@Input() arrow: ElementRef<HTMLDivElement>;
|
||||
@Input() run: boolean = false;
|
||||
|
||||
@ViewChild('sparkleAnchor')
|
||||
sparkleAnchor: ElementRef<HTMLDivElement>;
|
||||
|
||||
constructor(
|
||||
private cd: ChangeDetectorRef,
|
||||
) {}
|
||||
|
||||
endTimeout: any;
|
||||
lastSparkle: number = 0;
|
||||
sparkleWidth: number = 0;
|
||||
sparkles: any[] = [];
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.run) {
|
||||
if (this.endTimeout) {
|
||||
clearTimeout(this.endTimeout);
|
||||
this.endTimeout = null;
|
||||
}
|
||||
if (this.run) {
|
||||
this.doSparkle();
|
||||
} else {
|
||||
this.endTimeout = setTimeout(() => {
|
||||
this.sparkles = [];
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
doSparkle(): void {
|
||||
if (this.run) {
|
||||
const now = performance.now();
|
||||
if (now - this.lastSparkle > 20) {
|
||||
this.lastSparkle = now;
|
||||
if (this.arrow?.nativeElement && this.sparkleAnchor?.nativeElement) {
|
||||
const anchor = this.sparkleAnchor.nativeElement.getBoundingClientRect().right;
|
||||
const right = this.arrow.nativeElement.getBoundingClientRect().right;
|
||||
const dx = (anchor - right) + 30;
|
||||
const numSparkles = Math.ceil(Math.random() * 3);
|
||||
for (let i = 0; i < numSparkles; i++) {
|
||||
this.sparkles.push({
|
||||
style: {
|
||||
right: (dx + (Math.random() * 10)) + 'px',
|
||||
top: (15 + (Math.random() * 30)) + 'px',
|
||||
},
|
||||
rotation: {
|
||||
transform: `rotate(${Math.random() * 360}deg)`,
|
||||
}
|
||||
});
|
||||
}
|
||||
while (this.sparkles.length > 200) {
|
||||
this.sparkles.shift();
|
||||
}
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
this.doSparkle();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
|
||||
import { echarts, EChartsOption } from '../../graphs/echarts';
|
||||
import { echarts, EChartsOption } from '@app/graphs/echarts';
|
||||
import { BehaviorSubject, Observable, Subscription, combineLatest, of } from 'rxjs';
|
||||
import { catchError, map, switchMap, tap } from 'rxjs/operators';
|
||||
import { AddressTxSummary, ChainStats } from '../../interfaces/electrs.interface';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
|
||||
import { AddressTxSummary, ChainStats } from '@interfaces/electrs.interface';
|
||||
import { ElectrsApiService } from '@app/services/electrs-api.service';
|
||||
import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe';
|
||||
import { Router } from '@angular/router';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { PriceService } from '../../services/price.service';
|
||||
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe';
|
||||
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
|
||||
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { PriceService } from '@app/services/price.service';
|
||||
import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe';
|
||||
import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe';
|
||||
|
||||
const periodSeconds = {
|
||||
'1d': (60 * 60 * 24),
|
||||
@@ -83,7 +83,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.isLoading = true;
|
||||
if (!this.address || !this.stats) {
|
||||
if (!this.addressSummary$ && (!this.address || !this.stats)) {
|
||||
return;
|
||||
}
|
||||
if (changes.address || changes.isPubkey || changes.addressSummary$ || changes.stats) {
|
||||
@@ -144,15 +144,16 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
prepareChartOptions(summary: AddressTxSummary[]) {
|
||||
if (!summary || !this.stats) {
|
||||
if (!summary) {
|
||||
return;
|
||||
}
|
||||
|
||||
let total = (this.stats.funded_txo_sum - this.stats.spent_txo_sum);
|
||||
const total = this.stats ? (this.stats.funded_txo_sum - this.stats.spent_txo_sum) : summary.reduce((acc, tx) => acc + tx.value, 0);
|
||||
let runningTotal = total;
|
||||
const processData = summary.map(d => {
|
||||
const balance = total;
|
||||
const fiatBalance = total * d.price / 100_000_000;
|
||||
total -= d.value;
|
||||
const balance = runningTotal;
|
||||
const fiatBalance = runningTotal * d.price / 100_000_000;
|
||||
runningTotal -= d.value;
|
||||
return {
|
||||
time: d.time * 1000,
|
||||
balance,
|
||||
@@ -172,7 +173,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
this.fiatData = this.fiatData.filter(d => d[0] >= startFiat);
|
||||
}
|
||||
this.data.push(
|
||||
{value: [now, this.stats.funded_txo_sum - this.stats.spent_txo_sum], symbol: 'none', tooltip: { show: false }}
|
||||
{value: [now, total], symbol: 'none', tooltip: { show: false }}
|
||||
);
|
||||
|
||||
const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1] ?? d.value[1])), 0);
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectorRef, HostListener } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { ElectrsApiService } from '@app/services/electrs-api.service';
|
||||
import { switchMap, catchError } from 'rxjs/operators';
|
||||
import { Address, Transaction } from '../../interfaces/electrs.interface';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { AudioService } from '../../services/audio.service';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { Address, Transaction } from '@interfaces/electrs.interface';
|
||||
import { WebsocketService } from '@app/services/websocket.service';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { AudioService } from '@app/services/audio.service';
|
||||
import { ApiService } from '@app/services/api.service';
|
||||
import { of, Subscription, forkJoin } from 'rxjs';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { AddressInformation } from '../../interfaces/node-api.interface';
|
||||
import { SeoService } from '@app/services/seo.service';
|
||||
import { AddressInformation } from '@interfaces/node-api.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'app-address-group',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core';
|
||||
import { Vin, Vout } from '../../interfaces/electrs.interface';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { AddressType, AddressTypeInfo } from '../../shared/address-utils';
|
||||
import { Vin, Vout } from '@interfaces/electrs.interface';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { AddressType, AddressTypeInfo } from '@app/shared/address-utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-address-labels',
|
||||
@@ -55,7 +55,7 @@ export class AddressLabelsComponent implements OnChanges {
|
||||
}
|
||||
|
||||
handleVin() {
|
||||
const address = new AddressTypeInfo(this.network || 'mainnet', this.vin.prevout?.scriptpubkey_address, this.vin.prevout?.scriptpubkey_type as AddressType, [this.vin])
|
||||
const address = new AddressTypeInfo(this.network || 'mainnet', this.vin.prevout?.scriptpubkey_address, this.vin.prevout?.scriptpubkey_type as AddressType, [this.vin]);
|
||||
if (address?.scripts.size) {
|
||||
const script = address?.scripts.values().next().value;
|
||||
if (script.template?.label) {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<app-truncate [text]="transaction.txid" [lastChars]="5"></app-truncate>
|
||||
</a>
|
||||
</td>
|
||||
<td class="table-cell-satoshis"><app-amount [satoshis]="transaction.value" digitsInfo="1.2-4" [noFiat]="true"></app-amount></td>
|
||||
<td class="table-cell-satoshis"><app-amount [satoshis]="transaction.value" [digitsInfo]="getAmountDigits(transaction.value)" [noFiat]="true"></app-amount></td>
|
||||
<td class="table-cell-fiat" ><app-fiat [value]="transaction.value" [blockConversion]="transaction.price" digitsInfo="1.0-0"></app-fiat></td>
|
||||
<td class="table-cell-date"><app-time kind="since" [time]="transaction.time" [fastRender]="true" [showTooltip]="true"></app-time></td>
|
||||
</tr>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Address, AddressTxSummary } from '../../interfaces/electrs.interface';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { Address, AddressTxSummary } from '@interfaces/electrs.interface';
|
||||
import { ElectrsApiService } from '@app/services/electrs-api.service';
|
||||
import { Observable, Subscription, catchError, map, of, switchMap, zip } from 'rxjs';
|
||||
import { PriceService } from '../../services/price.service';
|
||||
import { PriceService } from '@app/services/price.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-address-transactions-widget',
|
||||
@@ -43,7 +43,7 @@ export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, On
|
||||
|
||||
startAddressSubscription(): void {
|
||||
this.isLoading = true;
|
||||
if (!this.address || !this.addressInfo) {
|
||||
if (!this.addressSummary$ && (!this.address || !this.addressInfo)) {
|
||||
return;
|
||||
}
|
||||
this.transactions$ = (this.addressSummary$ || (this.isPubkey
|
||||
@@ -55,7 +55,7 @@ export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, On
|
||||
})
|
||||
)).pipe(
|
||||
map(summary => {
|
||||
return summary?.slice(0, 6);
|
||||
return summary?.filter(tx => Math.abs(tx.value) >= 1000000)?.slice(0, 6);
|
||||
}),
|
||||
switchMap(txs => {
|
||||
return (zip(txs.map(tx => this.priceService.getBlockPrice$(tx.time, txs.length < 3, this.currency).pipe(
|
||||
@@ -68,6 +68,12 @@ export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, On
|
||||
))));
|
||||
})
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
getAmountDigits(value: number): string {
|
||||
const decimals = Math.max(0, 4 - Math.ceil(Math.log10(Math.abs(value / 100_000_000))));
|
||||
return `1.${decimals}-${decimals}`;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { ElectrsApiService } from '@app/services/electrs-api.service';
|
||||
import { switchMap, filter, catchError, map, tap } from 'rxjs/operators';
|
||||
import { Address, Transaction } from '../../interfaces/electrs.interface';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { OpenGraphService } from '../../services/opengraph.service';
|
||||
import { AudioService } from '../../services/audio.service';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { Address, Transaction } from '@interfaces/electrs.interface';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { OpenGraphService } from '@app/services/opengraph.service';
|
||||
import { AudioService } from '@app/services/audio.service';
|
||||
import { ApiService } from '@app/services/api.service';
|
||||
import { of, merge, Subscription, Observable } from 'rxjs';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
import { AddressInformation } from '../../interfaces/node-api.interface';
|
||||
import { SeoService } from '@app/services/seo.service';
|
||||
import { seoDescriptionNetwork } from '@app/shared/common.utils';
|
||||
import { AddressInformation } from '@interfaces/node-api.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'app-address-preview',
|
||||
|
||||
@@ -94,6 +94,20 @@
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="(stateService.backend$ | async) === 'esplora' && address && utxos && utxos.length > 2">
|
||||
<br>
|
||||
<div class="title-tx">
|
||||
<h2 class="text-left" i18n="address.unspent-outputs">Unspent Outputs</h2>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<app-utxo-graph [utxos]="utxos" left="80" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<br>
|
||||
<div class="title-tx">
|
||||
<h2 class="text-left">
|
||||
@@ -103,7 +117,7 @@
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<app-transactions-list [transactions]="transactions" [showConfirmations]="true" [address]="address.address" (loadMore)="loadMore()"></app-transactions-list>
|
||||
<app-transactions-list [transactions]="transactions" [showConfirmations]="true" [addresses]="[address.address]" (loadMore)="loadMore()"></app-transactions-list>
|
||||
|
||||
<div class="text-center">
|
||||
<ng-template [ngIf]="isLoadingTransactions">
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Component, OnInit, OnDestroy, HostListener } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { ElectrsApiService } from '@app/services/electrs-api.service';
|
||||
import { switchMap, filter, catchError, map, tap } from 'rxjs/operators';
|
||||
import { Address, ChainStats, Transaction, Vin } from '../../interfaces/electrs.interface';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { AudioService } from '../../services/audio.service';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { of, merge, Subscription, Observable } from 'rxjs';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
import { AddressInformation } from '../../interfaces/node-api.interface';
|
||||
import { AddressTypeInfo } from '../../shared/address-utils';
|
||||
import { Address, ChainStats, Transaction, Utxo, Vin } from '@interfaces/electrs.interface';
|
||||
import { WebsocketService } from '@app/services/websocket.service';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { AudioService } from '@app/services/audio.service';
|
||||
import { ApiService } from '@app/services/api.service';
|
||||
import { of, merge, Subscription, Observable, forkJoin } from 'rxjs';
|
||||
import { SeoService } from '@app/services/seo.service';
|
||||
import { seoDescriptionNetwork } from '@app/shared/common.utils';
|
||||
import { AddressInformation } from '@interfaces/node-api.interface';
|
||||
import { AddressTypeInfo } from '@app/shared/address-utils';
|
||||
|
||||
class AddressStats implements ChainStats {
|
||||
address: string;
|
||||
@@ -104,6 +104,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
addressString: string;
|
||||
isLoadingAddress = true;
|
||||
transactions: Transaction[];
|
||||
utxos: Utxo[];
|
||||
isLoadingTransactions = true;
|
||||
retryLoadMore = false;
|
||||
error: any;
|
||||
@@ -159,6 +160,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
this.address = null;
|
||||
this.isLoadingTransactions = true;
|
||||
this.transactions = null;
|
||||
this.utxos = null;
|
||||
this.addressInfo = null;
|
||||
this.exampleChannel = null;
|
||||
document.body.scrollTo(0, 0);
|
||||
@@ -212,11 +214,23 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
this.updateChainStats();
|
||||
this.isLoadingAddress = false;
|
||||
this.isLoadingTransactions = true;
|
||||
return address.is_pubkey
|
||||
const utxoCount = this.chainStats.utxos + this.mempoolStats.utxos;
|
||||
return forkJoin([
|
||||
address.is_pubkey
|
||||
? this.electrsApiService.getScriptHashTransactions$((address.address.length === 66 ? '21' : '41') + address.address + 'ac')
|
||||
: this.electrsApiService.getAddressTransactions$(address.address);
|
||||
: this.electrsApiService.getAddressTransactions$(address.address),
|
||||
(utxoCount > 2 && utxoCount <= 500 ? (address.is_pubkey
|
||||
? this.electrsApiService.getScriptHashUtxos$((address.address.length === 66 ? '21' : '41') + address.address + 'ac')
|
||||
: this.electrsApiService.getAddressUtxos$(address.address)) : of(null)).pipe(
|
||||
catchError(() => {
|
||||
return of(null);
|
||||
})
|
||||
)
|
||||
]);
|
||||
}),
|
||||
switchMap((transactions) => {
|
||||
switchMap(([transactions, utxos]) => {
|
||||
this.utxos = utxos;
|
||||
|
||||
this.tempTransactions = transactions;
|
||||
if (transactions.length) {
|
||||
this.lastTransactionTxId = transactions[transactions.length - 1].txid;
|
||||
@@ -309,6 +323,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
this.transactions = this.transactions.slice();
|
||||
this.mempoolStats.removeTx(transaction);
|
||||
this.audioService.playSound('magic');
|
||||
this.confirmTransaction(tx);
|
||||
} else {
|
||||
if (this.addTransaction(transaction, false)) {
|
||||
this.audioService.playSound('magic');
|
||||
@@ -334,6 +349,31 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
// update utxos in-place
|
||||
if (this.utxos != null) {
|
||||
let utxosChanged = false;
|
||||
for (const vin of transaction.vin) {
|
||||
const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === vin.txid && utxo.vout === vin.vout);
|
||||
if (utxoIndex !== -1) {
|
||||
this.utxos.splice(utxoIndex, 1);
|
||||
utxosChanged = true;
|
||||
}
|
||||
}
|
||||
for (const [index, vout] of transaction.vout.entries()) {
|
||||
if (vout.scriptpubkey_address === this.address.address) {
|
||||
this.utxos.push({
|
||||
txid: transaction.txid,
|
||||
vout: index,
|
||||
value: vout.value,
|
||||
status: JSON.parse(JSON.stringify(transaction.status)),
|
||||
});
|
||||
utxosChanged = true;
|
||||
}
|
||||
}
|
||||
if (utxosChanged) {
|
||||
this.utxos = this.utxos.slice();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -346,9 +386,65 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
this.transactions.splice(index, 1);
|
||||
this.transactions = this.transactions.slice();
|
||||
|
||||
// update utxos in-place
|
||||
if (this.utxos != null) {
|
||||
let utxosChanged = false;
|
||||
for (const vin of transaction.vin) {
|
||||
if (vin.prevout?.scriptpubkey_address === this.address.address) {
|
||||
this.utxos.push({
|
||||
txid: vin.txid,
|
||||
vout: vin.vout,
|
||||
value: vin.prevout.value,
|
||||
status: { confirmed: true }, // Assuming the input was confirmed
|
||||
});
|
||||
utxosChanged = true;
|
||||
}
|
||||
}
|
||||
for (const [index, vout] of transaction.vout.entries()) {
|
||||
if (vout.scriptpubkey_address === this.address.address) {
|
||||
const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === transaction.txid && utxo.vout === index);
|
||||
if (utxoIndex !== -1) {
|
||||
this.utxos.splice(utxoIndex, 1);
|
||||
utxosChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (utxosChanged) {
|
||||
this.utxos = this.utxos.slice();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
confirmTransaction(transaction: Transaction): void {
|
||||
// update utxos in-place
|
||||
if (this.utxos != null) {
|
||||
let utxosChanged = false;
|
||||
for (const vin of transaction.vin) {
|
||||
if (vin.prevout?.scriptpubkey_address === this.address.address) {
|
||||
const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === vin.txid && utxo.vout === vin.vout);
|
||||
if (utxoIndex !== -1) {
|
||||
this.utxos[utxoIndex].status = JSON.parse(JSON.stringify(transaction.status));
|
||||
utxosChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [index, vout] of transaction.vout.entries()) {
|
||||
if (vout.scriptpubkey_address === this.address.address) {
|
||||
const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === transaction.txid && utxo.vout === index);
|
||||
if (utxoIndex !== -1) {
|
||||
this.utxos[utxoIndex].status = JSON.parse(JSON.stringify(transaction.status));
|
||||
utxosChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (utxosChanged) {
|
||||
this.utxos = this.utxos.slice();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadMore(): void {
|
||||
if (this.isLoadingTransactions || this.fullyLoaded) {
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<div class="addresses-treemap-container">
|
||||
<div *ngIf="addresses" style="height: 300px">
|
||||
<div *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!stateService.isBrowser || isLoading" class="text-center loading-spinner">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
.node-channels-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.spinner-border {
|
||||
position: relative;
|
||||
top: 225px;
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { EChartsOption, TreemapSeriesOption } from '@app/graphs/echarts';
|
||||
import { lerpColor } from '@app/shared/graphs.utils';
|
||||
import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe';
|
||||
import { LightningApiService } from '@app/lightning/lightning-api.service';
|
||||
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { Address } from '@interfaces/electrs.interface';
|
||||
import { formatNumber } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-addresses-treemap',
|
||||
templateUrl: './addresses-treemap.component.html',
|
||||
styleUrls: ['./addresses-treemap.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AddressesTreemap implements OnChanges {
|
||||
@Input() addresses: Address[];
|
||||
@Input() isLoading: boolean = false;
|
||||
|
||||
chartInstance: any;
|
||||
chartOptions: EChartsOption = {};
|
||||
chartInitOptions = {
|
||||
renderer: 'svg',
|
||||
};
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) public locale: string,
|
||||
private lightningApiService: LightningApiService,
|
||||
private amountShortenerPipe: AmountShortenerPipe,
|
||||
private zone: NgZone,
|
||||
private router: Router,
|
||||
public stateService: StateService,
|
||||
) {}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.prepareChartOptions();
|
||||
}
|
||||
|
||||
prepareChartOptions(): void {
|
||||
const data = this.addresses.map(address => ({
|
||||
address: address.address,
|
||||
value: address.chain_stats.funded_txo_sum - address.chain_stats.spent_txo_sum,
|
||||
stats: address.chain_stats,
|
||||
}));
|
||||
// only consider visible items for the color gradient
|
||||
const totalValue = data.reduce((acc, address) => acc + address.value, 0);
|
||||
const maxTxs = data.filter(address => address.value > (totalValue / 2000)).reduce((max, address) => Math.max(max, address.stats.tx_count), 0);
|
||||
const dataItems = data.map(address => ({
|
||||
...address,
|
||||
itemStyle: {
|
||||
color: lerpColor('#1E88E5', '#D81B60', address.stats.tx_count / maxTxs),
|
||||
}
|
||||
}));
|
||||
this.chartOptions = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
textStyle: {
|
||||
align: 'left',
|
||||
}
|
||||
},
|
||||
series: <TreemapSeriesOption[]>[
|
||||
{
|
||||
height: 300,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
top: 0,
|
||||
roam: false,
|
||||
type: 'treemap',
|
||||
data: dataItems,
|
||||
nodeClick: 'link',
|
||||
progressive: 100,
|
||||
tooltip: {
|
||||
show: true,
|
||||
backgroundColor: 'rgba(17, 19, 31, 1)',
|
||||
borderRadius: 4,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
textStyle: {
|
||||
color: '#b1b1b1',
|
||||
},
|
||||
borderColor: '#000',
|
||||
formatter: (value): string => {
|
||||
if (!value.data.address) {
|
||||
return '';
|
||||
}
|
||||
return `
|
||||
<table style="table-layout: fixed;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="2"><b style="color: white; margin-left: 2px">${value.data.address}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Received</td>
|
||||
<td style="text-align: right">${this.formatValue(value.data.stats.funded_txo_sum)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Sent</td>
|
||||
<td style="text-align: right">${this.formatValue(value.data.stats.spent_txo_sum)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Balance</td>
|
||||
<td style="text-align: right">${this.formatValue(value.data.stats.funded_txo_sum - value.data.stats.spent_txo_sum)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Transaction count</td>
|
||||
<td style="text-align: right">${value.data.stats.tx_count}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
},
|
||||
itemStyle: {
|
||||
borderColor: 'black',
|
||||
borderWidth: 1,
|
||||
},
|
||||
breadcrumb: {
|
||||
show: false,
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
formatValue(sats: number): string {
|
||||
if (sats > 100000000) {
|
||||
return formatNumber(sats / 100000000, this.locale, '1.2-2') + ' BTC';
|
||||
} else {
|
||||
return this.amountShortenerPipe.transform(sats, 2) + ' sats';
|
||||
}
|
||||
}
|
||||
|
||||
onChartInit(ec: any): void {
|
||||
this.chartInstance = ec;
|
||||
|
||||
this.chartInstance.on('click', (e) => {
|
||||
//@ts-ignore
|
||||
if (!e.data.address) {
|
||||
return;
|
||||
}
|
||||
this.zone.run(() => {
|
||||
//@ts-ignore
|
||||
const url = new RelativeUrlPipe(this.stateService).transform(`/address/${e.data.address}`);
|
||||
this.router.navigate([url]);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<div [formGroup]="amountForm" class="text-small text-center">
|
||||
<select formControlName="mode" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 70px;" (change)="changeMode()">
|
||||
<option value="btc" i18n="shared.btc|BTC">BTC</option>
|
||||
<option value="sats" i18n="shared.sats">sats</option>
|
||||
<option value="fiat" i18n="shared.fiat|Fiat">Fiat</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -0,0 +1,36 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||
import { StorageService } from '@app/services/storage.service';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-amount-selector',
|
||||
templateUrl: './amount-selector.component.html',
|
||||
styleUrls: ['./amount-selector.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class AmountSelectorComponent implements OnInit {
|
||||
amountForm: UntypedFormGroup;
|
||||
modes = ['btc', 'sats', 'fiat'];
|
||||
|
||||
constructor(
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
private stateService: StateService,
|
||||
private storageService: StorageService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.amountForm = this.formBuilder.group({
|
||||
mode: ['btc']
|
||||
});
|
||||
this.stateService.viewAmountMode$.subscribe((mode) => {
|
||||
this.amountForm.get('mode')?.setValue(mode);
|
||||
});
|
||||
}
|
||||
|
||||
changeMode() {
|
||||
const newMode = this.amountForm.get('mode')?.value;
|
||||
this.storageService.setValue('view-amount-mode', newMode);
|
||||
this.stateService.viewAmountMode$.next(newMode);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user