Compare commits
270
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35158de7c6 | ||
|
|
e58579ed8a | ||
|
|
f57eb047f6 | ||
|
|
dcae94ba66 | ||
|
|
1d2a5e9c94 | ||
|
|
522b4d914f | ||
|
|
59cefc2b4b | ||
|
|
9714789062 | ||
|
|
13405b4494 | ||
|
|
6d51ce1f38 | ||
|
|
bce9ea3661 | ||
|
|
05a21f3867 | ||
|
|
d50cfe135f | ||
|
|
8ae8430711 | ||
|
|
12daea0f62 | ||
|
|
0d31143fed | ||
|
|
7f3cdbfdb6 | ||
|
|
5be4346dc1 | ||
|
|
a0596cd366 | ||
|
|
0f14aa7ad3 | ||
|
|
44a0f92cc1 | ||
|
|
97a9ea47fc | ||
|
|
d573147ad4 | ||
|
|
679745fb6c | ||
|
|
c089920e4b | ||
|
|
62c96272d8 | ||
|
|
d87b668353 | ||
|
|
ebd4170da1 | ||
|
|
0310452dfb | ||
|
|
969687ef39 | ||
|
|
0831256cce | ||
|
|
af40cac284 | ||
|
|
e4868b70c1 | ||
|
|
5f45ce80f1 | ||
|
|
14e49126c3 | ||
|
|
a4d73130b7 | ||
|
|
cd702955fc | ||
|
|
4c66bf61f0 | ||
|
|
ffa582558b | ||
|
|
423b41939e | ||
|
|
9a81db8e6c | ||
|
|
cb3326d691 | ||
|
|
535e5313ef | ||
|
|
8bd6d40ed2 | ||
|
|
7516db0c71 | ||
|
|
abe9aa1fdc | ||
|
|
60b3f9ace6 | ||
|
|
073fe8e8cd | ||
|
|
bfe7b996a4 | ||
|
|
bfd771056d | ||
|
|
e05f5ee751 | ||
|
|
d9f3611da3 | ||
|
|
7c7419ab1c | ||
|
|
96afbca029 | ||
|
|
8c80358e71 | ||
|
|
a5fbc94182 | ||
|
|
fd7f340854 | ||
|
|
7f784944af | ||
|
|
cab01f7f26 | ||
|
|
3b4eda432f | ||
|
|
8b699da721 | ||
|
|
5b2f613856 | ||
|
|
f1e2c893cc | ||
|
|
7d3d59c348 | ||
|
|
8719b424e5 | ||
|
|
ef498b55ed | ||
|
|
9718610104 | ||
|
|
937e82bb89 | ||
|
|
91bf35bb65 | ||
|
|
f0f6ee1919 | ||
|
|
7b837b96da | ||
|
|
c417470be2 | ||
|
|
dfd7877f82 | ||
|
|
136e80e5cf | ||
|
|
b3aed2f58b | ||
|
|
e75f913af3 | ||
|
|
0a9703f164 | ||
|
|
db321c3fa5 | ||
|
|
b6aeb5661f | ||
|
|
f08fa034cc | ||
|
|
c0ef01d4da | ||
|
|
c6711d8191 | ||
|
|
216bd5ad23 | ||
|
|
a7d59d6b2e | ||
|
|
a257bcc12a | ||
|
|
66c0ea7ca3 | ||
|
|
7692b2af66 | ||
|
|
397f53f42d | ||
|
|
2ea76d9c38 | ||
|
|
59ac27b104 | ||
|
|
d27bb7e156 | ||
|
|
4eadfc0a3b | ||
|
|
76cfa3ca47 | ||
|
|
3a4a4d9ffd | ||
|
|
8b01a83948 | ||
|
|
2ceb9001a1 | ||
|
|
d44b7926d2 | ||
|
|
57299e086e | ||
|
|
c1d17dac43 | ||
|
|
cb49f9d929 | ||
|
|
185be3d598 | ||
|
|
aa9888a2fe | ||
|
|
c950e3d0ae | ||
|
|
3909148d6e | ||
|
|
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 | ||
|
|
92de208414 | ||
|
|
4220f99477 | ||
|
|
06e699e52b | ||
|
|
b90cd4c7e3 | ||
|
|
25482b9a06 | ||
|
|
7865574bd4 | ||
|
|
b3ca8840e5 | ||
|
|
a133ddf062 | ||
|
|
e513f05c09 | ||
|
|
555425d97e | ||
|
|
6db4afe878 | ||
|
|
4596394100 | ||
|
|
ae2ed8fdae |
No files matched your search
@@ -27,8 +27,9 @@
|
|||||||
"AUTOMATIC_POOLS_UPDATE": false,
|
"AUTOMATIC_POOLS_UPDATE": false,
|
||||||
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json",
|
"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_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
|
||||||
|
"POOLS_UPDATE_DELAY": 604800,
|
||||||
"AUDIT": false,
|
"AUDIT": false,
|
||||||
"RUST_GBT": false,
|
"RUST_GBT": true,
|
||||||
"LIMIT_GBT": false,
|
"LIMIT_GBT": false,
|
||||||
"CPFP_INDEXING": false,
|
"CPFP_INDEXING": false,
|
||||||
"DISK_CACHE_BLOCK_INTERVAL": 6,
|
"DISK_CACHE_BLOCK_INTERVAL": 6,
|
||||||
@@ -45,7 +46,8 @@
|
|||||||
"PASSWORD": "mempool",
|
"PASSWORD": "mempool",
|
||||||
"TIMEOUT": 60000,
|
"TIMEOUT": 60000,
|
||||||
"COOKIE": false,
|
"COOKIE": false,
|
||||||
"COOKIE_PATH": "/path/to/bitcoin/.cookie"
|
"COOKIE_PATH": "/path/to/bitcoin/.cookie",
|
||||||
|
"DEBUG_LOG_PATH": "/path/to/bitcoin/debug.log"
|
||||||
},
|
},
|
||||||
"ELECTRUM": {
|
"ELECTRUM": {
|
||||||
"HOST": "127.0.0.1",
|
"HOST": "127.0.0.1",
|
||||||
|
|||||||
Generated
+15
-15
@@ -16,7 +16,7 @@
|
|||||||
"axios": "1.7.2",
|
"axios": "1.7.2",
|
||||||
"bitcoinjs-lib": "~6.1.3",
|
"bitcoinjs-lib": "~6.1.3",
|
||||||
"crypto-js": "~4.2.0",
|
"crypto-js": "~4.2.0",
|
||||||
"express": "~4.21.0",
|
"express": "~4.21.1",
|
||||||
"maxmind": "~4.3.11",
|
"maxmind": "~4.3.11",
|
||||||
"mysql2": "~3.11.0",
|
"mysql2": "~3.11.0",
|
||||||
"redis": "^4.7.0",
|
"redis": "^4.7.0",
|
||||||
@@ -2827,9 +2827,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/cookie": {
|
"node_modules/cookie": {
|
||||||
"version": "0.6.0",
|
"version": "0.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
|
||||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
@@ -3461,16 +3461,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "4.21.0",
|
"version": "4.21.1",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
|
||||||
"integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
|
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "~1.3.8",
|
"accepts": "~1.3.8",
|
||||||
"array-flatten": "1.1.1",
|
"array-flatten": "1.1.1",
|
||||||
"body-parser": "1.20.3",
|
"body-parser": "1.20.3",
|
||||||
"content-disposition": "0.5.4",
|
"content-disposition": "0.5.4",
|
||||||
"content-type": "~1.0.4",
|
"content-type": "~1.0.4",
|
||||||
"cookie": "0.6.0",
|
"cookie": "0.7.1",
|
||||||
"cookie-signature": "1.0.6",
|
"cookie-signature": "1.0.6",
|
||||||
"debug": "2.6.9",
|
"debug": "2.6.9",
|
||||||
"depd": "2.0.0",
|
"depd": "2.0.0",
|
||||||
@@ -9865,9 +9865,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"cookie": {
|
"cookie": {
|
||||||
"version": "0.6.0",
|
"version": "0.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
|
||||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="
|
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="
|
||||||
},
|
},
|
||||||
"cookie-signature": {
|
"cookie-signature": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
@@ -10319,16 +10319,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"express": {
|
"express": {
|
||||||
"version": "4.21.0",
|
"version": "4.21.1",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
|
||||||
"integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
|
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"accepts": "~1.3.8",
|
"accepts": "~1.3.8",
|
||||||
"array-flatten": "1.1.1",
|
"array-flatten": "1.1.1",
|
||||||
"body-parser": "1.20.3",
|
"body-parser": "1.20.3",
|
||||||
"content-disposition": "0.5.4",
|
"content-disposition": "0.5.4",
|
||||||
"content-type": "~1.0.4",
|
"content-type": "~1.0.4",
|
||||||
"cookie": "0.6.0",
|
"cookie": "0.7.1",
|
||||||
"cookie-signature": "1.0.6",
|
"cookie-signature": "1.0.6",
|
||||||
"debug": "2.6.9",
|
"debug": "2.6.9",
|
||||||
"depd": "2.0.0",
|
"depd": "2.0.0",
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
"axios": "1.7.2",
|
"axios": "1.7.2",
|
||||||
"bitcoinjs-lib": "~6.1.3",
|
"bitcoinjs-lib": "~6.1.3",
|
||||||
"crypto-js": "~4.2.0",
|
"crypto-js": "~4.2.0",
|
||||||
"express": "~4.21.0",
|
"express": "~4.21.1",
|
||||||
"maxmind": "~4.3.11",
|
"maxmind": "~4.3.11",
|
||||||
"mysql2": "~3.11.0",
|
"mysql2": "~3.11.0",
|
||||||
"rust-gbt": "file:./rust-gbt",
|
"rust-gbt": "file:./rust-gbt",
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
"INDEXING_BLOCKS_AMOUNT": 14,
|
"INDEXING_BLOCKS_AMOUNT": 14,
|
||||||
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
|
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
|
||||||
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
|
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
|
||||||
|
"POOLS_UPDATE_DELAY": 604800,
|
||||||
"AUDIT": true,
|
"AUDIT": true,
|
||||||
"RUST_GBT": false,
|
"RUST_GBT": false,
|
||||||
"LIMIT_GBT": false,
|
"LIMIT_GBT": false,
|
||||||
@@ -46,7 +47,8 @@
|
|||||||
"PASSWORD": "__CORE_RPC_PASSWORD__",
|
"PASSWORD": "__CORE_RPC_PASSWORD__",
|
||||||
"TIMEOUT": 1000,
|
"TIMEOUT": 1000,
|
||||||
"COOKIE": false,
|
"COOKIE": false,
|
||||||
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__"
|
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__",
|
||||||
|
"DEBUG_LOG_PATH": "__CORE_RPC_DEBUG_LOG_PATH__"
|
||||||
},
|
},
|
||||||
"ELECTRUM": {
|
"ELECTRUM": {
|
||||||
"HOST": "__ELECTRUM_HOST__",
|
"HOST": "__ELECTRUM_HOST__",
|
||||||
|
|||||||
@@ -41,8 +41,9 @@ describe('Mempool Backend Config', () => {
|
|||||||
STDOUT_LOG_MIN_PRIORITY: 'debug',
|
STDOUT_LOG_MIN_PRIORITY: 'debug',
|
||||||
POOLS_JSON_TREE_URL: 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
|
POOLS_JSON_TREE_URL: 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
|
||||||
POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json',
|
POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json',
|
||||||
|
POOLS_UPDATE_DELAY: 604800,
|
||||||
AUDIT: false,
|
AUDIT: false,
|
||||||
RUST_GBT: false,
|
RUST_GBT: true,
|
||||||
LIMIT_GBT: false,
|
LIMIT_GBT: false,
|
||||||
CPFP_INDEXING: false,
|
CPFP_INDEXING: false,
|
||||||
MAX_BLOCKS_BULK_QUERY: 0,
|
MAX_BLOCKS_BULK_QUERY: 0,
|
||||||
@@ -73,7 +74,8 @@ describe('Mempool Backend Config', () => {
|
|||||||
PASSWORD: 'mempool',
|
PASSWORD: 'mempool',
|
||||||
TIMEOUT: 60000,
|
TIMEOUT: 60000,
|
||||||
COOKIE: false,
|
COOKIE: false,
|
||||||
COOKIE_PATH: '/bitcoin/.cookie'
|
COOKIE_PATH: '/bitcoin/.cookie',
|
||||||
|
DEBUG_LOG_PATH: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(config.SECOND_CORE_RPC).toStrictEqual({
|
expect(config.SECOND_CORE_RPC).toStrictEqual({
|
||||||
|
|||||||
@@ -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';
|
import { IEsploraApi } from './esplora-api.interface';
|
||||||
|
|
||||||
export interface AbstractBitcoinApi {
|
export interface AbstractBitcoinApi {
|
||||||
@@ -23,12 +23,14 @@ export interface AbstractBitcoinApi {
|
|||||||
$getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
|
$getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
|
||||||
$sendRawTransaction(rawTransaction: string): Promise<string>;
|
$sendRawTransaction(rawTransaction: string): Promise<string>;
|
||||||
$testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]>;
|
$testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]>;
|
||||||
|
$submitPackage(rawTransactions: string[], maxfeerate?: number, maxburnamount?: number): Promise<SubmitPackageResult>;
|
||||||
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
|
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
|
||||||
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
|
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
|
||||||
$getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
|
$getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
|
||||||
$getBatchedOutspendsInternal(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
|
$getBatchedOutspendsInternal(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
|
||||||
$getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]>;
|
$getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]>;
|
||||||
$getCoinbaseTx(blockhash: string): Promise<IEsploraApi.Transaction>;
|
$getCoinbaseTx(blockhash: string): Promise<IEsploraApi.Transaction>;
|
||||||
|
$getAddressTransactionSummary(address: string): Promise<IEsploraApi.AddressTxSummary[]>;
|
||||||
|
|
||||||
startHealthChecks(): void;
|
startHealthChecks(): void;
|
||||||
getHealthStatus(): HealthCheckHost[];
|
getHealthStatus(): HealthCheckHost[];
|
||||||
|
|||||||
@@ -218,3 +218,21 @@ export interface TestMempoolAcceptResult {
|
|||||||
},
|
},
|
||||||
['reject-reason']?: string,
|
['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 * as bitcoinjs from 'bitcoinjs-lib';
|
||||||
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
|
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 { IEsploraApi } from './esplora-api.interface';
|
||||||
import blocks from '../blocks';
|
import blocks from '../blocks';
|
||||||
import mempool from '../mempool';
|
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> {
|
async $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
|
||||||
const txOut = await this.bitcoindClient.getTxOut(txId, vout, false);
|
const txOut = await this.bitcoindClient.getTxOut(txId, vout, false);
|
||||||
return {
|
return {
|
||||||
@@ -251,6 +255,10 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
return this.$getRawTransaction(txids[0]);
|
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> {
|
$getEstimatedHashrate(blockHeight: number): Promise<number> {
|
||||||
// 120 is the default block span in Core
|
// 120 is the default block span in Core
|
||||||
return this.bitcoindClient.getNetworkHashPs(120, blockHeight);
|
return this.bitcoindClient.getNetworkHashPs(120, blockHeight);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Application, NextFunction, Request, Response } from 'express';
|
import { Application, NextFunction, Request, Response } from 'express';
|
||||||
import logger from '../../logger';
|
import logger from '../../logger';
|
||||||
import bitcoinClient from './bitcoin-client';
|
import bitcoinClient from './bitcoin-client';
|
||||||
|
import config from '../../config';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define a set of routes used by the accelerator server
|
* Define a set of routes used by the accelerator server
|
||||||
@@ -11,15 +12,15 @@ class BitcoinBackendRoutes {
|
|||||||
|
|
||||||
public initRoutes(app: Application) {
|
public initRoutes(app: Application) {
|
||||||
app
|
app
|
||||||
.get('/api/internal/bitcoin-core/' + 'get-mempool-entry', this.disableCache, this.$getMempoolEntry)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-mempool-entry', this.disableCache, this.$getMempoolEntry)
|
||||||
.post('/api/internal/bitcoin-core/' + 'decode-raw-transaction', this.disableCache, this.$decodeRawTransaction)
|
.post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'decode-raw-transaction', this.disableCache, this.$decodeRawTransaction)
|
||||||
.get('/api/internal/bitcoin-core/' + 'get-raw-transaction', this.disableCache, this.$getRawTransaction)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-raw-transaction', this.disableCache, this.$getRawTransaction)
|
||||||
.post('/api/internal/bitcoin-core/' + 'send-raw-transaction', this.disableCache, this.$sendRawTransaction)
|
.post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'send-raw-transaction', this.disableCache, this.$sendRawTransaction)
|
||||||
.post('/api/internal/bitcoin-core/' + 'test-mempool-accept', this.disableCache, this.$testMempoolAccept)
|
.post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'test-mempool-accept', this.disableCache, this.$testMempoolAccept)
|
||||||
.get('/api/internal/bitcoin-core/' + 'get-mempool-ancestors', this.disableCache, this.$getMempoolAncestors)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-mempool-ancestors', this.disableCache, this.$getMempoolAncestors)
|
||||||
.get('/api/internal/bitcoin-core/' + 'get-block', this.disableCache, this.$getBlock)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block', this.disableCache, this.$getBlock)
|
||||||
.get('/api/internal/bitcoin-core/' + 'get-block-hash', this.disableCache, this.$getBlockHash)
|
.get(config.MEMPOOL.API_URL_PREFIX + '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-block-count', this.disableCache, this.$getBlockCount)
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,12 +42,15 @@ class BitcoinRoutes {
|
|||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this))
|
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this))
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/summary', this.getStrippedBlockTransaction)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
|
||||||
.post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion)
|
.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', this.getBlocksByBulk.bind(this))
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', 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') {
|
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||||
@@ -319,6 +322,20 @@ class BitcoinRoutes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getStrippedBlockTransaction(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const transaction = await blocks.$getSingleTxFromSummary(req.params.hash, req.params.txid);
|
||||||
|
if (!transaction) {
|
||||||
|
handleError(req, res, 404, `transaction not found in summary`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
|
||||||
|
res.json(transaction);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async getBlock(req: Request, res: Response) {
|
private async getBlock(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const block = await blocks.$getBlock(req.params.hash);
|
const block = await blocks.$getBlock(req.params.hash);
|
||||||
@@ -794,6 +811,19 @@ class BitcoinRoutes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new BitcoinRoutes();
|
export default new BitcoinRoutes();
|
||||||
@@ -179,4 +179,11 @@ export namespace IEsploraApi {
|
|||||||
burn_count: number;
|
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 { IEsploraApi } from './esplora-api.interface';
|
||||||
import logger from '../../logger';
|
import logger from '../../logger';
|
||||||
import { Common } from '../common';
|
import { Common } from '../common';
|
||||||
import { TestMempoolAcceptResult } from './bitcoin-api.interface';
|
import { SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface';
|
||||||
|
|
||||||
interface FailoverHost {
|
interface FailoverHost {
|
||||||
host: string,
|
host: string,
|
||||||
@@ -305,7 +305,7 @@ class ElectrsApi implements AbstractBitcoinApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$getAddress(address: string): Promise<IEsploraApi.Address> {
|
$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[]> {
|
$getAddressTransactions(address: string, txId?: string): Promise<IEsploraApi.Transaction[]> {
|
||||||
@@ -332,6 +332,10 @@ class ElectrsApi implements AbstractBitcoinApi {
|
|||||||
throw new Error('Method not implemented.');
|
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> {
|
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
|
||||||
return this.failoverRouter.$get<IEsploraApi.Outspend>('/tx/' + txId + '/outspend/' + vout);
|
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);
|
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 {
|
public startHealthChecks(): void {
|
||||||
this.failoverRouter.startHealthChecks();
|
this.failoverRouter.startHealthChecks();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp';
|
|||||||
import mempool from './mempool';
|
import mempool from './mempool';
|
||||||
import CpfpRepository from '../repositories/CpfpRepository';
|
import CpfpRepository from '../repositories/CpfpRepository';
|
||||||
import accelerationApi from './services/acceleration';
|
import accelerationApi from './services/acceleration';
|
||||||
|
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
|
||||||
|
|
||||||
class Blocks {
|
class Blocks {
|
||||||
private blocks: BlockExtended[] = [];
|
private blocks: BlockExtended[] = [];
|
||||||
@@ -342,7 +343,12 @@ class Blocks {
|
|||||||
id: pool.uniqueId,
|
id: pool.uniqueId,
|
||||||
name: pool.name,
|
name: pool.name,
|
||||||
slug: pool.slug,
|
slug: pool.slug,
|
||||||
|
minerNames: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (extras.pool.name === 'OCEAN') {
|
||||||
|
extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extras.matchRate = null;
|
extras.matchRate = null;
|
||||||
@@ -406,8 +412,16 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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
|
// 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 indexedBlockSummariesHashesArray = await BlocksSummariesRepository.$getIndexedSummariesId();
|
||||||
|
|
||||||
const indexedBlockSummariesHashes = {}; // Use a map for faster seek during the indexing loop
|
const indexedBlockSummariesHashes = {}; // Use a map for faster seek during the indexing loop
|
||||||
@@ -1210,6 +1224,11 @@ class Blocks {
|
|||||||
return summary.transactions;
|
return summary.transactions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $getSingleTxFromSummary(hash: string, txid: string): Promise<TransactionClassified | null> {
|
||||||
|
const txs = await this.$getStrippedBlockTransactions(hash);
|
||||||
|
return txs.find(tx => tx.txid === txid) || null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get 15 blocks
|
* Get 15 blocks
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
|
|||||||
import { RowDataPacket } from 'mysql2';
|
import { RowDataPacket } from 'mysql2';
|
||||||
|
|
||||||
class DatabaseMigration {
|
class DatabaseMigration {
|
||||||
private static currentVersion = 82;
|
private static currentVersion = 93;
|
||||||
private queryTimeout = 3600_000;
|
private queryTimeout = 3600_000;
|
||||||
private statisticsAddedIndexed = false;
|
private statisticsAddedIndexed = false;
|
||||||
private uniqueLogs: string[] = [];
|
private uniqueLogs: string[] = [];
|
||||||
@@ -705,6 +705,102 @@ class DatabaseMigration {
|
|||||||
await this.$fixBadV1AuditBlocks();
|
await this.$fixBadV1AuditBlocks();
|
||||||
await this.updateToSchemaVersion(82);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add new pools indexes
|
||||||
|
if (databaseSchemaVersion < 84 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery(`
|
||||||
|
ALTER TABLE \`pools\`
|
||||||
|
ADD INDEX \`slug\` (\`slug\`),
|
||||||
|
ADD INDEX \`unique_id\` (\`unique_id\`)
|
||||||
|
`);
|
||||||
|
await this.updateToSchemaVersion(84);
|
||||||
|
}
|
||||||
|
|
||||||
|
// lightning channels indexes
|
||||||
|
if (databaseSchemaVersion < 85 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery(`
|
||||||
|
ALTER TABLE \`channels\`
|
||||||
|
ADD INDEX \`created\` (\`created\`),
|
||||||
|
ADD INDEX \`capacity\` (\`capacity\`),
|
||||||
|
ADD INDEX \`closing_reason\` (\`closing_reason\`),
|
||||||
|
ADD INDEX \`closing_resolved\` (\`closing_resolved\`)
|
||||||
|
`);
|
||||||
|
await this.updateToSchemaVersion(85);
|
||||||
|
}
|
||||||
|
|
||||||
|
// lightning nodes indexes
|
||||||
|
if (databaseSchemaVersion < 86 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery(`
|
||||||
|
ALTER TABLE \`nodes\`
|
||||||
|
ADD INDEX \`status\` (\`status\`),
|
||||||
|
ADD INDEX \`channels\` (\`channels\`),
|
||||||
|
ADD INDEX \`country_id\` (\`country_id\`),
|
||||||
|
ADD INDEX \`as_number\` (\`as_number\`),
|
||||||
|
ADD INDEX \`first_seen\` (\`first_seen\`)
|
||||||
|
`);
|
||||||
|
await this.updateToSchemaVersion(86);
|
||||||
|
}
|
||||||
|
|
||||||
|
// lightning node sockets indexes
|
||||||
|
if (databaseSchemaVersion < 87 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery('ALTER TABLE `nodes_sockets` ADD INDEX `type` (`type`)');
|
||||||
|
await this.updateToSchemaVersion(87);
|
||||||
|
}
|
||||||
|
|
||||||
|
// lightning stats indexes
|
||||||
|
if (databaseSchemaVersion < 88 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD INDEX `added` (`added`)');
|
||||||
|
await this.updateToSchemaVersion(88);
|
||||||
|
}
|
||||||
|
|
||||||
|
// geo names indexes
|
||||||
|
if (databaseSchemaVersion < 89 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery('ALTER TABLE `geo_names` ADD INDEX `names` (`names`)');
|
||||||
|
await this.updateToSchemaVersion(89);
|
||||||
|
}
|
||||||
|
|
||||||
|
// hashrates indexes
|
||||||
|
if (databaseSchemaVersion < 90 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery('ALTER TABLE `hashrates` ADD INDEX `type` (`type`)');
|
||||||
|
await this.updateToSchemaVersion(90);
|
||||||
|
}
|
||||||
|
|
||||||
|
// block audits indexes
|
||||||
|
if (databaseSchemaVersion < 91 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `time` (`time`)');
|
||||||
|
await this.updateToSchemaVersion(91);
|
||||||
|
}
|
||||||
|
|
||||||
|
// elements_pegs indexes
|
||||||
|
if (databaseSchemaVersion < 92 && config.MEMPOOL.NETWORK === 'liquid') {
|
||||||
|
await this.$executeQuery(`
|
||||||
|
ALTER TABLE \`elements_pegs\`
|
||||||
|
ADD INDEX \`block\` (\`block\`),
|
||||||
|
ADD INDEX \`datetime\` (\`datetime\`),
|
||||||
|
ADD INDEX \`amount\` (\`amount\`),
|
||||||
|
ADD INDEX \`bitcoinaddress\` (\`bitcoinaddress\`),
|
||||||
|
ADD INDEX \`bitcointxid\` (\`bitcointxid\`)
|
||||||
|
`);
|
||||||
|
await this.updateToSchemaVersion(92);
|
||||||
|
}
|
||||||
|
|
||||||
|
// federation_txos indexes
|
||||||
|
if (databaseSchemaVersion < 93 && config.MEMPOOL.NETWORK === 'liquid') {
|
||||||
|
await this.$executeQuery(`
|
||||||
|
ALTER TABLE \`federation_txos\`
|
||||||
|
ADD INDEX \`unspent\` (\`unspent\`),
|
||||||
|
ADD INDEX \`lastblockupdate\` (\`lastblockupdate\`),
|
||||||
|
ADD INDEX \`blocktime\` (\`blocktime\`),
|
||||||
|
ADD INDEX \`emergencyKey\` (\`emergencyKey\`),
|
||||||
|
ADD INDEX \`expiredAt\` (\`expiredAt\`)
|
||||||
|
`);
|
||||||
|
await this.updateToSchemaVersion(93);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ class MempoolBlocks {
|
|||||||
|
|
||||||
const ancestors: Ancestor[] = [];
|
const ancestors: Ancestor[] = [];
|
||||||
const descendants: Ancestor[] = [];
|
const descendants: Ancestor[] = [];
|
||||||
let ancestor: MempoolTransactionExtended
|
let ancestor: MempoolTransactionExtended;
|
||||||
for (const cluster of clusters) {
|
for (const cluster of clusters) {
|
||||||
for (const memberTxid of cluster) {
|
for (const memberTxid of cluster) {
|
||||||
const mempoolTx = mempool[memberTxid];
|
const mempoolTx = mempool[memberTxid];
|
||||||
@@ -462,7 +462,7 @@ class MempoolBlocks {
|
|||||||
|
|
||||||
for (let i = 0; i < block.length; i++) {
|
for (let i = 0; i < block.length; i++) {
|
||||||
const txid = block[i];
|
const txid = block[i];
|
||||||
if (txid) {
|
if (txid in mempool) {
|
||||||
mempoolTx = mempool[txid];
|
mempoolTx = mempool[txid];
|
||||||
// save position in projected blocks
|
// save position in projected blocks
|
||||||
mempoolTx.position = {
|
mempoolTx.position = {
|
||||||
@@ -481,6 +481,9 @@ class MempoolBlocks {
|
|||||||
mempoolTx.acceleratedAt = acceleration?.added;
|
mempoolTx.acceleratedAt = acceleration?.added;
|
||||||
mempoolTx.feeDelta = acceleration?.feeDelta;
|
mempoolTx.feeDelta = acceleration?.feeDelta;
|
||||||
for (const ancestor of mempoolTx.ancestors || []) {
|
for (const ancestor of mempoolTx.ancestors || []) {
|
||||||
|
if (!(ancestor.txid in mempool)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!mempool[ancestor.txid].acceleration) {
|
if (!mempool[ancestor.txid].acceleration) {
|
||||||
mempool[ancestor.txid].cpfpDirty = true;
|
mempool[ancestor.txid].cpfpDirty = true;
|
||||||
}
|
}
|
||||||
@@ -688,7 +691,7 @@ class MempoolBlocks {
|
|||||||
[pool: string]: { name: string, block: number, vsize: number, accelerations: string[], complete: boolean };
|
[pool: string]: { name: string, block: number, vsize: number, accelerations: string[], complete: boolean };
|
||||||
} = {};
|
} = {};
|
||||||
// prepare a list of accelerations in ascending order (we'll pop items off the end of the list)
|
// prepare a list of accelerations in ascending order (we'll pop items off the end of the list)
|
||||||
const accQueue: { acceleration: Acceleration, rate: number, vsize: number }[] = Object.values(accelerations).map(acc => {
|
const accQueue: { acceleration: Acceleration, rate: number, vsize: number }[] = Object.values(accelerations).filter(acc => acc.txid in mempoolCache).map(acc => {
|
||||||
let vsize = mempoolCache[acc.txid].vsize;
|
let vsize = mempoolCache[acc.txid].vsize;
|
||||||
for (const ancestor of mempoolCache[acc.txid].ancestors || []) {
|
for (const ancestor of mempoolCache[acc.txid].ancestors || []) {
|
||||||
vsize += (ancestor.weight / 4);
|
vsize += (ancestor.weight / 4);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import bitcoinClient from './bitcoin/bitcoin-client';
|
|||||||
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
||||||
import rbfCache from './rbf-cache';
|
import rbfCache from './rbf-cache';
|
||||||
import { Acceleration } from './services/acceleration';
|
import { Acceleration } from './services/acceleration';
|
||||||
|
import accelerationApi from './services/acceleration';
|
||||||
import redisCache from './redis-cache';
|
import redisCache from './redis-cache';
|
||||||
import blocks from './blocks';
|
import blocks from './blocks';
|
||||||
|
|
||||||
@@ -207,7 +208,7 @@ class Mempool {
|
|||||||
return txTimes;
|
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...`);
|
logger.debug(`Updating mempool...`);
|
||||||
|
|
||||||
// warn if this run stalls the main loop for more than 2 minutes
|
// warn if this run stalls the main loop for more than 2 minutes
|
||||||
@@ -354,7 +355,7 @@ class Mempool {
|
|||||||
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
|
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
|
||||||
this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6);
|
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) {
|
if (accelerationDelta.length) {
|
||||||
hasChange = true;
|
hasChange = true;
|
||||||
}
|
}
|
||||||
@@ -399,58 +400,11 @@ class Mempool {
|
|||||||
return this.accelerations;
|
return this.accelerations;
|
||||||
}
|
}
|
||||||
|
|
||||||
public $updateAccelerations(newAccelerations: Acceleration[]): string[] {
|
public updateAccelerations(newAccelerationMap: Record<string, Acceleration>): string[] {
|
||||||
try {
|
try {
|
||||||
const changed: string[] = [];
|
const accelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, newAccelerationMap);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.accelerations = newAccelerationMap;
|
this.accelerations = newAccelerationMap;
|
||||||
|
return accelerationDelta;
|
||||||
return changed;
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.debug(`Failed to update accelerations: ` + (e instanceof Error ? e.message : e));
|
logger.debug(`Failed to update accelerations: ` + (e instanceof Error ? e.message : e));
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ class MiningRoutes {
|
|||||||
private async $getHistoricalHashrate(req: Request, res: Response) {
|
private async $getHistoricalHashrate(req: Request, res: Response) {
|
||||||
let currentHashrate = 0, currentDifficulty = 0;
|
let currentHashrate = 0, currentDifficulty = 0;
|
||||||
try {
|
try {
|
||||||
currentHashrate = await bitcoinClient.getNetworkHashPs();
|
currentHashrate = await bitcoinClient.getNetworkHashPs(1008);
|
||||||
currentDifficulty = await bitcoinClient.getDifficulty();
|
currentDifficulty = await bitcoinClient.getDifficulty();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate and difficulty');
|
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate and difficulty');
|
||||||
@@ -459,7 +459,7 @@ class MiningRoutes {
|
|||||||
handleError(req, res, 400, 'Acceleration data is not available.');
|
handleError(req, res, 400, 'Acceleration data is not available.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.status(200).send(accelerationApi.accelerations || []);
|
res.status(200).send(Object.values(accelerationApi.getAccelerations() || {}));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleError(req, res, 500, 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;
|
poolsStatistics['blockCount'] = blockCount;
|
||||||
|
|
||||||
const totalBlock24h: number = await BlocksRepository.$blockCount(null, '24h');
|
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 {
|
try {
|
||||||
poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h);
|
poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h);
|
||||||
|
poolsStatistics['lastEstimatedHashrate3d'] = await bitcoinClient.getNetworkHashPs(totalBlock3d);
|
||||||
|
poolsStatistics['lastEstimatedHashrate1w'] = await bitcoinClient.getNetworkHashPs(totalBlock1w);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
poolsStatistics['lastEstimatedHashrate'] = 0;
|
poolsStatistics['lastEstimatedHashrate'] = 0;
|
||||||
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate', logger.tags.mining);
|
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate', logger.tags.mining);
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import { Application, Request, Response } from 'express';
|
import { Application, Request, Response } from 'express';
|
||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import pricesUpdater from '../../tasks/price-updater';
|
import pricesUpdater from '../../tasks/price-updater';
|
||||||
|
import logger from '../../logger';
|
||||||
|
import PricesRepository from '../../repositories/PricesRepository';
|
||||||
|
|
||||||
class PricesRoutes {
|
class PricesRoutes {
|
||||||
public initRoutes(app: Application): void {
|
public initRoutes(app: Application): void {
|
||||||
app.get(config.MEMPOOL.API_URL_PREFIX + 'prices', this.$getCurrentPrices.bind(this));
|
app
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'prices', this.$getCurrentPrices.bind(this))
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/usd-price-history', this.$getAllPrices.bind(this))
|
||||||
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
private $getCurrentPrices(req: Request, res: Response): void {
|
private $getCurrentPrices(req: Request, res: Response): void {
|
||||||
@@ -14,6 +19,23 @@ class PricesRoutes {
|
|||||||
|
|
||||||
res.json(pricesUpdater.getLatestPrices());
|
res.json(pricesUpdater.getLatestPrices());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async $getAllPrices(req: Request, res: Response): Promise<void> {
|
||||||
|
res.header('Pragma', 'public');
|
||||||
|
res.header('Cache-control', 'public');
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 360_0000 / config.MEMPOOL.PRICE_UPDATES_PER_HOUR).toUTCString());
|
||||||
|
|
||||||
|
try {
|
||||||
|
const usdPriceHistory = await PricesRepository.$getPricesTimesAndId();
|
||||||
|
const responseData = usdPriceHistory.map(p => {
|
||||||
|
return { time: p.time, USD: p.USD };
|
||||||
|
});
|
||||||
|
res.status(200).json(responseData);
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Exception ${e} in PricesRoutes::$getAllPrices. Code: ${e.code}. Message: ${e.message}`);
|
||||||
|
res.status(403).send();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new PricesRoutes();
|
export default new PricesRoutes();
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import { WebSocket } from 'ws';
|
||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import logger from '../../logger';
|
import logger from '../../logger';
|
||||||
import { BlockExtended } from '../../mempool.interfaces';
|
import { BlockExtended } from '../../mempool.interfaces';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import mempool from '../mempool';
|
||||||
|
import websocketHandler from '../websocket-handler';
|
||||||
|
|
||||||
type MyAccelerationStatus = 'requested' | 'accelerating' | 'done';
|
type MyAccelerationStatus = 'requested' | 'accelerating' | 'done';
|
||||||
|
|
||||||
@@ -37,14 +40,23 @@ export interface AccelerationHistory {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class AccelerationApi {
|
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 onDemandPollingEnabled = !config.MEMPOOL_SERVICES.ACCELERATIONS;
|
||||||
private apiPath = config.MEMPOOL.OFFICIAL ? (config.MEMPOOL_SERVICES.API + '/accelerator/accelerations') : (config.EXTERNAL_DATA_SERVER.MEMPOOL_API + '/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 websocketPath = config.MEMPOOL_SERVICES?.API ? `${config.MEMPOOL_SERVICES.API.replace('https://', 'wss://').replace('http://', 'ws://')}/accelerator/ws` : '/';
|
||||||
|
private _accelerations: Record<string, Acceleration> = {};
|
||||||
private lastPoll = 0;
|
private lastPoll = 0;
|
||||||
|
private lastPing = Date.now();
|
||||||
|
private lastPong = Date.now();
|
||||||
private forcePoll = false;
|
private forcePoll = false;
|
||||||
private myAccelerations: Record<string, { status: MyAccelerationStatus, added: number, acceleration?: Acceleration }> = {};
|
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;
|
return this._accelerations;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,11 +84,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) {
|
if (!this.onDemandPollingEnabled) {
|
||||||
const accelerations = await this.$fetchAccelerations();
|
const accelerations = await this.$fetchAccelerations();
|
||||||
if (accelerations) {
|
if (accelerations) {
|
||||||
this._accelerations = accelerations;
|
const latestAccelerations = {};
|
||||||
|
for (const acc of accelerations) {
|
||||||
|
latestAccelerations[acc.txid] = acc;
|
||||||
|
}
|
||||||
|
this._accelerations = latestAccelerations;
|
||||||
return this._accelerations;
|
return this._accelerations;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -85,7 +104,7 @@ class AccelerationApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async $updateAccelerationsOnDemand(): Promise<Acceleration[] | null> {
|
private async $updateAccelerationsOnDemand(): Promise<Record<string, Acceleration> | null> {
|
||||||
const shouldUpdate = this.forcePoll
|
const shouldUpdate = this.forcePoll
|
||||||
|| this.countMyAccelerationsWithStatus('requested') > 0
|
|| this.countMyAccelerationsWithStatus('requested') > 0
|
||||||
|| (this.countMyAccelerationsWithStatus('accelerating') > 0 && this.lastPoll < (Date.now() - (10 * 60 * 1000)));
|
|| (this.countMyAccelerationsWithStatus('accelerating') > 0 && this.lastPoll < (Date.now() - (10 * 60 * 1000)));
|
||||||
@@ -120,7 +139,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;
|
return this._accelerations;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +175,148 @@ class AccelerationApi {
|
|||||||
}
|
}
|
||||||
return anyAccelerated;
|
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(this.websocketPath);
|
||||||
|
this.lastPing = 0;
|
||||||
|
|
||||||
|
this.ws.on('open', () => {
|
||||||
|
logger.info(`Acceleration websocket opened to ${this.websocketPath}`);
|
||||||
|
this.websocketConnected = true;
|
||||||
|
this.ws?.send(JSON.stringify({
|
||||||
|
'watch-accelerations': true
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.on('error', (error) => {
|
||||||
|
let errMsg = `Acceleration websocket error on ${this.websocketPath}: ${error['code']}`;
|
||||||
|
if (error['errors']) {
|
||||||
|
errMsg += ' - ' + error['errors'].join(' - ');
|
||||||
|
}
|
||||||
|
logger.err(errMsg);
|
||||||
|
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 msg = (isBinary ? data : data.toString()) as string;
|
||||||
|
const parsedMsg = msg?.length ? JSON.parse(msg) : null;
|
||||||
|
this.handleWebsocketMessage(parsedMsg);
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('Failed to parse acceleration websocket message: ' + (e instanceof Error ? e.message : e));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.on('ping', () => {
|
||||||
|
logger.debug('received ping from acceleration websocket server');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.on('pong', () => {
|
||||||
|
logger.debug('received pong from acceleration websocket server');
|
||||||
|
this.lastPong = Date.now();
|
||||||
|
});
|
||||||
|
} else if (this.websocketConnected) {
|
||||||
|
if (this.lastPing && this.lastPing > this.lastPong && (Date.now() - this.lastPing > 10000)) {
|
||||||
|
logger.warn('No pong received within 10 seconds, terminating connection');
|
||||||
|
try {
|
||||||
|
this.ws?.terminate();
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('failed to terminate acceleration websocket connection: ' + (e instanceof Error ? e.message : e));
|
||||||
|
} finally {
|
||||||
|
this.ws = null;
|
||||||
|
this.websocketConnected = false;
|
||||||
|
this.lastPing = 0;
|
||||||
|
}
|
||||||
|
} else if (!this.lastPing || (Date.now() - this.lastPing > 30000)) {
|
||||||
|
logger.debug('sending ping to acceleration websocket server');
|
||||||
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||||
|
try {
|
||||||
|
this.ws?.ping();
|
||||||
|
this.lastPing = Date.now();
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('failed to send ping to acceleration websocket server: ' + (e instanceof Error ? e.message : e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new AccelerationApi();
|
export default new AccelerationApi();
|
||||||
@@ -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();
|
||||||
@@ -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 adjustedVsize = Math.max(fractionalVsize, sigops * 5); // adjusted vsize = Max(weight, sigops * bytes_per_sigop) / witness_scale_factor
|
||||||
const feePerVbytes = (transaction.fee || 0) / fractionalVsize;
|
const feePerVbytes = (transaction.fee || 0) / fractionalVsize;
|
||||||
const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize;
|
const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize;
|
||||||
|
const effectiveFeePerVsize = transaction['effectiveFeePerVsize'] || adjustedFeePerVsize || feePerVbytes;
|
||||||
const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, {
|
const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, {
|
||||||
order: this.txidToOrdering(transaction.txid),
|
order: this.txidToOrdering(transaction.txid),
|
||||||
vsize,
|
vsize,
|
||||||
@@ -128,7 +129,7 @@ class TransactionUtils {
|
|||||||
sigops,
|
sigops,
|
||||||
feePerVsize: feePerVbytes,
|
feePerVsize: feePerVbytes,
|
||||||
adjustedFeePerVsize: adjustedFeePerVsize,
|
adjustedFeePerVsize: adjustedFeePerVsize,
|
||||||
effectiveFeePerVsize: adjustedFeePerVsize,
|
effectiveFeePerVsize: effectiveFeePerVsize,
|
||||||
});
|
});
|
||||||
if (!transactionExtended?.status?.confirmed && !transactionExtended.firstSeen) {
|
if (!transactionExtended?.status?.confirmed && !transactionExtended.firstSeen) {
|
||||||
transactionExtended.firstSeen = Math.round((Date.now() / 1000));
|
transactionExtended.firstSeen = Math.round((Date.now() / 1000));
|
||||||
|
|||||||
@@ -16,16 +16,19 @@ import transactionUtils from './transaction-utils';
|
|||||||
import rbfCache, { ReplacementInfo } from './rbf-cache';
|
import rbfCache, { ReplacementInfo } from './rbf-cache';
|
||||||
import difficultyAdjustment from './difficulty-adjustment';
|
import difficultyAdjustment from './difficulty-adjustment';
|
||||||
import feeApi from './fee-api';
|
import feeApi from './fee-api';
|
||||||
|
import BlocksRepository from '../repositories/BlocksRepository';
|
||||||
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
|
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
|
||||||
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
|
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
|
||||||
import Audit from './audit';
|
import Audit from './audit';
|
||||||
import priceUpdater from '../tasks/price-updater';
|
import priceUpdater from '../tasks/price-updater';
|
||||||
import { ApiPrice } from '../repositories/PricesRepository';
|
import { ApiPrice } from '../repositories/PricesRepository';
|
||||||
|
import { Acceleration } from './services/acceleration';
|
||||||
import accelerationApi from './services/acceleration';
|
import accelerationApi from './services/acceleration';
|
||||||
import mempool from './mempool';
|
import mempool from './mempool';
|
||||||
import statistics from './statistics/statistics';
|
import statistics from './statistics/statistics';
|
||||||
import accelerationRepository from '../repositories/AccelerationRepository';
|
import accelerationRepository from '../repositories/AccelerationRepository';
|
||||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||||
|
import walletApi from './services/wallets';
|
||||||
|
|
||||||
interface AddressTransactions {
|
interface AddressTransactions {
|
||||||
mempool: MempoolTransactionExtended[],
|
mempool: MempoolTransactionExtended[],
|
||||||
@@ -34,6 +37,7 @@ interface AddressTransactions {
|
|||||||
}
|
}
|
||||||
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
||||||
import { calculateMempoolTxCpfp } from './cpfp';
|
import { calculateMempoolTxCpfp } from './cpfp';
|
||||||
|
import { getRecentFirstSeen } from '../utils/file-read';
|
||||||
|
|
||||||
// valid 'want' subscriptions
|
// valid 'want' subscriptions
|
||||||
const wantable = [
|
const wantable = [
|
||||||
@@ -57,6 +61,8 @@ class WebsocketHandler {
|
|||||||
private lastRbfSummary: ReplacementInfo[] | null = null;
|
private lastRbfSummary: ReplacementInfo[] | null = null;
|
||||||
private mempoolSequence: number = 0;
|
private mempoolSequence: number = 0;
|
||||||
|
|
||||||
|
private accelerations: Record<string, Acceleration> = {};
|
||||||
|
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
addWebsocketServer(wss: WebSocket.Server) {
|
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 (parsedMessage && parsedMessage['track-asset']) {
|
||||||
if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-asset'])) {
|
if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-asset'])) {
|
||||||
client['track-asset'] = 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 {
|
handleReorg(): void {
|
||||||
if (!this.webSocketServers.length) {
|
if (!this.webSocketServers.length) {
|
||||||
throw new Error('No WebSocket.Server have been set');
|
throw new Error('No WebSocket.Server have been set');
|
||||||
@@ -560,7 +610,7 @@ class WebsocketHandler {
|
|||||||
const vBytesPerSecond = memPool.getVBytesPerSecond();
|
const vBytesPerSecond = memPool.getVBytesPerSecond();
|
||||||
const rbfTransactions = Common.findRbfTransactions(newTransactions, recentlyDeletedTransactions.flat());
|
const rbfTransactions = Common.findRbfTransactions(newTransactions, recentlyDeletedTransactions.flat());
|
||||||
const da = difficultyAdjustment.getDifficultyAdjustment();
|
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||||
const accelerations = memPool.getAccelerations();
|
const accelerations = accelerationApi.getAccelerations();
|
||||||
memPool.handleRbfTransactions(rbfTransactions);
|
memPool.handleRbfTransactions(rbfTransactions);
|
||||||
const rbfChanges = rbfCache.getRbfChanges();
|
const rbfChanges = rbfCache.getRbfChanges();
|
||||||
let rbfReplacements;
|
let rbfReplacements;
|
||||||
@@ -668,10 +718,13 @@ class WebsocketHandler {
|
|||||||
const addressCache = this.makeAddressCache(newTransactions);
|
const addressCache = this.makeAddressCache(newTransactions);
|
||||||
const removedAddressCache = this.makeAddressCache(deletedTransactions);
|
const removedAddressCache = this.makeAddressCache(deletedTransactions);
|
||||||
|
|
||||||
|
const websocketAccelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, accelerations);
|
||||||
|
this.accelerations = accelerations;
|
||||||
|
|
||||||
// pre-compute acceleration delta
|
// pre-compute acceleration delta
|
||||||
const accelerationUpdate = {
|
const accelerationUpdate = {
|
||||||
added: accelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
|
added: websocketAccelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
|
||||||
removed: accelerationDelta.filter(txid => !accelerations[txid]),
|
removed: websocketAccelerationDelta.filter(txid => !accelerations[txid]),
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO - Fix indentation after PR is merged
|
// TODO - Fix indentation after PR is merged
|
||||||
@@ -1028,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 } = {};
|
const confirmedTxids: { [txid: string]: boolean } = {};
|
||||||
|
|
||||||
// Update mempool to remove transactions included in the new block
|
// Update mempool to remove transactions included in the new block
|
||||||
@@ -1102,6 +1163,9 @@ class WebsocketHandler {
|
|||||||
replaced: replacedTransactions,
|
replaced: replacedTransactions,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// check for wallet transactions
|
||||||
|
const walletTransactions = config.WALLETS.ENABLED ? walletApi.processBlock(block, transactions) : [];
|
||||||
|
|
||||||
const responseCache = { ...this.socketData };
|
const responseCache = { ...this.socketData };
|
||||||
function getCachedResponse(key, data): string {
|
function getCachedResponse(key, data): string {
|
||||||
if (!responseCache[key]) {
|
if (!responseCache[key]) {
|
||||||
@@ -1306,6 +1370,11 @@ class WebsocketHandler {
|
|||||||
response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta);
|
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) {
|
if (Object.keys(response).length) {
|
||||||
client.send(this.serializeResponse(response));
|
client.send(this.serializeResponse(response));
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-2
@@ -32,6 +32,7 @@ interface IConfig {
|
|||||||
AUTOMATIC_POOLS_UPDATE: boolean;
|
AUTOMATIC_POOLS_UPDATE: boolean;
|
||||||
POOLS_JSON_URL: string,
|
POOLS_JSON_URL: string,
|
||||||
POOLS_JSON_TREE_URL: string,
|
POOLS_JSON_TREE_URL: string,
|
||||||
|
POOLS_UPDATE_DELAY: number,
|
||||||
AUDIT: boolean;
|
AUDIT: boolean;
|
||||||
RUST_GBT: boolean;
|
RUST_GBT: boolean;
|
||||||
LIMIT_GBT: boolean;
|
LIMIT_GBT: boolean;
|
||||||
@@ -85,6 +86,7 @@ interface IConfig {
|
|||||||
TIMEOUT: number;
|
TIMEOUT: number;
|
||||||
COOKIE: boolean;
|
COOKIE: boolean;
|
||||||
COOKIE_PATH: string;
|
COOKIE_PATH: string;
|
||||||
|
DEBUG_LOG_PATH: string;
|
||||||
};
|
};
|
||||||
SECOND_CORE_RPC: {
|
SECOND_CORE_RPC: {
|
||||||
HOST: string;
|
HOST: string;
|
||||||
@@ -160,6 +162,10 @@ interface IConfig {
|
|||||||
PAID: boolean;
|
PAID: boolean;
|
||||||
API_KEY: string;
|
API_KEY: string;
|
||||||
},
|
},
|
||||||
|
WALLETS: {
|
||||||
|
ENABLED: boolean;
|
||||||
|
WALLETS: string[];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaults: IConfig = {
|
const defaults: IConfig = {
|
||||||
@@ -192,8 +198,9 @@ const defaults: IConfig = {
|
|||||||
'AUTOMATIC_POOLS_UPDATE': false,
|
'AUTOMATIC_POOLS_UPDATE': false,
|
||||||
'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json',
|
'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_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,
|
'AUDIT': false,
|
||||||
'RUST_GBT': false,
|
'RUST_GBT': true,
|
||||||
'LIMIT_GBT': false,
|
'LIMIT_GBT': false,
|
||||||
'CPFP_INDEXING': false,
|
'CPFP_INDEXING': false,
|
||||||
'MAX_BLOCKS_BULK_QUERY': 0,
|
'MAX_BLOCKS_BULK_QUERY': 0,
|
||||||
@@ -225,7 +232,8 @@ const defaults: IConfig = {
|
|||||||
'PASSWORD': 'mempool',
|
'PASSWORD': 'mempool',
|
||||||
'TIMEOUT': 60000,
|
'TIMEOUT': 60000,
|
||||||
'COOKIE': false,
|
'COOKIE': false,
|
||||||
'COOKIE_PATH': '/bitcoin/.cookie'
|
'COOKIE_PATH': '/bitcoin/.cookie',
|
||||||
|
'DEBUG_LOG_PATH': '',
|
||||||
},
|
},
|
||||||
'SECOND_CORE_RPC': {
|
'SECOND_CORE_RPC': {
|
||||||
'HOST': '127.0.0.1',
|
'HOST': '127.0.0.1',
|
||||||
@@ -320,6 +328,10 @@ const defaults: IConfig = {
|
|||||||
'PAID': false,
|
'PAID': false,
|
||||||
'API_KEY': '',
|
'API_KEY': '',
|
||||||
},
|
},
|
||||||
|
'WALLETS': {
|
||||||
|
'ENABLED': false,
|
||||||
|
'WALLETS': [],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
class Config implements IConfig {
|
class Config implements IConfig {
|
||||||
@@ -341,6 +353,7 @@ class Config implements IConfig {
|
|||||||
MEMPOOL_SERVICES: IConfig['MEMPOOL_SERVICES'];
|
MEMPOOL_SERVICES: IConfig['MEMPOOL_SERVICES'];
|
||||||
REDIS: IConfig['REDIS'];
|
REDIS: IConfig['REDIS'];
|
||||||
FIAT_PRICE: IConfig['FIAT_PRICE'];
|
FIAT_PRICE: IConfig['FIAT_PRICE'];
|
||||||
|
WALLETS: IConfig['WALLETS'];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const configs = this.merge(configFromFile, defaults);
|
const configs = this.merge(configFromFile, defaults);
|
||||||
@@ -362,6 +375,7 @@ class Config implements IConfig {
|
|||||||
this.MEMPOOL_SERVICES = configs.MEMPOOL_SERVICES;
|
this.MEMPOOL_SERVICES = configs.MEMPOOL_SERVICES;
|
||||||
this.REDIS = configs.REDIS;
|
this.REDIS = configs.REDIS;
|
||||||
this.FIAT_PRICE = configs.FIAT_PRICE;
|
this.FIAT_PRICE = configs.FIAT_PRICE;
|
||||||
|
this.WALLETS = configs.WALLETS;
|
||||||
}
|
}
|
||||||
|
|
||||||
merge = (...objects: object[]): IConfig => {
|
merge = (...objects: object[]): IConfig => {
|
||||||
|
|||||||
+16
-3
@@ -32,6 +32,7 @@ import pricesRoutes from './api/prices/prices.routes';
|
|||||||
import miningRoutes from './api/mining/mining-routes';
|
import miningRoutes from './api/mining/mining-routes';
|
||||||
import liquidRoutes from './api/liquid/liquid.routes';
|
import liquidRoutes from './api/liquid/liquid.routes';
|
||||||
import bitcoinRoutes from './api/bitcoin/bitcoin.routes';
|
import bitcoinRoutes from './api/bitcoin/bitcoin.routes';
|
||||||
|
import servicesRoutes from './api/services/services-routes';
|
||||||
import fundingTxFetcher from './tasks/lightning/sync-tasks/funding-tx-fetcher';
|
import fundingTxFetcher from './tasks/lightning/sync-tasks/funding-tx-fetcher';
|
||||||
import forensicsService from './tasks/lightning/forensics.service';
|
import forensicsService from './tasks/lightning/forensics.service';
|
||||||
import priceUpdater from './tasks/price-updater';
|
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 accelerationRoutes from './api/acceleration/acceleration.routes';
|
||||||
import aboutRoutes from './api/about.routes';
|
import aboutRoutes from './api/about.routes';
|
||||||
import mempoolBlocks from './api/mempool-blocks';
|
import mempoolBlocks from './api/mempool-blocks';
|
||||||
|
import walletApi from './api/services/wallets';
|
||||||
|
|
||||||
class Server {
|
class Server {
|
||||||
private wss: WebSocket.Server | undefined;
|
private wss: WebSocket.Server | undefined;
|
||||||
@@ -211,6 +213,8 @@ class Server {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
poolsUpdater.$startService();
|
||||||
}
|
}
|
||||||
|
|
||||||
async runMainUpdateLoop(): Promise<void> {
|
async runMainUpdateLoop(): Promise<void> {
|
||||||
@@ -229,13 +233,17 @@ class Server {
|
|||||||
const newMempool = await bitcoinApi.$getRawMempool();
|
const newMempool = await bitcoinApi.$getRawMempool();
|
||||||
const minFeeMempool = memPool.limitGBT ? await bitcoinSecondClient.getRawMemPool() : null;
|
const minFeeMempool = memPool.limitGBT ? await bitcoinSecondClient.getRawMemPool() : null;
|
||||||
const minFeeTip = memPool.limitGBT ? await bitcoinSecondClient.getBlockCount() : -1;
|
const minFeeTip = memPool.limitGBT ? await bitcoinSecondClient.getBlockCount() : -1;
|
||||||
const newAccelerations = await accelerationApi.$updateAccelerations();
|
const latestAccelerations = await accelerationApi.$updateAccelerations();
|
||||||
const numHandledBlocks = await blocks.$updateBlocks();
|
const numHandledBlocks = await blocks.$updateBlocks();
|
||||||
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1);
|
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1);
|
||||||
if (numHandledBlocks === 0) {
|
if (numHandledBlocks === 0) {
|
||||||
await memPool.$updateMempool(newMempool, newAccelerations, minFeeMempool, minFeeTip, pollRate);
|
await memPool.$updateMempool(newMempool, latestAccelerations, minFeeMempool, minFeeTip, pollRate);
|
||||||
}
|
}
|
||||||
indexer.$run();
|
indexer.$run();
|
||||||
|
if (config.WALLETS.ENABLED) {
|
||||||
|
// might take a while, so run in the background
|
||||||
|
walletApi.$syncWallets();
|
||||||
|
}
|
||||||
if (config.FIAT_PRICE.ENABLED) {
|
if (config.FIAT_PRICE.ENABLED) {
|
||||||
priceUpdater.$run();
|
priceUpdater.$run();
|
||||||
}
|
}
|
||||||
@@ -310,8 +318,10 @@ class Server {
|
|||||||
priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
|
priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
|
||||||
}
|
}
|
||||||
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
|
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
|
||||||
|
|
||||||
|
accelerationApi.connectWebsocket();
|
||||||
}
|
}
|
||||||
|
|
||||||
setUpHttpApiRoutes(): void {
|
setUpHttpApiRoutes(): void {
|
||||||
bitcoinRoutes.initRoutes(this.app);
|
bitcoinRoutes.initRoutes(this.app);
|
||||||
bitcoinCoreRoutes.initRoutes(this.app);
|
bitcoinCoreRoutes.initRoutes(this.app);
|
||||||
@@ -333,6 +343,9 @@ class Server {
|
|||||||
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
|
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
|
||||||
accelerationRoutes.initRoutes(this.app);
|
accelerationRoutes.initRoutes(this.app);
|
||||||
}
|
}
|
||||||
|
if (config.WALLETS.ENABLED) {
|
||||||
|
servicesRoutes.initRoutes(this.app);
|
||||||
|
}
|
||||||
if (!config.MEMPOOL.OFFICIAL) {
|
if (!config.MEMPOOL.OFFICIAL) {
|
||||||
aboutRoutes.initRoutes(this.app);
|
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`
|
id: number; // Note - This is the `unique_id`, not to mix with the auto increment `id`
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
minerNames: string[] | null;
|
||||||
};
|
};
|
||||||
avgFee: number;
|
avgFee: number;
|
||||||
avgFeeRate: number;
|
avgFeeRate: number;
|
||||||
@@ -319,6 +320,7 @@ export interface BlockExtension {
|
|||||||
segwitTotalSize: number;
|
segwitTotalSize: number;
|
||||||
segwitTotalWeight: number;
|
segwitTotalWeight: number;
|
||||||
header: string;
|
header: string;
|
||||||
|
firstSeen: number | null;
|
||||||
utxoSetChange: number;
|
utxoSetChange: number;
|
||||||
// Requires coinstatsindex, will be set to NULL otherwise
|
// Requires coinstatsindex, will be set to NULL otherwise
|
||||||
utxoSetSize: number | null;
|
utxoSetSize: number | null;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import chainTips from '../api/chain-tips';
|
|||||||
import blocks from '../api/blocks';
|
import blocks from '../api/blocks';
|
||||||
import BlocksAuditsRepository from './BlocksAuditsRepository';
|
import BlocksAuditsRepository from './BlocksAuditsRepository';
|
||||||
import transactionUtils from '../api/transaction-utils';
|
import transactionUtils from '../api/transaction-utils';
|
||||||
|
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
|
||||||
|
|
||||||
interface DatabaseBlock {
|
interface DatabaseBlock {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -56,6 +57,7 @@ interface DatabaseBlock {
|
|||||||
utxoSetChange: number;
|
utxoSetChange: number;
|
||||||
utxoSetSize: number;
|
utxoSetSize: number;
|
||||||
totalInputAmt: number;
|
totalInputAmt: number;
|
||||||
|
firstSeen: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BLOCK_DB_FIELDS = `
|
const BLOCK_DB_FIELDS = `
|
||||||
@@ -98,7 +100,8 @@ const BLOCK_DB_FIELDS = `
|
|||||||
blocks.header,
|
blocks.header,
|
||||||
blocks.utxoset_change AS utxoSetChange,
|
blocks.utxoset_change AS utxoSetChange,
|
||||||
blocks.utxoset_size AS utxoSetSize,
|
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 {
|
class BlocksRepository {
|
||||||
@@ -498,7 +501,7 @@ class BlocksRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
query += ` ORDER BY height DESC
|
query += ` ORDER BY height DESC
|
||||||
LIMIT 10`;
|
LIMIT 100`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [rows]: any[] = await DB.query(query, params);
|
const [rows]: any[] = await DB.query(query, params);
|
||||||
@@ -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
|
* Convert a mysql row block into a BlockExtended. Note that you
|
||||||
* must provide the correct field into dbBlk object param
|
* must provide the correct field into dbBlk object param
|
||||||
@@ -1054,6 +1075,7 @@ class BlocksRepository {
|
|||||||
id: dbBlk.poolId,
|
id: dbBlk.poolId,
|
||||||
name: dbBlk.poolName,
|
name: dbBlk.poolName,
|
||||||
slug: dbBlk.poolSlug,
|
slug: dbBlk.poolSlug,
|
||||||
|
minerNames: null,
|
||||||
};
|
};
|
||||||
extras.avgFee = dbBlk.avgFee;
|
extras.avgFee = dbBlk.avgFee;
|
||||||
extras.avgFeeRate = dbBlk.avgFeeRate;
|
extras.avgFeeRate = dbBlk.avgFeeRate;
|
||||||
@@ -1076,6 +1098,7 @@ class BlocksRepository {
|
|||||||
extras.utxoSetSize = dbBlk.utxoSetSize;
|
extras.utxoSetSize = dbBlk.utxoSetSize;
|
||||||
extras.totalInputAmt = dbBlk.totalInputAmt;
|
extras.totalInputAmt = dbBlk.totalInputAmt;
|
||||||
extras.virtualSize = dbBlk.weight / 4.0;
|
extras.virtualSize = dbBlk.weight / 4.0;
|
||||||
|
extras.firstSeen = dbBlk.firstSeen;
|
||||||
|
|
||||||
// Re-org can happen after indexing so we need to always get the
|
// Re-org can happen after indexing so we need to always get the
|
||||||
// latest state from core
|
// latest state from core
|
||||||
@@ -1123,6 +1146,10 @@ class BlocksRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (extras.pool.name === 'OCEAN') {
|
||||||
|
extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw);
|
||||||
|
}
|
||||||
|
|
||||||
blk.extras = <BlockExtension>extras;
|
blk.extras = <BlockExtension>extras;
|
||||||
return <BlockExtended>blk;
|
return <BlockExtended>blk;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ module.exports = {
|
|||||||
signRawTransaction: 'signrawtransaction', // bitcoind v0.7.0+
|
signRawTransaction: 'signrawtransaction', // bitcoind v0.7.0+
|
||||||
stop: 'stop',
|
stop: 'stop',
|
||||||
submitBlock: 'submitblock', // bitcoind v0.7.0+
|
submitBlock: 'submitblock', // bitcoind v0.7.0+
|
||||||
|
submitPackage: 'submitpackage',
|
||||||
validateAddress: 'validateaddress',
|
validateAddress: 'validateaddress',
|
||||||
verifyChain: 'verifychain', // bitcoind v0.9.0+
|
verifyChain: 'verifychain', // bitcoind v0.9.0+
|
||||||
verifyMessage: 'verifymessage',
|
verifyMessage: 'verifymessage',
|
||||||
|
|||||||
@@ -6,16 +6,30 @@ import backendInfo from '../api/backend-info';
|
|||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import { SocksProxyAgent } from 'socks-proxy-agent';
|
import { SocksProxyAgent } from 'socks-proxy-agent';
|
||||||
import * as https from 'https';
|
import * as https from 'https';
|
||||||
|
import { Common } from '../api/common';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maintain the most recent version of pools-v2.json
|
* Maintain the most recent version of pools-v2.json
|
||||||
*/
|
*/
|
||||||
class PoolsUpdater {
|
class PoolsUpdater {
|
||||||
|
tag = 'PoolsUpdater';
|
||||||
|
|
||||||
lastRun: number = 0;
|
lastRun: number = 0;
|
||||||
currentSha: string | null = null;
|
currentSha: string | null = null;
|
||||||
poolsUrl: string = config.MEMPOOL.POOLS_JSON_URL;
|
poolsUrl: string = config.MEMPOOL.POOLS_JSON_URL;
|
||||||
treeUrl: string = config.MEMPOOL.POOLS_JSON_TREE_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> {
|
public async updatePoolsJson(): Promise<void> {
|
||||||
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false ||
|
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false ||
|
||||||
config.MEMPOOL.ENABLED === false
|
config.MEMPOOL.ENABLED === false
|
||||||
@@ -23,11 +37,8 @@ class PoolsUpdater {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const oneWeek = 604800;
|
|
||||||
const oneDay = 86400;
|
|
||||||
|
|
||||||
const now = new Date().getTime() / 1000;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +54,7 @@ class PoolsUpdater {
|
|||||||
this.currentSha = await this.getShaFromDb();
|
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) {
|
if (this.currentSha !== null && this.currentSha === githubSha) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -53,16 +64,16 @@ class PoolsUpdater {
|
|||||||
config.MEMPOOL.AUTOMATIC_POOLS_UPDATE !== true && // Automatic pools update is disabled
|
config.MEMPOOL.AUTOMATIC_POOLS_UPDATE !== true && // Automatic pools update is disabled
|
||||||
!process.env.npm_config_update_pools // We're not manually updating mining pool
|
!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.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`);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const network = config.SOCKS5PROXY.ENABLED ? 'tor' : 'clearnet';
|
const network = config.SOCKS5PROXY.ENABLED ? 'tor' : 'clearnet';
|
||||||
if (this.currentSha === null) {
|
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 {
|
} 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);
|
const poolsJson = await this.query(this.poolsUrl);
|
||||||
if (poolsJson === undefined) {
|
if (poolsJson === undefined) {
|
||||||
@@ -71,7 +82,7 @@ class PoolsUpdater {
|
|||||||
poolsParser.setMiningPools(poolsJson);
|
poolsParser.setMiningPools(poolsJson);
|
||||||
|
|
||||||
if (config.DATABASE.ENABLED === false) { // Don't run db operations
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,14 +92,14 @@ class PoolsUpdater {
|
|||||||
await this.updateDBSha(githubSha);
|
await this.updateDBSha(githubSha);
|
||||||
await DB.query('COMMIT;');
|
await DB.query('COMMIT;');
|
||||||
} catch (e) {
|
} 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;');
|
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) {
|
} catch (e) {
|
||||||
this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week
|
this.lastRun = now - 600; // Try again in 10 minutes
|
||||||
logger.err(`PoolsUpdater failed. Will try again in 24h. Exception: ${JSON.stringify(e)}`, logger.tags.mining);
|
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('DELETE FROM state where name="pools_json_sha"');
|
||||||
await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`);
|
await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`);
|
||||||
} catch (e) {
|
} 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"');
|
const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"');
|
||||||
return (rows.length > 0 ? rows[0].string : null);
|
return (rows.length > 0 ? rows[0].string : null);
|
||||||
} catch (e) {
|
} 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;
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +197,7 @@ class PoolsUpdater {
|
|||||||
}
|
}
|
||||||
return data.data;
|
return data.data;
|
||||||
} catch (e) {
|
} 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++;
|
retry++;
|
||||||
}
|
}
|
||||||
await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL);
|
await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL);
|
||||||
|
|||||||
@@ -200,4 +200,28 @@ export function getVarIntLength(n: number): number {
|
|||||||
} else {
|
} else {
|
||||||
return 9;
|
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, ''));
|
||||||
}
|
}
|
||||||
@@ -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,
|
"AUTOMATIC_POOLS_UPDATE": false,
|
||||||
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json",
|
"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_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
|
||||||
|
"POOLS_UPDATE_DELAY": 604800,
|
||||||
"CPFP_INDEXING": false,
|
"CPFP_INDEXING": false,
|
||||||
"MAX_BLOCKS_BULK_QUERY": 0,
|
"MAX_BLOCKS_BULK_QUERY": 0,
|
||||||
"DISK_CACHE_BLOCK_INTERVAL": 6,
|
"DISK_CACHE_BLOCK_INTERVAL": 6,
|
||||||
@@ -140,6 +141,7 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
MEMPOOL_AUTOMATIC_POOLS_UPDATE: ""
|
MEMPOOL_AUTOMATIC_POOLS_UPDATE: ""
|
||||||
MEMPOOL_POOLS_JSON_URL: ""
|
MEMPOOL_POOLS_JSON_URL: ""
|
||||||
MEMPOOL_POOLS_JSON_TREE_URL: ""
|
MEMPOOL_POOLS_JSON_TREE_URL: ""
|
||||||
|
MEMPOOL_POOLS_UPDATE_DELAY: ""
|
||||||
MEMPOOL_CPFP_INDEXING: ""
|
MEMPOOL_CPFP_INDEXING: ""
|
||||||
MEMPOOL_MAX_BLOCKS_BULK_QUERY: ""
|
MEMPOOL_MAX_BLOCKS_BULK_QUERY: ""
|
||||||
MEMPOOL_DISK_CACHE_BLOCK_INTERVAL: ""
|
MEMPOOL_DISK_CACHE_BLOCK_INTERVAL: ""
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"ALLOW_UNREACHABLE": __MEMPOOL_ALLOW_UNREACHABLE__,
|
"ALLOW_UNREACHABLE": __MEMPOOL_ALLOW_UNREACHABLE__,
|
||||||
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
|
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
|
||||||
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
|
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
|
||||||
|
"POOLS_UPDATE_DELAY": __MEMPOOL_POOLS_UPDATE_DELAY__,
|
||||||
"PRICE_UPDATES_PER_HOUR": __MEMPOOL_PRICE_UPDATES_PER_HOUR__,
|
"PRICE_UPDATES_PER_HOUR": __MEMPOOL_PRICE_UPDATES_PER_HOUR__,
|
||||||
"MAX_TRACKED_ADDRESSES": __MEMPOOL_MAX_TRACKED_ADDRESSES__
|
"MAX_TRACKED_ADDRESSES": __MEMPOOL_MAX_TRACKED_ADDRESSES__
|
||||||
},
|
},
|
||||||
@@ -46,7 +47,8 @@
|
|||||||
"PASSWORD": "__CORE_RPC_PASSWORD__",
|
"PASSWORD": "__CORE_RPC_PASSWORD__",
|
||||||
"TIMEOUT": __CORE_RPC_TIMEOUT__,
|
"TIMEOUT": __CORE_RPC_TIMEOUT__,
|
||||||
"COOKIE": __CORE_RPC_COOKIE__,
|
"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": {
|
"ELECTRUM": {
|
||||||
"HOST": "__ELECTRUM_HOST__",
|
"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_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_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_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_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_LIMIT_GBT__=${MEMPOOL_LIMIT_GBT:=false}
|
||||||
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
|
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
|
||||||
__MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0}
|
__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_TIMEOUT__=${CORE_RPC_TIMEOUT:=60000}
|
||||||
__CORE_RPC_COOKIE__=${CORE_RPC_COOKIE:=false}
|
__CORE_RPC_COOKIE__=${CORE_RPC_COOKIE:=false}
|
||||||
__CORE_RPC_COOKIE_PATH__=${CORE_RPC_COOKIE_PATH:=""}
|
__CORE_RPC_COOKIE_PATH__=${CORE_RPC_COOKIE_PATH:=""}
|
||||||
|
__CORE_RPC_DEBUG_LOG_PATH__=${CORE_RPC_DEBUG_LOG_PATH:=""}
|
||||||
|
|
||||||
# ELECTRUM
|
# ELECTRUM
|
||||||
__ELECTRUM_HOST__=${ELECTRUM_HOST:=127.0.0.1}
|
__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_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_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_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_AUDIT__!${__MEMPOOL_AUDIT__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_RUST_GBT__!${__MEMPOOL_RUST_GBT__}!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
|
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_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__!${__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_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_HOST__!${__ELECTRUM_HOST__}!g" mempool-config.json
|
||||||
sed -i "s!__ELECTRUM_PORT__!${__ELECTRUM_PORT__}!g" mempool-config.json
|
sed -i "s!__ELECTRUM_PORT__!${__ELECTRUM_PORT__}!g" mempool-config.json
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"theme": "contrast",
|
||||||
|
"enterprise": "meta",
|
||||||
|
"branding": {
|
||||||
|
"name": "metaplanet",
|
||||||
|
"title": "Metaplanet",
|
||||||
|
"site_id": 21,
|
||||||
|
"header_img": "/resources/metalogo.svg",
|
||||||
|
"footer_img": "/resources/metalogo.svg"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"widgets": [
|
||||||
|
{
|
||||||
|
"component": "fees",
|
||||||
|
"mobileOrder": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "walletBalance",
|
||||||
|
"mobileOrder": 1,
|
||||||
|
"props": {
|
||||||
|
"wallet": "3350"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "twitter",
|
||||||
|
"mobileOrder": 5,
|
||||||
|
"props": {
|
||||||
|
"handle": "Metaplanet_JP"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "wallet",
|
||||||
|
"mobileOrder": 2,
|
||||||
|
"props": {
|
||||||
|
"wallet": "3350",
|
||||||
|
"period": "all"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "blocks"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "walletTransactions",
|
||||||
|
"mobileOrder": 3,
|
||||||
|
"props": {
|
||||||
|
"wallet": "3350"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+345
-566
File diff suppressed because it is too large.
Load diff
@@ -95,7 +95,7 @@
|
|||||||
"esbuild": "^0.24.0",
|
"esbuild": "^0.24.0",
|
||||||
"tinyify": "^4.0.0",
|
"tinyify": "^4.0.0",
|
||||||
"tlite": "^0.1.9",
|
"tlite": "^0.1.9",
|
||||||
"tslib": "~2.7.0",
|
"tslib": "~2.8.0",
|
||||||
"zone.js": "~0.14.4"
|
"zone.js": "~0.14.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^7.4.0",
|
"@typescript-eslint/eslint-plugin": "^7.4.0",
|
||||||
"@typescript-eslint/parser": "^7.4.0",
|
"@typescript-eslint/parser": "^7.4.0",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"browser-sync": "^3.0.0",
|
"browser-sync": "^3.0.3",
|
||||||
"http-proxy-middleware": "~2.0.6",
|
"http-proxy-middleware": "~2.0.6",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@cypress/schematic": "^2.5.0",
|
"@cypress/schematic": "^2.5.0",
|
||||||
"@types/cypress": "^1.1.3",
|
"@types/cypress": "^1.1.3",
|
||||||
"cypress": "^13.14.0",
|
"cypress": "^13.15.0",
|
||||||
"cypress-fail-on-console-error": "~5.1.0",
|
"cypress-fail-on-console-error": "~5.1.0",
|
||||||
"cypress-wait-until": "^2.0.1",
|
"cypress-wait-until": "^2.0.1",
|
||||||
"mock-socket": "~9.3.1",
|
"mock-socket": "~9.3.1",
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { Routes, RouterModule } from '@angular/router';
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
import { AppPreloadingStrategy } from './app.preloading-strategy'
|
import { AppPreloadingStrategy } from '@app/app.preloading-strategy'
|
||||||
import { BlockViewComponent } from './components/block-view/block-view.component';
|
import { BlockViewComponent } from '@components/block-view/block-view.component';
|
||||||
import { EightBlocksComponent } from './components/eight-blocks/eight-blocks.component';
|
import { EightBlocksComponent } from '@components/eight-blocks/eight-blocks.component';
|
||||||
import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component';
|
import { MempoolBlockViewComponent } from '@components/mempool-block-view/mempool-block-view.component';
|
||||||
import { ClockComponent } from './components/clock/clock.component';
|
import { ClockComponent } from '@components/clock/clock.component';
|
||||||
import { StatusViewComponent } from './components/status-view/status-view.component';
|
import { StatusViewComponent } from '@components/status-view/status-view.component';
|
||||||
import { AddressGroupComponent } from './components/address-group/address-group.component';
|
import { AddressGroupComponent } from '@components/address-group/address-group.component';
|
||||||
import { TrackerComponent } from './components/tracker/tracker.component';
|
import { TrackerComponent } from '@components/tracker/tracker.component';
|
||||||
import { AccelerateCheckout } from './components/accelerate-checkout/accelerate-checkout.component';
|
import { AccelerateCheckout } from '@components/accelerate-checkout/accelerate-checkout.component';
|
||||||
import { TrackerGuard } from './route-guards';
|
import { TrackerGuard } from '@app/route-guards';
|
||||||
|
|
||||||
const browserWindow = window || {};
|
const browserWindow = window || {};
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -22,16 +22,16 @@ let routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
pathMatch: 'full',
|
pathMatch: 'full',
|
||||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||||
data: { preload: true },
|
data: { preload: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
|
||||||
data: { preload: true },
|
data: { preload: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'wallet',
|
path: 'widget/wallet',
|
||||||
children: [],
|
children: [],
|
||||||
component: AddressGroupComponent,
|
component: AddressGroupComponent,
|
||||||
data: {
|
data: {
|
||||||
@@ -45,7 +45,7 @@ let routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||||
data: { preload: true },
|
data: { preload: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -60,12 +60,12 @@ let routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
pathMatch: 'full',
|
pathMatch: 'full',
|
||||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||||
data: { preload: true },
|
data: { preload: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
|
||||||
data: { preload: true },
|
data: { preload: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -83,7 +83,7 @@ let routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||||
data: { preload: true },
|
data: { preload: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -103,16 +103,16 @@ let routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
pathMatch: 'full',
|
pathMatch: 'full',
|
||||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||||
data: { preload: true },
|
data: { preload: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
|
||||||
data: { preload: true },
|
data: { preload: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'wallet',
|
path: 'widget/wallet',
|
||||||
children: [],
|
children: [],
|
||||||
component: AddressGroupComponent,
|
component: AddressGroupComponent,
|
||||||
data: {
|
data: {
|
||||||
@@ -126,7 +126,7 @@ let routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||||
data: { preload: true },
|
data: { preload: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -138,22 +138,22 @@ let routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
pathMatch: 'full',
|
pathMatch: 'full',
|
||||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||||
data: { preload: true },
|
data: { preload: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'tx',
|
path: 'tx',
|
||||||
canMatch: [TrackerGuard],
|
canMatch: [TrackerGuard],
|
||||||
runGuardsAndResolvers: 'always',
|
runGuardsAndResolvers: 'always',
|
||||||
loadChildren: () => import('./components/tracker/tracker.module').then(m => m.TrackerModule),
|
loadChildren: () => import('@components/tracker/tracker.module').then(m => m.TrackerModule),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
|
||||||
data: { preload: true },
|
data: { preload: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'wallet',
|
path: 'widget/wallet',
|
||||||
children: [],
|
children: [],
|
||||||
component: AddressGroupComponent,
|
component: AddressGroupComponent,
|
||||||
data: {
|
data: {
|
||||||
@@ -165,19 +165,19 @@ let routes: Routes = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
|
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'testnet',
|
path: 'testnet',
|
||||||
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
|
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'testnet4',
|
path: 'testnet4',
|
||||||
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
|
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'signet',
|
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: '',
|
path: '',
|
||||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||||
data: { preload: true },
|
data: { preload: true },
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -225,16 +225,16 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
|||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
pathMatch: 'full',
|
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 },
|
data: { preload: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '',
|
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 },
|
data: { preload: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'wallet',
|
path: 'widget/wallet',
|
||||||
children: [],
|
children: [],
|
||||||
component: AddressGroupComponent,
|
component: AddressGroupComponent,
|
||||||
data: {
|
data: {
|
||||||
@@ -248,7 +248,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '',
|
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 },
|
data: { preload: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -260,16 +260,16 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
|||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
pathMatch: 'full',
|
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 },
|
data: { preload: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '',
|
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 },
|
data: { preload: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'wallet',
|
path: 'widget/wallet',
|
||||||
children: [],
|
children: [],
|
||||||
component: AddressGroupComponent,
|
component: AddressGroupComponent,
|
||||||
data: {
|
data: {
|
||||||
@@ -281,11 +281,11 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
|
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'testnet',
|
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: '',
|
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 },
|
data: { preload: true },
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import { HTTP_INTERCEPTORS } from '@angular/common/http';
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { ServerModule } from '@angular/platform-server';
|
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 { AppModule } from './app.module';
|
||||||
import { AppComponent } from './components/app/app.component';
|
import { AppComponent } from '@components/app/app.component';
|
||||||
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
|
import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor';
|
||||||
import { ZoneService } from './services/zone.service';
|
import { ZoneService } from '@app/services/zone.service';
|
||||||
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@@ -20,4 +20,4 @@ import { ZoneService } from './services/zone.service';
|
|||||||
],
|
],
|
||||||
bootstrap: [AppComponent],
|
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 { ModuleWithProviders, NgModule } from '@angular/core';
|
||||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
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 { AppRoutingModule } from './app-routing.module';
|
||||||
import { AppComponent } from './components/app/app.component';
|
import { AppComponent } from '@components/app/app.component';
|
||||||
import { ElectrsApiService } from './services/electrs-api.service';
|
import { ElectrsApiService } from '@app/services/electrs-api.service';
|
||||||
import { StateService } from './services/state.service';
|
import { OrdApiService } from '@app/services/ord-api.service';
|
||||||
import { CacheService } from './services/cache.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
import { PriceService } from './services/price.service';
|
import { CacheService } from '@app/services/cache.service';
|
||||||
import { EnterpriseService } from './services/enterprise.service';
|
import { PriceService } from '@app/services/price.service';
|
||||||
import { WebsocketService } from './services/websocket.service';
|
import { EnterpriseService } from '@app/services/enterprise.service';
|
||||||
import { AudioService } from './services/audio.service';
|
import { WebsocketService } from '@app/services/websocket.service';
|
||||||
import { PreloadService } from './services/preload.service';
|
import { AudioService } from '@app/services/audio.service';
|
||||||
import { SeoService } from './services/seo.service';
|
import { PreloadService } from '@app/services/preload.service';
|
||||||
import { OpenGraphService } from './services/opengraph.service';
|
import { SeoService } from '@app/services/seo.service';
|
||||||
import { ZoneService } from './services/zone-shim.service';
|
import { OpenGraphService } from '@app/services/opengraph.service';
|
||||||
import { SharedModule } from './shared/shared.module';
|
import { ZoneService } from '@app/services/zone-shim.service';
|
||||||
import { StorageService } from './services/storage.service';
|
import { SharedModule } from '@app/shared/shared.module';
|
||||||
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
|
import { StorageService } from '@app/services/storage.service';
|
||||||
import { LanguageService } from './services/language.service';
|
import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor';
|
||||||
import { ThemeService } from './services/theme.service';
|
import { LanguageService } from '@app/services/language.service';
|
||||||
import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe';
|
import { ThemeService } from '@app/services/theme.service';
|
||||||
import { FiatCurrencyPipe } from './shared/pipes/fiat-currency.pipe';
|
import { TimeService } from '@app/services/time.service';
|
||||||
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
|
import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe';
|
||||||
import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe';
|
import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe';
|
||||||
import { AppPreloadingStrategy } from './app.preloading-strategy';
|
import { ShortenStringPipe } from '@app/shared/pipes/shorten-string-pipe/shorten-string.pipe';
|
||||||
import { ServicesApiServices } from './services/services-api.service';
|
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';
|
import { DatePipe } from '@angular/common';
|
||||||
|
|
||||||
const providers = [
|
const providers = [
|
||||||
ElectrsApiService,
|
ElectrsApiService,
|
||||||
|
OrdApiService,
|
||||||
StateService,
|
StateService,
|
||||||
CacheService,
|
CacheService,
|
||||||
PriceService,
|
PriceService,
|
||||||
@@ -42,6 +45,7 @@ const providers = [
|
|||||||
EnterpriseService,
|
EnterpriseService,
|
||||||
LanguageService,
|
LanguageService,
|
||||||
ThemeService,
|
ThemeService,
|
||||||
|
TimeService,
|
||||||
ShortenStringPipe,
|
ShortenStringPipe,
|
||||||
FiatShortenerPipe,
|
FiatShortenerPipe,
|
||||||
FiatCurrencyPipe,
|
FiatCurrencyPipe,
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Routes, RouterModule } from '@angular/router';
|
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 = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: MasterPageComponent,
|
component: MasterPageComponent,
|
||||||
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule),
|
loadChildren: () => import('@app/graphs/graphs.module').then(m => m.GraphsModule),
|
||||||
data: { preload: true },
|
data: { preload: true },
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Transaction, Vin } from './interfaces/electrs.interface';
|
import { Transaction, Vin } from '@interfaces/electrs.interface';
|
||||||
import { Hash } from './shared/sha256';
|
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_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
|
const P2SH_P2WSH_COST = 35 * 4; // the WU cost for the non-witness part of P2SH-P2WSH
|
||||||
@@ -303,4 +303,4 @@ export async function calcScriptHash$(script: string): Promise<string> {
|
|||||||
return hashArray
|
return hashArray
|
||||||
.map((bytes) => bytes.toString(16).padStart(2, '0'))
|
.map((bytes) => bytes.toString(16).padStart(2, '0'))
|
||||||
.join('');
|
.join('');
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
import { EnterpriseService } from '../../services/enterprise.service';
|
import { EnterpriseService } from '@app/services/enterprise.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-about-sponsors',
|
selector: 'app-about-sponsors',
|
||||||
|
|||||||
@@ -201,12 +201,17 @@
|
|||||||
<img class="image" src="/resources/profile/leather.svg" />
|
<img class="image" src="/resources/profile/leather.svg" />
|
||||||
<span>Leather</span>
|
<span>Leather</span>
|
||||||
</a>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-container>
|
<ng-container>
|
||||||
<div *ngIf="profiles$ | async as profiles" id="community-sponsors-anchor">
|
<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>
|
<h3 i18n="about.sponsors.withHeart">Whale Sponsors</h3>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<ng-container>
|
<ng-container>
|
||||||
|
|||||||
@@ -92,6 +92,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.whale-sponsor {
|
||||||
|
img {
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.alliances {
|
.alliances {
|
||||||
margin-bottom: 100px;
|
margin-bottom: 100px;
|
||||||
a {
|
a {
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { ChangeDetectionStrategy, Component, ElementRef, Inject, LOCALE_ID, OnInit, ViewChild } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, ElementRef, Inject, LOCALE_ID, OnInit, ViewChild } from '@angular/core';
|
||||||
import { WebsocketService } from '../../services/websocket.service';
|
import { WebsocketService } from '@app/services/websocket.service';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '@app/services/seo.service';
|
||||||
import { OpenGraphService } from '../../services/opengraph.service';
|
import { OpenGraphService } from '@app/services/opengraph.service';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '@app/services/api.service';
|
||||||
import { IBackendInfo } from '../../interfaces/websocket.interface';
|
import { IBackendInfo } from '@interfaces/websocket.interface';
|
||||||
import { Router, ActivatedRoute } from '@angular/router';
|
import { Router, ActivatedRoute } from '@angular/router';
|
||||||
import { map, share, tap } from 'rxjs/operators';
|
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 { DOCUMENT } from '@angular/common';
|
||||||
import { EnterpriseService } from '../../services/enterprise.service';
|
import { EnterpriseService } from '@app/services/enterprise.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-about',
|
selector: 'app-about',
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Routes, RouterModule } from '@angular/router';
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
import { AboutComponent } from './about.component';
|
import { AboutComponent } from '@components/about/about.component';
|
||||||
import { AboutSponsorsComponent } from './about-sponsors.component';
|
import { AboutSponsorsComponent } from '@components/about/about-sponsors.component';
|
||||||
import { SharedModule } from '../../shared/shared.module';
|
import { SharedModule } from '@app/shared/shared.module';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -172,10 +172,6 @@
|
|||||||
background-color: var(--tertiary);
|
background-color: var(--tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-small-height {
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-row {
|
.summary-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core';
|
import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core';
|
||||||
import { Subscription, tap, of, catchError, Observable, switchMap } from 'rxjs';
|
import { Subscription, tap, of, catchError, Observable, switchMap } from 'rxjs';
|
||||||
import { ServicesApiServices } from '../../services/services-api.service';
|
import { ServicesApiServices } from '@app/services/services-api.service';
|
||||||
import { md5, insecureRandomUUID } from '../../shared/common.utils';
|
import { md5 } from '@app/shared/common.utils';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
import { AudioService } from '../../services/audio.service';
|
import { AudioService } from '@app/services/audio.service';
|
||||||
import { ETA, EtaService } from '../../services/eta.service';
|
import { ETA, EtaService } from '@app/services/eta.service';
|
||||||
import { Transaction } from '../../interfaces/electrs.interface';
|
import { Transaction } from '@interfaces/electrs.interface';
|
||||||
import { MiningStats } from '../../services/mining.service';
|
import { MiningStats } from '@app/services/mining.service';
|
||||||
import { IAuth, AuthServiceMempool } from '../../services/auth.service';
|
import { IAuth, AuthServiceMempool } from '@app/services/auth.service';
|
||||||
import { EnterpriseService } from '../../services/enterprise.service';
|
import { EnterpriseService } from '@app/services/enterprise.service';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '@app/services/api.service';
|
||||||
import { isDevMode } from '@angular/core';
|
import { isDevMode } from '@angular/core';
|
||||||
|
|
||||||
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay';
|
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay';
|
||||||
@@ -84,13 +84,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
timePaid: number = 0; // time acceleration requested
|
timePaid: number = 0; // time acceleration requested
|
||||||
math = Math;
|
math = Math;
|
||||||
isMobile: boolean = window.innerWidth <= 767.98;
|
isMobile: boolean = window.innerWidth <= 767.98;
|
||||||
isProdDomain = ['mempool.space',
|
isProdDomain = false;
|
||||||
'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;
|
|
||||||
|
|
||||||
private _step: CheckoutStep = 'summary';
|
private _step: CheckoutStep = 'summary';
|
||||||
simpleMode: boolean = true;
|
simpleMode: boolean = true;
|
||||||
@@ -100,7 +94,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
auth: IAuth | null = null;
|
auth: IAuth | null = null;
|
||||||
|
|
||||||
// accelerator stuff
|
// accelerator stuff
|
||||||
accelerationUUID: string;
|
|
||||||
accelerationSubscription: Subscription;
|
accelerationSubscription: Subscription;
|
||||||
difficultySubscription: Subscription;
|
difficultySubscription: Subscription;
|
||||||
estimateSubscription: Subscription;
|
estimateSubscription: Subscription;
|
||||||
@@ -143,7 +136,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
private authService: AuthServiceMempool,
|
private authService: AuthServiceMempool,
|
||||||
private enterpriseService: EnterpriseService,
|
private enterpriseService: EnterpriseService,
|
||||||
) {
|
) {
|
||||||
this.accelerationUUID = insecureRandomUUID();
|
this.isProdDomain = this.stateService.env.PROD_DOMAINS.indexOf(document.location.hostname) > -1;
|
||||||
|
|
||||||
// Check if Apple Pay available
|
// Check if Apple Pay available
|
||||||
// https://developer.apple.com/documentation/apple_pay_on_the_web/apple_pay_js_api/checking_for_apple_pay_availability#overview
|
// https://developer.apple.com/documentation/apple_pay_on_the_web/apple_pay_js_api/checking_for_apple_pay_availability#overview
|
||||||
@@ -207,6 +200,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
moveToStep(step: CheckoutStep): void {
|
moveToStep(step: CheckoutStep): void {
|
||||||
|
this.processing = false;
|
||||||
this._step = step;
|
this._step = step;
|
||||||
if (this.timeoutTimer) {
|
if (this.timeoutTimer) {
|
||||||
clearTimeout(this.timeoutTimer);
|
clearTimeout(this.timeoutTimer);
|
||||||
@@ -374,6 +368,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.selectFeeRateIndex = index;
|
this.selectFeeRateIndex = index;
|
||||||
this.userBid = Math.max(0, fee);
|
this.userBid = Math.max(0, fee);
|
||||||
this.cost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
|
this.cost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
|
||||||
|
this.validateChoice();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,7 +386,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.accelerationSubscription = this.servicesApiService.accelerate$(
|
this.accelerationSubscription = this.servicesApiService.accelerate$(
|
||||||
this.tx.txid,
|
this.tx.txid,
|
||||||
this.userBid,
|
this.userBid,
|
||||||
this.accelerationUUID
|
|
||||||
).subscribe({
|
).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
@@ -525,7 +519,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
tokenResult.token,
|
tokenResult.token,
|
||||||
cardTag,
|
cardTag,
|
||||||
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
||||||
this.accelerationUUID
|
costUSD
|
||||||
).subscribe({
|
).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
@@ -624,7 +618,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
tokenResult.token,
|
tokenResult.token,
|
||||||
cardTag,
|
cardTag,
|
||||||
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
||||||
this.accelerationUUID
|
costUSD
|
||||||
).subscribe({
|
).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
@@ -714,7 +708,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
tokenResult.token,
|
tokenResult.token,
|
||||||
tokenResult.details.cashAppPay.cashtag,
|
tokenResult.details.cashAppPay.cashtag,
|
||||||
tokenResult.details.cashAppPay.referenceId,
|
tokenResult.details.cashAppPay.referenceId,
|
||||||
this.accelerationUUID
|
costUSD
|
||||||
).subscribe({
|
).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Component, Input, Output, OnChanges, EventEmitter, HostListener, OnInit, ViewChild, ElementRef, AfterViewInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
|
import { Component, Input, Output, OnChanges, EventEmitter, HostListener, OnInit, ViewChild, ElementRef, AfterViewInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
|
||||||
import { Transaction } from '../../interfaces/electrs.interface';
|
import { Transaction } from '@interfaces/electrs.interface';
|
||||||
import { AccelerationEstimate, RateOption } from './accelerate-checkout.component';
|
import { AccelerationEstimate, RateOption } from '@components/accelerate-checkout/accelerate-checkout.component';
|
||||||
|
|
||||||
interface GraphBar {
|
interface GraphBar {
|
||||||
rate: number;
|
rate: number;
|
||||||
|
|||||||
+23
-21
@@ -1,6 +1,6 @@
|
|||||||
<div class="acceleration-timeline box" [class.lower-padding]="!tx.status.confirmed">
|
<div class="acceleration-timeline box" [class.lower-padding]="!tx.status.confirmed">
|
||||||
<div class="timeline-wrapper">
|
<div class="timeline-wrapper">
|
||||||
@if (!tx.status.confirmed) {
|
@if (!tx.status.confirmed || canceled) {
|
||||||
<div class="timeline">
|
<div class="timeline">
|
||||||
<div class="intervals">
|
<div class="intervals">
|
||||||
<div class="node-spacer"></div>
|
<div class="node-spacer"></div>
|
||||||
@@ -8,8 +8,8 @@
|
|||||||
<div class="node-spacer"></div>
|
<div class="node-spacer"></div>
|
||||||
<div class="interval">
|
<div class="interval">
|
||||||
<div class="interval-time">
|
<div class="interval-time">
|
||||||
@if (eta) {
|
@if (eta && !canceled) {
|
||||||
~<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>
|
||||||
</div>
|
</div>
|
||||||
@@ -19,16 +19,20 @@
|
|||||||
<div class="node-spacer"></div>
|
<div class="node-spacer"></div>
|
||||||
<div class="interval-spacer"></div>
|
<div class="interval-spacer"></div>
|
||||||
<div class="node">
|
<div class="node">
|
||||||
<div class="acc-to-confirmed right go-faster"></div>
|
<div class="acc-to-confirmed right go-faster" [class.no-animation]="canceled"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="interval-spacer">
|
<div class="interval-spacer">
|
||||||
</div>
|
</div>
|
||||||
<div class="node" [id]="'confirmed'">
|
<div class="node" [id]="'confirmed'">
|
||||||
<div class="acc-to-confirmed left go-faster"></div>
|
<div class="acc-to-confirmed left go-faster" [class.no-animation]="canceled"></div>
|
||||||
<div class="shape-border waiting">
|
<div class="shape-border waiting">
|
||||||
<div class="shape"></div>
|
<div class="shape"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="status"><span class="badge badge-waiting" i18n="transaction.rbf.mined">Mined</span></div>
|
@if (canceled) {
|
||||||
|
<div class="status"><span class="badge badge-danger" i18n="accelerator.canceled">Canceled</span></div>
|
||||||
|
} @else {
|
||||||
|
<div class="status"><span class="badge badge-waiting" i18n="transaction.rbf.mined">Mined</span></div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -38,18 +42,16 @@
|
|||||||
<div class="node-spacer"></div>
|
<div class="node-spacer"></div>
|
||||||
<div class="interval">
|
<div class="interval">
|
||||||
<div class="interval-time">
|
<div class="interval-time">
|
||||||
<app-time [time]="acceleratedAt - transactionTime"></app-time>
|
<app-time [time]="firstSeenToAccelerated"></app-time>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="node-spacer"></div>
|
<div class="node-spacer"></div>
|
||||||
<div class="interval">
|
<div class="interval">
|
||||||
<div class="interval-time">
|
<div class="interval-time">
|
||||||
@if (tx.status.confirmed) {
|
@if (tx.status.confirmed) {
|
||||||
<div class="interval-time">
|
<app-time [time]="acceleratedToMined"></app-time>
|
||||||
<app-time [time]="tx.status.block_time - acceleratedAt"></app-time>
|
} @else if (eta && canceled) {
|
||||||
</div>
|
~<app-time [time]="eta?.wait / 1000"></app-time>
|
||||||
} @else if (standardETA && !tx.status.confirmed) {
|
|
||||||
<!-- ~<app-time [time]="standardETA / 1000 - now"></app-time> -->
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,42 +75,42 @@
|
|||||||
<div class="interval-spacer">
|
<div class="interval-spacer">
|
||||||
<div class="seen-to-acc"></div>
|
<div class="seen-to-acc"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="node" [class.accelerated]="!tx.status.confirmed" [id]="'accelerated'">
|
<div class="node" [class.accelerated]="!tx.status.confirmed && !canceled" [id]="'accelerated'">
|
||||||
<div class="seen-to-acc left"></div>
|
<div class="seen-to-acc left"></div>
|
||||||
@if (tx.status.confirmed) {
|
@if (tx.status.confirmed && !canceled) {
|
||||||
<div class="acc-to-confirmed right"></div>
|
<div class="acc-to-confirmed right"></div>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="seen-to-acc right"></div>
|
<div class="seen-to-acc right"></div>
|
||||||
}
|
}
|
||||||
<div class="shape-border hovering" (pointerover)="onHover($event, 'accelerated');" (pointerout)="onBlur($event);">
|
<div class="shape-border hovering" (pointerover)="onHover($event, 'accelerated');" (pointerout)="onBlur($event);">
|
||||||
<div class="shape"></div>
|
<div class="shape"></div>
|
||||||
@if (!tx.status.confirmed) {
|
@if (!tx.status.confirmed || canceled) {
|
||||||
<div class="connector down loading"></div>
|
<div class="connector down" [class.loading]="!canceled"></div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@if (tx.status.confirmed) {
|
@if (tx.status.confirmed && !canceled) {
|
||||||
<div class="status"><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></div>
|
<div class="status"><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></div>
|
||||||
}
|
}
|
||||||
<div class="time" [class.no-margin]="!tx.status.confirmed" [class.offset-left]="!tx.status.confirmed">
|
<div class="time" [class.no-margin]="!tx.status.confirmed || canceled" [class.offset-left]="!tx.status.confirmed || canceled">
|
||||||
@if (!tx.status.confirmed) {
|
@if (!tx.status.confirmed) {
|
||||||
<span i18n="transaction.audit.accelerated">Accelerated</span>{{ "" }}
|
<span i18n="transaction.audit.accelerated">Accelerated</span>{{ "" }}
|
||||||
}
|
}
|
||||||
@if (useAbsoluteTime) {
|
@if (useAbsoluteTime) {
|
||||||
<span>{{ acceleratedAt * 1000 | date }}</span>
|
<span>{{ acceleratedAt * 1000 | date }}</span>
|
||||||
} @else {
|
} @else {
|
||||||
<app-time kind="since" [time]="acceleratedAt" [lowercaseStart]="!tx.status.confirmed"></app-time>
|
<app-time kind="since" [time]="acceleratedAt" [lowercaseStart]="!tx.status.confirmed || canceled"></app-time>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="interval-spacer">
|
<div class="interval-spacer">
|
||||||
@if (tx.status.confirmed) {
|
@if (tx.status.confirmed && !canceled) {
|
||||||
<div class="acc-to-confirmed"></div>
|
<div class="acc-to-confirmed"></div>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="seen-to-acc"></div>
|
<div class="seen-to-acc"></div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="node" [class.selected]="tx.status.confirmed" [id]="'confirmed'">
|
<div class="node" [class.selected]="tx.status.confirmed" [id]="'confirmed'">
|
||||||
@if (tx.status.confirmed) {
|
@if (tx.status.confirmed && !canceled) {
|
||||||
<div class="acc-to-confirmed left"></div>
|
<div class="acc-to-confirmed left"></div>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="seen-to-acc left"></div>
|
<div class="seen-to-acc left"></div>
|
||||||
|
|||||||
@@ -129,6 +129,9 @@
|
|||||||
margin-left: calc(-4em + 5px);
|
margin-left: calc(-4em + 5px);
|
||||||
animation: goFasterLeft 0.8s infinite linear;
|
animation: goFasterLeft 0.8s infinite linear;
|
||||||
}
|
}
|
||||||
|
&.no-animation {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.left {
|
&.left {
|
||||||
|
|||||||
+15
-31
@@ -1,8 +1,8 @@
|
|||||||
import { Component, Input, OnInit, OnChanges, HostListener } from '@angular/core';
|
import { Component, Input, OnInit, OnChanges, HostListener } from '@angular/core';
|
||||||
import { ETA } from '../../services/eta.service';
|
import { ETA } from '@app/services/eta.service';
|
||||||
import { Transaction } from '../../interfaces/electrs.interface';
|
import { Transaction } from '@interfaces/electrs.interface';
|
||||||
import { Acceleration, SinglePoolStats } from '../../interfaces/node-api.interface';
|
import { Acceleration, SinglePoolStats } from '@interfaces/node-api.interface';
|
||||||
import { MiningService } from '../../services/mining.service';
|
import { MiningService } from '@app/services/mining.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-acceleration-timeline',
|
selector: 'app-acceleration-timeline',
|
||||||
@@ -11,19 +11,17 @@ import { MiningService } from '../../services/mining.service';
|
|||||||
})
|
})
|
||||||
export class AccelerationTimelineComponent implements OnInit, OnChanges {
|
export class AccelerationTimelineComponent implements OnInit, OnChanges {
|
||||||
@Input() transactionTime: number;
|
@Input() transactionTime: number;
|
||||||
|
@Input() acceleratedAt: number;
|
||||||
@Input() tx: Transaction;
|
@Input() tx: Transaction;
|
||||||
@Input() accelerationInfo: Acceleration;
|
@Input() accelerationInfo: Acceleration;
|
||||||
@Input() eta: ETA;
|
@Input() eta: ETA;
|
||||||
// A mined transaction has standard ETA and accelerated ETA undefined
|
@Input() canceled: boolean;
|
||||||
// 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;
|
now: number;
|
||||||
accelerateRatio: number;
|
accelerateRatio: number;
|
||||||
useAbsoluteTime: boolean = false;
|
useAbsoluteTime: boolean = false;
|
||||||
interval: number;
|
firstSeenToAccelerated: number;
|
||||||
|
acceleratedToMined: number;
|
||||||
|
|
||||||
tooltipPosition = null;
|
tooltipPosition = null;
|
||||||
hoverInfo: any = null;
|
hoverInfo: any = null;
|
||||||
@@ -34,38 +32,24 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.acceleratedAt = this.tx.acceleratedAt ?? new Date().getTime() / 1000;
|
this.updateTimes();
|
||||||
this.now = Math.floor(new Date().getTime() / 1000);
|
|
||||||
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
|
|
||||||
|
|
||||||
this.miningService.getPools().subscribe(pools => {
|
this.miningService.getPools().subscribe(pools => {
|
||||||
for (const pool of pools) {
|
for (const pool of pools) {
|
||||||
this.poolsData[pool.unique_id] = pool;
|
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 {
|
ngOnChanges(changes): void {
|
||||||
// Hide standard ETA while we don't have a proper standard ETA calculation, see https://github.com/mempool/mempool/issues/65
|
this.updateTimes();
|
||||||
|
|
||||||
// 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));
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
updateTimes(): void {
|
||||||
clearInterval(this.interval);
|
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 {
|
onHover(event, status: string): void {
|
||||||
|
|||||||
+9
-9
@@ -1,18 +1,18 @@
|
|||||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
|
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 { Observable, Subject, Subscription, combineLatest, fromEvent, merge, share } from 'rxjs';
|
||||||
import { startWith, switchMap, tap } from 'rxjs/operators';
|
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 { formatNumber } from '@angular/common';
|
||||||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../../shared/graphs.utils';
|
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '@app/shared/graphs.utils';
|
||||||
import { StorageService } from '../../../services/storage.service';
|
import { StorageService } from '@app/services/storage.service';
|
||||||
import { MiningService } from '../../../services/mining.service';
|
import { MiningService } from '@app/services/mining.service';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { Acceleration } from '../../../interfaces/node-api.interface';
|
import { Acceleration } from '@interfaces/node-api.interface';
|
||||||
import { ServicesApiServices } from '../../../services/services-api.service';
|
import { ServicesApiServices } from '@app/services/services-api.service';
|
||||||
import { StateService } from '../../../services/state.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
import { RelativeUrlPipe } from '../../../shared/pipes/relative-url/relative-url.pipe';
|
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-acceleration-fees-graph',
|
selector: 'app-acceleration-fees-graph',
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { ServicesApiServices } from '../../../services/services-api.service';
|
import { ServicesApiServices } from '@app/services/services-api.service';
|
||||||
|
|
||||||
export type AccelerationStats = {
|
export type AccelerationStats = {
|
||||||
totalRequested: number;
|
totalRequested: number;
|
||||||
|
|||||||
+4
-4
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
|
|
||||||
<div class="acceleration-list">
|
<div class="acceleration-list" *ngIf="{ accelerations: accelerationList$ | async } as state">
|
||||||
<table *ngIf="nonEmptyAccelerations; else noData" class="table table-borderless table-fixed">
|
<table *ngIf="nonEmptyAccelerations; else noData" class="table table-borderless table-fixed">
|
||||||
<thead>
|
<thead>
|
||||||
<th class="txid text-left" i18n="dashboard.latest-transactions.txid">TXID</th>
|
<th class="txid text-left" i18n="dashboard.latest-transactions.txid">TXID</th>
|
||||||
@@ -21,8 +21,8 @@
|
|||||||
<th class="date text-right" i18n="accelerator.requested" *ngIf="!this.widget">Requested</th>
|
<th class="date text-right" i18n="accelerator.requested" *ngIf="!this.widget">Requested</th>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody *ngIf="accelerationList$ | async as accelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
|
<tbody *ngIf="state.accelerations && nonEmptyAccelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
|
||||||
<tr *ngFor="let acceleration of accelerations; let i= index;">
|
<tr *ngFor="let acceleration of state.accelerations; let i= index;">
|
||||||
<td class="txid text-left">
|
<td class="txid text-left">
|
||||||
<a [routerLink]="['/tx' | relativeUrl, acceleration.txid]">
|
<a [routerLink]="['/tx' | relativeUrl, acceleration.txid]">
|
||||||
<app-truncate [text]="acceleration.txid" [lastChars]="5"></app-truncate>
|
<app-truncate [text]="acceleration.txid" [lastChars]="5"></app-truncate>
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
<span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="accelerator.pending">Pending</span>
|
<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="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('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>
|
||||||
<td class="date text-right" *ngIf="!this.widget">
|
<td class="date text-right" *ngIf="!this.widget">
|
||||||
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
|
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
|
||||||
|
|||||||
+7
-7
@@ -1,12 +1,12 @@
|
|||||||
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy, Inject, LOCALE_ID } from '@angular/core';
|
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy, Inject, LOCALE_ID } from '@angular/core';
|
||||||
import { BehaviorSubject, Observable, Subscription, catchError, combineLatest, filter, of, switchMap, tap, throttleTime, timer } from 'rxjs';
|
import { BehaviorSubject, Observable, Subscription, catchError, combineLatest, filter, of, switchMap, tap, throttleTime, timer } from 'rxjs';
|
||||||
import { Acceleration, BlockExtended, SinglePoolStats } from '../../../interfaces/node-api.interface';
|
import { Acceleration, BlockExtended, SinglePoolStats } from '@interfaces/node-api.interface';
|
||||||
import { StateService } from '../../../services/state.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
import { WebsocketService } from '../../../services/websocket.service';
|
import { WebsocketService } from '@app/services/websocket.service';
|
||||||
import { ServicesApiServices } from '../../../services/services-api.service';
|
import { ServicesApiServices } from '@app/services/services-api.service';
|
||||||
import { SeoService } from '../../../services/seo.service';
|
import { SeoService } from '@app/services/seo.service';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { MiningService } from '../../../services/mining.service';
|
import { MiningService } from '@app/services/mining.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-accelerations-list',
|
selector: 'app-accelerations-list',
|
||||||
@@ -151,4 +151,4 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
|
|||||||
this.paramSubscription?.unsubscribe();
|
this.paramSubscription?.unsubscribe();
|
||||||
this.keyNavigationSubscription?.unsubscribe();
|
this.keyNavigationSubscription?.unsubscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+13
-13
@@ -1,18 +1,18 @@
|
|||||||
import { ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
|
||||||
import { SeoService } from '../../../services/seo.service';
|
import { SeoService } from '@app/services/seo.service';
|
||||||
import { OpenGraphService } from '../../../services/opengraph.service';
|
import { OpenGraphService } from '@app/services/opengraph.service';
|
||||||
import { WebsocketService } from '../../../services/websocket.service';
|
import { WebsocketService } from '@app/services/websocket.service';
|
||||||
import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface';
|
import { Acceleration, BlockExtended } from '@interfaces/node-api.interface';
|
||||||
import { StateService } from '../../../services/state.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
import { Observable, Subscription, catchError, combineLatest, distinctUntilChanged, map, of, share, switchMap, tap } from 'rxjs';
|
import { Observable, Subscription, catchError, combineLatest, distinctUntilChanged, map, of, share, switchMap, tap } from 'rxjs';
|
||||||
import { Color } from '../../block-overview-graph/sprite-types';
|
import { Color } from '@components/block-overview-graph/sprite-types';
|
||||||
import { hexToColor } from '../../block-overview-graph/utils';
|
import { hexToColor } from '@components/block-overview-graph/utils';
|
||||||
import TxView from '../../block-overview-graph/tx-view';
|
import TxView from '@components/block-overview-graph/tx-view';
|
||||||
import { feeLevels, defaultMempoolFeeColors, contrastMempoolFeeColors } from '../../../app.constants';
|
import { feeLevels, defaultMempoolFeeColors, contrastMempoolFeeColors } from '@app/app.constants';
|
||||||
import { ServicesApiServices } from '../../../services/services-api.service';
|
import { ServicesApiServices } from '@app/services/services-api.service';
|
||||||
import { detectWebGL } from '../../../shared/graphs.utils';
|
import { detectWebGL } from '@app/shared/graphs.utils';
|
||||||
import { AudioService } from '../../../services/audio.service';
|
import { AudioService } from '@app/services/audio.service';
|
||||||
import { ThemeService } from '../../../services/theme.service';
|
import { ThemeService } from '@app/services/theme.service';
|
||||||
|
|
||||||
const acceleratedColor: Color = hexToColor('8F5FF6');
|
const acceleratedColor: Color = hexToColor('8F5FF6');
|
||||||
const normalColors = defaultMempoolFeeColors.map(hex => hexToColor(hex + '5F'));
|
const normalColors = defaultMempoolFeeColors.map(hex => hexToColor(hex + '5F'));
|
||||||
|
|||||||
+2
-2
@@ -20,7 +20,7 @@
|
|||||||
<td class="pie-chart" rowspan="2" *ngIf="!chartPositionLeft">
|
<td class="pie-chart" rowspan="2" *ngIf="!chartPositionLeft">
|
||||||
<div class="d-flex justify-content-between align-items-start">
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
@if (hasCpfp) {
|
@if (hasCpfp) {
|
||||||
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
|
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP</button>
|
||||||
}
|
}
|
||||||
<ng-container *ngTemplateOutlet="pieChart"></ng-container>
|
<ng-container *ngTemplateOutlet="pieChart"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td colspan="3" class="pt-0">
|
<td colspan="3" class="pt-0">
|
||||||
<div class="d-flex justify-content-end align-items-start">
|
<div class="d-flex justify-content-end align-items-start">
|
||||||
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
|
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
+15
-9
@@ -1,8 +1,8 @@
|
|||||||
import { Component, ChangeDetectionStrategy, Input, Output, OnChanges, SimpleChanges, EventEmitter, ChangeDetectorRef } from '@angular/core';
|
import { Component, ChangeDetectionStrategy, Input, Output, OnChanges, SimpleChanges, EventEmitter, ChangeDetectorRef } from '@angular/core';
|
||||||
import { Transaction } from '../../../interfaces/electrs.interface';
|
import { Transaction } from '@interfaces/electrs.interface';
|
||||||
import { Acceleration, SinglePoolStats } from '../../../interfaces/node-api.interface';
|
import { Acceleration, SinglePoolStats } from '@interfaces/node-api.interface';
|
||||||
import { EChartsOption, PieSeriesOption } from '../../../graphs/echarts';
|
import { EChartsOption, PieSeriesOption } from '@app/graphs/echarts';
|
||||||
import { MiningStats } from '../../../services/mining.service';
|
import { MiningStats } from '@app/services/mining.service';
|
||||||
|
|
||||||
function lighten(color, p): { r, g, b } {
|
function lighten(color, p): { r, g, b } {
|
||||||
return {
|
return {
|
||||||
@@ -76,15 +76,21 @@ export class ActiveAccelerationBox implements OnChanges {
|
|||||||
acceleratingPools.forEach((poolId, index) => {
|
acceleratingPools.forEach((poolId, index) => {
|
||||||
const pool = pools[poolId];
|
const pool = pools[poolId];
|
||||||
const poolShare = ((pool.lastEstimatedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1);
|
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(
|
data.push(getDataItem(
|
||||||
pool.lastEstimatedHashrate,
|
pool.lastEstimatedHashrate,
|
||||||
index >= firstSignificantPool
|
color,
|
||||||
? toRGB(lighten({ r: 147, g: 57, b: 244 }, 1 - (index - firstSignificantPool) / (numSignificantPools - 1)))
|
|
||||||
: 'white',
|
|
||||||
`<b style="color: white">${pool.name} (${poolShare}%)</b>`,
|
`<b style="color: white">${pool.name} (${poolShare}%)</b>`,
|
||||||
true,
|
true,
|
||||||
) as PieSeriesOption);
|
) as PieSeriesOption);
|
||||||
})
|
});
|
||||||
this.acceleratedByPercentage = ((totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1) + '%';
|
this.acceleratedByPercentage = ((totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1) + '%';
|
||||||
const notAcceleratedByPercentage = ((1 - (totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate)) * 100).toFixed(1) + '%';
|
const notAcceleratedByPercentage = ((1 - (totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate)) * 100).toFixed(1) + '%';
|
||||||
data.push(getDataItem(
|
data.push(getDataItem(
|
||||||
@@ -148,4 +154,4 @@ export class ActiveAccelerationBox implements OnChanges {
|
|||||||
onToggleCpfp(): void {
|
onToggleCpfp(): void {
|
||||||
this.toggleCpfp.emit();
|
this.toggleCpfp.emit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||||
import { Observable, of } from 'rxjs';
|
import { Observable, of } from 'rxjs';
|
||||||
import { switchMap } from 'rxjs/operators';
|
import { switchMap } from 'rxjs/operators';
|
||||||
import { Acceleration } from '../../../interfaces/node-api.interface';
|
import { Acceleration } from '@interfaces/node-api.interface';
|
||||||
import { StateService } from '../../../services/state.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
import { WebsocketService } from '../../../services/websocket.service';
|
import { WebsocketService } from '@app/services/websocket.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-pending-stats',
|
selector: 'app-pending-stats',
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
|
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 { BehaviorSubject, Observable, Subscription, combineLatest, of } from 'rxjs';
|
||||||
import { catchError, map, switchMap, tap } from 'rxjs/operators';
|
import { catchError, map, switchMap, tap } from 'rxjs/operators';
|
||||||
import { AddressTxSummary, ChainStats } from '../../interfaces/electrs.interface';
|
import { AddressTxSummary, ChainStats } from '@interfaces/electrs.interface';
|
||||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
import { ElectrsApiService } from '@app/services/electrs-api.service';
|
||||||
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
|
import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
import { PriceService } from '../../services/price.service';
|
import { PriceService } from '@app/services/price.service';
|
||||||
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe';
|
import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe';
|
||||||
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
|
|
||||||
|
|
||||||
const periodSeconds = {
|
const periodSeconds = {
|
||||||
'1d': (60 * 60 * 24),
|
'1d': (60 * 60 * 24),
|
||||||
@@ -45,14 +44,18 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
@Input() right: number | string = 10;
|
@Input() right: number | string = 10;
|
||||||
@Input() left: number | string = 70;
|
@Input() left: number | string = 70;
|
||||||
@Input() widget: boolean = false;
|
@Input() widget: boolean = false;
|
||||||
|
@Input() defaultFiat: boolean = false;
|
||||||
|
@Input() showLegend: boolean = true;
|
||||||
|
@Input() showYAxis: boolean = true;
|
||||||
|
|
||||||
|
adjustedLeft: number;
|
||||||
|
adjustedRight: number;
|
||||||
data: any[] = [];
|
data: any[] = [];
|
||||||
fiatData: any[] = [];
|
fiatData: any[] = [];
|
||||||
hoverData: any[] = [];
|
hoverData: any[] = [];
|
||||||
conversions: any;
|
conversions: any;
|
||||||
allowZoom: boolean = false;
|
allowZoom: boolean = false;
|
||||||
initialRight = this.right;
|
|
||||||
initialLeft = this.left;
|
|
||||||
selected = { [$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`]: true, 'Fiat': false };
|
selected = { [$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`]: true, 'Fiat': false };
|
||||||
|
|
||||||
subscription: Subscription;
|
subscription: Subscription;
|
||||||
@@ -77,15 +80,17 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
private relativeUrlPipe: RelativeUrlPipe,
|
private relativeUrlPipe: RelativeUrlPipe,
|
||||||
private priceService: PriceService,
|
private priceService: PriceService,
|
||||||
private fiatCurrencyPipe: FiatCurrencyPipe,
|
private fiatCurrencyPipe: FiatCurrencyPipe,
|
||||||
private fiatShortenerPipe: FiatShortenerPipe,
|
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
if (!this.address || !this.stats) {
|
if (!this.addressSummary$ && (!this.address || !this.stats)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (changes.defaultFiat) {
|
||||||
|
this.selected['Fiat'] = !!this.defaultFiat;
|
||||||
|
}
|
||||||
if (changes.address || changes.isPubkey || changes.addressSummary$ || changes.stats) {
|
if (changes.address || changes.isPubkey || changes.addressSummary$ || changes.stats) {
|
||||||
if (this.subscription) {
|
if (this.subscription) {
|
||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
@@ -144,15 +149,16 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prepareChartOptions(summary: AddressTxSummary[]) {
|
prepareChartOptions(summary: AddressTxSummary[]) {
|
||||||
if (!summary || !this.stats) {
|
if (!summary) {
|
||||||
return;
|
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 processData = summary.map(d => {
|
||||||
const balance = total;
|
const balance = runningTotal;
|
||||||
const fiatBalance = total * d.price / 100_000_000;
|
const fiatBalance = runningTotal * d.price / 100_000_000;
|
||||||
total -= d.value;
|
runningTotal -= d.value;
|
||||||
return {
|
return {
|
||||||
time: d.time * 1000,
|
time: d.time * 1000,
|
||||||
balance,
|
balance,
|
||||||
@@ -160,7 +166,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
d
|
d
|
||||||
};
|
};
|
||||||
}).reverse();
|
}).reverse();
|
||||||
|
|
||||||
this.data = processData.filter(({ d }) => d.txid !== undefined).map(({ time, balance, d }) => [time, balance, d]);
|
this.data = processData.filter(({ d }) => d.txid !== undefined).map(({ time, balance, d }) => [time, balance, d]);
|
||||||
this.fiatData = processData.map(({ time, fiatBalance, balance, d }) => [time, fiatBalance, d, balance]);
|
this.fiatData = processData.map(({ time, fiatBalance, balance, d }) => [time, fiatBalance, d, balance]);
|
||||||
|
|
||||||
@@ -172,12 +178,15 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
this.fiatData = this.fiatData.filter(d => d[0] >= startFiat);
|
this.fiatData = this.fiatData.filter(d => d[0] >= startFiat);
|
||||||
}
|
}
|
||||||
this.data.push(
|
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);
|
const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1] ?? d.value[1])), 0);
|
||||||
const minValue = this.data.reduce((acc, d) => Math.min(acc, Math.abs(d[1] ?? d.value[1])), maxValue);
|
const minValue = this.data.reduce((acc, d) => Math.min(acc, Math.abs(d[1] ?? d.value[1])), maxValue);
|
||||||
|
|
||||||
|
this.adjustedRight = this.selected['Fiat'] ? +this.right + 40 : +this.right;
|
||||||
|
this.adjustedLeft = this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`] ? +this.left : +this.left - 40;
|
||||||
|
|
||||||
this.chartOptions = {
|
this.chartOptions = {
|
||||||
color: [
|
color: [
|
||||||
new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
@@ -193,10 +202,10 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
grid: {
|
grid: {
|
||||||
top: 20,
|
top: 20,
|
||||||
bottom: this.allowZoom ? 65 : 20,
|
bottom: this.allowZoom ? 65 : 20,
|
||||||
right: this.right,
|
right: this.adjustedRight,
|
||||||
left: this.left,
|
left: this.adjustedLeft,
|
||||||
},
|
},
|
||||||
legend: !this.stateService.isAnyTestnet() ? {
|
legend: (this.showLegend && !this.stateService.isAnyTestnet()) ? {
|
||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
name: $localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`,
|
name: $localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`,
|
||||||
@@ -244,21 +253,22 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
let tooltip = '<div>';
|
let tooltip = '<div>';
|
||||||
|
|
||||||
const hasTx = data[0].data[2].txid;
|
const hasTx = data[0].data[2].txid;
|
||||||
|
const date = new Date(data[0].data[0]).toLocaleTimeString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
|
||||||
|
|
||||||
|
tooltip += `<div>
|
||||||
|
<div style="text-align: right;">
|
||||||
|
<div><b>${date}</b></div>`;
|
||||||
|
|
||||||
if (hasTx) {
|
if (hasTx) {
|
||||||
const header = data.length === 1
|
const header = data.length === 1
|
||||||
? `${data[0].data[2].txid.slice(0, 6)}...${data[0].data[2].txid.slice(-6)}`
|
? `${data[0].data[2].txid.slice(0, 6)}...${data[0].data[2].txid.slice(-6)}`
|
||||||
: `${data.length} transactions`;
|
: `${data.length} transactions`;
|
||||||
tooltip += `<span><b>${header}</b></span>`;
|
tooltip += `<div><b>${header}</b></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const date = new Date(data[0].data[0]).toLocaleTimeString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
|
|
||||||
|
|
||||||
tooltip += `<div>
|
|
||||||
<div style="text-align: right;">`;
|
|
||||||
|
|
||||||
const formatBTC = (val, decimal) => (val / 100_000_000).toFixed(decimal);
|
const formatBTC = (val, decimal) => (val / 100_000_000).toFixed(decimal);
|
||||||
const formatFiat = (val) => this.fiatCurrencyPipe.transform(val, null, 'USD');
|
const formatFiat = (val) => this.fiatCurrencyPipe.transform(val, null, 'USD');
|
||||||
|
|
||||||
const btcVal = btcData.reduce((total, d) => total + d.data[2].value, 0);
|
const btcVal = btcData.reduce((total, d) => total + d.data[2].value, 0);
|
||||||
const fiatVal = fiatData.reduce((total, d) => total + d.data[2].value * d.data[2].price / 100_000_000, 0);
|
const fiatVal = fiatData.reduce((total, d) => total + d.data[2].value * d.data[2].price / 100_000_000, 0);
|
||||||
const btcColor = btcVal === 0 ? '' : (btcVal > 0 ? 'var(--green)' : 'var(--red)');
|
const btcColor = btcVal === 0 ? '' : (btcVal > 0 ? 'var(--green)' : 'var(--red)');
|
||||||
@@ -290,7 +300,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tooltip += `</div><span>${date}</span></div>`;
|
tooltip += `</div></div>`;
|
||||||
return tooltip;
|
return tooltip;
|
||||||
}.bind(this)
|
}.bind(this)
|
||||||
},
|
},
|
||||||
@@ -306,22 +316,26 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
type: 'value',
|
type: 'value',
|
||||||
position: 'left',
|
position: 'left',
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
|
show: this.showYAxis,
|
||||||
color: 'rgb(110, 112, 121)',
|
color: 'rgb(110, 112, 121)',
|
||||||
formatter: (val): string => {
|
formatter: (val): string => {
|
||||||
let valSpan = maxValue - (this.period === 'all' ? 0 : minValue);
|
let valSpan = maxValue - (this.period === 'all' ? 0 : minValue);
|
||||||
if (valSpan > 100_000_000_000) {
|
if (valSpan > 100_000_000_000) {
|
||||||
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 0)} BTC`;
|
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 0, undefined, true)} BTC`;
|
||||||
}
|
}
|
||||||
else if (valSpan > 1_000_000_000) {
|
else if (valSpan > 1_000_000_000) {
|
||||||
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 2)} BTC`;
|
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 2, undefined, true)} BTC`;
|
||||||
} else if (valSpan > 100_000_000) {
|
} else if (valSpan > 100_000_000) {
|
||||||
return `${(val / 100_000_000).toFixed(1)} BTC`;
|
return `${(val / 100_000_000).toFixed(1)} BTC`;
|
||||||
} else if (valSpan > 10_000_000) {
|
} else if (valSpan > 10_000_000) {
|
||||||
return `${(val / 100_000_000).toFixed(2)} BTC`;
|
return `${(val / 100_000_000).toFixed(2)} BTC`;
|
||||||
} else if (valSpan > 1_000_000) {
|
} else if (valSpan > 1_000_000) {
|
||||||
|
if (maxValue > 100_000_000_000) {
|
||||||
|
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 3, undefined, true)} BTC`;
|
||||||
|
}
|
||||||
return `${(val / 100_000_000).toFixed(3)} BTC`;
|
return `${(val / 100_000_000).toFixed(3)} BTC`;
|
||||||
} else {
|
} else {
|
||||||
return `${this.amountShortenerPipe.transform(val, 0)} sats`;
|
return `${this.amountShortenerPipe.transform(val, 0, undefined, true)} sats`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -333,9 +347,10 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
{
|
{
|
||||||
type: 'value',
|
type: 'value',
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
|
show: this.showYAxis,
|
||||||
color: 'rgb(110, 112, 121)',
|
color: 'rgb(110, 112, 121)',
|
||||||
formatter: function(val) {
|
formatter: function(val) {
|
||||||
return this.fiatShortenerPipe.transform(val, null, 'USD');
|
return `$${this.amountShortenerPipe.transform(val, 0, undefined, true)}`;
|
||||||
}.bind(this)
|
}.bind(this)
|
||||||
},
|
},
|
||||||
splitLine: {
|
splitLine: {
|
||||||
@@ -389,8 +404,8 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
type: 'slider',
|
type: 'slider',
|
||||||
brushSelect: false,
|
brushSelect: false,
|
||||||
realtime: true,
|
realtime: true,
|
||||||
left: this.left,
|
left: this.adjustedLeft,
|
||||||
right: this.right,
|
right: this.adjustedRight,
|
||||||
selectedDataBackground: {
|
selectedDataBackground: {
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
@@ -420,26 +435,26 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
|
|
||||||
onLegendSelectChanged(e) {
|
onLegendSelectChanged(e) {
|
||||||
this.selected = e.selected;
|
this.selected = e.selected;
|
||||||
this.right = this.selected['Fiat'] ? +this.initialRight + 40 : this.initialRight;
|
this.adjustedRight = this.selected['Fiat'] ? +this.right + 40 : +this.right;
|
||||||
this.left = this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`] ? this.initialLeft : +this.initialLeft - 40;
|
this.adjustedLeft = this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`] ? +this.left : +this.left - 40;
|
||||||
|
|
||||||
this.chartOptions = {
|
this.chartOptions = {
|
||||||
grid: {
|
grid: {
|
||||||
right: this.right,
|
right: this.adjustedRight,
|
||||||
left: this.left,
|
left: this.adjustedLeft,
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
selected: this.selected,
|
selected: this.selected,
|
||||||
},
|
},
|
||||||
dataZoom: this.allowZoom ? [{
|
dataZoom: this.allowZoom ? [{
|
||||||
left: this.left,
|
left: this.adjustedLeft,
|
||||||
right: this.right,
|
right: this.adjustedRight,
|
||||||
}, {
|
}, {
|
||||||
left: this.left,
|
left: this.adjustedLeft,
|
||||||
right: this.right,
|
right: this.adjustedRight,
|
||||||
}] : undefined
|
}] : undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.chartInstance) {
|
if (this.chartInstance) {
|
||||||
this.chartInstance.setOption(this.chartOptions);
|
this.chartInstance.setOption(this.chartOptions);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { Component, OnInit, OnDestroy, ChangeDetectorRef, HostListener } from '@angular/core';
|
import { Component, OnInit, OnDestroy, ChangeDetectorRef, HostListener } from '@angular/core';
|
||||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
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 { switchMap, catchError } from 'rxjs/operators';
|
||||||
import { Address, Transaction } from '../../interfaces/electrs.interface';
|
import { Address, Transaction } from '@interfaces/electrs.interface';
|
||||||
import { WebsocketService } from '../../services/websocket.service';
|
import { WebsocketService } from '@app/services/websocket.service';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
import { AudioService } from '../../services/audio.service';
|
import { AudioService } from '@app/services/audio.service';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '@app/services/api.service';
|
||||||
import { of, Subscription, forkJoin } from 'rxjs';
|
import { of, Subscription, forkJoin } from 'rxjs';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '@app/services/seo.service';
|
||||||
import { AddressInformation } from '../../interfaces/node-api.interface';
|
import { AddressInformation } from '@interfaces/node-api.interface';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-address-group',
|
selector: 'app-address-group',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core';
|
import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core';
|
||||||
import { Vin, Vout } from '../../interfaces/electrs.interface';
|
import { Vin, Vout } from '@interfaces/electrs.interface';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
import { AddressType, AddressTypeInfo } from '../../shared/address-utils';
|
import { AddressType, AddressTypeInfo } from '@app/shared/address-utils';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-address-labels',
|
selector: 'app-address-labels',
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@
|
|||||||
<app-truncate [text]="transaction.txid" [lastChars]="5"></app-truncate>
|
<app-truncate [text]="transaction.txid" [lastChars]="5"></app-truncate>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</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-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>
|
<td class="table-cell-date"><app-time kind="since" [time]="transaction.time" [fastRender]="true" [showTooltip]="true"></app-time></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
+12
-6
@@ -1,9 +1,9 @@
|
|||||||
import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
|
import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
import { Address, AddressTxSummary } from '../../interfaces/electrs.interface';
|
import { Address, AddressTxSummary } from '@interfaces/electrs.interface';
|
||||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
import { ElectrsApiService } from '@app/services/electrs-api.service';
|
||||||
import { Observable, Subscription, catchError, map, of, switchMap, zip } from 'rxjs';
|
import { Observable, Subscription, catchError, map, of, switchMap, zip } from 'rxjs';
|
||||||
import { PriceService } from '../../services/price.service';
|
import { PriceService } from '@app/services/price.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-address-transactions-widget',
|
selector: 'app-address-transactions-widget',
|
||||||
@@ -43,7 +43,7 @@ export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, On
|
|||||||
|
|
||||||
startAddressSubscription(): void {
|
startAddressSubscription(): void {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
if (!this.address || !this.addressInfo) {
|
if (!this.addressSummary$ && (!this.address || !this.addressInfo)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.transactions$ = (this.addressSummary$ || (this.isPubkey
|
this.transactions$ = (this.addressSummary$ || (this.isPubkey
|
||||||
@@ -55,7 +55,7 @@ export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, On
|
|||||||
})
|
})
|
||||||
)).pipe(
|
)).pipe(
|
||||||
map(summary => {
|
map(summary => {
|
||||||
return summary?.slice(0, 6);
|
return summary?.filter(tx => Math.abs(tx.value) >= 1000000)?.slice(0, 6);
|
||||||
}),
|
}),
|
||||||
switchMap(txs => {
|
switchMap(txs => {
|
||||||
return (zip(txs.map(tx => this.priceService.getBlockPrice$(tx.time, txs.length < 3, this.currency).pipe(
|
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 {
|
ngOnDestroy(): void {
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
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 { switchMap, filter, catchError, map, tap } from 'rxjs/operators';
|
||||||
import { Address, Transaction } from '../../interfaces/electrs.interface';
|
import { Address, Transaction } from '@interfaces/electrs.interface';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
import { OpenGraphService } from '../../services/opengraph.service';
|
import { OpenGraphService } from '@app/services/opengraph.service';
|
||||||
import { AudioService } from '../../services/audio.service';
|
import { AudioService } from '@app/services/audio.service';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '@app/services/api.service';
|
||||||
import { of, merge, Subscription, Observable } from 'rxjs';
|
import { of, merge, Subscription, Observable } from 'rxjs';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '@app/services/seo.service';
|
||||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
import { seoDescriptionNetwork } from '@app/shared/common.utils';
|
||||||
import { AddressInformation } from '../../interfaces/node-api.interface';
|
import { AddressInformation } from '@interfaces/node-api.interface';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-address-preview',
|
selector: 'app-address-preview',
|
||||||
|
|||||||
@@ -117,7 +117,7 @@
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</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">
|
<div class="text-center">
|
||||||
<ng-template [ngIf]="isLoadingTransactions">
|
<ng-template [ngIf]="isLoadingTransactions">
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { Component, OnInit, OnDestroy, HostListener } from '@angular/core';
|
import { Component, OnInit, OnDestroy, HostListener } from '@angular/core';
|
||||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
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 { switchMap, filter, catchError, map, tap } from 'rxjs/operators';
|
||||||
import { Address, ChainStats, Transaction, Utxo, Vin } from '../../interfaces/electrs.interface';
|
import { Address, ChainStats, Transaction, Utxo, Vin } from '@interfaces/electrs.interface';
|
||||||
import { WebsocketService } from '../../services/websocket.service';
|
import { WebsocketService } from '@app/services/websocket.service';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
import { AudioService } from '../../services/audio.service';
|
import { AudioService } from '@app/services/audio.service';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '@app/services/api.service';
|
||||||
import { of, merge, Subscription, Observable, forkJoin } from 'rxjs';
|
import { of, merge, Subscription, Observable, forkJoin } from 'rxjs';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '@app/services/seo.service';
|
||||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
import { seoDescriptionNetwork } from '@app/shared/common.utils';
|
||||||
import { AddressInformation } from '../../interfaces/node-api.interface';
|
import { AddressInformation } from '@interfaces/node-api.interface';
|
||||||
import { AddressTypeInfo } from '../../shared/address-utils';
|
import { AddressTypeInfo } from '@app/shared/address-utils';
|
||||||
|
|
||||||
class AddressStats implements ChainStats {
|
class AddressStats implements ChainStats {
|
||||||
address: string;
|
address: string;
|
||||||
@@ -219,9 +219,13 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
address.is_pubkey
|
address.is_pubkey
|
||||||
? this.electrsApiService.getScriptHashTransactions$((address.address.length === 66 ? '21' : '41') + address.address + 'ac')
|
? 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
|
(utxoCount > 2 && utxoCount <= 500 ? (address.is_pubkey
|
||||||
? this.electrsApiService.getScriptHashUtxos$((address.address.length === 66 ? '21' : '41') + address.address + 'ac')
|
? this.electrsApiService.getScriptHashUtxos$((address.address.length === 66 ? '21' : '41') + address.address + 'ac')
|
||||||
: this.electrsApiService.getAddressUtxos$(address.address)) : of([])
|
: this.electrsApiService.getAddressUtxos$(address.address)) : of(null)).pipe(
|
||||||
|
catchError(() => {
|
||||||
|
return of(null);
|
||||||
|
})
|
||||||
|
)
|
||||||
]);
|
]);
|
||||||
}),
|
}),
|
||||||
switchMap(([transactions, utxos]) => {
|
switchMap(([transactions, utxos]) => {
|
||||||
@@ -319,6 +323,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
this.transactions = this.transactions.slice();
|
this.transactions = this.transactions.slice();
|
||||||
this.mempoolStats.removeTx(transaction);
|
this.mempoolStats.removeTx(transaction);
|
||||||
this.audioService.playSound('magic');
|
this.audioService.playSound('magic');
|
||||||
|
this.confirmTransaction(tx);
|
||||||
} else {
|
} else {
|
||||||
if (this.addTransaction(transaction, false)) {
|
if (this.addTransaction(transaction, false)) {
|
||||||
this.audioService.playSound('magic');
|
this.audioService.playSound('magic');
|
||||||
@@ -345,20 +350,28 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// update utxos in-place
|
// update utxos in-place
|
||||||
for (const vin of transaction.vin) {
|
if (this.utxos != null) {
|
||||||
const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === vin.txid && utxo.vout === vin.vout);
|
let utxosChanged = false;
|
||||||
if (utxoIndex !== -1) {
|
for (const vin of transaction.vin) {
|
||||||
this.utxos.splice(utxoIndex, 1);
|
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()) {
|
||||||
for (const [index, vout] of transaction.vout.entries()) {
|
if (vout.scriptpubkey_address === this.address.address) {
|
||||||
if (vout.scriptpubkey_address === this.address.address) {
|
this.utxos.push({
|
||||||
this.utxos.push({
|
txid: transaction.txid,
|
||||||
txid: transaction.txid,
|
vout: index,
|
||||||
vout: index,
|
value: vout.value,
|
||||||
value: vout.value,
|
status: JSON.parse(JSON.stringify(transaction.status)),
|
||||||
status: JSON.parse(JSON.stringify(transaction.status)),
|
});
|
||||||
});
|
utxosChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (utxosChanged) {
|
||||||
|
this.utxos = this.utxos.slice();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -374,28 +387,64 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
this.transactions = this.transactions.slice();
|
this.transactions = this.transactions.slice();
|
||||||
|
|
||||||
// update utxos in-place
|
// update utxos in-place
|
||||||
for (const vin of transaction.vin) {
|
if (this.utxos != null) {
|
||||||
if (vin.prevout?.scriptpubkey_address === this.address.address) {
|
let utxosChanged = false;
|
||||||
this.utxos.push({
|
for (const vin of transaction.vin) {
|
||||||
txid: vin.txid,
|
if (vin.prevout?.scriptpubkey_address === this.address.address) {
|
||||||
vout: vin.vout,
|
this.utxos.push({
|
||||||
value: vin.prevout.value,
|
txid: vin.txid,
|
||||||
status: { confirmed: true }, // Assuming the input was confirmed
|
vout: vin.vout,
|
||||||
});
|
value: vin.prevout.value,
|
||||||
}
|
status: { confirmed: true }, // Assuming the input was confirmed
|
||||||
}
|
});
|
||||||
for (const [index, vout] of transaction.vout.entries()) {
|
utxosChanged = true;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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;
|
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 {
|
loadMore(): void {
|
||||||
if (this.isLoadingTransactions || this.fullyLoaded) {
|
if (this.isLoadingTransactions || this.fullyLoaded) {
|
||||||
return;
|
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]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { StorageService } from '../../services/storage.service';
|
import { StorageService } from '@app/services/storage.service';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-amount-selector',
|
selector: 'app-amount-selector',
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
@if (digitsInfo === '1.8-8') {
|
@if (digitsInfo === '1.8-8') {
|
||||||
‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | number }}
|
‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | number }}
|
||||||
} @else {
|
} @else {
|
||||||
‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | amountShortener : satoshis < 1000 && satoshis > -1000 ? 0 : 1 }}
|
‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | amountShortener : (satoshis < 1000 && satoshis > -1000 ? 0 : 1) : undefined : true }}
|
||||||
}
|
}
|
||||||
<span class="symbol">
|
<span class="symbol">
|
||||||
<ng-container *ngTemplateOutlet="prefix"></ng-container>sats
|
<ng-container *ngTemplateOutlet="prefix"></ng-container>sats
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component, OnInit, OnDestroy, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
import { Component, OnInit, OnDestroy, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
import { Observable, Subscription } from 'rxjs';
|
import { Observable, Subscription } from 'rxjs';
|
||||||
import { Price } from '../../services/price.service';
|
import { Price } from '@app/services/price.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-amount',
|
selector: 'app-amount',
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Location } from '@angular/common';
|
import { Location } from '@angular/common';
|
||||||
import { Component, HostListener, OnInit, Inject, LOCALE_ID, HostBinding } from '@angular/core';
|
import { Component, HostListener, OnInit, Inject, LOCALE_ID, HostBinding } from '@angular/core';
|
||||||
import { Router, NavigationEnd } from '@angular/router';
|
import { Router, NavigationEnd } from '@angular/router';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
import { OpenGraphService } from '../../services/opengraph.service';
|
import { OpenGraphService } from '@app/services/opengraph.service';
|
||||||
import { NgbTooltipConfig } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbTooltipConfig } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { ThemeService } from '../../services/theme.service';
|
import { ThemeService } from '@app/services/theme.service';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '@app/services/seo.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||||
import { combineLatest, Observable } from 'rxjs';
|
import { combineLatest, Observable } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import { moveDec } from '../../bitcoin.utils';
|
import { moveDec } from '@app/bitcoin.utils';
|
||||||
import { AssetsService } from '../../services/assets.service';
|
import { AssetsService } from '@app/services/assets.service';
|
||||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
import { ElectrsApiService } from '@app/services/electrs-api.service';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '@environments/environment';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-asset-circulation',
|
selector: 'app-asset-circulation',
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
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, take } from 'rxjs/operators';
|
import { switchMap, filter, catchError, take } from 'rxjs/operators';
|
||||||
import { Asset, Transaction } from '../../interfaces/electrs.interface';
|
import { Asset, Transaction } from '@interfaces/electrs.interface';
|
||||||
import { WebsocketService } from '../../services/websocket.service';
|
import { WebsocketService } from '@app/services/websocket.service';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
import { AudioService } from '../../services/audio.service';
|
import { AudioService } from '@app/services/audio.service';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '@app/services/api.service';
|
||||||
import { of, merge, Subscription, combineLatest } from 'rxjs';
|
import { of, merge, Subscription, combineLatest } from 'rxjs';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '@app/services/seo.service';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '@environments/environment';
|
||||||
import { AssetsService } from '../../services/assets.service';
|
import { AssetsService } from '@app/services/assets.service';
|
||||||
import { moveDec } from '../../bitcoin.utils';
|
import { moveDec } from '@app/bitcoin.utils';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-asset',
|
selector: 'app-asset',
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { Component, OnInit } from '@angular/core';
|
|||||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||||
import { combineLatest, Observable } from 'rxjs';
|
import { combineLatest, Observable } from 'rxjs';
|
||||||
import { map, switchMap } from 'rxjs/operators';
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
import { ApiService } from '../../../services/api.service';
|
import { ApiService } from '@app/services/api.service';
|
||||||
import { AssetsService } from '../../../services/assets.service';
|
import { AssetsService } from '@app/services/assets.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-asset-group',
|
selector: 'app-asset-group',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { ApiService } from '../../../services/api.service';
|
import { ApiService } from '@app/services/api.service';
|
||||||
import { StateService } from '../../../services/state.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-assets-featured',
|
selector: 'app-assets-featured',
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import { Router } from '@angular/router';
|
|||||||
import { NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { merge, Observable, of, Subject } from 'rxjs';
|
import { merge, Observable, of, Subject } from 'rxjs';
|
||||||
import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators';
|
||||||
import { AssetExtended } from '../../../interfaces/electrs.interface';
|
import { AssetExtended } from '@interfaces/electrs.interface';
|
||||||
import { AssetsService } from '../../../services/assets.service';
|
import { AssetsService } from '@app/services/assets.service';
|
||||||
import { SeoService } from '../../../services/seo.service';
|
import { SeoService } from '@app/services/seo.service';
|
||||||
import { StateService } from '../../../services/state.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
import { RelativeUrlPipe } from '../../../shared/pipes/relative-url/relative-url.pipe';
|
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
|
||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '@environments/environment';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-assets-nav',
|
selector: 'app-assets-nav',
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
|
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
|
||||||
import { AssetsService } from '../../services/assets.service';
|
import { AssetsService } from '@app/services/assets.service';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '@environments/environment';
|
||||||
import { UntypedFormGroup } from '@angular/forms';
|
import { UntypedFormGroup } from '@angular/forms';
|
||||||
import { filter, map, switchMap, take } from 'rxjs/operators';
|
import { filter, map, switchMap, take } from 'rxjs/operators';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { combineLatest, Observable } from 'rxjs';
|
import { combineLatest, Observable } from 'rxjs';
|
||||||
import { AssetExtended } from '../../interfaces/electrs.interface';
|
import { AssetExtended } from '@interfaces/electrs.interface';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '@app/services/seo.service';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-assets',
|
selector: 'app-assets',
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
<div class="item">
|
<div class="item">
|
||||||
<h5 class="card-title" i18n="dashboard.btc-holdings">BTC Holdings</h5>
|
<h5 class="card-title" i18n="dashboard.btc-holdings">BTC Holdings</h5>
|
||||||
<div class="card-text">
|
<div class="card-text">
|
||||||
{{ ((addressInfo.chain_stats.funded_txo_sum - addressInfo.chain_stats.spent_txo_sum) / 100_000_000) | number: '1.2-2' }} <span class="symbol" i18n="shared.btc|BTC">BTC</span>
|
{{ ((total) / 100_000_000) | number: '1.2-2' }} <span class="symbol" i18n="shared.btc|BTC">BTC</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="symbol">
|
<div class="symbol">
|
||||||
<app-fiat [value]="(addressInfo.chain_stats.funded_txo_sum - addressInfo.chain_stats.spent_txo_sum)"></app-fiat>
|
<app-fiat [value]="(total)"></app-fiat>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
import { Address, AddressTxSummary } from '../../interfaces/electrs.interface';
|
import { Address, AddressTxSummary } from '@interfaces/electrs.interface';
|
||||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
import { ElectrsApiService } from '@app/services/electrs-api.service';
|
||||||
import { Observable, catchError, of } from 'rxjs';
|
import { Observable, catchError, of } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -19,6 +19,7 @@ export class BalanceWidgetComponent implements OnInit, OnChanges {
|
|||||||
isLoading: boolean = true;
|
isLoading: boolean = true;
|
||||||
error: any;
|
error: any;
|
||||||
|
|
||||||
|
total: number = 0;
|
||||||
delta7d: number = 0;
|
delta7d: number = 0;
|
||||||
delta30d: number = 0;
|
delta30d: number = 0;
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ export class BalanceWidgetComponent implements OnInit, OnChanges {
|
|||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
if (!this.address || !this.addressInfo) {
|
if (!this.addressSummary$ && (!this.address || !this.addressInfo)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
(this.addressSummary$ || (this.isPubkey
|
(this.addressSummary$ || (this.isPubkey
|
||||||
@@ -57,6 +58,7 @@ export class BalanceWidgetComponent implements OnInit, OnChanges {
|
|||||||
calculateStats(summary: AddressTxSummary[]): void {
|
calculateStats(summary: AddressTxSummary[]): void {
|
||||||
let weekTotal = 0;
|
let weekTotal = 0;
|
||||||
let monthTotal = 0;
|
let monthTotal = 0;
|
||||||
|
this.total = this.addressInfo ? this.addressInfo.chain_stats.funded_txo_sum - this.addressInfo.chain_stats.spent_txo_sum : summary.reduce((acc, tx) => acc + tx.value, 0);
|
||||||
|
|
||||||
const weekAgo = (new Date(new Date().setHours(0, 0, 0, 0) - (7 * 24 * 60 * 60 * 1000)).getTime()) / 1000;
|
const weekAgo = (new Date(new Date().setHours(0, 0, 0, 0) - (7 * 24 * 60 * 60 * 1000)).getTime()) / 1000;
|
||||||
const monthAgo = (new Date(new Date().setHours(0, 0, 0, 0) - (30 * 24 * 60 * 60 * 1000)).getTime()) / 1000;
|
const monthAgo = (new Date(new Date().setHours(0, 0, 0, 0) - (30 * 24 * 60 * 60 * 1000)).getTime()) / 1000;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
|
|||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { Subscription, of, timer } from 'rxjs';
|
import { Subscription, of, timer } from 'rxjs';
|
||||||
import { filter, repeat, retry, switchMap, take, tap } from 'rxjs/operators';
|
import { filter, repeat, retry, switchMap, take, tap } from 'rxjs/operators';
|
||||||
import { ServicesApiServices } from '../../services/services-api.service';
|
import { ServicesApiServices } from '@app/services/services-api.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-bitcoin-invoice',
|
selector: 'app-bitcoin-invoice',
|
||||||
|
|||||||
+9
-9
@@ -1,17 +1,17 @@
|
|||||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core';
|
||||||
import { echarts, EChartsOption } from '../../graphs/echarts';
|
import { echarts, EChartsOption } from '@app/graphs/echarts';
|
||||||
import { Observable, combineLatest, of } from 'rxjs';
|
import { Observable, combineLatest, of } from 'rxjs';
|
||||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '@app/services/api.service';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '@app/services/seo.service';
|
||||||
import { formatNumber } from '@angular/common';
|
import { formatNumber } from '@angular/common';
|
||||||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
|
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '@app/shared/graphs.utils';
|
||||||
import { StorageService } from '../../services/storage.service';
|
import { StorageService } from '@app/services/storage.service';
|
||||||
import { MiningService } from '../../services/mining.service';
|
import { MiningService } from '@app/services/mining.service';
|
||||||
import { selectPowerOfTen } from '../../bitcoin.utils';
|
import { selectPowerOfTen } from '@app/bitcoin.utils';
|
||||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||||
import { echarts, EChartsOption } from '../../graphs/echarts';
|
import { echarts, EChartsOption } from '@app/graphs/echarts';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '@app/services/api.service';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '@app/services/seo.service';
|
||||||
import { formatNumber } from '@angular/common';
|
import { formatNumber } from '@angular/common';
|
||||||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { download, formatterXAxis } from '../../shared/graphs.utils';
|
import { download, formatterXAxis } from '@app/shared/graphs.utils';
|
||||||
import { StorageService } from '../../services/storage.service';
|
import { StorageService } from '@app/services/storage.service';
|
||||||
import { MiningService } from '../../services/mining.service';
|
import { MiningService } from '@app/services/mining.service';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
|
import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe';
|
||||||
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe';
|
import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-block-fees-graph',
|
selector: 'app-block-fees-graph',
|
||||||
|
|||||||
+10
-10
@@ -1,19 +1,19 @@
|
|||||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core';
|
||||||
import { EChartsOption } from '../../graphs/echarts';
|
import { EChartsOption } from '@app/graphs/echarts';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { catchError, map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
import { catchError, map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '@app/services/api.service';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '@app/services/seo.service';
|
||||||
import { formatNumber } from '@angular/common';
|
import { formatNumber } from '@angular/common';
|
||||||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { download, formatterXAxis } from '../../shared/graphs.utils';
|
import { download, formatterXAxis } from '@app/shared/graphs.utils';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
|
import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe';
|
||||||
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe';
|
import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
import { MiningService } from '../../services/mining.service';
|
import { MiningService } from '@app/services/mining.service';
|
||||||
import { StorageService } from '../../services/storage.service';
|
import { StorageService } from '@app/services/storage.service';
|
||||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-block-fees-subsidy-graph',
|
selector: 'app-block-fees-subsidy-graph',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Component, EventEmitter, Output, HostListener, Input, ChangeDetectorRef, OnChanges, SimpleChanges, OnInit, OnDestroy } from '@angular/core';
|
import { Component, EventEmitter, Output, HostListener, Input, ChangeDetectorRef, OnChanges, SimpleChanges, OnInit, OnDestroy } from '@angular/core';
|
||||||
import { ActiveFilter, FilterGroups, FilterMode, GradientMode, TransactionFilters } from '../../shared/filters.utils';
|
import { ActiveFilter, FilterGroups, FilterMode, GradientMode, TransactionFilters } from '@app/shared/filters.utils';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
|
|
||||||
@@ -115,4 +115,4 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.filterSubscription.unsubscribe();
|
this.filterSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core';
|
||||||
import { EChartsOption } from '../../graphs/echarts';
|
import { EChartsOption } from '@app/graphs/echarts';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '@app/services/api.service';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '@app/services/seo.service';
|
||||||
import { formatNumber } from '@angular/common';
|
import { formatNumber } from '@angular/common';
|
||||||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
|
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '@app/shared/graphs.utils';
|
||||||
import { StorageService } from '../../services/storage.service';
|
import { StorageService } from '@app/services/storage.service';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-block-health-graph',
|
selector: 'app-block-health-graph',
|
||||||
|
|||||||
+12
-12
@@ -1,17 +1,17 @@
|
|||||||
import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, NgZone, AfterViewInit, OnDestroy, OnChanges } from '@angular/core';
|
import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, NgZone, AfterViewInit, OnDestroy, OnChanges } from '@angular/core';
|
||||||
import { TransactionStripped } from '../../interfaces/node-api.interface';
|
import { TransactionStripped } from '@interfaces/node-api.interface';
|
||||||
import { FastVertexArray } from './fast-vertex-array';
|
import { FastVertexArray } from '@components/block-overview-graph/fast-vertex-array';
|
||||||
import BlockScene from './block-scene';
|
import BlockScene from '@components/block-overview-graph/block-scene';
|
||||||
import TxSprite from './tx-sprite';
|
import TxSprite from '@components/block-overview-graph/tx-sprite';
|
||||||
import TxView from './tx-view';
|
import TxView from '@components/block-overview-graph/tx-view';
|
||||||
import { Color, Position } from './sprite-types';
|
import { Color, Position } from '@components/block-overview-graph/sprite-types';
|
||||||
import { Price } from '../../services/price.service';
|
import { Price } from '@app/services/price.service';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
import { ThemeService } from '../../services/theme.service';
|
import { ThemeService } from '@app/services/theme.service';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { defaultColorFunction, setOpacity, defaultAuditColors, defaultColors, ageColorFunction, contrastColorFunction, contrastAuditColors, contrastColors } from './utils';
|
import { defaultColorFunction, setOpacity, defaultAuditColors, defaultColors, ageColorFunction, contrastColorFunction, contrastAuditColors, contrastColors } from '@components/block-overview-graph/utils';
|
||||||
import { ActiveFilter, FilterMode, toFlags } from '../../shared/filters.utils';
|
import { ActiveFilter, FilterMode, toFlags } from '@app/shared/filters.utils';
|
||||||
import { detectWebGL } from '../../shared/graphs.utils';
|
import { detectWebGL } from '@app/shared/graphs.utils';
|
||||||
|
|
||||||
const unmatchedOpacity = 0.2;
|
const unmatchedOpacity = 0.2;
|
||||||
const unmatchedAuditColors = {
|
const unmatchedAuditColors = {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { FastVertexArray } from './fast-vertex-array';
|
import { FastVertexArray } from '@components/block-overview-graph/fast-vertex-array';
|
||||||
import TxView from './tx-view';
|
import TxView from '@components/block-overview-graph/tx-view';
|
||||||
import { TransactionStripped } from '../../interfaces/node-api.interface';
|
import { TransactionStripped } from '@interfaces/node-api.interface';
|
||||||
import { Color, Position, Square, ViewUpdateParams } from './sprite-types';
|
import { Color, Position, Square, ViewUpdateParams } from '@components/block-overview-graph/sprite-types';
|
||||||
import { defaultColorFunction, contrastColorFunction } from './utils';
|
import { defaultColorFunction, contrastColorFunction } from '@components/block-overview-graph/utils';
|
||||||
import { ThemeService } from '../../services/theme.service';
|
import { ThemeService } from '@app/services/theme.service';
|
||||||
|
|
||||||
export default class BlockScene {
|
export default class BlockScene {
|
||||||
scene: { count: number, offset: { x: number, y: number}};
|
scene: { count: number, offset: { x: number, y: number}};
|
||||||
@@ -917,4 +917,4 @@ class BlockLayout {
|
|||||||
|
|
||||||
function feeRateDescending(a: TxView, b: TxView) {
|
function feeRateDescending(a: TxView, b: TxView) {
|
||||||
return b.feerate - a.feerate;
|
return b.feerate - a.feerate;
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
or compacting into a smaller Float32Array when there's space to do so.
|
or compacting into a smaller Float32Array when there's space to do so.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import TxSprite from './tx-sprite';
|
import TxSprite from '@components/block-overview-graph/tx-sprite';
|
||||||
|
|
||||||
export class FastVertexArray {
|
export class FastVertexArray {
|
||||||
length: number;
|
length: number;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FastVertexArray } from './fast-vertex-array';
|
import { FastVertexArray } from '@components/block-overview-graph/fast-vertex-array';
|
||||||
import { InterpolatedAttribute, Attributes, OptionalAttributes, SpriteUpdateParams, Update } from './sprite-types';
|
import { InterpolatedAttribute, Attributes, OptionalAttributes, SpriteUpdateParams, Update } from '@components/block-overview-graph/sprite-types';
|
||||||
|
|
||||||
const attribKeys = ['a', 'b', 't', 'v'];
|
const attribKeys = ['a', 'b', 't', 'v'];
|
||||||
const updateKeys = ['x', 'y', 's', 'r', 'g', 'b', 'a'];
|
const updateKeys = ['x', 'y', 's', 'r', 'g', 'b', 'a'];
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import TxSprite from './tx-sprite';
|
import TxSprite from '@components/block-overview-graph/tx-sprite';
|
||||||
import { FastVertexArray } from './fast-vertex-array';
|
import { FastVertexArray } from '@components/block-overview-graph/fast-vertex-array';
|
||||||
import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from './sprite-types';
|
import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from '@components/block-overview-graph/sprite-types';
|
||||||
import { hexToColor } from './utils';
|
import { hexToColor } from '@components/block-overview-graph/utils';
|
||||||
import BlockScene from './block-scene';
|
import BlockScene from '@components/block-overview-graph/block-scene';
|
||||||
import { TransactionStripped } from '../../interfaces/node-api.interface';
|
import { TransactionStripped } from '@interfaces/node-api.interface';
|
||||||
import { TransactionFlags } from '../../shared/filters.utils';
|
import { TransactionFlags } from '@app/shared/filters.utils';
|
||||||
|
|
||||||
const hoverTransitionTime = 300;
|
const hoverTransitionTime = 300;
|
||||||
const defaultHoverColor = hexToColor('1bd8f4');
|
const defaultHoverColor = hexToColor('1bd8f4');
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { feeLevels, defaultMempoolFeeColors, contrastMempoolFeeColors } from '../../app.constants';
|
import { feeLevels, defaultMempoolFeeColors, contrastMempoolFeeColors } from '@app/app.constants';
|
||||||
import { Color } from './sprite-types';
|
import { Color } from '@components/block-overview-graph/sprite-types';
|
||||||
import TxView from './tx-view';
|
import TxView from '@components/block-overview-graph/tx-view';
|
||||||
|
|
||||||
export function hexToColor(hex: string): Color {
|
export function hexToColor(hex: string): Color {
|
||||||
return {
|
return {
|
||||||
@@ -11,6 +11,10 @@ export function hexToColor(hex: string): Color {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function colorToHex(color: Color): string {
|
||||||
|
return [color.r, color.g, color.b].map(c => Math.round(c * 255).toString(16)).join('');
|
||||||
|
}
|
||||||
|
|
||||||
export function desaturate(color: Color, amount: number): Color {
|
export function desaturate(color: Color, amount: number): Color {
|
||||||
const gray = (color.r + color.g + color.b) / 6;
|
const gray = (color.r + color.g + color.b) / 6;
|
||||||
return {
|
return {
|
||||||
@@ -30,6 +34,15 @@ export function darken(color: Color, amount: number): Color {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mix(color1: Color, color2: Color, amount: number): Color {
|
||||||
|
return {
|
||||||
|
r: color1.r * (1 - amount) + color2.r * amount,
|
||||||
|
g: color1.g * (1 - amount) + color2.g * amount,
|
||||||
|
b: color1.b * (1 - amount) + color2.b * amount,
|
||||||
|
a: color1.a * (1 - amount) + color2.a * amount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function setOpacity(color: Color, opacity: number): Color {
|
export function setOpacity(color: Color, opacity: number): Color {
|
||||||
return {
|
return {
|
||||||
...color,
|
...color,
|
||||||
|
|||||||
+5
-5
@@ -1,9 +1,9 @@
|
|||||||
import { Component, ElementRef, ViewChild, Input, OnChanges, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
import { Component, ElementRef, ViewChild, Input, OnChanges, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
||||||
import { Position } from '../../components/block-overview-graph/sprite-types.js';
|
import { Position } from '@components/block-overview-graph/sprite-types.js';
|
||||||
import { Price } from '../../services/price.service';
|
import { Price } from '@app/services/price.service';
|
||||||
import { TransactionStripped } from '../../interfaces/node-api.interface.js';
|
import { TransactionStripped } from '@interfaces/node-api.interface.js';
|
||||||
import { Filter, FilterMode, TransactionFlags, toFilters } from '../../shared/filters.utils';
|
import { Filter, FilterMode, TransactionFlags, toFilters } from '@app/shared/filters.utils';
|
||||||
import { Block } from '../../interfaces/electrs.interface.js';
|
import { Block } from '@interfaces/electrs.interface.js';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-block-overview-tooltip',
|
selector: 'app-block-overview-tooltip',
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||||
import { echarts, EChartsOption } from '../../graphs/echarts';
|
import { echarts, EChartsOption } from '@app/graphs/echarts';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '@app/services/api.service';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '@app/services/seo.service';
|
||||||
import { formatNumber } from '@angular/common';
|
import { formatNumber } from '@angular/common';
|
||||||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { download, formatterXAxis } from '../../shared/graphs.utils';
|
import { download, formatterXAxis } from '@app/shared/graphs.utils';
|
||||||
import { MiningService } from '../../services/mining.service';
|
import { MiningService } from '@app/services/mining.service';
|
||||||
import { StorageService } from '../../services/storage.service';
|
import { StorageService } from '@app/services/storage.service';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
|
import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe';
|
||||||
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe';
|
import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-block-rewards-graph',
|
selector: 'app-block-rewards-graph',
|
||||||
|
|||||||
Loaded 100 of 375 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user