Compare commits
465 Commits
v2.4.0-alp
...
v2.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad603a35e0 | ||
|
|
ed485fa16a | ||
|
|
507c8b18f4 | ||
|
|
185223bffd | ||
|
|
dd4e120ab0 | ||
|
|
ae0789a3fa | ||
|
|
ede5508397 | ||
|
|
a99b52a735 | ||
|
|
e58b71fd4f | ||
|
|
59d10fd3c6 | ||
|
|
30e8b134bc | ||
|
|
cdf0fe0335 | ||
|
|
f4667c0892 | ||
|
|
ac257b4165 | ||
|
|
75ab2bc920 | ||
|
|
307c30e33b | ||
|
|
c7835b1326 | ||
|
|
7e389c8863 | ||
|
|
d697c0c45e | ||
|
|
4fa4088694 | ||
|
|
07cb4a49bc | ||
|
|
067ee168dd | ||
|
|
d8a90cce47 | ||
|
|
e303a4c374 | ||
|
|
c75f485e54 | ||
|
|
f3f0c688d8 | ||
|
|
030020ea9e | ||
|
|
682682c74a | ||
|
|
b5daf205a0 | ||
|
|
1037fbe52b | ||
|
|
051d151fb7 | ||
|
|
7ba7440bb6 | ||
|
|
81f20e53ea | ||
|
|
05a2c05a9e | ||
|
|
44b1daeed2 | ||
|
|
bed266abac | ||
|
|
8487548271 | ||
|
|
c5e8a83ebb | ||
|
|
b6f81bc83a | ||
|
|
294c278c42 | ||
|
|
ff88a65936 | ||
|
|
76c7508224 | ||
|
|
3c02131133 | ||
|
|
c89fd8c39f | ||
|
|
69623d71b2 | ||
|
|
2d6f4d3bdb | ||
|
|
97ff1e37aa | ||
|
|
d0381e7850 | ||
|
|
c4638f2ac5 | ||
|
|
83c383b1ec | ||
|
|
4f22864080 | ||
|
|
92780daa78 | ||
|
|
a5e4b09e64 | ||
|
|
1501dd23ab | ||
|
|
15ab134fa4 | ||
|
|
05f0ba72e2 | ||
|
|
c9c5e8008c | ||
|
|
bdd3af6b6a | ||
|
|
6582c8b36f | ||
|
|
be838ec313 | ||
|
|
92eef3a6c1 | ||
|
|
63939981c1 | ||
|
|
c9f788e3a4 | ||
|
|
0a866b468a | ||
|
|
8f0f755014 | ||
|
|
5943b88ffe | ||
|
|
c5dfe92e60 | ||
|
|
753cf3cbac | ||
|
|
4a64984d7f | ||
|
|
eeb84e5d42 | ||
|
|
50cd8c80d8 | ||
|
|
567d4aebbc | ||
|
|
b53cc4c37c | ||
|
|
d46e1abd07 | ||
|
|
0b0c0b458f | ||
|
|
997b5a1c9d | ||
|
|
4345661a0b | ||
|
|
5867c79a1f | ||
|
|
41f0619572 | ||
|
|
96b4ea6b50 | ||
|
|
8dda51a92a | ||
|
|
b4bb54212c | ||
|
|
d57193c269 | ||
|
|
4bc03c2d60 | ||
|
|
bf969ec8f7 | ||
|
|
6ead907e08 | ||
|
|
243168a450 | ||
|
|
06d2cf1b88 | ||
|
|
d622162f33 | ||
|
|
12807583c2 | ||
|
|
47f3d539c3 | ||
|
|
993cd64126 | ||
|
|
a0e32ab0bd | ||
|
|
409763b885 | ||
|
|
e06819fc6f | ||
|
|
812783f2cd | ||
|
|
849373a6d3 | ||
|
|
0417d3b70d | ||
|
|
a55eb653f9 | ||
|
|
6b05ed764e | ||
|
|
766eeb2a6f | ||
|
|
753e6bd956 | ||
|
|
5e6295d79a | ||
|
|
3b611275d2 | ||
|
|
cb4dac3506 | ||
|
|
9a7dc3fa49 | ||
|
|
e1c833872e | ||
|
|
5de559f5ad | ||
|
|
bc068a0d9a | ||
|
|
13ccc96e03 | ||
|
|
bc56878039 | ||
|
|
8cb2149fbd | ||
|
|
7d7c331238 | ||
|
|
a8d58d14ff | ||
|
|
4723a9d41b | ||
|
|
4ee9a42f3f | ||
|
|
aae2dec16d | ||
|
|
43e0fe655e | ||
|
|
958d77ed6c | ||
|
|
ad32ba8a98 | ||
|
|
d7847c7630 | ||
|
|
284e6e5720 | ||
|
|
4223bb2047 | ||
|
|
0fa18be43e | ||
|
|
43f2faa077 | ||
|
|
f9dfbf94ef | ||
|
|
3424bb9d6a | ||
|
|
6288bcde51 | ||
|
|
80476a2b61 | ||
|
|
78ee671051 | ||
|
|
386a2de117 | ||
|
|
70b2731b82 | ||
|
|
ae3f8b8bd5 | ||
|
|
2d888d7c13 | ||
|
|
98db8b1b25 | ||
|
|
f710ffb7d0 | ||
|
|
b702782c27 | ||
|
|
5dc7fe6a72 | ||
|
|
39e8f75e07 | ||
|
|
f4e0b1125c | ||
|
|
663bd118a5 | ||
|
|
2a8e2d2d25 | ||
|
|
83a08b0f74 | ||
|
|
0887428066 | ||
|
|
1c018d18bd | ||
|
|
8040abaec4 | ||
|
|
a2e2b36a76 | ||
|
|
f17998cfce | ||
|
|
421375ba62 | ||
|
|
db1289f985 | ||
|
|
f5271bc7b4 | ||
|
|
1117324a7b | ||
|
|
57276b7abd | ||
|
|
b0c334fbe3 | ||
|
|
7a8fa6e056 | ||
|
|
72c4ea0065 | ||
|
|
1805b74edf | ||
|
|
327b2aa070 | ||
|
|
fdc3e7a95f | ||
|
|
9ed7b2aad3 | ||
|
|
42188dcef5 | ||
|
|
2b2f4f05b6 | ||
|
|
f5dab6f215 | ||
|
|
2911fbe5e4 | ||
|
|
92d7519d8d | ||
|
|
e94938d5dd | ||
|
|
950d874b9b | ||
|
|
81c68620a1 | ||
|
|
9d832f9bfc | ||
|
|
460ff68a52 | ||
|
|
96007509b5 | ||
|
|
bb74a25adc | ||
|
|
c36cad4619 | ||
|
|
989c74699f | ||
|
|
fa92ba4478 | ||
|
|
e8829e21e7 | ||
|
|
d61e599de0 | ||
|
|
4a6f3e189d | ||
|
|
7154d755c1 | ||
|
|
307ee50798 | ||
|
|
e8175a90f4 | ||
|
|
bbc9df486e | ||
|
|
c7014fc6c8 | ||
|
|
c22aee5e60 | ||
|
|
feeb93b298 | ||
|
|
a74dace594 | ||
|
|
e0e2a2a626 | ||
|
|
7424c65430 | ||
|
|
c17cf308d4 | ||
|
|
bb3f7fe61f | ||
|
|
f7fcc82933 | ||
|
|
351d9864fe | ||
|
|
8148e9a36d | ||
|
|
34195f0e45 | ||
|
|
9b529d075a | ||
|
|
38a98f70d9 | ||
|
|
544ab890b0 | ||
|
|
d0ad4742c1 | ||
|
|
ee5f7600dc | ||
|
|
5b400daf3b | ||
|
|
58882136a0 | ||
|
|
1ebf089d37 | ||
|
|
1499eb3ba8 | ||
|
|
67adf4c310 | ||
|
|
cd4ced8d6d | ||
|
|
acfdc8163b | ||
|
|
df73548f7e | ||
|
|
f4389e11ba | ||
|
|
f8c6a7c77b | ||
|
|
9c65ff3e12 | ||
|
|
79a90aeec2 | ||
|
|
8e8609371f | ||
|
|
174976ce82 | ||
|
|
57adce693a | ||
|
|
d9576bb2e4 | ||
|
|
97686e1c87 | ||
|
|
35db3ffbf0 | ||
|
|
0a747b5609 | ||
|
|
e947f3259e | ||
|
|
a16eb6e804 | ||
|
|
54334a1854 | ||
|
|
a34eb9ba88 | ||
|
|
f218efbeb2 | ||
|
|
570d8cfc74 | ||
|
|
d964ccca12 | ||
|
|
746e205d74 | ||
|
|
ad29462a6d | ||
|
|
d6cd17e4c8 | ||
|
|
1383c20703 | ||
|
|
5fc91fe466 | ||
|
|
77d9cba468 | ||
|
|
32cd93b689 | ||
|
|
083634826e | ||
|
|
29557ddd86 | ||
|
|
f80b97af53 | ||
|
|
543c1cee62 | ||
|
|
3c2171efb3 | ||
|
|
a7b28ca8e8 | ||
|
|
383e3e55a5 | ||
|
|
8660dc3eba | ||
|
|
00bb09faaa | ||
|
|
f13c8b36cd | ||
|
|
e4ac09ea57 | ||
|
|
72492c9b39 | ||
|
|
39b74a42e5 | ||
|
|
cdd2d9089b | ||
|
|
e086daeecb | ||
|
|
7f5ddaf930 | ||
|
|
0f39b3b7d0 | ||
|
|
bae43249b2 | ||
|
|
7f01bda06d | ||
|
|
2c73153db0 | ||
|
|
7e22fe1617 | ||
|
|
415ec685e6 | ||
|
|
35f8e06ec4 | ||
|
|
431c8c35b9 | ||
|
|
57c30da40f | ||
|
|
9ae2cb79c6 | ||
|
|
1b2fbfd506 | ||
|
|
db73b0f671 | ||
|
|
044e786379 | ||
|
|
7262f61ca0 | ||
|
|
19ae01defb | ||
|
|
1c40a22416 | ||
|
|
ceb0050ea9 | ||
|
|
b97ea010cb | ||
|
|
aad94a1af3 | ||
|
|
c738816cb6 | ||
|
|
f5f53c93f7 | ||
|
|
07415d3871 | ||
|
|
1d2841b2a6 | ||
|
|
4ea2a8244a | ||
|
|
ab0c55b0fa | ||
|
|
d6f594b95a | ||
|
|
a8de738e9b | ||
|
|
61c309cd1d | ||
|
|
5b9d6a31e5 | ||
|
|
8fa0539b5a | ||
|
|
7262485f3b | ||
|
|
24300eeac5 | ||
|
|
2be6c19ba2 | ||
|
|
001bd1d442 | ||
|
|
f30d26b83c | ||
|
|
198c52fd5f | ||
|
|
c5e0b0fc74 | ||
|
|
ecefddf2c3 | ||
|
|
4c8eaac144 | ||
|
|
9991d43b3b | ||
|
|
98b9f007c6 | ||
|
|
53812c3751 | ||
|
|
3e01207026 | ||
|
|
68f72e3074 | ||
|
|
03ade97c0e | ||
|
|
411e9c2e89 | ||
|
|
93dab57959 | ||
|
|
625dba943b | ||
|
|
b272d1e27e | ||
|
|
960513c370 | ||
|
|
61afa92d05 | ||
|
|
fa0373c181 | ||
|
|
5373078a30 | ||
|
|
17f0222e47 | ||
|
|
1479039fb5 | ||
|
|
da28e7b80e | ||
|
|
f2780e65cd | ||
|
|
f9a1f10b99 | ||
|
|
816263bd54 | ||
|
|
eb169cf58b | ||
|
|
2d6fcd6d67 | ||
|
|
9d1883f925 | ||
|
|
85e544dc8e | ||
|
|
aa86885e6b | ||
|
|
72a603ac37 | ||
|
|
a5d9d5e575 | ||
|
|
95d645255d | ||
|
|
6c0fe3d7a1 | ||
|
|
071d3e65a3 | ||
|
|
95323ac4cb | ||
|
|
f7b60f3da7 | ||
|
|
806a30c3d8 | ||
|
|
74570676b5 | ||
|
|
903471ee43 | ||
|
|
9a54a94dca | ||
|
|
532b7a430c | ||
|
|
7e08058d0a | ||
|
|
47d84d4ab6 | ||
|
|
288bddcaf2 | ||
|
|
2d529bd581 | ||
|
|
300f5375c8 | ||
|
|
7f4c6352ba | ||
|
|
225decd286 | ||
|
|
539d41f19e | ||
|
|
ca92834493 | ||
|
|
d28fe93360 | ||
|
|
6ff69c0fa8 | ||
|
|
0409c9a9c0 | ||
|
|
c5bcf76353 | ||
|
|
20a4b9fb5a | ||
|
|
9d20637dcb | ||
|
|
da4efdb2d0 | ||
|
|
9fe4cc2d2b | ||
|
|
b82abc2827 | ||
|
|
8de1fb5289 | ||
|
|
0031fbf886 | ||
|
|
e24efe7528 | ||
|
|
109de73691 | ||
|
|
dc475462d0 | ||
|
|
19883c03ad | ||
|
|
46ae76081d | ||
|
|
63a931a10a | ||
|
|
a15da76566 | ||
|
|
465053f3ff | ||
|
|
bf99407816 | ||
|
|
0493d57b2e | ||
|
|
947864cff8 | ||
|
|
1074d23a90 | ||
|
|
24ffc97317 | ||
|
|
1f6b59f2f5 | ||
|
|
5314eb2d45 | ||
|
|
035b29e70b | ||
|
|
68ec7bce12 | ||
|
|
02b34c9811 | ||
|
|
99fcca3cb7 | ||
|
|
fc2ff27928 | ||
|
|
abaaef2285 | ||
|
|
8154a4dd77 | ||
|
|
6bbea198e5 | ||
|
|
2d569b8bcf | ||
|
|
da3272df76 | ||
|
|
3243b1a3cb | ||
|
|
2492bc69ff | ||
|
|
feb1c051e1 | ||
|
|
6fb57cb1a9 | ||
|
|
b9f0e63341 | ||
|
|
ca41edea22 | ||
|
|
f901f06992 | ||
|
|
6038e04ccc | ||
|
|
4e792018ac | ||
|
|
80700fa031 | ||
|
|
aa02170e5c | ||
|
|
bb7bd1a504 | ||
|
|
04d1b8e7c2 | ||
|
|
435f9358b1 | ||
|
|
0d22bf5cae | ||
|
|
044c233598 | ||
|
|
c160ff9d27 | ||
|
|
915adf6397 | ||
|
|
c7ed2ed59d | ||
|
|
9b91274cf2 | ||
|
|
e5ee6bd6eb | ||
|
|
e1611d1e18 | ||
|
|
60c22cbb5d | ||
|
|
c7dd93275e | ||
|
|
3f0201df3a | ||
|
|
bce8a58cf8 | ||
|
|
752156281f | ||
|
|
0073322758 | ||
|
|
ca3ca4557e | ||
|
|
056a9980d6 | ||
|
|
936964d273 | ||
|
|
4d274a3cec | ||
|
|
acd342259f | ||
|
|
67456c151f | ||
|
|
13ccf55cc8 | ||
|
|
73bffb5552 | ||
|
|
be8ee52af0 | ||
|
|
fbb16d6f22 | ||
|
|
96f8bf4a34 | ||
|
|
2f9a86524a | ||
|
|
e617e09ae3 | ||
|
|
6934aef60b | ||
|
|
8f4de39e7b | ||
|
|
fcb0c51e51 | ||
|
|
ec80eac6b9 | ||
|
|
84e600ac9f | ||
|
|
c64d95b0ec | ||
|
|
3e2ced2e8b | ||
|
|
6cc04feda8 | ||
|
|
0b50c17ed0 | ||
|
|
81b9153d2b | ||
|
|
e7c5307ca4 | ||
|
|
8fb377b4eb | ||
|
|
5642358937 | ||
|
|
00cd1386b5 | ||
|
|
da6c72e9b7 | ||
|
|
c318993a79 | ||
|
|
87c6e957f0 | ||
|
|
e133467ea1 | ||
|
|
a0429b243f | ||
|
|
21ae1fce2a | ||
|
|
53bc80e899 | ||
|
|
56dc337672 | ||
|
|
a04bafdb4c | ||
|
|
6ff473ab5d | ||
|
|
40bfc6bff3 | ||
|
|
c610cacee4 | ||
|
|
e41a08789a | ||
|
|
9d5bbf1f44 | ||
|
|
22268b8a33 | ||
|
|
0f58ce2322 | ||
|
|
1aad89ac97 | ||
|
|
e99a684354 | ||
|
|
5360f6dd77 | ||
|
|
c8d5708155 | ||
|
|
ebda00dc74 | ||
|
|
789092c76a | ||
|
|
967a2a4461 | ||
|
|
9288628ad7 | ||
|
|
0384ebb2ff | ||
|
|
869c40e835 | ||
|
|
579af85544 | ||
|
|
97f72c1faf | ||
|
|
262c3af33e | ||
|
|
dd7d9b66e5 | ||
|
|
f688da957c | ||
|
|
866ac3d5b8 | ||
|
|
63fce2a3ca | ||
|
|
33e0859847 | ||
|
|
b71922fabf | ||
|
|
ce0564a89c | ||
|
|
2a287b8d66 | ||
|
|
69713ae156 | ||
|
|
b930b9bf4f | ||
|
|
412f118d22 | ||
|
|
b60c2a9341 |
3
.github/ISSUE_TEMPLATE/config.yml
vendored
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -3,3 +3,6 @@ contact_links:
|
|||||||
- name: 🙋 Need help? Chat with us on Matrix
|
- name: 🙋 Need help? Chat with us on Matrix
|
||||||
url: https://matrix.to/#/#mempool.support:bitcoin.kyoto
|
url: https://matrix.to/#/#mempool.support:bitcoin.kyoto
|
||||||
about: For support requests or general questions
|
about: For support requests or general questions
|
||||||
|
- name: 🌐 Want to help with translations? Use Transifex
|
||||||
|
url: https://www.transifex.com/mempool/mempool
|
||||||
|
about: All translations work is done on Transifex
|
||||||
|
|||||||
6
.github/pull_request_template.md
vendored
Normal file
6
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<!--
|
||||||
|
Please do not open pull requests for translations.
|
||||||
|
|
||||||
|
All translations work is done on Transifex:
|
||||||
|
https://www.transifex.com/mempool/mempool
|
||||||
|
-->
|
||||||
94
.github/workflows/ci.yml
vendored
Normal file
94
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
name: CI Pipeline for the Backend and Frontend
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, review_requested, synchronize]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
backend:
|
||||||
|
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node: ["16.16.0", "18.5.0"]
|
||||||
|
flavor: ["dev", "prod"]
|
||||||
|
fail-fast: false
|
||||||
|
runs-on: "ubuntu-latest"
|
||||||
|
|
||||||
|
name: Backend (${{ matrix.flavor }}) - node ${{ matrix.node }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
path: ${{ matrix.node }}/${{ matrix.flavor }}
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node }}
|
||||||
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|
||||||
|
- name: Install
|
||||||
|
if: ${{ matrix.flavor == 'dev'}}
|
||||||
|
run: npm ci
|
||||||
|
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/backend
|
||||||
|
|
||||||
|
- name: Install (Prod dependencies only)
|
||||||
|
if: ${{ matrix.flavor == 'prod'}}
|
||||||
|
run: npm ci --omit=dev --omit=optional
|
||||||
|
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/backend
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
if: ${{ matrix.flavor == 'dev'}}
|
||||||
|
run: npm run lint
|
||||||
|
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/backend
|
||||||
|
|
||||||
|
# - name: Test
|
||||||
|
# run: npm run test
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/backend
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node: ["16.15.0", "18.5.0"]
|
||||||
|
flavor: ["dev", "prod"]
|
||||||
|
fail-fast: false
|
||||||
|
runs-on: "ubuntu-latest"
|
||||||
|
|
||||||
|
name: Frontend (${{ matrix.flavor }}) - node ${{ matrix.node }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
path: ${{ matrix.node }}/${{ matrix.flavor }}
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node }}
|
||||||
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|
||||||
|
- name: Install (Prod dependencies only)
|
||||||
|
run: npm ci --omit=dev --omit=optional
|
||||||
|
if: ${{ matrix.flavor == 'prod'}}
|
||||||
|
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/frontend
|
||||||
|
|
||||||
|
- name: Install
|
||||||
|
if: ${{ matrix.flavor == 'dev'}}
|
||||||
|
run: npm ci
|
||||||
|
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/frontend
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
if: ${{ matrix.flavor == 'dev'}}
|
||||||
|
run: npm run lint
|
||||||
|
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/frontend
|
||||||
|
|
||||||
|
# - name: Test
|
||||||
|
# run: npm run test
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/frontend
|
||||||
23
.github/workflows/cypress.yml
vendored
23
.github/workflows/cypress.yml
vendored
@@ -1,12 +1,11 @@
|
|||||||
name: Cypress Tests
|
name: Cypress Tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
pull_request:
|
pull_request:
|
||||||
|
types: [ opened, review_requested, synchronize ]
|
||||||
jobs:
|
jobs:
|
||||||
cypress:
|
cypress:
|
||||||
|
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@@ -25,7 +24,7 @@ jobs:
|
|||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: frontend/package-lock.json
|
cache-dependency-path: frontend/package-lock.json
|
||||||
- name: ${{ matrix.browser }} browser tests (Mempool)
|
- name: ${{ matrix.browser }} browser tests (Mempool)
|
||||||
uses: cypress-io/github-action@v2
|
uses: cypress-io/github-action@v4
|
||||||
with:
|
with:
|
||||||
tag: ${{ github.event_name }}
|
tag: ${{ github.event_name }}
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
@@ -36,9 +35,9 @@ jobs:
|
|||||||
record: true
|
record: true
|
||||||
parallel: true
|
parallel: true
|
||||||
spec: |
|
spec: |
|
||||||
cypress/integration/mainnet/*.spec.ts
|
cypress/e2e/mainnet/*.spec.ts
|
||||||
cypress/integration/signet/*.spec.ts
|
cypress/e2e/signet/*.spec.ts
|
||||||
cypress/integration/testnet/*.spec.ts
|
cypress/e2e/testnet/*.spec.ts
|
||||||
group: Tests on ${{ matrix.browser }} (Mempool)
|
group: Tests on ${{ matrix.browser }} (Mempool)
|
||||||
browser: ${{ matrix.browser }}
|
browser: ${{ matrix.browser }}
|
||||||
ci-build-id: '${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}'
|
ci-build-id: '${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}'
|
||||||
@@ -49,7 +48,7 @@ jobs:
|
|||||||
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
|
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
|
||||||
|
|
||||||
- name: ${{ matrix.browser }} browser tests (Liquid)
|
- name: ${{ matrix.browser }} browser tests (Liquid)
|
||||||
uses: cypress-io/github-action@v2
|
uses: cypress-io/github-action@v4
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
tag: ${{ github.event_name }}
|
tag: ${{ github.event_name }}
|
||||||
@@ -61,8 +60,8 @@ jobs:
|
|||||||
record: true
|
record: true
|
||||||
parallel: true
|
parallel: true
|
||||||
spec: |
|
spec: |
|
||||||
cypress/integration/liquid/liquid.spec.ts
|
cypress/e2e/liquid/liquid.spec.ts
|
||||||
cypress/integration/liquidtestnet/liquidtestnet.spec.ts
|
cypress/e2e/liquidtestnet/liquidtestnet.spec.ts
|
||||||
group: Tests on ${{ matrix.browser }} (Liquid)
|
group: Tests on ${{ matrix.browser }} (Liquid)
|
||||||
browser: ${{ matrix.browser }}
|
browser: ${{ matrix.browser }}
|
||||||
ci-build-id: '${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}'
|
ci-build-id: '${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}'
|
||||||
@@ -73,7 +72,7 @@ jobs:
|
|||||||
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
|
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
|
||||||
|
|
||||||
- name: ${{ matrix.browser }} browser tests (Bisq)
|
- name: ${{ matrix.browser }} browser tests (Bisq)
|
||||||
uses: cypress-io/github-action@v2
|
uses: cypress-io/github-action@v4
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
tag: ${{ github.event_name }}
|
tag: ${{ github.event_name }}
|
||||||
@@ -84,7 +83,7 @@ jobs:
|
|||||||
wait-on-timeout: 120
|
wait-on-timeout: 120
|
||||||
record: true
|
record: true
|
||||||
parallel: true
|
parallel: true
|
||||||
spec: cypress/integration/bisq/bisq.spec.ts
|
spec: cypress/e2e/bisq/bisq.spec.ts
|
||||||
group: Tests on ${{ matrix.browser }} (Bisq)
|
group: Tests on ${{ matrix.browser }} (Bisq)
|
||||||
browser: ${{ matrix.browser }}
|
browser: ${{ matrix.browser }}
|
||||||
ci-build-id: '${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}'
|
ci-build-id: '${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}'
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@ sitemap
|
|||||||
data
|
data
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
backend/mempool-config.json
|
backend/mempool-config.json
|
||||||
|
*.swp
|
||||||
|
|||||||
@@ -29,5 +29,5 @@ Mempool can be conveniently installed on the following full-node distros:
|
|||||||
Mempool can be installed in other ways too, but we only recommend doing so if you're a developer, have experience managing servers, or otherwise know what you're doing.
|
Mempool can be installed in other ways too, but we only recommend doing so if you're a developer, have experience managing servers, or otherwise know what you're doing.
|
||||||
|
|
||||||
- See the [`docker/`](./docker/) directory for instructions on deploying Mempool with Docker.
|
- See the [`docker/`](./docker/) directory for instructions on deploying Mempool with Docker.
|
||||||
- See the [`backend/`](./backend/) and [`frontend/`](./frontend/) directories for manual install instructions oriented for developers and small-scale deployments.
|
- See the [`backend/`](./backend/) and [`frontend/`](./frontend/) directories for manual install instructions oriented for developers.
|
||||||
- See the [`production/`](./production/) directory for guidance on setting up a more serious Mempool instance designed for high performance at scale.
|
- See the [`production/`](./production/) directory for guidance on setting up a more serious Mempool instance designed for high performance at scale.
|
||||||
|
|||||||
17
backend/.editorconfig
Normal file
17
backend/.editorconfig
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Editor configuration, see https://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.ts]
|
||||||
|
quote_type = single
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
max_line_length = off
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
2
backend/.eslintignore
Normal file
2
backend/.eslintignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
33
backend/.eslintrc
Normal file
33
backend/.eslintrc
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"plugins": [
|
||||||
|
"@typescript-eslint"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/ban-ts-comment": 1,
|
||||||
|
"@typescript-eslint/ban-types": 1,
|
||||||
|
"@typescript-eslint/no-empty-function": 1,
|
||||||
|
"@typescript-eslint/no-explicit-any": 1,
|
||||||
|
"@typescript-eslint/no-inferrable-types": 1,
|
||||||
|
"@typescript-eslint/no-namespace": 1,
|
||||||
|
"@typescript-eslint/no-this-alias": 1,
|
||||||
|
"@typescript-eslint/no-var-requires": 1,
|
||||||
|
"no-console": 1,
|
||||||
|
"no-constant-condition": 1,
|
||||||
|
"no-dupe-else-if": 1,
|
||||||
|
"no-empty": 1,
|
||||||
|
"no-prototype-builtins": 1,
|
||||||
|
"no-self-assign": 1,
|
||||||
|
"no-useless-catch": 1,
|
||||||
|
"no-var": 1,
|
||||||
|
"prefer-const": 1,
|
||||||
|
"prefer-rest-params": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
2
backend/.prettierignore
Normal file
2
backend/.prettierignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
package-lock.json
|
||||||
6
backend/.prettierrc
Normal file
6
backend/.prettierrc
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"printWidth": 80,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5"
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Mempool Backend
|
# Mempool Backend
|
||||||
|
|
||||||
These instructions are mostly intended for developers, but can be used as a basis for personal or small-scale production setups.
|
These instructions are mostly intended for developers.
|
||||||
|
|
||||||
If you choose to use these instructions for a production setup, be aware that you will still probably need to do additional configuration for your specific OS, environment, use-case, etc. We do our best here to provide a good starting point, but only proceed if you know what you're doing. Mempool does not provide support for custom setups.
|
If you choose to use these instructions for a production setup, be aware that you will still probably need to do additional configuration for your specific OS, environment, use-case, etc. We do our best here to provide a good starting point, but only proceed if you know what you're doing. Mempool does not provide support for custom setups.
|
||||||
|
|
||||||
@@ -77,13 +77,13 @@ Query OK, 0 rows affected (0.00 sec)
|
|||||||
|
|
||||||
#### Build
|
#### Build
|
||||||
|
|
||||||
_Make sure to use Node.js 16.15 and npm 7._
|
_Make sure to use Node.js 16.10 and npm 7._
|
||||||
|
|
||||||
Install dependencies with `npm` and build the backend:
|
Install dependencies with `npm` and build the backend:
|
||||||
|
|
||||||
```
|
```
|
||||||
cd backend
|
cd backend
|
||||||
npm install # add --prod for production
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -159,3 +159,57 @@ nodemon src/index.ts --ignore cache/ --ignore pools.json
|
|||||||
```
|
```
|
||||||
|
|
||||||
`nodemon` should be in npm's global binary folder. If needed, you can determine where that is with `npm -g bin`.
|
`nodemon` should be in npm's global binary folder. If needed, you can determine where that is with `npm -g bin`.
|
||||||
|
|
||||||
|
### Useful Regtest Commands
|
||||||
|
|
||||||
|
Helpful link: https://gist.github.com/System-Glitch/cb4e87bf1ae3fec9925725bb3ebe223a
|
||||||
|
|
||||||
|
Run bitcoind on regtest:
|
||||||
|
```
|
||||||
|
bitcoind -regtest -rpcport=8332
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a new wallet, if needed:
|
||||||
|
```
|
||||||
|
bitcoin-cli -regtest -rpcport=8332 createwallet test
|
||||||
|
```
|
||||||
|
|
||||||
|
Load wallet (this command may take a while if you have lot of UTXOs):
|
||||||
|
```
|
||||||
|
bitcoin-cli -regtest -rpcport=8332 loadwallet test
|
||||||
|
```
|
||||||
|
|
||||||
|
Get a new address:
|
||||||
|
```
|
||||||
|
address=$(./src/bitcoin-cli -regtest -rpcport=8332 getnewaddress)
|
||||||
|
```
|
||||||
|
|
||||||
|
Mine blocks to the previously generated address. You need at least 101 blocks before you can spend. This will take some time to execute (~1 min):
|
||||||
|
```
|
||||||
|
bitcoin-cli -regtest -rpcport=8332 generatetoaddress 101 $address
|
||||||
|
```
|
||||||
|
|
||||||
|
Send 0.1 BTC at 5 sat/vB to another address:
|
||||||
|
```
|
||||||
|
./src/bitcoin-cli -named -regtest -rpcport=8332 sendtoaddress address=$(./src/bitcoin-cli -regtest -rpcport=8332 getnewaddress) amount=0.1 fee_rate=5
|
||||||
|
```
|
||||||
|
|
||||||
|
See more example of `sendtoaddress`:
|
||||||
|
```
|
||||||
|
./src/bitcoin-cli sendtoaddress # will print the help
|
||||||
|
```
|
||||||
|
|
||||||
|
Mini script to generate transactions with random TX fee-rate (between 1 to 100 sat/vB). It's slow so don't expect to use this to test mempool spam, except if you let it run for a long time, or maybe with multiple regtest nodes connected to each other.
|
||||||
|
```
|
||||||
|
#!/bin/bash
|
||||||
|
address=$(./src/bitcoin-cli -regtest -rpcport=8332 getnewaddress)
|
||||||
|
for i in {1..1000000}
|
||||||
|
do
|
||||||
|
./src/bitcoin-cli -regtest -rpcport=8332 -named sendtoaddress address=$address amount=0.01 fee_rate=$(jot -r 1 1 100)
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
Generate block at regular interval (every 10 seconds in this example):
|
||||||
|
```
|
||||||
|
watch -n 10 "./src/bitcoin-cli -regtest -rpcport=8332 generatetoaddress 1 $address"
|
||||||
|
```
|
||||||
|
|||||||
@@ -13,12 +13,15 @@
|
|||||||
"INITIAL_BLOCKS_AMOUNT": 8,
|
"INITIAL_BLOCKS_AMOUNT": 8,
|
||||||
"MEMPOOL_BLOCKS_AMOUNT": 8,
|
"MEMPOOL_BLOCKS_AMOUNT": 8,
|
||||||
"INDEXING_BLOCKS_AMOUNT": 11000,
|
"INDEXING_BLOCKS_AMOUNT": 11000,
|
||||||
|
"BLOCKS_SUMMARIES_INDEXING": false,
|
||||||
"PRICE_FEED_UPDATE_INTERVAL": 600,
|
"PRICE_FEED_UPDATE_INTERVAL": 600,
|
||||||
"USE_SECOND_NODE_FOR_MINFEE": false,
|
"USE_SECOND_NODE_FOR_MINFEE": false,
|
||||||
"EXTERNAL_ASSETS": [
|
"EXTERNAL_ASSETS": [],
|
||||||
"https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json"
|
"EXTERNAL_MAX_RETRY": 1,
|
||||||
],
|
"EXTERNAL_RETRY_INTERVAL": 0,
|
||||||
"STDOUT_LOG_MIN_PRIORITY": "debug"
|
"USER_AGENT": "mempool",
|
||||||
|
"STDOUT_LOG_MIN_PRIORITY": "debug",
|
||||||
|
"AUTOMATIC_BLOCK_REINDEXING": false
|
||||||
},
|
},
|
||||||
"CORE_RPC": {
|
"CORE_RPC": {
|
||||||
"HOST": "127.0.0.1",
|
"HOST": "127.0.0.1",
|
||||||
@@ -66,6 +69,7 @@
|
|||||||
},
|
},
|
||||||
"SOCKS5PROXY": {
|
"SOCKS5PROXY": {
|
||||||
"ENABLED": false,
|
"ENABLED": false,
|
||||||
|
"USE_ONION": true,
|
||||||
"HOST": "127.0.0.1",
|
"HOST": "127.0.0.1",
|
||||||
"PORT": 9050,
|
"PORT": 9050,
|
||||||
"USERNAME": "",
|
"USERNAME": "",
|
||||||
@@ -74,5 +78,13 @@
|
|||||||
"PRICE_DATA_SERVER": {
|
"PRICE_DATA_SERVER": {
|
||||||
"TOR_URL": "http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices",
|
"TOR_URL": "http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices",
|
||||||
"CLEARNET_URL": "https://price.bisq.wiz.biz/getAllMarketPrices"
|
"CLEARNET_URL": "https://price.bisq.wiz.biz/getAllMarketPrices"
|
||||||
|
},
|
||||||
|
"EXTERNAL_DATA_SERVER": {
|
||||||
|
"MEMPOOL_API": "https://mempool.space/api/v1",
|
||||||
|
"MEMPOOL_ONION": "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1",
|
||||||
|
"LIQUID_API": "https://liquid.network/api/v1",
|
||||||
|
"LIQUID_ONION": "http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1",
|
||||||
|
"BISQ_URL": "https://bisq.markets/api",
|
||||||
|
"BISQ_ONION": "http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2974
backend/package-lock.json
generated
2974
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mempool-backend",
|
"name": "mempool-backend",
|
||||||
"version": "2.4.0-dev",
|
"version": "2.4.2-dev",
|
||||||
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
||||||
"license": "GNU Affero General Public License v3.0",
|
"license": "GNU Affero General Public License v3.0",
|
||||||
"homepage": "https://mempool.space",
|
"homepage": "https://mempool.space",
|
||||||
@@ -20,29 +20,37 @@
|
|||||||
],
|
],
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "./node_modules/@angular/cli/bin/ng",
|
|
||||||
"tsc": "./node_modules/typescript/bin/tsc",
|
"tsc": "./node_modules/typescript/bin/tsc",
|
||||||
"build": "npm run tsc",
|
"build": "npm run tsc",
|
||||||
"start": "node --max-old-space-size=2048 dist/index.js",
|
"start": "node --max-old-space-size=2048 dist/index.js",
|
||||||
"start-production": "node --max-old-space-size=4096 dist/index.js",
|
"start-production": "node --max-old-space-size=4096 dist/index.js",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"lint": "./node_modules/.bin/eslint . --ext .ts",
|
||||||
|
"lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix",
|
||||||
|
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mempool/electrum-client": "^1.1.7",
|
"@mempool/electrum-client": "^1.1.7",
|
||||||
|
"@types/node": "^16.11.41",
|
||||||
"axios": "~0.27.2",
|
"axios": "~0.27.2",
|
||||||
"bitcoinjs-lib": "6.0.1",
|
"bitcoinjs-lib": "6.0.1",
|
||||||
"crypto-js": "^4.0.0",
|
"crypto-js": "^4.0.0",
|
||||||
"express": "^4.18.0",
|
"express": "^4.18.0",
|
||||||
"mysql2": "2.3.3",
|
"mysql2": "2.3.3",
|
||||||
"node-worker-threads-pool": "^1.5.1",
|
"node-worker-threads-pool": "^1.5.1",
|
||||||
"socks-proxy-agent": "^6.2.0",
|
"socks-proxy-agent": "~7.0.0",
|
||||||
"typescript": "~4.7.2",
|
"typescript": "~4.7.4",
|
||||||
"ws": "~8.7.0"
|
"ws": "~8.8.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/compression": "^1.7.2",
|
"@types/compression": "^1.7.2",
|
||||||
"@types/ws": "~8.5.3",
|
"@types/crypto-js": "^4.1.1",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
"tslint": "^6.1.0"
|
"@types/ws": "~8.5.3",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.30.5",
|
||||||
|
"@typescript-eslint/parser": "^5.30.5",
|
||||||
|
"eslint": "^8.19.0",
|
||||||
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"prettier": "^2.7.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import axios from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
|
import * as http from 'http';
|
||||||
|
import * as https from 'https';
|
||||||
|
import { SocksProxyAgent } from 'socks-proxy-agent';
|
||||||
import { BisqBlocks, BisqBlock, BisqTransaction, BisqStats, BisqTrade } from './interfaces';
|
import { BisqBlocks, BisqBlock, BisqTransaction, BisqStats, BisqTrade } from './interfaces';
|
||||||
import { Common } from '../common';
|
import { Common } from '../common';
|
||||||
import { BlockExtended } from '../../mempool.interfaces';
|
import { BlockExtended } from '../../mempool.interfaces';
|
||||||
import { StaticPool } from 'node-worker-threads-pool';
|
import { StaticPool } from 'node-worker-threads-pool';
|
||||||
|
import backendInfo from '../backend-info';
|
||||||
import logger from '../../logger';
|
import logger from '../../logger';
|
||||||
|
|
||||||
class Bisq {
|
class Bisq {
|
||||||
@@ -143,12 +147,59 @@ class Bisq {
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
private async updatePrice() {
|
||||||
|
type axiosOptions = {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': string
|
||||||
|
};
|
||||||
|
timeout: number;
|
||||||
|
httpAgent?: http.Agent;
|
||||||
|
httpsAgent?: https.Agent;
|
||||||
|
}
|
||||||
|
const setDelay = (secs: number = 1): Promise<void> => new Promise(resolve => setTimeout(() => resolve(), secs * 1000));
|
||||||
|
const BISQ_URL = (config.SOCKS5PROXY.ENABLED === true) && (config.SOCKS5PROXY.USE_ONION === true) ? config.EXTERNAL_DATA_SERVER.BISQ_ONION : config.EXTERNAL_DATA_SERVER.BISQ_URL;
|
||||||
|
const isHTTP = (new URL(BISQ_URL).protocol.split(':')[0] === 'http') ? true : false;
|
||||||
|
const axiosOptions: axiosOptions = {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': (config.MEMPOOL.USER_AGENT === 'mempool') ? `mempool/v${backendInfo.getBackendInfo().version}` : `${config.MEMPOOL.USER_AGENT}`
|
||||||
|
},
|
||||||
|
timeout: config.SOCKS5PROXY.ENABLED ? 30000 : 10000
|
||||||
|
};
|
||||||
|
let retry = 0;
|
||||||
|
|
||||||
private updatePrice() {
|
while(retry < config.MEMPOOL.EXTERNAL_MAX_RETRY) {
|
||||||
axios.get<BisqTrade[]>('https://bisq.markets/api/trades/?market=bsq_btc', { timeout: 10000 })
|
try {
|
||||||
.then((response) => {
|
if (config.SOCKS5PROXY.ENABLED) {
|
||||||
|
const socksOptions: any = {
|
||||||
|
agentOptions: {
|
||||||
|
keepAlive: true,
|
||||||
|
},
|
||||||
|
hostname: config.SOCKS5PROXY.HOST,
|
||||||
|
port: config.SOCKS5PROXY.PORT
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config.SOCKS5PROXY.USERNAME && config.SOCKS5PROXY.PASSWORD) {
|
||||||
|
socksOptions.username = config.SOCKS5PROXY.USERNAME;
|
||||||
|
socksOptions.password = config.SOCKS5PROXY.PASSWORD;
|
||||||
|
} else {
|
||||||
|
// Retry with different tor circuits https://stackoverflow.com/a/64960234
|
||||||
|
socksOptions.username = `circuit${retry}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle proxy agent for onion addresses
|
||||||
|
if (isHTTP) {
|
||||||
|
axiosOptions.httpAgent = new SocksProxyAgent(socksOptions);
|
||||||
|
} else {
|
||||||
|
axiosOptions.httpsAgent = new SocksProxyAgent(socksOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: AxiosResponse = await axios.get(`${BISQ_URL}/trades/?market=bsq_btc`, axiosOptions);
|
||||||
|
if (data.statusText === 'error' || !data.data) {
|
||||||
|
throw new Error(`Could not fetch data from Bisq market, Error: ${data.status}`);
|
||||||
|
}
|
||||||
const prices: number[] = [];
|
const prices: number[] = [];
|
||||||
response.data.forEach((trade) => {
|
data.data.forEach((trade) => {
|
||||||
prices.push(parseFloat(trade.price) * 100000000);
|
prices.push(parseFloat(trade.price) * 100000000);
|
||||||
});
|
});
|
||||||
prices.sort((a, b) => a - b);
|
prices.sort((a, b) => a - b);
|
||||||
@@ -156,9 +207,14 @@ class Bisq {
|
|||||||
if (this.priceUpdateCallbackFunction) {
|
if (this.priceUpdateCallbackFunction) {
|
||||||
this.priceUpdateCallbackFunction(this.price);
|
this.priceUpdateCallbackFunction(this.price);
|
||||||
}
|
}
|
||||||
}).catch((err) => {
|
logger.debug('Successfully updated Bisq market price');
|
||||||
logger.err('Error updating Bisq market price: ' + err);
|
break;
|
||||||
});
|
} catch (e) {
|
||||||
|
logger.err('Error updating Bisq market price: ' + (e instanceof Error ? e.message : e));
|
||||||
|
await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL);
|
||||||
|
retry++;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadBisqDumpFile(): Promise<void> {
|
private async loadBisqDumpFile(): Promise<void> {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export interface AbstractBitcoinApi {
|
|||||||
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>;
|
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>;
|
||||||
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>;
|
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>;
|
||||||
$getBlockHeightTip(): Promise<number>;
|
$getBlockHeightTip(): Promise<number>;
|
||||||
|
$getBlockHashTip(): Promise<string>;
|
||||||
$getTxIdsForBlock(hash: string): Promise<string[]>;
|
$getTxIdsForBlock(hash: string): Promise<string[]>;
|
||||||
$getBlockHash(height: number): Promise<string>;
|
$getBlockHash(height: number): Promise<string>;
|
||||||
$getBlockHeader(hash: string): Promise<string>;
|
$getBlockHeader(hash: string): Promise<string>;
|
||||||
@@ -13,6 +14,7 @@ export interface AbstractBitcoinApi {
|
|||||||
$getAddressPrefix(prefix: string): string[];
|
$getAddressPrefix(prefix: string): string[];
|
||||||
$sendRawTransaction(rawTransaction: string): Promise<string>;
|
$sendRawTransaction(rawTransaction: string): Promise<string>;
|
||||||
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
|
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
|
||||||
|
$getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
|
||||||
}
|
}
|
||||||
export interface BitcoinRpcCredentials {
|
export interface BitcoinRpcCredentials {
|
||||||
host: string;
|
host: string;
|
||||||
|
|||||||
@@ -73,6 +73,14 @@ export namespace IBitcoinApi {
|
|||||||
time: number; // (numeric) Same as blocktime
|
time: number; // (numeric) Same as blocktime
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface VerboseBlock extends Block {
|
||||||
|
tx: VerboseTransaction[]; // The transactions in the format of the getrawtransaction RPC. Different from verbosity = 1 "tx" result
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VerboseTransaction extends Transaction {
|
||||||
|
fee?: number; // (numeric) The transaction fee in BTC, omitted if block undo data is not available
|
||||||
|
}
|
||||||
|
|
||||||
export interface Vin {
|
export interface Vin {
|
||||||
txid?: string; // (string) The transaction id
|
txid?: string; // (string) The transaction id
|
||||||
vout?: number; // (string)
|
vout?: number; // (string)
|
||||||
|
|||||||
@@ -64,6 +64,13 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$getBlockHashTip(): Promise<string> {
|
||||||
|
return this.bitcoindClient.getChainTips()
|
||||||
|
.then((result: IBitcoinApi.ChainTips[]) => {
|
||||||
|
return result.find(tip => tip.status === 'active')!.hash;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$getTxIdsForBlock(hash: string): Promise<string[]> {
|
$getTxIdsForBlock(hash: string): Promise<string[]> {
|
||||||
return this.bitcoindClient.getBlock(hash, 1)
|
return this.bitcoindClient.getBlock(hash, 1)
|
||||||
.then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx);
|
.then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx);
|
||||||
@@ -141,6 +148,15 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
return outSpends;
|
return outSpends;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]> {
|
||||||
|
const outspends: IEsploraApi.Outspend[][] = [];
|
||||||
|
for (const tx of txId) {
|
||||||
|
const outspend = await this.$getOutspends(tx);
|
||||||
|
outspends.push(outspend);
|
||||||
|
}
|
||||||
|
return outspends;
|
||||||
|
}
|
||||||
|
|
||||||
$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,12 +1,11 @@
|
|||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
|
import Client from '@mempool/electrum-client';
|
||||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
|
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
|
||||||
import { IEsploraApi } from './esplora-api.interface';
|
import { IEsploraApi } from './esplora-api.interface';
|
||||||
import { IElectrumApi } from './electrum-api.interface';
|
import { IElectrumApi } from './electrum-api.interface';
|
||||||
import BitcoinApi from './bitcoin-api';
|
import BitcoinApi from './bitcoin-api';
|
||||||
import logger from '../../logger';
|
import logger from '../../logger';
|
||||||
import * as ElectrumClient from '@mempool/electrum-client';
|
import crypto from "crypto-js";
|
||||||
import * as sha256 from 'crypto-js/sha256';
|
|
||||||
import * as hexEnc from 'crypto-js/enc-hex';
|
|
||||||
import loadingIndicators from '../loading-indicators';
|
import loadingIndicators from '../loading-indicators';
|
||||||
import memoryCache from '../memory-cache';
|
import memoryCache from '../memory-cache';
|
||||||
|
|
||||||
@@ -26,7 +25,7 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
|
|||||||
onLog: (str) => { logger.debug(str); },
|
onLog: (str) => { logger.debug(str); },
|
||||||
};
|
};
|
||||||
|
|
||||||
this.electrumClient = new ElectrumClient(
|
this.electrumClient = new Client(
|
||||||
config.ELECTRUM.PORT,
|
config.ELECTRUM.PORT,
|
||||||
config.ELECTRUM.HOST,
|
config.ELECTRUM.HOST,
|
||||||
config.ELECTRUM.TLS_ENABLED ? 'tls' : 'tcp',
|
config.ELECTRUM.TLS_ENABLED ? 'tls' : 'tcp',
|
||||||
@@ -35,7 +34,7 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.electrumClient.initElectrum(electrumConfig, electrumPersistencePolicy)
|
this.electrumClient.initElectrum(electrumConfig, electrumPersistencePolicy)
|
||||||
.then(() => {})
|
.then(() => { })
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
logger.err(`Error connecting to Electrum Server at ${config.ELECTRUM.HOST}:${config.ELECTRUM.PORT}`);
|
logger.err(`Error connecting to Electrum Server at ${config.ELECTRUM.HOST}:${config.ELECTRUM.PORT}`);
|
||||||
});
|
});
|
||||||
@@ -95,7 +94,7 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
|
|||||||
async $getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]> {
|
async $getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]> {
|
||||||
const addressInfo = await this.bitcoindClient.validateAddress(address);
|
const addressInfo = await this.bitcoindClient.validateAddress(address);
|
||||||
if (!addressInfo || !addressInfo.isvalid) {
|
if (!addressInfo || !addressInfo.isvalid) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -144,8 +143,8 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private encodeScriptHash(scriptPubKey: string): string {
|
private encodeScriptHash(scriptPubKey: string): string {
|
||||||
const addrScripthash = hexEnc.stringify(sha256(hexEnc.parse(scriptPubKey)));
|
const addrScripthash = crypto.enc.Hex.stringify(crypto.SHA256(crypto.enc.Hex.parse(scriptPubKey)));
|
||||||
return addrScripthash.match(/.{2}/g).reverse().join('');
|
return addrScripthash!.match(/.{2}/g)!.reverse().join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ class ElectrsApi implements AbstractBitcoinApi {
|
|||||||
.then((response) => response.data);
|
.then((response) => response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$getBlockHashTip(): Promise<string> {
|
||||||
|
return axios.get<string>(config.ESPLORA.REST_API_URL + '/blocks/tip/hash', this.axiosConfig)
|
||||||
|
.then((response) => response.data);
|
||||||
|
}
|
||||||
|
|
||||||
$getTxIdsForBlock(hash: string): Promise<string[]> {
|
$getTxIdsForBlock(hash: string): Promise<string[]> {
|
||||||
return axios.get<string[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids', this.axiosConfig)
|
return axios.get<string[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids', this.axiosConfig)
|
||||||
.then((response) => response.data);
|
.then((response) => response.data);
|
||||||
@@ -61,8 +66,18 @@ class ElectrsApi implements AbstractBitcoinApi {
|
|||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$getOutspends(): Promise<IEsploraApi.Outspend[]> {
|
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> {
|
||||||
throw new Error('Method not implemented.');
|
return axios.get<IEsploraApi.Outspend[]>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends', this.axiosConfig)
|
||||||
|
.then((response) => response.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]> {
|
||||||
|
const outspends: IEsploraApi.Outspend[][] = [];
|
||||||
|
for (const tx of txId) {
|
||||||
|
const outspend = await this.$getOutspends(tx);
|
||||||
|
outspends.push(outspend);
|
||||||
|
}
|
||||||
|
return outspends;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import config from '../config';
|
|||||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import memPool from './mempool';
|
import memPool from './mempool';
|
||||||
import { BlockExtended, PoolTag, TransactionExtended, TransactionMinerInfo } from '../mempool.interfaces';
|
import { BlockExtended, BlockSummary, PoolTag, TransactionExtended, TransactionStripped, TransactionMinerInfo } from '../mempool.interfaces';
|
||||||
import { Common } from './common';
|
import { Common } from './common';
|
||||||
import diskCache from './disk-cache';
|
import diskCache from './disk-cache';
|
||||||
import transactionUtils from './transaction-utils';
|
import transactionUtils from './transaction-utils';
|
||||||
import bitcoinClient from './bitcoin/bitcoin-client';
|
import bitcoinClient from './bitcoin/bitcoin-client';
|
||||||
|
import { IBitcoinApi } from './bitcoin/bitcoin-api.interface';
|
||||||
import { IEsploraApi } from './bitcoin/esplora-api.interface';
|
import { IEsploraApi } from './bitcoin/esplora-api.interface';
|
||||||
import poolsRepository from '../repositories/PoolsRepository';
|
import poolsRepository from '../repositories/PoolsRepository';
|
||||||
import blocksRepository from '../repositories/BlocksRepository';
|
import blocksRepository from '../repositories/BlocksRepository';
|
||||||
@@ -16,11 +17,15 @@ import { prepareBlock } from '../utils/blocks-utils';
|
|||||||
import BlocksRepository from '../repositories/BlocksRepository';
|
import BlocksRepository from '../repositories/BlocksRepository';
|
||||||
import HashratesRepository from '../repositories/HashratesRepository';
|
import HashratesRepository from '../repositories/HashratesRepository';
|
||||||
import indexer from '../indexer';
|
import indexer from '../indexer';
|
||||||
import fiatConversion from './fiat-conversion';
|
import poolsParser from './pools-parser';
|
||||||
import RatesRepository from '../repositories/RatesRepository';
|
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
|
||||||
|
import mining from './mining';
|
||||||
|
import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmentsRepository';
|
||||||
|
import difficultyAdjustment from './difficulty-adjustment';
|
||||||
|
|
||||||
class Blocks {
|
class Blocks {
|
||||||
private blocks: BlockExtended[] = [];
|
private blocks: BlockExtended[] = [];
|
||||||
|
private blockSummaries: BlockSummary[] = [];
|
||||||
private currentBlockHeight = 0;
|
private currentBlockHeight = 0;
|
||||||
private currentDifficulty = 0;
|
private currentDifficulty = 0;
|
||||||
private lastDifficultyAdjustmentTime = 0;
|
private lastDifficultyAdjustmentTime = 0;
|
||||||
@@ -37,6 +42,14 @@ class Blocks {
|
|||||||
this.blocks = blocks;
|
this.blocks = blocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getBlockSummaries(): BlockSummary[] {
|
||||||
|
return this.blockSummaries;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setBlockSummaries(blockSummaries: BlockSummary[]) {
|
||||||
|
this.blockSummaries = blockSummaries;
|
||||||
|
}
|
||||||
|
|
||||||
public setNewBlockCallback(fn: (block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void) {
|
public setNewBlockCallback(fn: (block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void) {
|
||||||
this.newBlockCallbacks.push(fn);
|
this.newBlockCallbacks.push(fn);
|
||||||
}
|
}
|
||||||
@@ -105,6 +118,27 @@ class Blocks {
|
|||||||
return transactions;
|
return transactions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a block summary (list of stripped transactions)
|
||||||
|
* @param block
|
||||||
|
* @returns BlockSummary
|
||||||
|
*/
|
||||||
|
private summarizeBlock(block: IBitcoinApi.VerboseBlock): BlockSummary {
|
||||||
|
const stripped = block.tx.map((tx) => {
|
||||||
|
return {
|
||||||
|
txid: tx.txid,
|
||||||
|
vsize: tx.vsize,
|
||||||
|
fee: tx.fee ? Math.round(tx.fee * 100000000) : 0,
|
||||||
|
value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0) * 100000000)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: block.hash,
|
||||||
|
transactions: stripped
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a block with additional data (reward, coinbase, fees...)
|
* Return a block with additional data (reward, coinbase, fees...)
|
||||||
* @param block
|
* @param block
|
||||||
@@ -134,12 +168,16 @@ class Blocks {
|
|||||||
blockExtended.extras.avgFeeRate = stats.avgfeerate;
|
blockExtended.extras.avgFeeRate = stats.avgfeerate;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK)) {
|
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
|
||||||
let pool: PoolTag;
|
let pool: PoolTag;
|
||||||
if (blockExtended.extras?.coinbaseTx !== undefined) {
|
if (blockExtended.extras?.coinbaseTx !== undefined) {
|
||||||
pool = await this.$findBlockMiner(blockExtended.extras?.coinbaseTx);
|
pool = await this.$findBlockMiner(blockExtended.extras?.coinbaseTx);
|
||||||
} else {
|
} else {
|
||||||
pool = await poolsRepository.$getUnknownPool();
|
if (config.DATABASE.ENABLED === true) {
|
||||||
|
pool = await poolsRepository.$getUnknownPool();
|
||||||
|
} else {
|
||||||
|
pool = poolsParser.unknownPool;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!pool) { // We should never have this situation in practise
|
if (!pool) { // We should never have this situation in practise
|
||||||
@@ -165,13 +203,22 @@ class Blocks {
|
|||||||
*/
|
*/
|
||||||
private async $findBlockMiner(txMinerInfo: TransactionMinerInfo | undefined): Promise<PoolTag> {
|
private async $findBlockMiner(txMinerInfo: TransactionMinerInfo | undefined): Promise<PoolTag> {
|
||||||
if (txMinerInfo === undefined || txMinerInfo.vout.length < 1) {
|
if (txMinerInfo === undefined || txMinerInfo.vout.length < 1) {
|
||||||
return await poolsRepository.$getUnknownPool();
|
if (config.DATABASE.ENABLED === true) {
|
||||||
|
return await poolsRepository.$getUnknownPool();
|
||||||
|
} else {
|
||||||
|
return poolsParser.unknownPool;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const asciiScriptSig = transactionUtils.hex2ascii(txMinerInfo.vin[0].scriptsig);
|
const asciiScriptSig = transactionUtils.hex2ascii(txMinerInfo.vin[0].scriptsig);
|
||||||
const address = txMinerInfo.vout[0].scriptpubkey_address;
|
const address = txMinerInfo.vout[0].scriptpubkey_address;
|
||||||
|
|
||||||
const pools: PoolTag[] = await poolsRepository.$getPools();
|
let pools: PoolTag[] = [];
|
||||||
|
if (config.DATABASE.ENABLED === true) {
|
||||||
|
pools = await poolsRepository.$getPools();
|
||||||
|
} else {
|
||||||
|
pools = poolsParser.miningPools;
|
||||||
|
}
|
||||||
for (let i = 0; i < pools.length; ++i) {
|
for (let i = 0; i < pools.length; ++i) {
|
||||||
if (address !== undefined) {
|
if (address !== undefined) {
|
||||||
const addresses: string[] = JSON.parse(pools[i].addresses);
|
const addresses: string[] = JSON.parse(pools[i].addresses);
|
||||||
@@ -190,19 +237,78 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await poolsRepository.$getUnknownPool();
|
if (config.DATABASE.ENABLED === true) {
|
||||||
|
return await poolsRepository.$getUnknownPool();
|
||||||
|
} else {
|
||||||
|
return poolsParser.unknownPool;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [INDEXING] Index all blocks summaries for the block txs visualization
|
||||||
|
*/
|
||||||
|
public async $generateBlocksSummariesDatabase() {
|
||||||
|
if (Common.blocksSummariesIndexingEnabled() === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get all indexed block hash
|
||||||
|
const indexedBlocks = await blocksRepository.$getIndexedBlocks();
|
||||||
|
const indexedBlockSummariesHashesArray = await BlocksSummariesRepository.$getIndexedSummariesId();
|
||||||
|
|
||||||
|
const indexedBlockSummariesHashes = {}; // Use a map for faster seek during the indexing loop
|
||||||
|
for (const hash of indexedBlockSummariesHashesArray) {
|
||||||
|
indexedBlockSummariesHashes[hash] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
let newlyIndexed = 0;
|
||||||
|
let totalIndexed = indexedBlockSummariesHashesArray.length;
|
||||||
|
let indexedThisRun = 0;
|
||||||
|
let timer = new Date().getTime() / 1000;
|
||||||
|
const startedAt = new Date().getTime() / 1000;
|
||||||
|
|
||||||
|
for (const block of indexedBlocks) {
|
||||||
|
if (indexedBlockSummariesHashes[block.hash] === true) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
|
||||||
|
if (elapsedSeconds > 5) {
|
||||||
|
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
||||||
|
const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds);
|
||||||
|
const progress = Math.round(totalIndexed / indexedBlocks.length * 10000) / 100;
|
||||||
|
logger.debug(`Indexing block summary for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexedBlocks.length} (${progress}%) | elapsed: ${runningFor} seconds`);
|
||||||
|
timer = new Date().getTime() / 1000;
|
||||||
|
indexedThisRun = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.$getStrippedBlockTransactions(block.hash, true, true); // This will index the block summary
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
indexedThisRun++;
|
||||||
|
totalIndexed++;
|
||||||
|
newlyIndexed++;
|
||||||
|
}
|
||||||
|
if (newlyIndexed > 0) {
|
||||||
|
logger.notice(`Blocks summaries indexing completed: indexed ${newlyIndexed} blocks`);
|
||||||
|
} else {
|
||||||
|
logger.debug(`Blocks summaries indexing completed: indexed ${newlyIndexed} blocks`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Blocks summaries indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [INDEXING] Index all blocks metadata for the mining dashboard
|
* [INDEXING] Index all blocks metadata for the mining dashboard
|
||||||
*/
|
*/
|
||||||
public async $generateBlockDatabase() {
|
public async $generateBlockDatabase(): Promise<boolean> {
|
||||||
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
|
|
||||||
if (blockchainInfo.blocks !== blockchainInfo.headers) { // Wait for node to sync
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
|
||||||
let currentBlockHeight = blockchainInfo.blocks;
|
let currentBlockHeight = blockchainInfo.blocks;
|
||||||
|
|
||||||
let indexingBlockAmount = Math.min(config.MEMPOOL.INDEXING_BLOCKS_AMOUNT, blockchainInfo.blocks);
|
let indexingBlockAmount = Math.min(config.MEMPOOL.INDEXING_BLOCKS_AMOUNT, blockchainInfo.blocks);
|
||||||
@@ -243,10 +349,9 @@ class Blocks {
|
|||||||
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
|
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
|
||||||
if (elapsedSeconds > 5 || blockHeight === lastBlockToIndex) {
|
if (elapsedSeconds > 5 || blockHeight === lastBlockToIndex) {
|
||||||
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
||||||
const blockPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
|
const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds);
|
||||||
const progress = Math.round(totalIndexed / indexingBlockAmount * 10000) / 100;
|
const progress = Math.round(totalIndexed / indexingBlockAmount * 10000) / 100;
|
||||||
const timeLeft = Math.round((indexingBlockAmount - totalIndexed) / blockPerSeconds);
|
logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress}%) | elapsed: ${runningFor} seconds`);
|
||||||
logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress}%) | elapsed: ${runningFor} seconds | left: ~${timeLeft} seconds`);
|
|
||||||
timer = new Date().getTime() / 1000;
|
timer = new Date().getTime() / 1000;
|
||||||
indexedThisRun = 0;
|
indexedThisRun = 0;
|
||||||
loadingIndicators.setProgress('block-indexing', progress, false);
|
loadingIndicators.setProgress('block-indexing', progress, false);
|
||||||
@@ -262,18 +367,19 @@ class Blocks {
|
|||||||
|
|
||||||
currentBlockHeight -= chunkSize;
|
currentBlockHeight -= chunkSize;
|
||||||
}
|
}
|
||||||
logger.info(`Indexed ${newlyIndexed} blocks`);
|
if (newlyIndexed > 0) {
|
||||||
|
logger.notice(`Block indexing completed: indexed ${newlyIndexed} blocks`);
|
||||||
|
} else {
|
||||||
|
logger.debug(`Block indexing completed: indexed ${newlyIndexed} blocks`);
|
||||||
|
}
|
||||||
loadingIndicators.setProgress('block-indexing', 100);
|
loadingIndicators.setProgress('block-indexing', 100);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('Block indexing failed. Trying again later. Reason: ' + (e instanceof Error ? e.message : e));
|
logger.err('Block indexing failed. Trying again in 10 seconds. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
loadingIndicators.setProgress('block-indexing', 100);
|
loadingIndicators.setProgress('block-indexing', 100);
|
||||||
return;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chainValid = await BlocksRepository.$validateChain();
|
return await BlocksRepository.$validateChain();
|
||||||
if (!chainValid) {
|
|
||||||
indexer.reindex();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $updateBlocks() {
|
public async $updateBlocks() {
|
||||||
@@ -305,7 +411,7 @@ class Blocks {
|
|||||||
|
|
||||||
if (blockHeightTip >= 2016) {
|
if (blockHeightTip >= 2016) {
|
||||||
const previousPeriodBlockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff - 2016);
|
const previousPeriodBlockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff - 2016);
|
||||||
const previousPeriodBlock = await bitcoinApi.$getBlock(previousPeriodBlockHash);
|
const previousPeriodBlock = await bitcoinClient.getBlock(previousPeriodBlockHash)
|
||||||
this.previousDifficultyRetarget = (block.difficulty - previousPeriodBlock.difficulty) / previousPeriodBlock.difficulty * 100;
|
this.previousDifficultyRetarget = (block.difficulty - previousPeriodBlock.difficulty) / previousPeriodBlock.difficulty * 100;
|
||||||
logger.debug(`Initial difficulty adjustment data set.`);
|
logger.debug(`Initial difficulty adjustment data set.`);
|
||||||
}
|
}
|
||||||
@@ -323,10 +429,12 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const blockHash = await bitcoinApi.$getBlockHash(this.currentBlockHeight);
|
const blockHash = await bitcoinApi.$getBlockHash(this.currentBlockHeight);
|
||||||
const block = BitcoinApi.convertBlock(await bitcoinClient.getBlock(blockHash));
|
const verboseBlock = await bitcoinClient.getBlock(blockHash, 2);
|
||||||
|
const block = BitcoinApi.convertBlock(verboseBlock);
|
||||||
const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash);
|
const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash);
|
||||||
const transactions = await this.$getTransactionsExtended(blockHash, block.height, false);
|
const transactions = await this.$getTransactionsExtended(blockHash, block.height, false);
|
||||||
const blockExtended: BlockExtended = await this.$getBlockExtended(block, transactions);
|
const blockExtended: BlockExtended = await this.$getBlockExtended(block, transactions);
|
||||||
|
const blockSummary: BlockSummary = this.summarizeBlock(verboseBlock);
|
||||||
|
|
||||||
if (Common.indexingEnabled()) {
|
if (Common.indexingEnabled()) {
|
||||||
if (!fastForwarded) {
|
if (!fastForwarded) {
|
||||||
@@ -336,18 +444,35 @@ class Blocks {
|
|||||||
// We assume there won't be a reorg with more than 10 block depth
|
// We assume there won't be a reorg with more than 10 block depth
|
||||||
await BlocksRepository.$deleteBlocksFrom(lastBlock['height'] - 10);
|
await BlocksRepository.$deleteBlocksFrom(lastBlock['height'] - 10);
|
||||||
await HashratesRepository.$deleteLastEntries();
|
await HashratesRepository.$deleteLastEntries();
|
||||||
|
await BlocksSummariesRepository.$deleteBlocksFrom(lastBlock['height'] - 10);
|
||||||
for (let i = 10; i >= 0; --i) {
|
for (let i = 10; i >= 0; --i) {
|
||||||
await this.$indexBlock(lastBlock['height'] - i);
|
const newBlock = await this.$indexBlock(lastBlock['height'] - i);
|
||||||
|
await this.$getStrippedBlockTransactions(newBlock.id, true, true);
|
||||||
}
|
}
|
||||||
|
await mining.$indexDifficultyAdjustments();
|
||||||
|
await DifficultyAdjustmentsRepository.$deleteLastAdjustment();
|
||||||
|
logger.info(`Re-indexed 10 blocks and summaries. Also re-indexed the last difficulty adjustments. Will re-index latest hashrates in a few seconds.`);
|
||||||
|
indexer.reindex();
|
||||||
}
|
}
|
||||||
await blocksRepository.$saveBlockInDatabase(blockExtended);
|
await blocksRepository.$saveBlockInDatabase(blockExtended);
|
||||||
|
|
||||||
|
// Save blocks summary for visualization if it's enabled
|
||||||
|
if (Common.blocksSummariesIndexingEnabled() === true) {
|
||||||
|
await this.$getStrippedBlockTransactions(blockExtended.id, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (fiatConversion.ratesInitialized === true && config.DATABASE.ENABLED === true) {
|
|
||||||
await RatesRepository.$saveRate(blockExtended.height, fiatConversion.getConversionRates());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (block.height % 2016 === 0) {
|
if (block.height % 2016 === 0) {
|
||||||
|
if (Common.indexingEnabled()) {
|
||||||
|
await DifficultyAdjustmentsRepository.$saveAdjustments({
|
||||||
|
time: block.timestamp,
|
||||||
|
height: block.height,
|
||||||
|
difficulty: block.difficulty,
|
||||||
|
adjustment: Math.round((block.difficulty / this.currentDifficulty) * 1000000) / 1000000, // Remove float point noise
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.previousDifficultyRetarget = (block.difficulty - this.currentDifficulty) / this.currentDifficulty * 100;
|
this.previousDifficultyRetarget = (block.difficulty - this.currentDifficulty) / this.currentDifficulty * 100;
|
||||||
this.lastDifficultyAdjustmentTime = block.timestamp;
|
this.lastDifficultyAdjustmentTime = block.timestamp;
|
||||||
this.currentDifficulty = block.difficulty;
|
this.currentDifficulty = block.difficulty;
|
||||||
@@ -357,6 +482,10 @@ class Blocks {
|
|||||||
if (this.blocks.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) {
|
if (this.blocks.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) {
|
||||||
this.blocks = this.blocks.slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4);
|
this.blocks = this.blocks.slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4);
|
||||||
}
|
}
|
||||||
|
this.blockSummaries.push(blockSummary);
|
||||||
|
if (this.blockSummaries.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) {
|
||||||
|
this.blockSummaries = this.blockSummaries.slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.newBlockCallbacks.length) {
|
if (this.newBlockCallbacks.length) {
|
||||||
this.newBlockCallbacks.forEach((cb) => cb(blockExtended, txIds, transactions));
|
this.newBlockCallbacks.forEach((cb) => cb(blockExtended, txIds, transactions));
|
||||||
@@ -404,13 +533,15 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const block = await bitcoinApi.$getBlock(hash);
|
let block = await bitcoinClient.getBlock(hash);
|
||||||
|
|
||||||
// Not Bitcoin network, return the block as it
|
// Not Bitcoin network, return the block as it
|
||||||
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) {
|
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) {
|
||||||
return block;
|
return block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
block = prepareBlock(block);
|
||||||
|
|
||||||
// Bitcoin network, add our custom data on top
|
// Bitcoin network, add our custom data on top
|
||||||
const transactions = await this.$getTransactionsExtended(hash, block.height, true);
|
const transactions = await this.$getTransactionsExtended(hash, block.height, true);
|
||||||
const blockExtended = await this.$getBlockExtended(block, transactions);
|
const blockExtended = await this.$getBlockExtended(block, transactions);
|
||||||
@@ -422,48 +553,71 @@ class Blocks {
|
|||||||
return blockExtended;
|
return blockExtended;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $getBlocks(fromHeight?: number, limit: number = 15): Promise<BlockExtended[]> {
|
public async $getStrippedBlockTransactions(hash: string, skipMemoryCache: boolean = false,
|
||||||
try {
|
skipDBLookup: boolean = false): Promise<TransactionStripped[]>
|
||||||
let currentHeight = fromHeight !== undefined ? fromHeight : this.getCurrentBlockHeight();
|
{
|
||||||
const returnBlocks: BlockExtended[] = [];
|
if (skipMemoryCache === false) {
|
||||||
|
// Check the memory cache
|
||||||
if (currentHeight < 0) {
|
const cachedSummary = this.getBlockSummaries().find((b) => b.id === hash);
|
||||||
return returnBlocks;
|
if (cachedSummary) {
|
||||||
|
return cachedSummary.transactions;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentHeight === 0 && Common.indexingEnabled()) {
|
|
||||||
currentHeight = await blocksRepository.$mostRecentBlockHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if block height exist in local cache to skip the hash lookup
|
|
||||||
const blockByHeight = this.getBlocks().find((b) => b.height === currentHeight);
|
|
||||||
let startFromHash: string | null = null;
|
|
||||||
if (blockByHeight) {
|
|
||||||
startFromHash = blockByHeight.id;
|
|
||||||
} else if (!Common.indexingEnabled()) {
|
|
||||||
startFromHash = await bitcoinApi.$getBlockHash(currentHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
let nextHash = startFromHash;
|
|
||||||
for (let i = 0; i < limit && currentHeight >= 0; i++) {
|
|
||||||
let block = this.getBlocks().find((b) => b.height === currentHeight);
|
|
||||||
if (block) {
|
|
||||||
returnBlocks.push(block);
|
|
||||||
} else if (Common.indexingEnabled()) {
|
|
||||||
block = await this.$indexBlock(currentHeight);
|
|
||||||
returnBlocks.push(block);
|
|
||||||
} else if (nextHash != null) {
|
|
||||||
block = prepareBlock(await bitcoinApi.$getBlock(nextHash));
|
|
||||||
nextHash = block.previousblockhash;
|
|
||||||
returnBlocks.push(block);
|
|
||||||
}
|
|
||||||
currentHeight--;
|
|
||||||
}
|
|
||||||
|
|
||||||
return returnBlocks;
|
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if it's indexed in db
|
||||||
|
if (skipDBLookup === false && Common.blocksSummariesIndexingEnabled() === true) {
|
||||||
|
const indexedSummary = await BlocksSummariesRepository.$getByBlockId(hash);
|
||||||
|
if (indexedSummary !== undefined) {
|
||||||
|
return indexedSummary.transactions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call Core RPC
|
||||||
|
const block = await bitcoinClient.getBlock(hash, 2);
|
||||||
|
const summary = this.summarizeBlock(block);
|
||||||
|
|
||||||
|
// Index the response if needed
|
||||||
|
if (Common.blocksSummariesIndexingEnabled() === true) {
|
||||||
|
await BlocksSummariesRepository.$saveSummary(block.height, summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
return summary.transactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getBlocks(fromHeight?: number, limit: number = 15): Promise<BlockExtended[]> {
|
||||||
|
let currentHeight = fromHeight !== undefined ? fromHeight : await blocksRepository.$mostRecentBlockHeight();
|
||||||
|
const returnBlocks: BlockExtended[] = [];
|
||||||
|
|
||||||
|
if (currentHeight < 0) {
|
||||||
|
return returnBlocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if block height exist in local cache to skip the hash lookup
|
||||||
|
const blockByHeight = this.getBlocks().find((b) => b.height === currentHeight);
|
||||||
|
let startFromHash: string | null = null;
|
||||||
|
if (blockByHeight) {
|
||||||
|
startFromHash = blockByHeight.id;
|
||||||
|
} else if (!Common.indexingEnabled()) {
|
||||||
|
startFromHash = await bitcoinApi.$getBlockHash(currentHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextHash = startFromHash;
|
||||||
|
for (let i = 0; i < limit && currentHeight >= 0; i++) {
|
||||||
|
let block = this.getBlocks().find((b) => b.height === currentHeight);
|
||||||
|
if (block) {
|
||||||
|
returnBlocks.push(block);
|
||||||
|
} else if (Common.indexingEnabled()) {
|
||||||
|
block = await this.$indexBlock(currentHeight);
|
||||||
|
returnBlocks.push(block);
|
||||||
|
} else if (nextHash != null) {
|
||||||
|
block = prepareBlock(await bitcoinClient.getBlock(nextHash));
|
||||||
|
nextHash = block.previousblockhash;
|
||||||
|
returnBlocks.push(block);
|
||||||
|
}
|
||||||
|
currentHeight--;
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnBlocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getLastDifficultyAdjustmentTime(): number {
|
public getLastDifficultyAdjustmentTime(): number {
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export class Common {
|
|||||||
totalFees += tx.bestDescendant.fee;
|
totalFees += tx.bestDescendant.fee;
|
||||||
}
|
}
|
||||||
|
|
||||||
tx.effectiveFeePerVsize = Math.max(Common.isLiquid() ? 0.1 : 1, totalFees / (totalWeight / 4));
|
tx.effectiveFeePerVsize = Math.max(0, totalFees / (totalWeight / 4));
|
||||||
tx.cpfpChecked = true;
|
tx.cpfpChecked = true;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -177,4 +177,11 @@ export class Common {
|
|||||||
config.MEMPOOL.INDEXING_BLOCKS_AMOUNT !== 0
|
config.MEMPOOL.INDEXING_BLOCKS_AMOUNT !== 0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static blocksSummariesIndexingEnabled(): boolean {
|
||||||
|
return (
|
||||||
|
Common.indexingEnabled() &&
|
||||||
|
config.MEMPOOL.BLOCKS_SUMMARIES_INDEXING === true
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,27 @@ import logger from '../logger';
|
|||||||
import { Common } from './common';
|
import { Common } from './common';
|
||||||
|
|
||||||
class DatabaseMigration {
|
class DatabaseMigration {
|
||||||
private static currentVersion = 19;
|
private static currentVersion = 24;
|
||||||
private queryTimeout = 120000;
|
private queryTimeout = 120000;
|
||||||
private statisticsAddedIndexed = false;
|
private statisticsAddedIndexed = false;
|
||||||
|
private uniqueLogs: string[] = [];
|
||||||
|
|
||||||
|
private blocksTruncatedMessage = `'blocks' table has been truncated. Re-indexing from scratch.`;
|
||||||
|
private hashratesTruncatedMessage = `'hashrates' table has been truncated. Re-indexing from scratch.`;
|
||||||
|
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Avoid printing multiple time the same message
|
||||||
|
*/
|
||||||
|
private uniqueLog(loggerFunction: any, msg: string) {
|
||||||
|
if (this.uniqueLogs.includes(msg)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.uniqueLogs.push(msg);
|
||||||
|
loggerFunction(msg);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entry point
|
* Entry point
|
||||||
*/
|
*/
|
||||||
@@ -39,6 +55,16 @@ class DatabaseMigration {
|
|||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion === 0) {
|
||||||
|
logger.info('Initializing database (first run, clean install)');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion <= 2) {
|
||||||
|
// Disable some spam logs when they're not relevant
|
||||||
|
this.uniqueLogs.push(this.blocksTruncatedMessage);
|
||||||
|
this.uniqueLogs.push(this.hashratesTruncatedMessage);
|
||||||
|
}
|
||||||
|
|
||||||
logger.debug('MIGRATIONS: Current state.schema_version ' + databaseSchemaVersion);
|
logger.debug('MIGRATIONS: Current state.schema_version ' + databaseSchemaVersion);
|
||||||
logger.debug('MIGRATIONS: Latest DatabaseMigration.version is ' + DatabaseMigration.currentVersion);
|
logger.debug('MIGRATIONS: Latest DatabaseMigration.version is ' + DatabaseMigration.currentVersion);
|
||||||
if (databaseSchemaVersion >= DatabaseMigration.currentVersion) {
|
if (databaseSchemaVersion >= DatabaseMigration.currentVersion) {
|
||||||
@@ -56,10 +82,13 @@ class DatabaseMigration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (DatabaseMigration.currentVersion > databaseSchemaVersion) {
|
if (DatabaseMigration.currentVersion > databaseSchemaVersion) {
|
||||||
logger.notice('MIGRATIONS: Upgrading database schema');
|
|
||||||
try {
|
try {
|
||||||
await this.$migrateTableSchemaFromVersion(databaseSchemaVersion);
|
await this.$migrateTableSchemaFromVersion(databaseSchemaVersion);
|
||||||
logger.notice(`MIGRATIONS: OK. Database schema have been migrated from version ${databaseSchemaVersion} to ${DatabaseMigration.currentVersion} (latest version)`);
|
if (databaseSchemaVersion === 0) {
|
||||||
|
logger.notice(`MIGRATIONS: OK. Database schema has been properly initialized to version ${DatabaseMigration.currentVersion} (latest version)`);
|
||||||
|
} else {
|
||||||
|
logger.notice(`MIGRATIONS: OK. Database schema have been migrated from version ${databaseSchemaVersion} to ${DatabaseMigration.currentVersion} (latest version)`);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('MIGRATIONS: Unable to migrate database, aborting. ' + e);
|
logger.err('MIGRATIONS: Unable to migrate database, aborting. ' + e);
|
||||||
}
|
}
|
||||||
@@ -89,13 +118,13 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery(this.getCreateBlocksTableQuery(), await this.$checkIfTableExists('blocks'));
|
await this.$executeQuery(this.getCreateBlocksTableQuery(), await this.$checkIfTableExists('blocks'));
|
||||||
}
|
}
|
||||||
if (databaseSchemaVersion < 5 && isBitcoin === true) {
|
if (databaseSchemaVersion < 5 && isBitcoin === true) {
|
||||||
logger.warn(`'blocks' table has been truncated. Re-indexing from scratch.`);
|
this.uniqueLog(logger.notice, this.blocksTruncatedMessage);
|
||||||
await this.$executeQuery('TRUNCATE blocks;'); // Need to re-index
|
await this.$executeQuery('TRUNCATE blocks;'); // Need to re-index
|
||||||
await this.$executeQuery('ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 6 && isBitcoin === true) {
|
if (databaseSchemaVersion < 6 && isBitcoin === true) {
|
||||||
logger.warn(`'blocks' table has been truncated. Re-indexing from scratch.`);
|
this.uniqueLog(logger.notice, this.blocksTruncatedMessage);
|
||||||
await this.$executeQuery('TRUNCATE blocks;'); // Need to re-index
|
await this.$executeQuery('TRUNCATE blocks;'); // Need to re-index
|
||||||
// Cleanup original blocks fields type
|
// Cleanup original blocks fields type
|
||||||
await this.$executeQuery('ALTER TABLE blocks MODIFY `height` integer unsigned NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE blocks MODIFY `height` integer unsigned NOT NULL DEFAULT "0"');
|
||||||
@@ -122,7 +151,7 @@ class DatabaseMigration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 8 && isBitcoin === true) {
|
if (databaseSchemaVersion < 8 && isBitcoin === true) {
|
||||||
logger.warn(`'hashrates' table has been truncated. Re-indexing from scratch.`);
|
this.uniqueLog(logger.notice, this.blocksTruncatedMessage);
|
||||||
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index
|
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index
|
||||||
await this.$executeQuery('ALTER TABLE `hashrates` DROP INDEX `PRIMARY`');
|
await this.$executeQuery('ALTER TABLE `hashrates` DROP INDEX `PRIMARY`');
|
||||||
await this.$executeQuery('ALTER TABLE `hashrates` ADD `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST');
|
await this.$executeQuery('ALTER TABLE `hashrates` ADD `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST');
|
||||||
@@ -131,7 +160,7 @@ class DatabaseMigration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 9 && isBitcoin === true) {
|
if (databaseSchemaVersion < 9 && isBitcoin === true) {
|
||||||
logger.warn(`'hashrates' table has been truncated. Re-indexing from scratch.`);
|
this.uniqueLog(logger.notice, this.hashratesTruncatedMessage);
|
||||||
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index
|
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index
|
||||||
await this.$executeQuery('ALTER TABLE `state` CHANGE `name` `name` varchar(100)');
|
await this.$executeQuery('ALTER TABLE `state` CHANGE `name` `name` varchar(100)');
|
||||||
await this.$executeQuery('ALTER TABLE `hashrates` ADD UNIQUE `hashrate_timestamp_pool_id` (`hashrate_timestamp`, `pool_id`)');
|
await this.$executeQuery('ALTER TABLE `hashrates` ADD UNIQUE `hashrate_timestamp_pool_id` (`hashrate_timestamp`, `pool_id`)');
|
||||||
@@ -142,7 +171,7 @@ class DatabaseMigration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 11 && isBitcoin === true) {
|
if (databaseSchemaVersion < 11 && isBitcoin === true) {
|
||||||
logger.warn(`'blocks' table has been truncated. Re-indexing from scratch.`);
|
this.uniqueLog(logger.notice, this.blocksTruncatedMessage);
|
||||||
await this.$executeQuery('TRUNCATE blocks;'); // Need to re-index
|
await this.$executeQuery('TRUNCATE blocks;'); // Need to re-index
|
||||||
await this.$executeQuery(`ALTER TABLE blocks
|
await this.$executeQuery(`ALTER TABLE blocks
|
||||||
ADD avg_fee INT UNSIGNED NULL,
|
ADD avg_fee INT UNSIGNED NULL,
|
||||||
@@ -166,14 +195,14 @@ class DatabaseMigration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 14 && isBitcoin === true) {
|
if (databaseSchemaVersion < 14 && isBitcoin === true) {
|
||||||
logger.warn(`'hashrates' table has been truncated. Re-indexing from scratch.`);
|
this.uniqueLog(logger.notice, this.hashratesTruncatedMessage);
|
||||||
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index
|
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index
|
||||||
await this.$executeQuery('ALTER TABLE `hashrates` DROP FOREIGN KEY `hashrates_ibfk_1`');
|
await this.$executeQuery('ALTER TABLE `hashrates` DROP FOREIGN KEY `hashrates_ibfk_1`');
|
||||||
await this.$executeQuery('ALTER TABLE `hashrates` MODIFY `pool_id` SMALLINT UNSIGNED NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE `hashrates` MODIFY `pool_id` SMALLINT UNSIGNED NOT NULL DEFAULT "0"');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 16 && isBitcoin === true) {
|
if (databaseSchemaVersion < 16 && isBitcoin === true) {
|
||||||
logger.warn(`'hashrates' table has been truncated. Re-indexing from scratch.`);
|
this.uniqueLog(logger.notice, this.hashratesTruncatedMessage);
|
||||||
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index because we changed timestamps
|
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index because we changed timestamps
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,6 +217,37 @@ class DatabaseMigration {
|
|||||||
if (databaseSchemaVersion < 19) {
|
if (databaseSchemaVersion < 19) {
|
||||||
await this.$executeQuery(this.getCreateRatesTableQuery(), await this.$checkIfTableExists('rates'));
|
await this.$executeQuery(this.getCreateRatesTableQuery(), await this.$checkIfTableExists('rates'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 20 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery(this.getCreateBlocksSummariesTableQuery(), await this.$checkIfTableExists('blocks_summaries'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 21) {
|
||||||
|
await this.$executeQuery('DROP TABLE IF EXISTS `rates`');
|
||||||
|
await this.$executeQuery(this.getCreatePricesTableQuery(), await this.$checkIfTableExists('prices'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 22 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery('DROP TABLE IF EXISTS `difficulty_adjustments`');
|
||||||
|
await this.$executeQuery(this.getCreateDifficultyAdjustmentsTableQuery(), await this.$checkIfTableExists('difficulty_adjustments'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 23) {
|
||||||
|
await this.$executeQuery('TRUNCATE `prices`');
|
||||||
|
await this.$executeQuery('ALTER TABLE `prices` DROP `avg_prices`');
|
||||||
|
await this.$executeQuery('ALTER TABLE `prices` ADD `USD` float DEFAULT "0"');
|
||||||
|
await this.$executeQuery('ALTER TABLE `prices` ADD `EUR` float DEFAULT "0"');
|
||||||
|
await this.$executeQuery('ALTER TABLE `prices` ADD `GBP` float DEFAULT "0"');
|
||||||
|
await this.$executeQuery('ALTER TABLE `prices` ADD `CAD` float DEFAULT "0"');
|
||||||
|
await this.$executeQuery('ALTER TABLE `prices` ADD `CHF` float DEFAULT "0"');
|
||||||
|
await this.$executeQuery('ALTER TABLE `prices` ADD `AUD` float DEFAULT "0"');
|
||||||
|
await this.$executeQuery('ALTER TABLE `prices` ADD `JPY` float DEFAULT "0"');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 24 && isBitcoin == true) {
|
||||||
|
await this.$executeQuery('DROP TABLE IF EXISTS `blocks_audits`');
|
||||||
|
await this.$executeQuery(this.getCreateBlocksAuditsTableQuery(), await this.$checkIfTableExists('blocks_audits'));
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@@ -282,6 +342,8 @@ class DatabaseMigration {
|
|||||||
for (const query of this.getMigrationQueriesFromVersion(version)) {
|
for (const query of this.getMigrationQueriesFromVersion(version)) {
|
||||||
transactionQueries.push(query);
|
transactionQueries.push(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.notice(`MIGRATIONS: ${version > 0 ? 'Upgrading' : 'Initializing'} database schema version number to ${DatabaseMigration.currentVersion}`);
|
||||||
transactionQueries.push(this.getUpdateToLatestSchemaVersionQuery());
|
transactionQueries.push(this.getUpdateToLatestSchemaVersionQuery());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -305,6 +367,9 @@ class DatabaseMigration {
|
|||||||
|
|
||||||
if (version < 1) {
|
if (version < 1) {
|
||||||
if (config.MEMPOOL.NETWORK !== 'liquid' && config.MEMPOOL.NETWORK !== 'liquidtestnet') {
|
if (config.MEMPOOL.NETWORK !== 'liquid' && config.MEMPOOL.NETWORK !== 'liquidtestnet') {
|
||||||
|
if (version > 0) {
|
||||||
|
logger.notice(`MIGRATIONS: Migrating (shifting) statistics table data`);
|
||||||
|
}
|
||||||
queries.push(this.getShiftStatisticsQuery());
|
queries.push(this.getShiftStatisticsQuery());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -470,7 +535,7 @@ class DatabaseMigration {
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCreateRatesTableQuery(): string {
|
private getCreateRatesTableQuery(): string { // This table has been replaced by the prices table
|
||||||
return `CREATE TABLE IF NOT EXISTS rates (
|
return `CREATE TABLE IF NOT EXISTS rates (
|
||||||
height int(10) unsigned NOT NULL,
|
height int(10) unsigned NOT NULL,
|
||||||
bisq_rates JSON NOT NULL,
|
bisq_rates JSON NOT NULL,
|
||||||
@@ -478,8 +543,50 @@ class DatabaseMigration {
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getCreateBlocksSummariesTableQuery(): string {
|
||||||
|
return `CREATE TABLE IF NOT EXISTS blocks_summaries (
|
||||||
|
height int(10) unsigned NOT NULL,
|
||||||
|
id varchar(65) NOT NULL,
|
||||||
|
transactions JSON NOT NULL,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
INDEX (height)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCreatePricesTableQuery(): string {
|
||||||
|
return `CREATE TABLE IF NOT EXISTS prices (
|
||||||
|
time timestamp NOT NULL,
|
||||||
|
avg_prices JSON NOT NULL,
|
||||||
|
PRIMARY KEY (time)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCreateDifficultyAdjustmentsTableQuery(): string {
|
||||||
|
return `CREATE TABLE IF NOT EXISTS difficulty_adjustments (
|
||||||
|
time timestamp NOT NULL,
|
||||||
|
height int(10) unsigned NOT NULL,
|
||||||
|
difficulty double unsigned NOT NULL,
|
||||||
|
adjustment float NOT NULL,
|
||||||
|
PRIMARY KEY (height),
|
||||||
|
INDEX (time)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCreateBlocksAuditsTableQuery(): string {
|
||||||
|
return `CREATE TABLE IF NOT EXISTS blocks_audits (
|
||||||
|
time timestamp NOT NULL,
|
||||||
|
hash varchar(65) NOT NULL,
|
||||||
|
height int(10) unsigned NOT NULL,
|
||||||
|
missing_txs JSON NOT NULL,
|
||||||
|
added_txs JSON NOT NULL,
|
||||||
|
match_rate float unsigned NOT NULL,
|
||||||
|
PRIMARY KEY (hash),
|
||||||
|
INDEX (height)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
|
}
|
||||||
|
|
||||||
public async $truncateIndexedData(tables: string[]) {
|
public async $truncateIndexedData(tables: string[]) {
|
||||||
const allowedTables = ['blocks', 'hashrates'];
|
const allowedTables = ['blocks', 'hashrates', 'prices'];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (const table of tables) {
|
for (const table of tables) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
const fsPromises = fs.promises;
|
const fsPromises = fs.promises;
|
||||||
import * as cluster from 'cluster';
|
import cluster from 'cluster';
|
||||||
import memPool from './mempool';
|
import memPool from './mempool';
|
||||||
import blocks from './blocks';
|
import blocks from './blocks';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
@@ -19,7 +19,7 @@ class DiskCache {
|
|||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
async $saveCacheToDisk(): Promise<void> {
|
async $saveCacheToDisk(): Promise<void> {
|
||||||
if (!cluster.isMaster) {
|
if (!cluster.isPrimary) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.isWritingCache) {
|
if (this.isWritingCache) {
|
||||||
@@ -43,14 +43,15 @@ class DiskCache {
|
|||||||
await fsPromises.writeFile(DiskCache.FILE_NAME, JSON.stringify({
|
await fsPromises.writeFile(DiskCache.FILE_NAME, JSON.stringify({
|
||||||
cacheSchemaVersion: this.cacheSchemaVersion,
|
cacheSchemaVersion: this.cacheSchemaVersion,
|
||||||
blocks: blocks.getBlocks(),
|
blocks: blocks.getBlocks(),
|
||||||
|
blockSummaries: blocks.getBlockSummaries(),
|
||||||
mempool: {},
|
mempool: {},
|
||||||
mempoolArray: mempoolArray.splice(0, chunkSize),
|
mempoolArray: mempoolArray.splice(0, chunkSize),
|
||||||
}), {flag: 'w'});
|
}), { flag: 'w' });
|
||||||
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
|
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
|
||||||
await fsPromises.writeFile(DiskCache.FILE_NAMES.replace('{number}', i.toString()), JSON.stringify({
|
await fsPromises.writeFile(DiskCache.FILE_NAMES.replace('{number}', i.toString()), JSON.stringify({
|
||||||
mempool: {},
|
mempool: {},
|
||||||
mempoolArray: mempoolArray.splice(0, chunkSize),
|
mempoolArray: mempoolArray.splice(0, chunkSize),
|
||||||
}), {flag: 'w'});
|
}), { flag: 'w' });
|
||||||
}
|
}
|
||||||
logger.debug('Mempool and blocks data saved to disk cache');
|
logger.debug('Mempool and blocks data saved to disk cache');
|
||||||
this.isWritingCache = false;
|
this.isWritingCache = false;
|
||||||
@@ -66,7 +67,7 @@ class DiskCache {
|
|||||||
fs.unlinkSync(DiskCache.FILE_NAMES.replace('{number}', i.toString()));
|
fs.unlinkSync(DiskCache.FILE_NAMES.replace('{number}', i.toString()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadMempoolCache() {
|
loadMempoolCache() {
|
||||||
if (!fs.existsSync(DiskCache.FILE_NAME)) {
|
if (!fs.existsSync(DiskCache.FILE_NAME)) {
|
||||||
return;
|
return;
|
||||||
@@ -103,14 +104,15 @@ class DiskCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Error parsing ' + fileName + '. Skipping.');
|
logger.info('Error parsing ' + fileName + '. Skipping. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
memPool.setMempool(data.mempool);
|
memPool.setMempool(data.mempool);
|
||||||
blocks.setBlocks(data.blocks);
|
blocks.setBlocks(data.blocks);
|
||||||
|
blocks.setBlockSummaries(data.blockSummaries || []);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn('Failed to parse mempoool and blocks cache. Skipping.');
|
logger.warn('Failed to parse mempoool and blocks cache. Skipping. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
|
import * as http from 'http';
|
||||||
|
import * as https from 'https';
|
||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { IConversionRates } from '../mempool.interfaces';
|
import { IConversionRates } from '../mempool.interfaces';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
@@ -25,9 +27,10 @@ class FiatConversion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public startService() {
|
public startService() {
|
||||||
|
const fiatConversionUrl = (config.SOCKS5PROXY.ENABLED === true) && (config.SOCKS5PROXY.USE_ONION === true) ? config.PRICE_DATA_SERVER.TOR_URL : config.PRICE_DATA_SERVER.CLEARNET_URL;
|
||||||
logger.info('Starting currency rates service');
|
logger.info('Starting currency rates service');
|
||||||
if (config.SOCKS5PROXY.ENABLED) {
|
if (config.SOCKS5PROXY.ENABLED) {
|
||||||
logger.info(`Currency rates service will be queried over the Tor network using ${config.PRICE_DATA_SERVER.TOR_URL}`);
|
logger.info(`Currency rates service will be queried over the Tor network using ${fiatConversionUrl}`);
|
||||||
} else {
|
} else {
|
||||||
logger.info(`Currency rates service will be queried over clearnet using ${config.PRICE_DATA_SERVER.CLEARNET_URL}`);
|
logger.info(`Currency rates service will be queried over clearnet using ${config.PRICE_DATA_SERVER.CLEARNET_URL}`);
|
||||||
}
|
}
|
||||||
@@ -40,49 +43,79 @@ class FiatConversion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async updateCurrency(): Promise<void> {
|
private async updateCurrency(): Promise<void> {
|
||||||
const headers = { 'User-Agent': `mempool/v${backendInfo.getBackendInfo().version}` };
|
type axiosOptions = {
|
||||||
let fiatConversionUrl: string;
|
headers: {
|
||||||
let response: AxiosResponse;
|
'User-Agent': string
|
||||||
|
};
|
||||||
|
timeout: number;
|
||||||
|
httpAgent?: http.Agent;
|
||||||
|
httpsAgent?: https.Agent;
|
||||||
|
}
|
||||||
|
const setDelay = (secs: number = 1): Promise<void> => new Promise(resolve => setTimeout(() => resolve(), secs * 1000));
|
||||||
|
const fiatConversionUrl = (config.SOCKS5PROXY.ENABLED === true) && (config.SOCKS5PROXY.USE_ONION === true) ? config.PRICE_DATA_SERVER.TOR_URL : config.PRICE_DATA_SERVER.CLEARNET_URL;
|
||||||
|
const isHTTP = (new URL(fiatConversionUrl).protocol.split(':')[0] === 'http') ? true : false;
|
||||||
|
const axiosOptions: axiosOptions = {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': (config.MEMPOOL.USER_AGENT === 'mempool') ? `mempool/v${backendInfo.getBackendInfo().version}` : `${config.MEMPOOL.USER_AGENT}`
|
||||||
|
},
|
||||||
|
timeout: config.SOCKS5PROXY.ENABLED ? 30000 : 10000
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
let retry = 0;
|
||||||
if (config.SOCKS5PROXY.ENABLED) {
|
|
||||||
let socksOptions: any = {
|
|
||||||
agentOptions: {
|
|
||||||
keepAlive: true,
|
|
||||||
},
|
|
||||||
hostname: config.SOCKS5PROXY.HOST,
|
|
||||||
port: config.SOCKS5PROXY.PORT
|
|
||||||
};
|
|
||||||
|
|
||||||
if (config.SOCKS5PROXY.USERNAME && config.SOCKS5PROXY.PASSWORD) {
|
while(retry < config.MEMPOOL.EXTERNAL_MAX_RETRY) {
|
||||||
socksOptions.username = config.SOCKS5PROXY.USERNAME;
|
try {
|
||||||
socksOptions.password = config.SOCKS5PROXY.PASSWORD;
|
if (config.SOCKS5PROXY.ENABLED) {
|
||||||
|
let socksOptions: any = {
|
||||||
|
agentOptions: {
|
||||||
|
keepAlive: true,
|
||||||
|
},
|
||||||
|
hostname: config.SOCKS5PROXY.HOST,
|
||||||
|
port: config.SOCKS5PROXY.PORT
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config.SOCKS5PROXY.USERNAME && config.SOCKS5PROXY.PASSWORD) {
|
||||||
|
socksOptions.username = config.SOCKS5PROXY.USERNAME;
|
||||||
|
socksOptions.password = config.SOCKS5PROXY.PASSWORD;
|
||||||
|
} else {
|
||||||
|
// Retry with different tor circuits https://stackoverflow.com/a/64960234
|
||||||
|
socksOptions.username = `circuit${retry}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle proxy agent for onion addresses
|
||||||
|
if (isHTTP) {
|
||||||
|
axiosOptions.httpAgent = new SocksProxyAgent(socksOptions);
|
||||||
|
} else {
|
||||||
|
axiosOptions.httpsAgent = new SocksProxyAgent(socksOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('Querying currency rates service...');
|
||||||
|
|
||||||
|
const response: AxiosResponse = await axios.get(`${fiatConversionUrl}`, axiosOptions);
|
||||||
|
|
||||||
|
if (response.statusText === 'error' || !response.data) {
|
||||||
|
throw new Error(`Could not fetch data from ${fiatConversionUrl}, Error: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const agent = new SocksProxyAgent(socksOptions);
|
for (const rate of response.data.data) {
|
||||||
fiatConversionUrl = config.PRICE_DATA_SERVER.TOR_URL;
|
if (this.debasingFiatCurrencies.includes(rate.currencyCode) && rate.provider === 'Bisq-Aggregate') {
|
||||||
logger.debug('Querying currency rates service...');
|
this.conversionRates[rate.currencyCode] = Math.round(100 * rate.price) / 100;
|
||||||
response = await axios.get(fiatConversionUrl, { httpAgent: agent, headers: headers, timeout: 30000 });
|
}
|
||||||
} else {
|
|
||||||
fiatConversionUrl = config.PRICE_DATA_SERVER.CLEARNET_URL;
|
|
||||||
logger.debug('Querying currency rates service...');
|
|
||||||
response = await axios.get(fiatConversionUrl, { headers: headers, timeout: 10000 });
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const rate of response.data.data) {
|
|
||||||
if (this.debasingFiatCurrencies.includes(rate.currencyCode) && rate.provider === 'Bisq-Aggregate') {
|
|
||||||
this.conversionRates[rate.currencyCode] = Math.round(100 * rate.price) / 100;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
this.ratesInitialized = true;
|
this.ratesInitialized = true;
|
||||||
logger.debug(`USD Conversion Rate: ${this.conversionRates.USD}`);
|
logger.debug(`USD Conversion Rate: ${this.conversionRates.USD}`);
|
||||||
|
|
||||||
if (this.ratesChangedCallback) {
|
if (this.ratesChangedCallback) {
|
||||||
this.ratesChangedCallback(this.conversionRates);
|
this.ratesChangedCallback(this.conversionRates);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('Error updating fiat conversion rates: ' + (e instanceof Error ? e.message : e));
|
||||||
|
await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL);
|
||||||
|
retry++;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
logger.err('Error updating fiat conversion rates: ' + (e instanceof Error ? e.message : e));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { PoolInfo, PoolStats, RewardStats } from '../mempool.interfaces';
|
import { IndexedDifficultyAdjustment, PoolInfo, PoolStats, RewardStats } from '../mempool.interfaces';
|
||||||
import BlocksRepository from '../repositories/BlocksRepository';
|
import BlocksRepository from '../repositories/BlocksRepository';
|
||||||
import PoolsRepository from '../repositories/PoolsRepository';
|
import PoolsRepository from '../repositories/PoolsRepository';
|
||||||
import HashratesRepository from '../repositories/HashratesRepository';
|
import HashratesRepository from '../repositories/HashratesRepository';
|
||||||
@@ -7,11 +7,25 @@ import logger from '../logger';
|
|||||||
import { Common } from './common';
|
import { Common } from './common';
|
||||||
import loadingIndicators from './loading-indicators';
|
import loadingIndicators from './loading-indicators';
|
||||||
import { escape } from 'mysql2';
|
import { escape } from 'mysql2';
|
||||||
|
import indexer from '../indexer';
|
||||||
|
import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmentsRepository';
|
||||||
|
import config from '../config';
|
||||||
|
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
|
||||||
|
|
||||||
class Mining {
|
class Mining {
|
||||||
constructor() {
|
constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get historical block predictions match rate
|
||||||
|
*/
|
||||||
|
public async $getBlockPredictionsHistory(interval: string | null = null): Promise<any> {
|
||||||
|
return await BlocksAuditsRepository.$getBlockPredictionsHistory(
|
||||||
|
this.getTimeRange(interval),
|
||||||
|
Common.getSqlInterval(interval)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get historical block total fee
|
* Get historical block total fee
|
||||||
*/
|
*/
|
||||||
@@ -159,26 +173,25 @@ class Mining {
|
|||||||
*/
|
*/
|
||||||
public async $generatePoolHashrateHistory(): Promise<void> {
|
public async $generatePoolHashrateHistory(): Promise<void> {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
const lastestRunDate = await HashratesRepository.$getLatestRun('last_weekly_hashrates_indexing');
|
||||||
|
|
||||||
try {
|
// Run only if:
|
||||||
const lastestRunDate = await HashratesRepository.$getLatestRun('last_weekly_hashrates_indexing');
|
// * lastestRunDate is set to 0 (node backend restart, reorg)
|
||||||
|
// * we started a new week (around Monday midnight)
|
||||||
// Run only if:
|
const runIndexing = lastestRunDate === 0 || now.getUTCDay() === 1 && lastestRunDate !== now.getUTCDate();
|
||||||
// * lastestRunDate is set to 0 (node backend restart, reorg)
|
if (!runIndexing) {
|
||||||
// * we started a new week (around Monday midnight)
|
return;
|
||||||
const runIndexing = lastestRunDate === 0 || now.getUTCDay() === 1 && lastestRunDate !== now.getUTCDate();
|
|
||||||
if (!runIndexing) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const oldestConsecutiveBlockTimestamp = 1000 * (await BlocksRepository.$getOldestConsecutiveBlock()).timestamp;
|
||||||
|
|
||||||
|
const genesisBlock = await bitcoinClient.getBlock(await bitcoinClient.getBlockHash(0));
|
||||||
|
const genesisTimestamp = genesisBlock.time * 1000;
|
||||||
|
|
||||||
const indexedTimestamp = await HashratesRepository.$getWeeklyHashrateTimestamps();
|
const indexedTimestamp = await HashratesRepository.$getWeeklyHashrateTimestamps();
|
||||||
const hashrates: any[] = [];
|
const hashrates: any[] = [];
|
||||||
const genesisTimestamp = 1231006505000; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f
|
|
||||||
|
|
||||||
const lastMonday = new Date(now.setDate(now.getDate() - (now.getDay() + 6) % 7));
|
const lastMonday = new Date(now.setDate(now.getDate() - (now.getDay() + 6) % 7));
|
||||||
const lastMondayMidnight = this.getDateMidnight(lastMonday);
|
const lastMondayMidnight = this.getDateMidnight(lastMonday);
|
||||||
let toTimestamp = lastMondayMidnight.getTime();
|
let toTimestamp = lastMondayMidnight.getTime();
|
||||||
@@ -193,7 +206,7 @@ class Mining {
|
|||||||
logger.debug(`Indexing weekly mining pool hashrate`);
|
logger.debug(`Indexing weekly mining pool hashrate`);
|
||||||
loadingIndicators.setProgress('weekly-hashrate-indexing', 0);
|
loadingIndicators.setProgress('weekly-hashrate-indexing', 0);
|
||||||
|
|
||||||
while (toTimestamp > genesisTimestamp) {
|
while (toTimestamp > genesisTimestamp && toTimestamp > oldestConsecutiveBlockTimestamp) {
|
||||||
const fromTimestamp = toTimestamp - 604800000;
|
const fromTimestamp = toTimestamp - 604800000;
|
||||||
|
|
||||||
// Skip already indexed weeks
|
// Skip already indexed weeks
|
||||||
@@ -203,14 +216,6 @@ class Mining {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we have blocks for the previous week (which mean that the week
|
|
||||||
// we are currently indexing has complete data)
|
|
||||||
const blockStatsPreviousWeek: any = await BlocksRepository.$blockCountBetweenTimestamp(
|
|
||||||
null, (fromTimestamp - 604800000) / 1000, (toTimestamp - 604800000) / 1000);
|
|
||||||
if (blockStatsPreviousWeek.blockCount === 0) { // We are done indexing
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const blockStats: any = await BlocksRepository.$blockCountBetweenTimestamp(
|
const blockStats: any = await BlocksRepository.$blockCountBetweenTimestamp(
|
||||||
null, fromTimestamp / 1000, toTimestamp / 1000);
|
null, fromTimestamp / 1000, toTimestamp / 1000);
|
||||||
const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount,
|
const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount,
|
||||||
@@ -218,34 +223,35 @@ class Mining {
|
|||||||
|
|
||||||
let pools = await PoolsRepository.$getPoolsInfoBetween(fromTimestamp / 1000, toTimestamp / 1000);
|
let pools = await PoolsRepository.$getPoolsInfoBetween(fromTimestamp / 1000, toTimestamp / 1000);
|
||||||
const totalBlocks = pools.reduce((acc, pool) => acc + pool.blockCount, 0);
|
const totalBlocks = pools.reduce((acc, pool) => acc + pool.blockCount, 0);
|
||||||
pools = pools.map((pool: any) => {
|
if (totalBlocks > 0) {
|
||||||
pool.hashrate = (pool.blockCount / totalBlocks) * lastBlockHashrate;
|
pools = pools.map((pool: any) => {
|
||||||
pool.share = (pool.blockCount / totalBlocks);
|
pool.hashrate = (pool.blockCount / totalBlocks) * lastBlockHashrate;
|
||||||
return pool;
|
pool.share = (pool.blockCount / totalBlocks);
|
||||||
});
|
return pool;
|
||||||
|
|
||||||
for (const pool of pools) {
|
|
||||||
hashrates.push({
|
|
||||||
hashrateTimestamp: toTimestamp / 1000,
|
|
||||||
avgHashrate: pool['hashrate'],
|
|
||||||
poolId: pool.poolId,
|
|
||||||
share: pool['share'],
|
|
||||||
type: 'weekly',
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
newlyIndexed += hashrates.length;
|
for (const pool of pools) {
|
||||||
await HashratesRepository.$saveHashrates(hashrates);
|
hashrates.push({
|
||||||
hashrates.length = 0;
|
hashrateTimestamp: toTimestamp / 1000,
|
||||||
|
avgHashrate: pool['hashrate'] ,
|
||||||
|
poolId: pool.poolId,
|
||||||
|
share: pool['share'],
|
||||||
|
type: 'weekly',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
newlyIndexed += hashrates.length;
|
||||||
|
await HashratesRepository.$saveHashrates(hashrates);
|
||||||
|
hashrates.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
|
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
|
||||||
if (elapsedSeconds > 1) {
|
if (elapsedSeconds > 1) {
|
||||||
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
||||||
const weeksPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
|
const weeksPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
|
||||||
const progress = Math.round(totalIndexed / totalWeekIndexed * 10000) / 100;
|
const progress = Math.round(totalIndexed / totalWeekIndexed * 10000) / 100;
|
||||||
const timeLeft = Math.round((totalWeekIndexed - totalIndexed) / weeksPerSeconds);
|
|
||||||
const formattedDate = new Date(fromTimestamp).toUTCString();
|
const formattedDate = new Date(fromTimestamp).toUTCString();
|
||||||
logger.debug(`Getting weekly pool hashrate for ${formattedDate} | ~${weeksPerSeconds.toFixed(2)} weeks/sec | total: ~${totalIndexed}/${Math.round(totalWeekIndexed)} (${progress}%) | elapsed: ${runningFor} seconds | left: ~${timeLeft} seconds`);
|
logger.debug(`Getting weekly pool hashrate for ${formattedDate} | ~${weeksPerSeconds.toFixed(2)} weeks/sec | total: ~${totalIndexed}/${Math.round(totalWeekIndexed)} (${progress}%) | elapsed: ${runningFor} seconds`);
|
||||||
timer = new Date().getTime() / 1000;
|
timer = new Date().getTime() / 1000;
|
||||||
indexedThisRun = 0;
|
indexedThisRun = 0;
|
||||||
loadingIndicators.setProgress('weekly-hashrate-indexing', progress, false);
|
loadingIndicators.setProgress('weekly-hashrate-indexing', progress, false);
|
||||||
@@ -257,11 +263,14 @@ class Mining {
|
|||||||
}
|
}
|
||||||
await HashratesRepository.$setLatestRun('last_weekly_hashrates_indexing', new Date().getUTCDate());
|
await HashratesRepository.$setLatestRun('last_weekly_hashrates_indexing', new Date().getUTCDate());
|
||||||
if (newlyIndexed > 0) {
|
if (newlyIndexed > 0) {
|
||||||
logger.info(`Indexed ${newlyIndexed} pools weekly hashrate`);
|
logger.notice(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed}`);
|
||||||
|
} else {
|
||||||
|
logger.debug(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed}`);
|
||||||
}
|
}
|
||||||
loadingIndicators.setProgress('weekly-hashrate-indexing', 100);
|
loadingIndicators.setProgress('weekly-hashrate-indexing', 100);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
loadingIndicators.setProgress('weekly-hashrate-indexing', 100);
|
loadingIndicators.setProgress('weekly-hashrate-indexing', 100);
|
||||||
|
logger.err(`Weekly mining pools hashrates indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -270,20 +279,19 @@ class Mining {
|
|||||||
* [INDEXING] Generate daily hashrate data
|
* [INDEXING] Generate daily hashrate data
|
||||||
*/
|
*/
|
||||||
public async $generateNetworkHashrateHistory(): Promise<void> {
|
public async $generateNetworkHashrateHistory(): Promise<void> {
|
||||||
try {
|
// We only run this once a day around midnight
|
||||||
// We only run this once a day around midnight
|
const latestRunDate = await HashratesRepository.$getLatestRun('last_hashrates_indexing');
|
||||||
const latestRunDate = await HashratesRepository.$getLatestRun('last_hashrates_indexing');
|
const now = new Date().getUTCDate();
|
||||||
const now = new Date().getUTCDate();
|
if (now === latestRunDate) {
|
||||||
if (now === latestRunDate) {
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const oldestConsecutiveBlockTimestamp = 1000 * (await BlocksRepository.$getOldestConsecutiveBlock()).timestamp;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const indexedTimestamp = (await HashratesRepository.$getNetworkDailyHashrate(null)).map(hashrate => hashrate.timestamp);
|
const genesisBlock = await bitcoinClient.getBlock(await bitcoinClient.getBlockHash(0));
|
||||||
const genesisTimestamp = 1231006505000; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f
|
const genesisTimestamp = genesisBlock.time * 1000;
|
||||||
|
const indexedTimestamp = (await HashratesRepository.$getRawNetworkDailyHashrate(null)).map(hashrate => hashrate.timestamp);
|
||||||
const lastMidnight = this.getDateMidnight(new Date());
|
const lastMidnight = this.getDateMidnight(new Date());
|
||||||
let toTimestamp = Math.round(lastMidnight.getTime());
|
let toTimestamp = Math.round(lastMidnight.getTime());
|
||||||
const hashrates: any[] = [];
|
const hashrates: any[] = [];
|
||||||
@@ -298,27 +306,19 @@ class Mining {
|
|||||||
logger.debug(`Indexing daily network hashrate`);
|
logger.debug(`Indexing daily network hashrate`);
|
||||||
loadingIndicators.setProgress('daily-hashrate-indexing', 0);
|
loadingIndicators.setProgress('daily-hashrate-indexing', 0);
|
||||||
|
|
||||||
while (toTimestamp > genesisTimestamp) {
|
while (toTimestamp > genesisTimestamp && toTimestamp > oldestConsecutiveBlockTimestamp) {
|
||||||
const fromTimestamp = toTimestamp - 86400000;
|
const fromTimestamp = toTimestamp - 86400000;
|
||||||
|
|
||||||
// Skip already indexed weeks
|
// Skip already indexed days
|
||||||
if (indexedTimestamp.includes(toTimestamp / 1000)) {
|
if (indexedTimestamp.includes(toTimestamp / 1000)) {
|
||||||
toTimestamp -= 86400000;
|
toTimestamp -= 86400000;
|
||||||
++totalIndexed;
|
++totalIndexed;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we have blocks for the previous day (which mean that the day
|
|
||||||
// we are currently indexing has complete data)
|
|
||||||
const blockStatsPreviousDay: any = await BlocksRepository.$blockCountBetweenTimestamp(
|
|
||||||
null, (fromTimestamp - 86400000) / 1000, (toTimestamp - 86400000) / 1000);
|
|
||||||
if (blockStatsPreviousDay.blockCount === 0) { // We are done indexing
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const blockStats: any = await BlocksRepository.$blockCountBetweenTimestamp(
|
const blockStats: any = await BlocksRepository.$blockCountBetweenTimestamp(
|
||||||
null, fromTimestamp / 1000, toTimestamp / 1000);
|
null, fromTimestamp / 1000, toTimestamp / 1000);
|
||||||
const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount,
|
const lastBlockHashrate = blockStats.blockCount === 0 ? 0 : await bitcoinClient.getNetworkHashPs(blockStats.blockCount,
|
||||||
blockStats.lastBlockHeight);
|
blockStats.lastBlockHeight);
|
||||||
|
|
||||||
hashrates.push({
|
hashrates.push({
|
||||||
@@ -340,9 +340,8 @@ class Mining {
|
|||||||
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
||||||
const daysPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
|
const daysPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
|
||||||
const progress = Math.round(totalIndexed / totalDayIndexed * 10000) / 100;
|
const progress = Math.round(totalIndexed / totalDayIndexed * 10000) / 100;
|
||||||
const timeLeft = Math.round((totalDayIndexed - totalIndexed) / daysPerSeconds);
|
|
||||||
const formattedDate = new Date(fromTimestamp).toUTCString();
|
const formattedDate = new Date(fromTimestamp).toUTCString();
|
||||||
logger.debug(`Getting network daily hashrate for ${formattedDate} | ~${daysPerSeconds.toFixed(2)} days/sec | total: ~${totalIndexed}/${Math.round(totalDayIndexed)} (${progress}%) | elapsed: ${runningFor} seconds | left: ~${timeLeft} seconds`);
|
logger.debug(`Getting network daily hashrate for ${formattedDate} | ~${daysPerSeconds.toFixed(2)} days/sec | total: ~${totalIndexed}/${Math.round(totalDayIndexed)} (${progress}%) | elapsed: ${runningFor} seconds`);
|
||||||
timer = new Date().getTime() / 1000;
|
timer = new Date().getTime() / 1000;
|
||||||
indexedThisRun = 0;
|
indexedThisRun = 0;
|
||||||
loadingIndicators.setProgress('daily-hashrate-indexing', progress);
|
loadingIndicators.setProgress('daily-hashrate-indexing', progress);
|
||||||
@@ -354,11 +353,12 @@ class Mining {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add genesis block manually
|
// Add genesis block manually
|
||||||
if (toTimestamp <= genesisTimestamp && !indexedTimestamp.includes(genesisTimestamp)) {
|
if (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT === -1 && !indexedTimestamp.includes(genesisTimestamp / 1000)) {
|
||||||
hashrates.push({
|
hashrates.push({
|
||||||
hashrateTimestamp: genesisTimestamp,
|
hashrateTimestamp: genesisTimestamp / 1000,
|
||||||
avgHashrate: await bitcoinClient.getNetworkHashPs(1, 1),
|
avgHashrate: await bitcoinClient.getNetworkHashPs(1, 1),
|
||||||
poolId: null,
|
poolId: 0,
|
||||||
|
share: 1,
|
||||||
type: 'daily',
|
type: 'daily',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -368,15 +368,91 @@ class Mining {
|
|||||||
|
|
||||||
await HashratesRepository.$setLatestRun('last_hashrates_indexing', new Date().getUTCDate());
|
await HashratesRepository.$setLatestRun('last_hashrates_indexing', new Date().getUTCDate());
|
||||||
if (newlyIndexed > 0) {
|
if (newlyIndexed > 0) {
|
||||||
logger.info(`Indexed ${newlyIndexed} day of network hashrate`);
|
logger.notice(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`);
|
||||||
|
} else {
|
||||||
|
logger.debug(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`);
|
||||||
}
|
}
|
||||||
loadingIndicators.setProgress('daily-hashrate-indexing', 100);
|
loadingIndicators.setProgress('daily-hashrate-indexing', 100);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
loadingIndicators.setProgress('daily-hashrate-indexing', 100);
|
loadingIndicators.setProgress('daily-hashrate-indexing', 100);
|
||||||
|
logger.err(`Daily network hashrate indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Index difficulty adjustments
|
||||||
|
*/
|
||||||
|
public async $indexDifficultyAdjustments(): Promise<void> {
|
||||||
|
const indexedHeightsArray = await DifficultyAdjustmentsRepository.$getAdjustmentsHeights();
|
||||||
|
const indexedHeights = {};
|
||||||
|
for (const height of indexedHeightsArray) {
|
||||||
|
indexedHeights[height] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blocks: any = await BlocksRepository.$getBlocksDifficulty();
|
||||||
|
const genesisBlock = await bitcoinClient.getBlock(await bitcoinClient.getBlockHash(0));
|
||||||
|
let currentDifficulty = genesisBlock.difficulty;
|
||||||
|
let totalIndexed = 0;
|
||||||
|
|
||||||
|
if (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT === -1 && indexedHeights[0] !== true) {
|
||||||
|
await DifficultyAdjustmentsRepository.$saveAdjustments({
|
||||||
|
time: genesisBlock.time,
|
||||||
|
height: 0,
|
||||||
|
difficulty: currentDifficulty,
|
||||||
|
adjustment: 0.0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldestConsecutiveBlock = await BlocksRepository.$getOldestConsecutiveBlock();
|
||||||
|
if (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT !== -1) {
|
||||||
|
currentDifficulty = oldestConsecutiveBlock.difficulty;
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalBlockChecked = 0;
|
||||||
|
let timer = new Date().getTime() / 1000;
|
||||||
|
|
||||||
|
for (const block of blocks) {
|
||||||
|
if (block.difficulty !== currentDifficulty) {
|
||||||
|
if (indexedHeights[block.height] === true) { // Already indexed
|
||||||
|
if (block.height >= oldestConsecutiveBlock.height) {
|
||||||
|
currentDifficulty = block.difficulty;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let adjustment = block.difficulty / currentDifficulty;
|
||||||
|
adjustment = Math.round(adjustment * 1000000) / 1000000; // Remove float point noise
|
||||||
|
|
||||||
|
await DifficultyAdjustmentsRepository.$saveAdjustments({
|
||||||
|
time: block.time,
|
||||||
|
height: block.height,
|
||||||
|
difficulty: block.difficulty,
|
||||||
|
adjustment: adjustment,
|
||||||
|
});
|
||||||
|
|
||||||
|
totalIndexed++;
|
||||||
|
if (block.height >= oldestConsecutiveBlock.height) {
|
||||||
|
currentDifficulty = block.difficulty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
totalBlockChecked++;
|
||||||
|
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
|
||||||
|
if (elapsedSeconds > 5) {
|
||||||
|
const progress = Math.round(totalBlockChecked / blocks.length * 100);
|
||||||
|
logger.info(`Indexing difficulty adjustment at block #${block.height} | Progress: ${progress}%`);
|
||||||
|
timer = new Date().getTime() / 1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalIndexed > 0) {
|
||||||
|
logger.notice(`Indexed ${totalIndexed} difficulty adjustments`);
|
||||||
|
} else {
|
||||||
|
logger.debug(`Indexed ${totalIndexed} difficulty adjustments`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private getDateMidnight(date: Date): Date {
|
private getDateMidnight(date: Date): Date {
|
||||||
date.setUTCHours(0);
|
date.setUTCHours(0);
|
||||||
date.setUTCMinutes(0);
|
date.setUTCMinutes(0);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import DB from '../database';
|
import DB from '../database';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
|
import BlocksRepository from '../repositories/BlocksRepository';
|
||||||
|
|
||||||
interface Pool {
|
interface Pool {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -11,6 +12,14 @@ interface Pool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class PoolsParser {
|
class PoolsParser {
|
||||||
|
miningPools: any[] = [];
|
||||||
|
unknownPool: any = {
|
||||||
|
'name': "Unknown",
|
||||||
|
'link': "https://learnmeabitcoin.com/technical/coinbase-transaction",
|
||||||
|
'regexes': "[]",
|
||||||
|
'addresses': "[]",
|
||||||
|
'slug': 'unknown'
|
||||||
|
};
|
||||||
slugWarnFlag = false;
|
slugWarnFlag = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,7 +33,6 @@ class PoolsParser {
|
|||||||
// First we save every entries without paying attention to pool duplication
|
// First we save every entries without paying attention to pool duplication
|
||||||
const poolsDuplicated: Pool[] = [];
|
const poolsDuplicated: Pool[] = [];
|
||||||
|
|
||||||
logger.debug('Parse coinbase_tags');
|
|
||||||
const coinbaseTags = Object.entries(poolsJson['coinbase_tags']);
|
const coinbaseTags = Object.entries(poolsJson['coinbase_tags']);
|
||||||
for (let i = 0; i < coinbaseTags.length; ++i) {
|
for (let i = 0; i < coinbaseTags.length; ++i) {
|
||||||
poolsDuplicated.push({
|
poolsDuplicated.push({
|
||||||
@@ -35,7 +43,6 @@ class PoolsParser {
|
|||||||
'slug': ''
|
'slug': ''
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
logger.debug('Parse payout_addresses');
|
|
||||||
const addressesTags = Object.entries(poolsJson['payout_addresses']);
|
const addressesTags = Object.entries(poolsJson['payout_addresses']);
|
||||||
for (let i = 0; i < addressesTags.length; ++i) {
|
for (let i = 0; i < addressesTags.length; ++i) {
|
||||||
poolsDuplicated.push({
|
poolsDuplicated.push({
|
||||||
@@ -48,7 +55,6 @@ class PoolsParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Then, we find unique mining pool names
|
// Then, we find unique mining pool names
|
||||||
logger.debug('Identify unique mining pools');
|
|
||||||
const poolNames: string[] = [];
|
const poolNames: string[] = [];
|
||||||
for (let i = 0; i < poolsDuplicated.length; ++i) {
|
for (let i = 0; i < poolsDuplicated.length; ++i) {
|
||||||
if (poolNames.indexOf(poolsDuplicated[i].name) === -1) {
|
if (poolNames.indexOf(poolsDuplicated[i].name) === -1) {
|
||||||
@@ -60,12 +66,18 @@ class PoolsParser {
|
|||||||
// Get existing pools from the db
|
// Get existing pools from the db
|
||||||
let existingPools;
|
let existingPools;
|
||||||
try {
|
try {
|
||||||
[existingPools] = await DB.query({ sql: 'SELECT * FROM pools;', timeout: 120000 });
|
if (config.DATABASE.ENABLED === true) {
|
||||||
|
[existingPools] = await DB.query({ sql: 'SELECT * FROM pools;', timeout: 120000 });
|
||||||
|
} else {
|
||||||
|
existingPools = [];
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('Cannot get existing pools from the database, skipping pools.json import');
|
logger.err('Cannot get existing pools from the database, skipping pools.json import');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.miningPools = [];
|
||||||
|
|
||||||
// Finally, we generate the final consolidated pools data
|
// Finally, we generate the final consolidated pools data
|
||||||
const finalPoolDataAdd: Pool[] = [];
|
const finalPoolDataAdd: Pool[] = [];
|
||||||
const finalPoolDataUpdate: Pool[] = [];
|
const finalPoolDataUpdate: Pool[] = [];
|
||||||
@@ -97,60 +109,87 @@ class PoolsParser {
|
|||||||
logger.warn(`No slug found for '${poolNames[i]}', generating it => '${slug}'`);
|
logger.warn(`No slug found for '${poolNames[i]}', generating it => '${slug}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingPools.find((pool) => pool.name === poolNames[i]) !== undefined) {
|
const poolObj = {
|
||||||
finalPoolDataUpdate.push({
|
'name': finalPoolName,
|
||||||
'name': finalPoolName,
|
'link': match[0].link,
|
||||||
'link': match[0].link,
|
'regexes': allRegexes,
|
||||||
'regexes': allRegexes,
|
'addresses': allAddresses,
|
||||||
'addresses': allAddresses,
|
'slug': slug
|
||||||
'slug': slug
|
};
|
||||||
});
|
|
||||||
|
const existingPool = existingPools.find((pool) => pool.name === poolNames[i]);
|
||||||
|
if (existingPool !== undefined) {
|
||||||
|
// Check if any data was actually updated
|
||||||
|
const equals = (a, b) =>
|
||||||
|
a.length === b.length &&
|
||||||
|
a.every((v, i) => v === b[i]);
|
||||||
|
if (!equals(JSON.parse(existingPool.addresses), poolObj.addresses) || !equals(JSON.parse(existingPool.regexes), poolObj.regexes)) {
|
||||||
|
finalPoolDataUpdate.push(poolObj);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.debug(`Add '${finalPoolName}' mining pool`);
|
logger.debug(`Add '${finalPoolName}' mining pool`);
|
||||||
finalPoolDataAdd.push({
|
finalPoolDataAdd.push(poolObj);
|
||||||
'name': finalPoolName,
|
|
||||||
'link': match[0].link,
|
|
||||||
'regexes': allRegexes,
|
|
||||||
'addresses': allAddresses,
|
|
||||||
'slug': slug
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.miningPools.push({
|
||||||
|
'name': finalPoolName,
|
||||||
|
'link': match[0].link,
|
||||||
|
'regexes': JSON.stringify(allRegexes),
|
||||||
|
'addresses': JSON.stringify(allAddresses),
|
||||||
|
'slug': slug
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`Update pools table now`);
|
if (config.DATABASE.ENABLED === false) { // Don't run db operations
|
||||||
|
logger.info('Mining pools.json import completed (no database)');
|
||||||
// Add new mining pools into the database
|
return;
|
||||||
let queryAdd: string = 'INSERT INTO pools(name, link, regexes, addresses, slug) VALUES ';
|
|
||||||
for (let i = 0; i < finalPoolDataAdd.length; ++i) {
|
|
||||||
queryAdd += `('${finalPoolDataAdd[i].name}', '${finalPoolDataAdd[i].link}',
|
|
||||||
'${JSON.stringify(finalPoolDataAdd[i].regexes)}', '${JSON.stringify(finalPoolDataAdd[i].addresses)}',
|
|
||||||
${JSON.stringify(finalPoolDataAdd[i].slug)}),`;
|
|
||||||
}
|
}
|
||||||
queryAdd = queryAdd.slice(0, -1) + ';';
|
|
||||||
|
|
||||||
// Add new mining pools into the database
|
if (finalPoolDataAdd.length > 0 || finalPoolDataUpdate.length > 0) {
|
||||||
const updateQueries: string[] = [];
|
logger.debug(`Update pools table now`);
|
||||||
for (let i = 0; i < finalPoolDataUpdate.length; ++i) {
|
|
||||||
updateQueries.push(`
|
// Add new mining pools into the database
|
||||||
UPDATE pools
|
let queryAdd: string = 'INSERT INTO pools(name, link, regexes, addresses, slug) VALUES ';
|
||||||
SET name='${finalPoolDataUpdate[i].name}', link='${finalPoolDataUpdate[i].link}',
|
for (let i = 0; i < finalPoolDataAdd.length; ++i) {
|
||||||
regexes='${JSON.stringify(finalPoolDataUpdate[i].regexes)}', addresses='${JSON.stringify(finalPoolDataUpdate[i].addresses)}',
|
queryAdd += `('${finalPoolDataAdd[i].name}', '${finalPoolDataAdd[i].link}',
|
||||||
slug='${finalPoolDataUpdate[i].slug}'
|
'${JSON.stringify(finalPoolDataAdd[i].regexes)}', '${JSON.stringify(finalPoolDataAdd[i].addresses)}',
|
||||||
WHERE name='${finalPoolDataUpdate[i].name}'
|
${JSON.stringify(finalPoolDataAdd[i].slug)}),`;
|
||||||
;`);
|
}
|
||||||
|
queryAdd = queryAdd.slice(0, -1) + ';';
|
||||||
|
|
||||||
|
// Updated existing mining pools in the database
|
||||||
|
const updateQueries: string[] = [];
|
||||||
|
for (let i = 0; i < finalPoolDataUpdate.length; ++i) {
|
||||||
|
updateQueries.push(`
|
||||||
|
UPDATE pools
|
||||||
|
SET name='${finalPoolDataUpdate[i].name}', link='${finalPoolDataUpdate[i].link}',
|
||||||
|
regexes='${JSON.stringify(finalPoolDataUpdate[i].regexes)}', addresses='${JSON.stringify(finalPoolDataUpdate[i].addresses)}',
|
||||||
|
slug='${finalPoolDataUpdate[i].slug}'
|
||||||
|
WHERE name='${finalPoolDataUpdate[i].name}'
|
||||||
|
;`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.$deleteBlocskToReindex(finalPoolDataUpdate);
|
||||||
|
|
||||||
|
if (finalPoolDataAdd.length > 0) {
|
||||||
|
await DB.query({ sql: queryAdd, timeout: 120000 });
|
||||||
|
}
|
||||||
|
for (const query of updateQueries) {
|
||||||
|
await DB.query({ sql: query, timeout: 120000 });
|
||||||
|
}
|
||||||
|
await this.insertUnknownPool();
|
||||||
|
logger.info('Mining pools.json import completed');
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Cannot import pools in the database`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (finalPoolDataAdd.length > 0) {
|
|
||||||
await DB.query({ sql: queryAdd, timeout: 120000 });
|
|
||||||
}
|
|
||||||
for (const query of updateQueries) {
|
|
||||||
await DB.query({ sql: query, timeout: 120000 });
|
|
||||||
}
|
|
||||||
await this.insertUnknownPool();
|
await this.insertUnknownPool();
|
||||||
logger.info('Mining pools.json import completed');
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err(`Cannot import pools in the database`);
|
logger.err(`Cannot insert unknown pool in the database`);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,6 +217,36 @@ class PoolsParser {
|
|||||||
logger.err('Unable to insert "Unknown" mining pool');
|
logger.err('Unable to insert "Unknown" mining pool');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete blocks which needs to be reindexed
|
||||||
|
*/
|
||||||
|
private async $deleteBlocskToReindex(finalPoolDataUpdate: any[]) {
|
||||||
|
if (config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blockCount = await BlocksRepository.$blockCount(null, null);
|
||||||
|
if (blockCount === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const updatedPool of finalPoolDataUpdate) {
|
||||||
|
const [pool]: any[] = await DB.query(`SELECT id, name from pools where slug = "${updatedPool.slug}"`);
|
||||||
|
if (pool.length > 0) {
|
||||||
|
logger.notice(`Deleting blocks from ${pool[0].name} mining pool for future re-indexing`);
|
||||||
|
await DB.query(`DELETE FROM blocks WHERE pool_id = ${pool[0].id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore early days of Bitcoin as there were not mining pool yet
|
||||||
|
logger.notice('Deleting blocks with unknown mining pool from height 130635 for future re-indexing');
|
||||||
|
const [unknownPool] = await DB.query(`SELECT id from pools where slug = "unknown"`);
|
||||||
|
await DB.query(`DELETE FROM blocks WHERE pool_id = ${unknownPool[0].id} AND height > 130635`);
|
||||||
|
|
||||||
|
logger.notice('Truncating hashrates for future re-indexing');
|
||||||
|
await DB.query(`DELETE FROM hashrates`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new PoolsParser();
|
export default new PoolsParser();
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import * as WebSocket from 'ws';
|
import * as WebSocket from 'ws';
|
||||||
import { BlockExtended, TransactionExtended, WebsocketResponse, MempoolBlock, MempoolBlockDelta,
|
import {
|
||||||
OptimizedStatistic, ILoadingIndicators, IConversionRates } from '../mempool.interfaces';
|
BlockExtended, TransactionExtended, WebsocketResponse, MempoolBlock, MempoolBlockDelta,
|
||||||
|
OptimizedStatistic, ILoadingIndicators, IConversionRates
|
||||||
|
} from '../mempool.interfaces';
|
||||||
import blocks from './blocks';
|
import blocks from './blocks';
|
||||||
import memPool from './mempool';
|
import memPool from './mempool';
|
||||||
import backendInfo from './backend-info';
|
import backendInfo from './backend-info';
|
||||||
@@ -14,6 +16,7 @@ import transactionUtils from './transaction-utils';
|
|||||||
import rbfCache from './rbf-cache';
|
import rbfCache from './rbf-cache';
|
||||||
import difficultyAdjustment from './difficulty-adjustment';
|
import difficultyAdjustment from './difficulty-adjustment';
|
||||||
import feeApi from './fee-api';
|
import feeApi from './fee-api';
|
||||||
|
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
|
||||||
|
|
||||||
class WebsocketHandler {
|
class WebsocketHandler {
|
||||||
private wss: WebSocket.Server | undefined;
|
private wss: WebSocket.Server | undefined;
|
||||||
@@ -116,12 +119,10 @@ class WebsocketHandler {
|
|||||||
const index = parsedMessage['track-mempool-block'];
|
const index = parsedMessage['track-mempool-block'];
|
||||||
client['track-mempool-block'] = index;
|
client['track-mempool-block'] = index;
|
||||||
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
|
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
|
||||||
if (mBlocksWithTransactions[index]) {
|
response['projected-block-transactions'] = {
|
||||||
response['projected-block-transactions'] = {
|
index: index,
|
||||||
index: index,
|
blockTransactions: mBlocksWithTransactions[index]?.transactions || [],
|
||||||
blockTransactions: mBlocksWithTransactions[index].transactions
|
};
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
client['track-mempool-block'] = null;
|
client['track-mempool-block'] = null;
|
||||||
}
|
}
|
||||||
@@ -166,7 +167,7 @@ class WebsocketHandler {
|
|||||||
throw new Error('WebSocket.Server is not set');
|
throw new Error('WebSocket.Server is not set');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.wss.clients.forEach((client: WebSocket) => {
|
this.wss.clients.forEach((client) => {
|
||||||
if (client.readyState !== WebSocket.OPEN) {
|
if (client.readyState !== WebSocket.OPEN) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -181,7 +182,7 @@ class WebsocketHandler {
|
|||||||
throw new Error('WebSocket.Server is not set');
|
throw new Error('WebSocket.Server is not set');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.wss.clients.forEach((client: WebSocket) => {
|
this.wss.clients.forEach((client) => {
|
||||||
if (client.readyState !== WebSocket.OPEN) {
|
if (client.readyState !== WebSocket.OPEN) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -194,7 +195,7 @@ class WebsocketHandler {
|
|||||||
throw new Error('WebSocket.Server is not set');
|
throw new Error('WebSocket.Server is not set');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.wss.clients.forEach((client: WebSocket) => {
|
this.wss.clients.forEach((client) => {
|
||||||
if (client.readyState !== WebSocket.OPEN) {
|
if (client.readyState !== WebSocket.OPEN) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -226,7 +227,7 @@ class WebsocketHandler {
|
|||||||
throw new Error('WebSocket.Server is not set');
|
throw new Error('WebSocket.Server is not set');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.wss.clients.forEach((client: WebSocket) => {
|
this.wss.clients.forEach((client) => {
|
||||||
if (client.readyState !== WebSocket.OPEN) {
|
if (client.readyState !== WebSocket.OPEN) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -257,7 +258,7 @@ class WebsocketHandler {
|
|||||||
memPool.handleRbfTransactions(rbfTransactions);
|
memPool.handleRbfTransactions(rbfTransactions);
|
||||||
const recommendedFees = feeApi.getRecommendedFee();
|
const recommendedFees = feeApi.getRecommendedFee();
|
||||||
|
|
||||||
this.wss.clients.forEach(async (client: WebSocket) => {
|
this.wss.clients.forEach(async (client) => {
|
||||||
if (client.readyState !== WebSocket.OPEN) {
|
if (client.readyState !== WebSocket.OPEN) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -416,17 +417,40 @@ class WebsocketHandler {
|
|||||||
|
|
||||||
if (_mempoolBlocks[0]) {
|
if (_mempoolBlocks[0]) {
|
||||||
const matches: string[] = [];
|
const matches: string[] = [];
|
||||||
|
const added: string[] = [];
|
||||||
|
const missing: string[] = [];
|
||||||
|
|
||||||
for (const txId of txIds) {
|
for (const txId of txIds) {
|
||||||
if (_mempoolBlocks[0].transactionIds.indexOf(txId) > -1) {
|
if (_mempoolBlocks[0].transactionIds.indexOf(txId) > -1) {
|
||||||
matches.push(txId);
|
matches.push(txId);
|
||||||
|
} else {
|
||||||
|
added.push(txId);
|
||||||
}
|
}
|
||||||
delete _memPool[txId];
|
delete _memPool[txId];
|
||||||
}
|
}
|
||||||
|
|
||||||
matchRate = Math.round((matches.length / (txIds.length - 1)) * 100);
|
for (const txId of _mempoolBlocks[0].transactionIds) {
|
||||||
|
if (matches.includes(txId) || added.includes(txId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
missing.push(txId);
|
||||||
|
}
|
||||||
|
|
||||||
|
matchRate = Math.round((Math.max(0, matches.length - missing.length - added.length) / txIds.length * 100) * 100) / 100;
|
||||||
mempoolBlocks.updateMempoolBlocks(_memPool);
|
mempoolBlocks.updateMempoolBlocks(_memPool);
|
||||||
mBlocks = mempoolBlocks.getMempoolBlocks();
|
mBlocks = mempoolBlocks.getMempoolBlocks();
|
||||||
mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
|
mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
|
||||||
|
|
||||||
|
if (Common.indexingEnabled()) {
|
||||||
|
BlocksAuditsRepository.$saveAudit({
|
||||||
|
time: block.timestamp,
|
||||||
|
height: block.height,
|
||||||
|
hash: block.id,
|
||||||
|
addedTxs: added,
|
||||||
|
missingTxs: missing,
|
||||||
|
matchRate: matchRate,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (block.extras) {
|
if (block.extras) {
|
||||||
|
|||||||
@@ -15,10 +15,15 @@ interface IConfig {
|
|||||||
INITIAL_BLOCKS_AMOUNT: number;
|
INITIAL_BLOCKS_AMOUNT: number;
|
||||||
MEMPOOL_BLOCKS_AMOUNT: number;
|
MEMPOOL_BLOCKS_AMOUNT: number;
|
||||||
INDEXING_BLOCKS_AMOUNT: number;
|
INDEXING_BLOCKS_AMOUNT: number;
|
||||||
|
BLOCKS_SUMMARIES_INDEXING: boolean;
|
||||||
PRICE_FEED_UPDATE_INTERVAL: number;
|
PRICE_FEED_UPDATE_INTERVAL: number;
|
||||||
USE_SECOND_NODE_FOR_MINFEE: boolean;
|
USE_SECOND_NODE_FOR_MINFEE: boolean;
|
||||||
EXTERNAL_ASSETS: string[];
|
EXTERNAL_ASSETS: string[];
|
||||||
|
EXTERNAL_MAX_RETRY: number;
|
||||||
|
EXTERNAL_RETRY_INTERVAL: number;
|
||||||
|
USER_AGENT: string;
|
||||||
STDOUT_LOG_MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug';
|
STDOUT_LOG_MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug';
|
||||||
|
AUTOMATIC_BLOCK_REINDEXING: boolean;
|
||||||
};
|
};
|
||||||
ESPLORA: {
|
ESPLORA: {
|
||||||
REST_API_URL: string;
|
REST_API_URL: string;
|
||||||
@@ -66,6 +71,7 @@ interface IConfig {
|
|||||||
};
|
};
|
||||||
SOCKS5PROXY: {
|
SOCKS5PROXY: {
|
||||||
ENABLED: boolean;
|
ENABLED: boolean;
|
||||||
|
USE_ONION: boolean;
|
||||||
HOST: string;
|
HOST: string;
|
||||||
PORT: number;
|
PORT: number;
|
||||||
USERNAME: string;
|
USERNAME: string;
|
||||||
@@ -75,6 +81,14 @@ interface IConfig {
|
|||||||
TOR_URL: string;
|
TOR_URL: string;
|
||||||
CLEARNET_URL: string;
|
CLEARNET_URL: string;
|
||||||
};
|
};
|
||||||
|
EXTERNAL_DATA_SERVER: {
|
||||||
|
MEMPOOL_API: string;
|
||||||
|
MEMPOOL_ONION: string;
|
||||||
|
LIQUID_API: string;
|
||||||
|
LIQUID_ONION: string;
|
||||||
|
BISQ_URL: string;
|
||||||
|
BISQ_ONION: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaults: IConfig = {
|
const defaults: IConfig = {
|
||||||
@@ -92,12 +106,15 @@ const defaults: IConfig = {
|
|||||||
'INITIAL_BLOCKS_AMOUNT': 8,
|
'INITIAL_BLOCKS_AMOUNT': 8,
|
||||||
'MEMPOOL_BLOCKS_AMOUNT': 8,
|
'MEMPOOL_BLOCKS_AMOUNT': 8,
|
||||||
'INDEXING_BLOCKS_AMOUNT': 11000, // 0 = disable indexing, -1 = index all blocks
|
'INDEXING_BLOCKS_AMOUNT': 11000, // 0 = disable indexing, -1 = index all blocks
|
||||||
|
'BLOCKS_SUMMARIES_INDEXING': false,
|
||||||
'PRICE_FEED_UPDATE_INTERVAL': 600,
|
'PRICE_FEED_UPDATE_INTERVAL': 600,
|
||||||
'USE_SECOND_NODE_FOR_MINFEE': false,
|
'USE_SECOND_NODE_FOR_MINFEE': false,
|
||||||
'EXTERNAL_ASSETS': [
|
'EXTERNAL_ASSETS': [],
|
||||||
'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json'
|
'EXTERNAL_MAX_RETRY': 1,
|
||||||
],
|
'EXTERNAL_RETRY_INTERVAL': 0,
|
||||||
|
'USER_AGENT': 'mempool',
|
||||||
'STDOUT_LOG_MIN_PRIORITY': 'debug',
|
'STDOUT_LOG_MIN_PRIORITY': 'debug',
|
||||||
|
'AUTOMATIC_BLOCK_REINDEXING': false,
|
||||||
},
|
},
|
||||||
'ESPLORA': {
|
'ESPLORA': {
|
||||||
'REST_API_URL': 'http://127.0.0.1:3000',
|
'REST_API_URL': 'http://127.0.0.1:3000',
|
||||||
@@ -145,6 +162,7 @@ const defaults: IConfig = {
|
|||||||
},
|
},
|
||||||
'SOCKS5PROXY': {
|
'SOCKS5PROXY': {
|
||||||
'ENABLED': false,
|
'ENABLED': false,
|
||||||
|
'USE_ONION': true,
|
||||||
'HOST': '127.0.0.1',
|
'HOST': '127.0.0.1',
|
||||||
'PORT': 9050,
|
'PORT': 9050,
|
||||||
'USERNAME': '',
|
'USERNAME': '',
|
||||||
@@ -153,6 +171,14 @@ const defaults: IConfig = {
|
|||||||
"PRICE_DATA_SERVER": {
|
"PRICE_DATA_SERVER": {
|
||||||
'TOR_URL': 'http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices',
|
'TOR_URL': 'http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices',
|
||||||
'CLEARNET_URL': 'https://price.bisq.wiz.biz/getAllMarketPrices'
|
'CLEARNET_URL': 'https://price.bisq.wiz.biz/getAllMarketPrices'
|
||||||
|
},
|
||||||
|
"EXTERNAL_DATA_SERVER": {
|
||||||
|
'MEMPOOL_API': 'https://mempool.space/api/v1',
|
||||||
|
'MEMPOOL_ONION': 'http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1',
|
||||||
|
'LIQUID_API': 'https://liquid.network/api/v1',
|
||||||
|
'LIQUID_ONION': 'http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1',
|
||||||
|
'BISQ_URL': 'https://bisq.markets/api',
|
||||||
|
'BISQ_ONION': 'http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -168,6 +194,7 @@ class Config implements IConfig {
|
|||||||
BISQ: IConfig['BISQ'];
|
BISQ: IConfig['BISQ'];
|
||||||
SOCKS5PROXY: IConfig['SOCKS5PROXY'];
|
SOCKS5PROXY: IConfig['SOCKS5PROXY'];
|
||||||
PRICE_DATA_SERVER: IConfig['PRICE_DATA_SERVER'];
|
PRICE_DATA_SERVER: IConfig['PRICE_DATA_SERVER'];
|
||||||
|
EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER'];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const configs = this.merge(configFile, defaults);
|
const configs = this.merge(configFile, defaults);
|
||||||
@@ -182,6 +209,7 @@ class Config implements IConfig {
|
|||||||
this.BISQ = configs.BISQ;
|
this.BISQ = configs.BISQ;
|
||||||
this.SOCKS5PROXY = configs.SOCKS5PROXY;
|
this.SOCKS5PROXY = configs.SOCKS5PROXY;
|
||||||
this.PRICE_DATA_SERVER = configs.PRICE_DATA_SERVER;
|
this.PRICE_DATA_SERVER = configs.PRICE_DATA_SERVER;
|
||||||
|
this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER;
|
||||||
}
|
}
|
||||||
|
|
||||||
merge = (...objects: object[]): IConfig => {
|
merge = (...objects: object[]): IConfig => {
|
||||||
|
|||||||
@@ -22,12 +22,20 @@ import { PoolOptions } from 'mysql2/typings/mysql';
|
|||||||
timezone: '+00:00',
|
timezone: '+00:00',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private checkDBFlag() {
|
||||||
|
if (config.DATABASE.ENABLED === false) {
|
||||||
|
logger.err('Trying to use DB feature but config.DATABASE.ENABLED is set to false, please open an issue');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async query(query, params?) {
|
public async query(query, params?) {
|
||||||
|
this.checkDBFlag();
|
||||||
const pool = await this.getPool();
|
const pool = await this.getPool();
|
||||||
return pool.query(query, params);
|
return pool.query(query, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async checkDbConnection() {
|
public async checkDbConnection() {
|
||||||
|
this.checkDBFlag();
|
||||||
try {
|
try {
|
||||||
await this.query('SELECT ?', [1]);
|
await this.query('SELECT ?', [1]);
|
||||||
logger.info('Database connection established.');
|
logger.info('Database connection established.');
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Express, Request, Response, NextFunction } from 'express';
|
import express from "express";
|
||||||
import * as express from 'express';
|
import { Application, Request, Response, NextFunction, Express } from 'express';
|
||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import * as WebSocket from 'ws';
|
import * as WebSocket from 'ws';
|
||||||
import * as cluster from 'cluster';
|
import cluster from 'cluster';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
import DB from './database';
|
import DB from './database';
|
||||||
@@ -27,11 +27,13 @@ import icons from './api/liquid/icons';
|
|||||||
import { Common } from './api/common';
|
import { Common } from './api/common';
|
||||||
import poolsUpdater from './tasks/pools-updater';
|
import poolsUpdater from './tasks/pools-updater';
|
||||||
import indexer from './indexer';
|
import indexer from './indexer';
|
||||||
|
import priceUpdater from './tasks/price-updater';
|
||||||
|
import BlocksAuditsRepository from './repositories/BlocksAuditsRepository';
|
||||||
|
|
||||||
class Server {
|
class Server {
|
||||||
private wss: WebSocket.Server | undefined;
|
private wss: WebSocket.Server | undefined;
|
||||||
private server: http.Server | undefined;
|
private server: http.Server | undefined;
|
||||||
private app: Express;
|
private app: Application;
|
||||||
private currentBackendRetryInterval = 5;
|
private currentBackendRetryInterval = 5;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -42,7 +44,7 @@ class Server {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cluster.isMaster) {
|
if (cluster.isPrimary) {
|
||||||
logger.notice(`Mempool Server (Master) is running on port ${config.MEMPOOL.HTTP_PORT} (${backendInfo.getShortCommitHash()})`);
|
logger.notice(`Mempool Server (Master) is running on port ${config.MEMPOOL.HTTP_PORT} (${backendInfo.getShortCommitHash()})`);
|
||||||
|
|
||||||
const numCPUs = config.MEMPOOL.SPAWN_CLUSTER_PROCS;
|
const numCPUs = config.MEMPOOL.SPAWN_CLUSTER_PROCS;
|
||||||
@@ -76,7 +78,7 @@ class Server {
|
|||||||
})
|
})
|
||||||
.use(express.urlencoded({ extended: true }))
|
.use(express.urlencoded({ extended: true }))
|
||||||
.use(express.text())
|
.use(express.text())
|
||||||
;
|
;
|
||||||
|
|
||||||
this.server = http.createServer(this.app);
|
this.server = http.createServer(this.app);
|
||||||
this.wss = new WebSocket.Server({ server: this.server });
|
this.wss = new WebSocket.Server({ server: this.server });
|
||||||
@@ -104,7 +106,7 @@ class Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && cluster.isMaster) {
|
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && cluster.isPrimary) {
|
||||||
statistics.startStatistics();
|
statistics.startStatistics();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,6 +155,7 @@ class Server {
|
|||||||
await blocks.$updateBlocks();
|
await blocks.$updateBlocks();
|
||||||
await memPool.$updateMempool();
|
await memPool.$updateMempool();
|
||||||
indexer.$run();
|
indexer.$run();
|
||||||
|
priceUpdater.$run();
|
||||||
|
|
||||||
setTimeout(this.runMainUpdateLoop.bind(this), config.MEMPOOL.POLL_RATE_MS);
|
setTimeout(this.runMainUpdateLoop.bind(this), config.MEMPOOL.POLL_RATE_MS);
|
||||||
this.currentBackendRetryInterval = 5;
|
this.currentBackendRetryInterval = 5;
|
||||||
@@ -195,6 +198,7 @@ class Server {
|
|||||||
setUpHttpApiRoutes() {
|
setUpHttpApiRoutes() {
|
||||||
this.app
|
this.app
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'transaction-times', routes.getTransactionTimes)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'transaction-times', routes.getTransactionTimes)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'outspends', routes.$getBatchedOutspends)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'cpfp/:txId', routes.getCpfpInfo)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'cpfp/:txId', routes.getCpfpInfo)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'difficulty-adjustment', routes.getDifficultyChange)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'difficulty-adjustment', routes.getDifficultyChange)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/recommended', routes.getRecommendedFees)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/recommended', routes.getRecommendedFees)
|
||||||
@@ -205,7 +209,7 @@ class Server {
|
|||||||
.post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', routes.$postTransactionForm)
|
.post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', routes.$postTransactionForm)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
|
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('https://mempool.space/api/v1/donations', { responseType: 'stream', timeout: 10000 });
|
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations`, { responseType: 'stream', timeout: 10000 });
|
||||||
response.data.pipe(res);
|
response.data.pipe(res);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).end();
|
res.status(500).end();
|
||||||
@@ -213,7 +217,7 @@ class Server {
|
|||||||
})
|
})
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'donations/images/:id', async (req, res) => {
|
.get(config.MEMPOOL.API_URL_PREFIX + 'donations/images/:id', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('https://mempool.space/api/v1/donations/images/' + req.params.id, {
|
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations/images/${req.params.id}`, {
|
||||||
responseType: 'stream', timeout: 10000
|
responseType: 'stream', timeout: 10000
|
||||||
});
|
});
|
||||||
response.data.pipe(res);
|
response.data.pipe(res);
|
||||||
@@ -223,7 +227,7 @@ class Server {
|
|||||||
})
|
})
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'contributors', async (req, res) => {
|
.get(config.MEMPOOL.API_URL_PREFIX + 'contributors', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('https://mempool.space/api/v1/contributors', { responseType: 'stream', timeout: 10000 });
|
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors`, { responseType: 'stream', timeout: 10000 });
|
||||||
response.data.pipe(res);
|
response.data.pipe(res);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).end();
|
res.status(500).end();
|
||||||
@@ -231,7 +235,7 @@ class Server {
|
|||||||
})
|
})
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'contributors/images/:id', async (req, res) => {
|
.get(config.MEMPOOL.API_URL_PREFIX + 'contributors/images/:id', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('https://mempool.space/api/v1/contributors/images/' + req.params.id, {
|
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors/images/${req.params.id}`, {
|
||||||
responseType: 'stream', timeout: 10000
|
responseType: 'stream', timeout: 10000
|
||||||
});
|
});
|
||||||
response.data.pipe(res);
|
response.data.pipe(res);
|
||||||
@@ -241,7 +245,7 @@ class Server {
|
|||||||
})
|
})
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'translators', async (req, res) => {
|
.get(config.MEMPOOL.API_URL_PREFIX + 'translators', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('https://mempool.space/api/v1/translators', { responseType: 'stream', timeout: 10000 });
|
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators`, { responseType: 'stream', timeout: 10000 });
|
||||||
response.data.pipe(res);
|
response.data.pipe(res);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).end();
|
res.status(500).end();
|
||||||
@@ -249,7 +253,7 @@ class Server {
|
|||||||
})
|
})
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'translators/images/:id', async (req, res) => {
|
.get(config.MEMPOOL.API_URL_PREFIX + 'translators/images/:id', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('https://mempool.space/api/v1/translators/images/' + req.params.id, {
|
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators/images/${req.params.id}`, {
|
||||||
responseType: 'stream', timeout: 10000
|
responseType: 'stream', timeout: 10000
|
||||||
});
|
});
|
||||||
response.data.pipe(res);
|
response.data.pipe(res);
|
||||||
@@ -257,7 +261,7 @@ class Server {
|
|||||||
res.status(500).end();
|
res.status(500).end();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
;
|
;
|
||||||
|
|
||||||
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED) {
|
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED) {
|
||||||
this.app
|
this.app
|
||||||
@@ -282,12 +286,15 @@ class Server {
|
|||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug', routes.$getPool)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug', routes.$getPool)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/pools/:interval', routes.$getPoolsHistoricalHashrate)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/pools/:interval', routes.$getPoolsHistoricalHashrate)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/:interval', routes.$getHistoricalHashrate)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/:interval', routes.$getHistoricalHashrate)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments', routes.$getDifficultyAdjustments)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/reward-stats/:blockCount', routes.$getRewardStats)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/reward-stats/:blockCount', routes.$getRewardStats)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fees/:interval', routes.$getHistoricalBlockFees)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fees/:interval', routes.$getHistoricalBlockFees)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/rewards/:interval', routes.$getHistoricalBlockRewards)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/rewards/:interval', routes.$getHistoricalBlockRewards)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fee-rates/:interval', routes.$getHistoricalBlockFeeRates)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fee-rates/:interval', routes.$getHistoricalBlockFeeRates)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/sizes-weights/:interval', routes.$getHistoricalBlockSizeAndWeight)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/sizes-weights/:interval', routes.$getHistoricalBlockSizeAndWeight)
|
||||||
;
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments/:interval', routes.$getDifficultyAdjustments)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/predictions/:interval', routes.$getHistoricalBlockPrediction)
|
||||||
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.BISQ.ENABLED) {
|
if (config.BISQ.ENABLED) {
|
||||||
@@ -314,7 +321,8 @@ class Server {
|
|||||||
this.app
|
this.app
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks', routes.getBlocks.bind(routes))
|
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks', routes.getBlocks.bind(routes))
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', routes.getBlocks.bind(routes))
|
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', routes.getBlocks.bind(routes))
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', routes.getBlock);
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', routes.getBlock)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', routes.getStrippedBlockTransactions);
|
||||||
|
|
||||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||||
this.app
|
this.app
|
||||||
@@ -328,6 +336,7 @@ class Server {
|
|||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', routes.getTransactionOutspends)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', routes.getTransactionOutspends)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/header', routes.getBlockHeader)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/header', routes.getBlockHeader)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', routes.getBlockTipHeight)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', routes.getBlockTipHeight)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/hash', routes.getBlockTipHash)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs', routes.getBlockTransactions)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs', routes.getBlockTransactions)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs/:index', routes.getBlockTransactions)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs/:index', routes.getBlockTransactions)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txids', routes.getTxIdsForBlock)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txids', routes.getTxIdsForBlock)
|
||||||
@@ -336,7 +345,7 @@ class Server {
|
|||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs', routes.getAddressTransactions)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs', routes.getAddressTransactions)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs/chain/:txId', routes.getAddressTransactions)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs/chain/:txId', routes.getAddressTransactions)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'address-prefix/:prefix', routes.getAddressPrefix)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'address-prefix/:prefix', routes.getAddressPrefix)
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Common.isLiquid()) {
|
if (Common.isLiquid()) {
|
||||||
@@ -345,13 +354,13 @@ class Server {
|
|||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'assets/featured', routes.$getAllFeaturedLiquidAssets)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'assets/featured', routes.$getAllFeaturedLiquidAssets)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'asset/:assetId/icon', routes.getLiquidIcon)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'asset/:assetId/icon', routes.getLiquidIcon)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'assets/group/:id', routes.$getAssetGroup)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'assets/group/:id', routes.$getAssetGroup)
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Common.isLiquid() && config.DATABASE.ENABLED) {
|
if (Common.isLiquid() && config.DATABASE.ENABLED) {
|
||||||
this.app
|
this.app
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs/month', routes.$getElementsPegsByMonth)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs/month', routes.$getElementsPegsByMonth)
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import mempool from './api/mempool';
|
|||||||
import mining from './api/mining';
|
import mining from './api/mining';
|
||||||
import logger from './logger';
|
import logger from './logger';
|
||||||
import HashratesRepository from './repositories/HashratesRepository';
|
import HashratesRepository from './repositories/HashratesRepository';
|
||||||
|
import bitcoinClient from './api/bitcoin/bitcoin-client';
|
||||||
|
|
||||||
class Indexer {
|
class Indexer {
|
||||||
runIndexer = true;
|
runIndexer = true;
|
||||||
@@ -13,7 +14,9 @@ class Indexer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public reindex() {
|
public reindex() {
|
||||||
this.runIndexer = true;
|
if (Common.indexingEnabled()) {
|
||||||
|
this.runIndexer = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $run() {
|
public async $run() {
|
||||||
@@ -23,20 +26,45 @@ class Indexer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Do not attempt to index anything unless Bitcoin Core is fully synced
|
||||||
|
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
|
||||||
|
if (blockchainInfo.blocks !== blockchainInfo.headers) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.runIndexer = false;
|
this.runIndexer = false;
|
||||||
this.indexerRunning = true;
|
this.indexerRunning = true;
|
||||||
|
|
||||||
|
logger.debug(`Running mining indexer`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await blocks.$generateBlockDatabase();
|
const chainValid = await blocks.$generateBlockDatabase();
|
||||||
|
if (chainValid === false) {
|
||||||
|
// Chain of block hash was invalid, so we need to reindex. Stop here and continue at the next iteration
|
||||||
|
logger.warn(`The chain of block hash is invalid, re-indexing invalid data in 10 seconds.`);
|
||||||
|
setTimeout(() => this.reindex(), 10000);
|
||||||
|
this.indexerRunning = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await mining.$indexDifficultyAdjustments();
|
||||||
await this.$resetHashratesIndexingState();
|
await this.$resetHashratesIndexingState();
|
||||||
await mining.$generateNetworkHashrateHistory();
|
await mining.$generateNetworkHashrateHistory();
|
||||||
await mining.$generatePoolHashrateHistory();
|
await mining.$generatePoolHashrateHistory();
|
||||||
|
await blocks.$generateBlocksSummariesDatabase();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.reindex();
|
this.indexerRunning = false;
|
||||||
logger.err(`Indexer failed, trying again later. Reason: ` + (e instanceof Error ? e.message : e));
|
logger.err(`Indexer failed, trying again in 10 seconds. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
setTimeout(() => this.reindex(), 10000);
|
||||||
|
this.indexerRunning = false;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.indexerRunning = false;
|
this.indexerRunning = false;
|
||||||
|
|
||||||
|
const runEvery = 1000 * 3600; // 1 hour
|
||||||
|
logger.debug(`Indexing completed. Next run planned at ${new Date(new Date().getTime() + runEvery).toUTCString()}`);
|
||||||
|
setTimeout(() => this.reindex(), runEvery);
|
||||||
}
|
}
|
||||||
|
|
||||||
async $resetHashratesIndexingState() {
|
async $resetHashratesIndexingState() {
|
||||||
@@ -45,6 +73,7 @@ class Indexer {
|
|||||||
await HashratesRepository.$setLatestRun('last_weekly_hashrates_indexing', 0);
|
await HashratesRepository.$setLatestRun('last_weekly_hashrates_indexing', 0);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err(`Cannot reset hashrate indexing timestamps. Reason: ` + (e instanceof Error ? e.message : e));
|
logger.err(`Cannot reset hashrate indexing timestamps. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,15 @@ export interface PoolStats extends PoolInfo {
|
|||||||
emptyBlocks: number;
|
emptyBlocks: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BlockAudit {
|
||||||
|
time: number,
|
||||||
|
height: number,
|
||||||
|
hash: string,
|
||||||
|
missingTxs: string[],
|
||||||
|
addedTxs: string[],
|
||||||
|
matchRate: number,
|
||||||
|
}
|
||||||
|
|
||||||
export interface MempoolBlock {
|
export interface MempoolBlock {
|
||||||
blockSize: number;
|
blockSize: number;
|
||||||
blockVSize: number;
|
blockVSize: number;
|
||||||
@@ -106,6 +115,11 @@ export interface BlockExtended extends IEsploraApi.Block {
|
|||||||
extras: BlockExtension;
|
extras: BlockExtension;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BlockSummary {
|
||||||
|
id: string;
|
||||||
|
transactions: TransactionStripped[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface TransactionMinerInfo {
|
export interface TransactionMinerInfo {
|
||||||
vin: VinStrippedToScriptsig[];
|
vin: VinStrippedToScriptsig[];
|
||||||
vout: VoutStrippedToScriptPubkey[];
|
vout: VoutStrippedToScriptPubkey[];
|
||||||
@@ -219,6 +233,13 @@ export interface IDifficultyAdjustment {
|
|||||||
timeOffset: number;
|
timeOffset: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IndexedDifficultyAdjustment {
|
||||||
|
time: number; // UNIX timestamp
|
||||||
|
height: number; // Block height
|
||||||
|
difficulty: number;
|
||||||
|
adjustment: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RewardStats {
|
export interface RewardStats {
|
||||||
totalReward: number;
|
totalReward: number;
|
||||||
totalFee: number;
|
totalFee: number;
|
||||||
|
|||||||
51
backend/src/repositories/BlocksAuditsRepository.ts
Normal file
51
backend/src/repositories/BlocksAuditsRepository.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import DB from '../database';
|
||||||
|
import logger from '../logger';
|
||||||
|
import { BlockAudit } from '../mempool.interfaces';
|
||||||
|
|
||||||
|
class BlocksAuditRepositories {
|
||||||
|
public async $saveAudit(audit: BlockAudit): Promise<void> {
|
||||||
|
try {
|
||||||
|
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, match_rate)
|
||||||
|
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
|
||||||
|
JSON.stringify(audit.addedTxs), audit.matchRate]);
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
|
||||||
|
logger.debug(`Cannot save block audit for block ${audit.hash} because it has already been indexed, ignoring`);
|
||||||
|
} else {
|
||||||
|
logger.err(`Cannot save block audit into db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getBlockPredictionsHistory(div: number, interval: string | null): Promise<any> {
|
||||||
|
try {
|
||||||
|
let query = `SELECT UNIX_TIMESTAMP(time) as time, height, match_rate FROM blocks_audits`;
|
||||||
|
|
||||||
|
if (interval !== null) {
|
||||||
|
query += ` WHERE time BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
|
||||||
|
}
|
||||||
|
|
||||||
|
query += ` GROUP BY UNIX_TIMESTAMP(time) DIV ${div} ORDER BY height`;
|
||||||
|
|
||||||
|
const [rows] = await DB.query(query);
|
||||||
|
return rows;
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Cannot fetch block prediction history. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getPredictionsCount(): Promise<number> {
|
||||||
|
try {
|
||||||
|
const [rows] = await DB.query(`SELECT count(hash) as count FROM blocks_audits`);
|
||||||
|
return rows[0].count;
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Cannot fetch block prediction history. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new BlocksAuditRepositories();
|
||||||
|
|
||||||
@@ -6,6 +6,8 @@ import { prepareBlock } from '../utils/blocks-utils';
|
|||||||
import PoolsRepository from './PoolsRepository';
|
import PoolsRepository from './PoolsRepository';
|
||||||
import HashratesRepository from './HashratesRepository';
|
import HashratesRepository from './HashratesRepository';
|
||||||
import { escape } from 'mysql2';
|
import { escape } from 'mysql2';
|
||||||
|
import BlocksSummariesRepository from './BlocksSummariesRepository';
|
||||||
|
import DifficultyAdjustmentsRepository from './DifficultyAdjustmentsRepository';
|
||||||
|
|
||||||
class BlocksRepository {
|
class BlocksRepository {
|
||||||
/**
|
/**
|
||||||
@@ -380,48 +382,9 @@ class BlocksRepository {
|
|||||||
/**
|
/**
|
||||||
* Return blocks difficulty
|
* Return blocks difficulty
|
||||||
*/
|
*/
|
||||||
public async $getBlocksDifficulty(interval: string | null): Promise<object[]> {
|
public async $getBlocksDifficulty(): Promise<object[]> {
|
||||||
interval = Common.getSqlInterval(interval);
|
|
||||||
|
|
||||||
// :D ... Yeah don't ask me about this one https://stackoverflow.com/a/40303162
|
|
||||||
// Basically, using temporary user defined fields, we are able to extract all
|
|
||||||
// difficulty adjustments from the blocks tables.
|
|
||||||
// This allow use to avoid indexing it in another table.
|
|
||||||
let query = `
|
|
||||||
SELECT
|
|
||||||
*
|
|
||||||
FROM
|
|
||||||
(
|
|
||||||
SELECT
|
|
||||||
UNIX_TIMESTAMP(blockTimestamp) as timestamp, difficulty, height,
|
|
||||||
IF(@prevStatus = YT.difficulty, @rn := @rn + 1,
|
|
||||||
IF(@prevStatus := YT.difficulty, @rn := 1, @rn := 1)
|
|
||||||
) AS rn
|
|
||||||
FROM blocks YT
|
|
||||||
CROSS JOIN
|
|
||||||
(
|
|
||||||
SELECT @prevStatus := -1, @rn := 1
|
|
||||||
) AS var
|
|
||||||
`;
|
|
||||||
|
|
||||||
if (interval) {
|
|
||||||
query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
|
|
||||||
}
|
|
||||||
|
|
||||||
query += `
|
|
||||||
ORDER BY YT.height
|
|
||||||
) AS t
|
|
||||||
WHERE t.rn = 1
|
|
||||||
ORDER BY t.height
|
|
||||||
`;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [rows]: any[] = await DB.query(query);
|
const [rows]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(blockTimestamp) as time, height, difficulty FROM blocks`);
|
||||||
|
|
||||||
for (const row of rows) {
|
|
||||||
delete row['rn'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return rows;
|
return rows;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('Cannot generate difficulty history. Reason: ' + (e instanceof Error ? e.message : e));
|
logger.err('Cannot generate difficulty history. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
@@ -451,26 +414,6 @@ class BlocksRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Check if the last 10 blocks chain is valid
|
|
||||||
*/
|
|
||||||
public async $validateRecentBlocks(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const [lastBlocks]: any[] = await DB.query(`SELECT height, hash, previous_block_hash FROM blocks ORDER BY height DESC LIMIT 10`);
|
|
||||||
|
|
||||||
for (let i = 0; i < lastBlocks.length - 1; ++i) {
|
|
||||||
if (lastBlocks[i].previous_block_hash !== lastBlocks[i + 1].hash) {
|
|
||||||
logger.warn(`Chain divergence detected at block ${lastBlocks[i].height}, re-indexing most recent data`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
return true; // Don't do anything if there is a db error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the chain of block hash is valid and delete data from the stale branch if needed
|
* Check if the chain of block hash is valid and delete data from the stale branch if needed
|
||||||
*/
|
*/
|
||||||
@@ -493,15 +436,17 @@ class BlocksRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (blocks[idx].previous_block_hash !== blocks[idx - 1].hash) {
|
if (blocks[idx].previous_block_hash !== blocks[idx - 1].hash) {
|
||||||
logger.warn(`Chain divergence detected at block ${blocks[idx - 1].height}, re-indexing newer blocks and hashrates`);
|
logger.warn(`Chain divergence detected at block ${blocks[idx - 1].height}`);
|
||||||
await this.$deleteBlocksFrom(blocks[idx - 1].height);
|
await this.$deleteBlocksFrom(blocks[idx - 1].height);
|
||||||
|
await BlocksSummariesRepository.$deleteBlocksFrom(blocks[idx - 1].height);
|
||||||
await HashratesRepository.$deleteHashratesFromTimestamp(blocks[idx - 1].timestamp - 604800);
|
await HashratesRepository.$deleteHashratesFromTimestamp(blocks[idx - 1].timestamp - 604800);
|
||||||
|
await DifficultyAdjustmentsRepository.$deleteAdjustementsFromHeight(blocks[idx - 1].height);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
++idx;
|
++idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`${idx} blocks hash validated in ${new Date().getTime() - start} ms`);
|
logger.debug(`${idx} blocks hash validated in ${new Date().getTime() - start} ms`);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('Cannot validate chain of block hash. Reason: ' + (e instanceof Error ? e.message : e));
|
logger.err('Cannot validate chain of block hash. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
@@ -652,6 +597,37 @@ class BlocksRepository {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of blocks that have been indexed
|
||||||
|
*/
|
||||||
|
public async $getIndexedBlocks(): Promise<any[]> {
|
||||||
|
try {
|
||||||
|
const [rows]: any = await DB.query(`SELECT height, hash FROM blocks ORDER BY height DESC`);
|
||||||
|
return rows;
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('Cannot generate block size and weight history. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the oldest block from a consecutive chain of block from the most recent one
|
||||||
|
*/
|
||||||
|
public async $getOldestConsecutiveBlock(): Promise<any> {
|
||||||
|
try {
|
||||||
|
const [rows]: any = await DB.query(`SELECT height, UNIX_TIMESTAMP(blockTimestamp) as timestamp, difficulty FROM blocks ORDER BY height DESC`);
|
||||||
|
for (let i = 0; i < rows.length - 1; ++i) {
|
||||||
|
if (rows[i].height - rows[i + 1].height > 1) {
|
||||||
|
return rows[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows[rows.length - 1];
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('Cannot generate block size and weight history. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new BlocksRepository();
|
export default new BlocksRepository();
|
||||||
|
|||||||
59
backend/src/repositories/BlocksSummariesRepository.ts
Normal file
59
backend/src/repositories/BlocksSummariesRepository.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import DB from '../database';
|
||||||
|
import logger from '../logger';
|
||||||
|
import { BlockSummary } from '../mempool.interfaces';
|
||||||
|
|
||||||
|
class BlocksSummariesRepository {
|
||||||
|
public async $getByBlockId(id: string): Promise<BlockSummary | undefined> {
|
||||||
|
try {
|
||||||
|
const [summary]: any[] = await DB.query(`SELECT * from blocks_summaries WHERE id = ?`, [id]);
|
||||||
|
if (summary.length > 0) {
|
||||||
|
summary[0].transactions = JSON.parse(summary[0].transactions);
|
||||||
|
return summary[0];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Cannot get block summary for block id ${id}. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $saveSummary(height: number, summary: BlockSummary) {
|
||||||
|
try {
|
||||||
|
await DB.query(`INSERT INTO blocks_summaries VALUE (?, ?, ?)`, [height, summary.id, JSON.stringify(summary.transactions)]);
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
|
||||||
|
logger.debug(`Cannot save block summary for ${summary.id} because it has already been indexed, ignoring`);
|
||||||
|
} else {
|
||||||
|
logger.debug(`Cannot save block summary for ${summary.id}. Reason: ${e instanceof Error ? e.message : e}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getIndexedSummariesId(): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const [rows]: any[] = await DB.query(`SELECT id from blocks_summaries`);
|
||||||
|
return rows.map(row => row.id);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Cannot get block summaries id list. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete blocks from the database from blockHeight
|
||||||
|
*/
|
||||||
|
public async $deleteBlocksFrom(blockHeight: number) {
|
||||||
|
logger.info(`Delete newer blocks summary from height ${blockHeight} from the database`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await DB.query(`DELETE FROM blocks_summaries where height >= ${blockHeight}`);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('Cannot delete indexed blocks summaries. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new BlocksSummariesRepository();
|
||||||
|
|
||||||
124
backend/src/repositories/DifficultyAdjustmentsRepository.ts
Normal file
124
backend/src/repositories/DifficultyAdjustmentsRepository.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import { Common } from '../api/common';
|
||||||
|
import config from '../config';
|
||||||
|
import DB from '../database';
|
||||||
|
import logger from '../logger';
|
||||||
|
import { IndexedDifficultyAdjustment } from '../mempool.interfaces';
|
||||||
|
|
||||||
|
class DifficultyAdjustmentsRepository {
|
||||||
|
public async $saveAdjustments(adjustment: IndexedDifficultyAdjustment): Promise<void> {
|
||||||
|
if (adjustment.height === 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query = `INSERT INTO difficulty_adjustments(time, height, difficulty, adjustment) VALUE (FROM_UNIXTIME(?), ?, ?, ?)`;
|
||||||
|
const params: any[] = [
|
||||||
|
adjustment.time,
|
||||||
|
adjustment.height,
|
||||||
|
adjustment.difficulty,
|
||||||
|
adjustment.adjustment,
|
||||||
|
];
|
||||||
|
await DB.query(query, params);
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
|
||||||
|
logger.debug(`Cannot save difficulty adjustment at block ${adjustment.height}, already indexed, ignoring`);
|
||||||
|
} else {
|
||||||
|
logger.err(`Cannot save difficulty adjustment at block ${adjustment.height}. Reason: ${e instanceof Error ? e.message : e}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getAdjustments(interval: string | null, descOrder: boolean = false): Promise<IndexedDifficultyAdjustment[]> {
|
||||||
|
interval = Common.getSqlInterval(interval);
|
||||||
|
|
||||||
|
let query = `SELECT
|
||||||
|
CAST(AVG(UNIX_TIMESTAMP(time)) as INT) as time,
|
||||||
|
CAST(AVG(height) AS INT) as height,
|
||||||
|
CAST(AVG(difficulty) as DOUBLE) as difficulty,
|
||||||
|
CAST(AVG(adjustment) as DOUBLE) as adjustment
|
||||||
|
FROM difficulty_adjustments`;
|
||||||
|
|
||||||
|
if (interval) {
|
||||||
|
query += ` WHERE time BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
|
||||||
|
}
|
||||||
|
|
||||||
|
query += ` GROUP BY UNIX_TIMESTAMP(time) DIV ${86400}`;
|
||||||
|
|
||||||
|
if (descOrder === true) {
|
||||||
|
query += ` ORDER BY height DESC`;
|
||||||
|
} else {
|
||||||
|
query += ` ORDER BY height`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [rows] = await DB.query(query);
|
||||||
|
return rows as IndexedDifficultyAdjustment[];
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Cannot get difficulty adjustments from the database. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getRawAdjustments(interval: string | null, descOrder: boolean = false): Promise<IndexedDifficultyAdjustment[]> {
|
||||||
|
interval = Common.getSqlInterval(interval);
|
||||||
|
|
||||||
|
let query = `SELECT
|
||||||
|
UNIX_TIMESTAMP(time) as time,
|
||||||
|
height as height,
|
||||||
|
difficulty as difficulty,
|
||||||
|
adjustment as adjustment
|
||||||
|
FROM difficulty_adjustments`;
|
||||||
|
|
||||||
|
if (interval) {
|
||||||
|
query += ` WHERE time BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (descOrder === true) {
|
||||||
|
query += ` ORDER BY height DESC`;
|
||||||
|
} else {
|
||||||
|
query += ` ORDER BY height`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [rows] = await DB.query(query);
|
||||||
|
return rows as IndexedDifficultyAdjustment[];
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Cannot get difficulty adjustments from the database. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getAdjustmentsHeights(): Promise<number[]> {
|
||||||
|
try {
|
||||||
|
const [rows]: any[] = await DB.query(`SELECT height FROM difficulty_adjustments`);
|
||||||
|
return rows.map(block => block.height);
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Cannot get difficulty adjustment block heights. Reason: ${e instanceof Error ? e.message : e}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $deleteAdjustementsFromHeight(height: number): Promise<void> {
|
||||||
|
try {
|
||||||
|
logger.info(`Delete newer difficulty adjustments from height ${height} from the database`);
|
||||||
|
await DB.query(`DELETE FROM difficulty_adjustments WHERE height >= ?`, [height]);
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Cannot delete difficulty adjustments from the database. Reason: ${e instanceof Error ? e.message : e}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $deleteLastAdjustment(): Promise<void> {
|
||||||
|
try {
|
||||||
|
logger.info(`Delete last difficulty adjustment from the database`);
|
||||||
|
await DB.query(`DELETE FROM difficulty_adjustments ORDER BY time LIMIT 1`);
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Cannot delete last difficulty adjustment from the database. Reason: ${e instanceof Error ? e.message : e}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new DifficultyAdjustmentsRepository();
|
||||||
|
|
||||||
@@ -29,10 +29,12 @@ class HashratesRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $getNetworkDailyHashrate(interval: string | null): Promise<any[]> {
|
public async $getRawNetworkDailyHashrate(interval: string | null): Promise<any[]> {
|
||||||
interval = Common.getSqlInterval(interval);
|
interval = Common.getSqlInterval(interval);
|
||||||
|
|
||||||
let query = `SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp, avg_hashrate as avgHashrate
|
let query = `SELECT
|
||||||
|
UNIX_TIMESTAMP(hashrate_timestamp) as timestamp,
|
||||||
|
avg_hashrate as avgHashrate
|
||||||
FROM hashrates`;
|
FROM hashrates`;
|
||||||
|
|
||||||
if (interval) {
|
if (interval) {
|
||||||
@@ -53,6 +55,33 @@ class HashratesRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $getNetworkDailyHashrate(interval: string | null): Promise<any[]> {
|
||||||
|
interval = Common.getSqlInterval(interval);
|
||||||
|
|
||||||
|
let query = `SELECT
|
||||||
|
CAST(AVG(UNIX_TIMESTAMP(hashrate_timestamp)) as INT) as timestamp,
|
||||||
|
CAST(AVG(avg_hashrate) as DOUBLE) as avgHashrate
|
||||||
|
FROM hashrates`;
|
||||||
|
|
||||||
|
if (interval) {
|
||||||
|
query += ` WHERE hashrate_timestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()
|
||||||
|
AND hashrates.type = 'daily'`;
|
||||||
|
} else {
|
||||||
|
query += ` WHERE hashrates.type = 'daily'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
query += ` GROUP BY UNIX_TIMESTAMP(hashrate_timestamp) DIV ${86400}`;
|
||||||
|
query += ` ORDER by hashrate_timestamp`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [rows]: any[] = await DB.query(query);
|
||||||
|
return rows;
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('Cannot fetch network hashrate history. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async $getWeeklyHashrateTimestamps(): Promise<number[]> {
|
public async $getWeeklyHashrateTimestamps(): Promise<number[]> {
|
||||||
const query = `SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp
|
const query = `SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp
|
||||||
FROM hashrates
|
FROM hashrates
|
||||||
@@ -75,6 +104,9 @@ class HashratesRepository {
|
|||||||
interval = Common.getSqlInterval(interval);
|
interval = Common.getSqlInterval(interval);
|
||||||
|
|
||||||
const topPoolsId = (await PoolsRepository.$getPoolsInfo('1w')).map((pool) => pool.poolId);
|
const topPoolsId = (await PoolsRepository.$getPoolsInfo('1w')).map((pool) => pool.poolId);
|
||||||
|
if (topPoolsId.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
let query = `SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp, avg_hashrate as avgHashrate, share, pools.name as poolName
|
let query = `SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp, avg_hashrate as avgHashrate, share, pools.name as poolName
|
||||||
FROM hashrates
|
FROM hashrates
|
||||||
|
|||||||
42
backend/src/repositories/PricesRepository.ts
Normal file
42
backend/src/repositories/PricesRepository.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import DB from '../database';
|
||||||
|
import logger from '../logger';
|
||||||
|
import { Prices } from '../tasks/price-updater';
|
||||||
|
|
||||||
|
class PricesRepository {
|
||||||
|
public async $savePrices(time: number, prices: Prices): Promise<void> {
|
||||||
|
if (prices.USD === -1) {
|
||||||
|
// Some historical price entries have not USD prices, so we just ignore them to avoid future UX issues
|
||||||
|
// As of today there are only 4 (on 2013-09-05, 2013-09-19, 2013-09-12 and 2013-09-26) so that's fine
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await DB.query(`
|
||||||
|
INSERT INTO prices(time, USD, EUR, GBP, CAD, CHF, AUD, JPY)
|
||||||
|
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ? )`,
|
||||||
|
[time, prices.USD, prices.EUR, prices.GBP, prices.CAD, prices.CHF, prices.AUD, prices.JPY]
|
||||||
|
);
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Cannot save exchange rate into db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getOldestPriceTime(): Promise<number> {
|
||||||
|
const [oldestRow] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != -1 ORDER BY time LIMIT 1`);
|
||||||
|
return oldestRow[0] ? oldestRow[0].time : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getLatestPriceTime(): Promise<number> {
|
||||||
|
const [oldestRow] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != -1 ORDER BY time DESC LIMIT 1`);
|
||||||
|
return oldestRow[0] ? oldestRow[0].time : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getPricesTimes(): Promise<number[]> {
|
||||||
|
const [times]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != -1`);
|
||||||
|
return times.map(time => time.time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new PricesRepository();
|
||||||
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import DB from '../database';
|
|
||||||
import logger from '../logger';
|
|
||||||
import { IConversionRates } from '../mempool.interfaces';
|
|
||||||
|
|
||||||
class RatesRepository {
|
|
||||||
public async $saveRate(height: number, rates: IConversionRates) {
|
|
||||||
try {
|
|
||||||
await DB.query(`INSERT INTO rates(height, bisq_rates) VALUE (?, ?)`, [height, JSON.stringify(rates)]);
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
|
|
||||||
logger.debug(`Rate already exists for block ${height}, ignoring`);
|
|
||||||
} else {
|
|
||||||
logger.err(`Cannot save exchange rate into db for block ${height} Reason: ` + (e instanceof Error ? e.message : e));
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new RatesRepository();
|
|
||||||
|
|
||||||
@@ -26,6 +26,8 @@ import mining from './api/mining';
|
|||||||
import BlocksRepository from './repositories/BlocksRepository';
|
import BlocksRepository from './repositories/BlocksRepository';
|
||||||
import HashratesRepository from './repositories/HashratesRepository';
|
import HashratesRepository from './repositories/HashratesRepository';
|
||||||
import difficultyAdjustment from './api/difficulty-adjustment';
|
import difficultyAdjustment from './api/difficulty-adjustment';
|
||||||
|
import DifficultyAdjustmentsRepository from './repositories/DifficultyAdjustmentsRepository';
|
||||||
|
import BlocksAuditsRepository from './repositories/BlocksAuditsRepository';
|
||||||
|
|
||||||
class Routes {
|
class Routes {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
@@ -120,6 +122,30 @@ class Routes {
|
|||||||
res.json(times);
|
res.json(times);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $getBatchedOutspends(req: Request, res: Response) {
|
||||||
|
if (!Array.isArray(req.query.txId)) {
|
||||||
|
res.status(500).send('Not an array');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (req.query.txId.length > 50) {
|
||||||
|
res.status(400).send('Too many txids requested');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const txIds: string[] = [];
|
||||||
|
for (const _txId in req.query.txId) {
|
||||||
|
if (typeof req.query.txId[_txId] === 'string') {
|
||||||
|
txIds.push(req.query.txId[_txId].toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txIds);
|
||||||
|
res.json(batchedOutspends);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public getCpfpInfo(req: Request, res: Response) {
|
public getCpfpInfo(req: Request, res: Response) {
|
||||||
if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
|
if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
|
||||||
res.status(501).send(`Invalid transaction ID.`);
|
res.status(501).send(`Invalid transaction ID.`);
|
||||||
@@ -629,7 +655,7 @@ class Routes {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const hashrates = await HashratesRepository.$getNetworkDailyHashrate(req.params.interval);
|
const hashrates = await HashratesRepository.$getNetworkDailyHashrate(req.params.interval);
|
||||||
const difficulty = await BlocksRepository.$getBlocksDifficulty(req.params.interval);
|
const difficulty = await DifficultyAdjustmentsRepository.$getAdjustments(req.params.interval, false);
|
||||||
const blockCount = await BlocksRepository.$blockCount(null, null);
|
const blockCount = await BlocksRepository.$blockCount(null, null);
|
||||||
res.header('Pragma', 'public');
|
res.header('Pragma', 'public');
|
||||||
res.header('Cache-control', 'public');
|
res.header('Cache-control', 'public');
|
||||||
@@ -706,6 +732,32 @@ class Routes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $getDifficultyAdjustments(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const difficulty = await DifficultyAdjustmentsRepository.$getRawAdjustments(req.params.interval, true);
|
||||||
|
res.header('Pragma', 'public');
|
||||||
|
res.header('Cache-control', 'public');
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
||||||
|
res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment]));
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getHistoricalBlockPrediction(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const blockPredictions = await mining.$getBlockPredictionsHistory(req.params.interval);
|
||||||
|
const blockCount = await BlocksAuditsRepository.$getPredictionsCount();
|
||||||
|
res.header('Pragma', 'public');
|
||||||
|
res.header('Cache-control', 'public');
|
||||||
|
res.header('X-total-count', blockCount.toString());
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
|
res.json(blockPredictions.map(prediction => [prediction.time, prediction.height, prediction.match_rate]));
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async getBlock(req: Request, res: Response) {
|
public async getBlock(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const block = await blocks.$getBlock(req.params.hash);
|
const block = await blocks.$getBlock(req.params.hash);
|
||||||
@@ -726,9 +778,19 @@ class Routes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getStrippedBlockTransactions(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const transactions = await blocks.$getStrippedBlockTransactions(req.params.hash);
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
|
||||||
|
res.json(transactions);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async getBlocks(req: Request, res: Response) {
|
public async getBlocks(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
if (['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin
|
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin
|
||||||
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
|
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(await blocks.$getBlocks(height, 15));
|
res.json(await blocks.$getBlocks(height, 15));
|
||||||
@@ -891,6 +953,16 @@ class Routes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getBlockTipHash(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const result = await bitcoinApi.$getBlockHashTip();
|
||||||
|
res.setHeader('content-type', 'text/plain');
|
||||||
|
res.send(result);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async getTxIdsForBlock(req: Request, res: Response) {
|
public async getTxIdsForBlock(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
|
const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
|
||||||
@@ -990,7 +1062,7 @@ class Routes {
|
|||||||
|
|
||||||
public async $getAllFeaturedLiquidAssets(req: Request, res: Response) {
|
public async $getAllFeaturedLiquidAssets(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('https://liquid.network/api/v1/assets/featured', { responseType: 'stream', timeout: 10000 });
|
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.LIQUID_API}/assets/featured`, { responseType: 'stream', timeout: 10000 });
|
||||||
response.data.pipe(res);
|
response.data.pipe(res);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).end();
|
res.status(500).end();
|
||||||
@@ -999,7 +1071,7 @@ class Routes {
|
|||||||
|
|
||||||
public async $getAssetGroup(req: Request, res: Response) {
|
public async $getAssetGroup(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('https://liquid.network/api/v1/assets/group/' + parseInt(req.params.id, 10),
|
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.LIQUID_API}/assets/group/${parseInt(req.params.id, 10)}`,
|
||||||
{ responseType: 'stream', timeout: 10000 });
|
{ responseType: 'stream', timeout: 10000 });
|
||||||
response.data.pipe(res);
|
response.data.pipe(res);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import config from './config';
|
import config from './config';
|
||||||
|
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';
|
||||||
|
|
||||||
@@ -42,6 +43,9 @@ class SyncAssets {
|
|||||||
|
|
||||||
logger.info(`Downloading external asset ${fileName} over the Tor network...`);
|
logger.info(`Downloading external asset ${fileName} over the Tor network...`);
|
||||||
return axios.get(url, {
|
return axios.get(url, {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': (config.MEMPOOL.USER_AGENT === 'mempool') ? `mempool/v${backendInfo.getBackendInfo().version}` : `${config.MEMPOOL.USER_AGENT}`
|
||||||
|
},
|
||||||
httpAgent: agent,
|
httpAgent: agent,
|
||||||
httpsAgent: agent,
|
httpsAgent: agent,
|
||||||
responseType: 'stream',
|
responseType: 'stream',
|
||||||
@@ -57,6 +61,9 @@ class SyncAssets {
|
|||||||
} else {
|
} else {
|
||||||
logger.info(`Downloading external asset ${fileName} over clearnet...`);
|
logger.info(`Downloading external asset ${fileName} over clearnet...`);
|
||||||
return axios.get(url, {
|
return axios.get(url, {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': (config.MEMPOOL.USER_AGENT === 'mempool') ? `mempool/v${backendInfo.getBackendInfo().version}` : `${config.MEMPOOL.USER_AGENT}`
|
||||||
|
},
|
||||||
responseType: 'stream',
|
responseType: 'stream',
|
||||||
timeout: 30000
|
timeout: 30000
|
||||||
}).then(function (response) {
|
}).then(function (response) {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import axios from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import poolsParser from '../api/pools-parser';
|
import poolsParser from '../api/pools-parser';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import DB from '../database';
|
import DB from '../database';
|
||||||
|
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';
|
||||||
@@ -11,12 +12,15 @@ import * as https from 'https';
|
|||||||
*/
|
*/
|
||||||
class PoolsUpdater {
|
class PoolsUpdater {
|
||||||
lastRun: number = 0;
|
lastRun: number = 0;
|
||||||
|
currentSha: any = undefined;
|
||||||
|
poolsUrl: string = 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json';
|
||||||
|
treeUrl: string = 'https://api.github.com/repos/mempool/mining-pools/git/trees/master';
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updatePoolsJson() {
|
public async updatePoolsJson() {
|
||||||
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false || config.DATABASE.ENABLED === false) {
|
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,27 +34,33 @@ class PoolsUpdater {
|
|||||||
|
|
||||||
this.lastRun = now;
|
this.lastRun = now;
|
||||||
|
|
||||||
logger.info('Updating latest mining pools from Github');
|
|
||||||
if (config.SOCKS5PROXY.ENABLED) {
|
if (config.SOCKS5PROXY.ENABLED) {
|
||||||
logger.info('List of public pools will be queried over the Tor network');
|
logger.info(`Updating latest mining pools from ${this.poolsUrl} over the Tor network`);
|
||||||
} else {
|
} else {
|
||||||
logger.info('List of public pools will be queried over clearnet');
|
logger.info(`Updating latest mining pools from ${this.poolsUrl} over clearnet`);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const dbSha = await this.getShaFromDb();
|
|
||||||
const githubSha = await this.fetchPoolsSha(); // Fetch pools.json sha from github
|
const githubSha = await this.fetchPoolsSha(); // Fetch pools.json sha from github
|
||||||
if (githubSha === undefined) {
|
if (githubSha === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`Pools.json sha | Current: ${dbSha} | Github: ${githubSha}`);
|
if (config.DATABASE.ENABLED === true) {
|
||||||
if (dbSha !== undefined && dbSha === githubSha) {
|
this.currentSha = await this.getShaFromDb();
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Pools.json sha | Current: ${this.currentSha} | Github: ${githubSha}`);
|
||||||
|
if (this.currentSha !== undefined && this.currentSha === githubSha) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.warn('Pools.json is outdated, fetch latest from github');
|
if (this.currentSha === undefined) {
|
||||||
const poolsJson = await this.query('https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json');
|
logger.info(`Downloading pools.json for the first time from ${this.poolsUrl}`);
|
||||||
|
} else {
|
||||||
|
logger.warn(`Pools.json is outdated, fetch latest from ${this.poolsUrl}`);
|
||||||
|
}
|
||||||
|
const poolsJson = await this.query(this.poolsUrl);
|
||||||
if (poolsJson === undefined) {
|
if (poolsJson === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -60,7 +70,7 @@ class PoolsUpdater {
|
|||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week
|
this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week
|
||||||
logger.err('PoolsUpdater failed. Will try again in 24h. Reason: ' + (e instanceof Error ? e.message : e));
|
logger.err('PoolsUpdater failed. Will try again in 24h. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,12 +78,14 @@ class PoolsUpdater {
|
|||||||
* Fetch our latest pools.json sha from the db
|
* Fetch our latest pools.json sha from the db
|
||||||
*/
|
*/
|
||||||
private async updateDBSha(githubSha: string) {
|
private async updateDBSha(githubSha: string) {
|
||||||
try {
|
this.currentSha = githubSha;
|
||||||
await DB.query('DELETE FROM state where name="pools_json_sha"');
|
if (config.DATABASE.ENABLED === true) {
|
||||||
await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`);
|
try {
|
||||||
} catch (e) {
|
await DB.query('DELETE FROM state where name="pools_json_sha"');
|
||||||
logger.err('Cannot save github pools.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e));
|
await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`);
|
||||||
return undefined;
|
} catch (e) {
|
||||||
|
logger.err('Cannot save github pools.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +97,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 : undefined);
|
return (rows.length > 0 ? rows[0].string : undefined);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('Cannot fetch pools.json sha from db. Reason: ' + (e instanceof Error ? e.message : e));
|
logger.err('Cannot fetch pools.json sha from db. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,7 +106,7 @@ class PoolsUpdater {
|
|||||||
* Fetch our latest pools.json sha from github
|
* Fetch our latest pools.json sha from github
|
||||||
*/
|
*/
|
||||||
private async fetchPoolsSha(): Promise<string | undefined> {
|
private async fetchPoolsSha(): Promise<string | undefined> {
|
||||||
const response = await this.query('https://api.github.com/repos/mempool/mining-pools/git/trees/master');
|
const response = await this.query(this.treeUrl);
|
||||||
|
|
||||||
if (response !== undefined) {
|
if (response !== undefined) {
|
||||||
for (const file of response['tree']) {
|
for (const file of response['tree']) {
|
||||||
@@ -104,7 +116,7 @@ class PoolsUpdater {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.err('Cannot to find latest pools.json sha from github api response');
|
logger.err(`Cannot find "pools.json" in git tree (${this.treeUrl})`);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,41 +125,53 @@ class PoolsUpdater {
|
|||||||
*/
|
*/
|
||||||
private async query(path): Promise<object | undefined> {
|
private async query(path): Promise<object | undefined> {
|
||||||
type axiosOptions = {
|
type axiosOptions = {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': string
|
||||||
|
};
|
||||||
|
timeout: number;
|
||||||
httpsAgent?: https.Agent;
|
httpsAgent?: https.Agent;
|
||||||
}
|
};
|
||||||
const setDelay = (secs: number = 1): Promise<void> => new Promise(resolve => setTimeout(() => resolve(), secs * 1000));
|
const setDelay = (secs: number = 1): Promise<void> => new Promise(resolve => setTimeout(() => resolve(), secs * 1000));
|
||||||
const axiosOptions: axiosOptions = {};
|
const axiosOptions: axiosOptions = {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': (config.MEMPOOL.USER_AGENT === 'mempool') ? `mempool/v${backendInfo.getBackendInfo().version}` : `${config.MEMPOOL.USER_AGENT}`
|
||||||
|
},
|
||||||
|
timeout: config.SOCKS5PROXY.ENABLED ? 30000 : 10000
|
||||||
|
};
|
||||||
let retry = 0;
|
let retry = 0;
|
||||||
|
|
||||||
if (config.SOCKS5PROXY.ENABLED) {
|
while (retry < config.MEMPOOL.EXTERNAL_MAX_RETRY) {
|
||||||
const socksOptions: any = {
|
|
||||||
agentOptions: {
|
|
||||||
keepAlive: true,
|
|
||||||
},
|
|
||||||
hostname: config.SOCKS5PROXY.HOST,
|
|
||||||
port: config.SOCKS5PROXY.PORT
|
|
||||||
};
|
|
||||||
|
|
||||||
if (config.SOCKS5PROXY.USERNAME && config.SOCKS5PROXY.PASSWORD) {
|
|
||||||
socksOptions.username = config.SOCKS5PROXY.USERNAME;
|
|
||||||
socksOptions.password = config.SOCKS5PROXY.PASSWORD;
|
|
||||||
}
|
|
||||||
|
|
||||||
axiosOptions.httpsAgent = new SocksProxyAgent(socksOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
while(retry < 5) {
|
|
||||||
try {
|
try {
|
||||||
const data = await axios.get(path, axiosOptions);
|
if (config.SOCKS5PROXY.ENABLED) {
|
||||||
if (data.statusText !== 'OK' || !data.data) {
|
const socksOptions: any = {
|
||||||
throw new Error(`Could not fetch data from Github, Error: ${data.status}`);
|
agentOptions: {
|
||||||
|
keepAlive: true,
|
||||||
|
},
|
||||||
|
hostname: config.SOCKS5PROXY.HOST,
|
||||||
|
port: config.SOCKS5PROXY.PORT
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config.SOCKS5PROXY.USERNAME && config.SOCKS5PROXY.PASSWORD) {
|
||||||
|
socksOptions.username = config.SOCKS5PROXY.USERNAME;
|
||||||
|
socksOptions.password = config.SOCKS5PROXY.PASSWORD;
|
||||||
|
} else {
|
||||||
|
// Retry with different tor circuits https://stackoverflow.com/a/64960234
|
||||||
|
socksOptions.username = `circuit${retry}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
axiosOptions.httpsAgent = new SocksProxyAgent(socksOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: AxiosResponse = await axios.get(path, axiosOptions);
|
||||||
|
if (data.statusText === 'error' || !data.data) {
|
||||||
|
throw new Error(`Could not fetch data from ${path}, Error: ${data.status}`);
|
||||||
}
|
}
|
||||||
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));
|
||||||
retry++;
|
retry++;
|
||||||
}
|
}
|
||||||
await setDelay();
|
await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL);
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
43
backend/src/tasks/price-feeds/bitfinex-api.ts
Normal file
43
backend/src/tasks/price-feeds/bitfinex-api.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { query } from '../../utils/axios-query';
|
||||||
|
import priceUpdater, { PriceFeed, PriceHistory } from '../price-updater';
|
||||||
|
|
||||||
|
class BitfinexApi implements PriceFeed {
|
||||||
|
public name: string = 'Bitfinex';
|
||||||
|
public currencies: string[] = ['USD', 'EUR', 'GPB', 'JPY'];
|
||||||
|
|
||||||
|
public url: string = 'https://api.bitfinex.com/v1/pubticker/BTC';
|
||||||
|
public urlHist: string = 'https://api-pub.bitfinex.com/v2/candles/trade:{GRANULARITY}:tBTC{CURRENCY}/hist';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $fetchPrice(currency): Promise<number> {
|
||||||
|
const response = await query(this.url + currency);
|
||||||
|
return response ? parseInt(response['last_price'], 10) : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $fetchRecentHourlyPrice(currencies: string[]): Promise<PriceHistory> {
|
||||||
|
const priceHistory: PriceHistory = {};
|
||||||
|
|
||||||
|
for (const currency of currencies) {
|
||||||
|
if (this.currencies.includes(currency) === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await query(this.urlHist.replace('{GRANULARITY}', '1h').replace('{CURRENCY}', currency));
|
||||||
|
const pricesRaw = response ? response : [];
|
||||||
|
|
||||||
|
for (const price of pricesRaw as any[]) {
|
||||||
|
const time = Math.round(price[0] / 1000);
|
||||||
|
if (priceHistory[time] === undefined) {
|
||||||
|
priceHistory[time] = priceUpdater.getEmptyPricesObj();
|
||||||
|
}
|
||||||
|
priceHistory[time][currency] = price[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return priceHistory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BitfinexApi;
|
||||||
24
backend/src/tasks/price-feeds/bitflyer-api.ts
Normal file
24
backend/src/tasks/price-feeds/bitflyer-api.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { query } from '../../utils/axios-query';
|
||||||
|
import { PriceFeed, PriceHistory } from '../price-updater';
|
||||||
|
|
||||||
|
class BitflyerApi implements PriceFeed {
|
||||||
|
public name: string = 'Bitflyer';
|
||||||
|
public currencies: string[] = ['USD', 'EUR', 'JPY'];
|
||||||
|
|
||||||
|
public url: string = 'https://api.bitflyer.com/v1/ticker?product_code=BTC_';
|
||||||
|
public urlHist: string = '';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $fetchPrice(currency): Promise<number> {
|
||||||
|
const response = await query(this.url + currency);
|
||||||
|
return response ? parseInt(response['ltp'], 10) : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $fetchRecentHourlyPrice(currencies: string[]): Promise<PriceHistory> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BitflyerApi;
|
||||||
42
backend/src/tasks/price-feeds/coinbase-api.ts
Normal file
42
backend/src/tasks/price-feeds/coinbase-api.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { query } from '../../utils/axios-query';
|
||||||
|
import priceUpdater, { PriceFeed, PriceHistory } from '../price-updater';
|
||||||
|
|
||||||
|
class CoinbaseApi implements PriceFeed {
|
||||||
|
public name: string = 'Coinbase';
|
||||||
|
public currencies: string[] = ['USD', 'EUR', 'GBP'];
|
||||||
|
|
||||||
|
public url: string = 'https://api.coinbase.com/v2/prices/spot?currency=';
|
||||||
|
public urlHist: string = 'https://api.exchange.coinbase.com/products/BTC-{CURRENCY}/candles?granularity={GRANULARITY}';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $fetchPrice(currency): Promise<number> {
|
||||||
|
const response = await query(this.url + currency);
|
||||||
|
return response ? parseInt(response['data']['amount'], 10) : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $fetchRecentHourlyPrice(currencies: string[]): Promise<PriceHistory> {
|
||||||
|
const priceHistory: PriceHistory = {};
|
||||||
|
|
||||||
|
for (const currency of currencies) {
|
||||||
|
if (this.currencies.includes(currency) === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await query(this.urlHist.replace('{GRANULARITY}', '3600').replace('{CURRENCY}', currency));
|
||||||
|
const pricesRaw = response ? response : [];
|
||||||
|
|
||||||
|
for (const price of pricesRaw as any[]) {
|
||||||
|
if (priceHistory[price[0]] === undefined) {
|
||||||
|
priceHistory[price[0]] = priceUpdater.getEmptyPricesObj();
|
||||||
|
}
|
||||||
|
priceHistory[price[0]][currency] = price[4];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return priceHistory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CoinbaseApi;
|
||||||
43
backend/src/tasks/price-feeds/ftx-api.ts
Normal file
43
backend/src/tasks/price-feeds/ftx-api.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { query } from '../../utils/axios-query';
|
||||||
|
import priceUpdater, { PriceFeed, PriceHistory } from '../price-updater';
|
||||||
|
|
||||||
|
class FtxApi implements PriceFeed {
|
||||||
|
public name: string = 'FTX';
|
||||||
|
public currencies: string[] = ['USD', 'BRZ', 'EUR', 'JPY', 'AUD'];
|
||||||
|
|
||||||
|
public url: string = 'https://ftx.com/api/markets/BTC/';
|
||||||
|
public urlHist: string = 'https://ftx.com/api/markets/BTC/{CURRENCY}/candles?resolution={GRANULARITY}';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $fetchPrice(currency): Promise<number> {
|
||||||
|
const response = await query(this.url + currency);
|
||||||
|
return response ? parseInt(response['result']['last'], 10) : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $fetchRecentHourlyPrice(currencies: string[]): Promise<PriceHistory> {
|
||||||
|
const priceHistory: PriceHistory = {};
|
||||||
|
|
||||||
|
for (const currency of currencies) {
|
||||||
|
if (this.currencies.includes(currency) === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await query(this.urlHist.replace('{GRANULARITY}', '3600').replace('{CURRENCY}', currency));
|
||||||
|
const pricesRaw = response ? response['result'] : [];
|
||||||
|
|
||||||
|
for (const price of pricesRaw as any[]) {
|
||||||
|
const time = Math.round(price['time'] / 1000);
|
||||||
|
if (priceHistory[time] === undefined) {
|
||||||
|
priceHistory[time] = priceUpdater.getEmptyPricesObj();
|
||||||
|
}
|
||||||
|
priceHistory[time][currency] = price['close'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return priceHistory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FtxApi;
|
||||||
43
backend/src/tasks/price-feeds/gemini-api.ts
Normal file
43
backend/src/tasks/price-feeds/gemini-api.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { query } from '../../utils/axios-query';
|
||||||
|
import priceUpdater, { PriceFeed, PriceHistory } from '../price-updater';
|
||||||
|
|
||||||
|
class GeminiApi implements PriceFeed {
|
||||||
|
public name: string = 'Gemini';
|
||||||
|
public currencies: string[] = ['USD', 'EUR', 'GBP', 'SGD'];
|
||||||
|
|
||||||
|
public url: string = 'https://api.gemini.com/v1/pubticker/BTC';
|
||||||
|
public urlHist: string = 'https://api.gemini.com/v2/candles/BTC{CURRENCY}/{GRANULARITY}';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $fetchPrice(currency): Promise<number> {
|
||||||
|
const response = await query(this.url + currency);
|
||||||
|
return response ? parseInt(response['last'], 10) : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $fetchRecentHourlyPrice(currencies: string[]): Promise<PriceHistory> {
|
||||||
|
const priceHistory: PriceHistory = {};
|
||||||
|
|
||||||
|
for (const currency of currencies) {
|
||||||
|
if (this.currencies.includes(currency) === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await query(this.urlHist.replace('{GRANULARITY}', '1hr').replace('{CURRENCY}', currency));
|
||||||
|
const pricesRaw = response ? response : [];
|
||||||
|
|
||||||
|
for (const price of pricesRaw as any[]) {
|
||||||
|
const time = Math.round(price[0] / 1000);
|
||||||
|
if (priceHistory[time] === undefined) {
|
||||||
|
priceHistory[time] = priceUpdater.getEmptyPricesObj();
|
||||||
|
}
|
||||||
|
priceHistory[time][currency] = price[4];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return priceHistory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GeminiApi;
|
||||||
95
backend/src/tasks/price-feeds/kraken-api.ts
Normal file
95
backend/src/tasks/price-feeds/kraken-api.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import logger from '../../logger';
|
||||||
|
import PricesRepository from '../../repositories/PricesRepository';
|
||||||
|
import { query } from '../../utils/axios-query';
|
||||||
|
import priceUpdater, { PriceFeed, PriceHistory } from '../price-updater';
|
||||||
|
|
||||||
|
class KrakenApi implements PriceFeed {
|
||||||
|
public name: string = 'Kraken';
|
||||||
|
public currencies: string[] = ['USD', 'EUR', 'GBP', 'CAD', 'CHF', 'AUD', 'JPY'];
|
||||||
|
|
||||||
|
public url: string = 'https://api.kraken.com/0/public/Ticker?pair=XBT';
|
||||||
|
public urlHist: string = 'https://api.kraken.com/0/public/OHLC?interval={GRANULARITY}&pair=XBT';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTicker(currency) {
|
||||||
|
let ticker = `XXBTZ${currency}`;
|
||||||
|
if (['CHF', 'AUD'].includes(currency)) {
|
||||||
|
ticker = `XBT${currency}`;
|
||||||
|
}
|
||||||
|
return ticker;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $fetchPrice(currency): Promise<number> {
|
||||||
|
const response = await query(this.url + currency);
|
||||||
|
return response ? parseInt(response['result'][this.getTicker(currency)]['c'][0], 10) : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $fetchRecentHourlyPrice(currencies: string[]): Promise<PriceHistory> {
|
||||||
|
const priceHistory: PriceHistory = {};
|
||||||
|
|
||||||
|
for (const currency of currencies) {
|
||||||
|
if (this.currencies.includes(currency) === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await query(this.urlHist.replace('{GRANULARITY}', '60') + currency);
|
||||||
|
const pricesRaw = response ? response['result'][this.getTicker(currency)] : [];
|
||||||
|
|
||||||
|
for (const price of pricesRaw) {
|
||||||
|
if (priceHistory[price[0]] === undefined) {
|
||||||
|
priceHistory[price[0]] = priceUpdater.getEmptyPricesObj();
|
||||||
|
}
|
||||||
|
priceHistory[price[0]][currency] = price[4];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return priceHistory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch weekly price and save it into the database
|
||||||
|
*/
|
||||||
|
public async $insertHistoricalPrice(): Promise<void> {
|
||||||
|
const existingPriceTimes = await PricesRepository.$getPricesTimes();
|
||||||
|
|
||||||
|
// EUR weekly price history goes back to timestamp 1378339200 (September 5, 2013)
|
||||||
|
// USD weekly price history goes back to timestamp 1380758400 (October 3, 2013)
|
||||||
|
// GBP weekly price history goes back to timestamp 1415232000 (November 6, 2014)
|
||||||
|
// JPY weekly price history goes back to timestamp 1415232000 (November 6, 2014)
|
||||||
|
// CAD weekly price history goes back to timestamp 1436400000 (July 9, 2015)
|
||||||
|
// CHF weekly price history goes back to timestamp 1575504000 (December 5, 2019)
|
||||||
|
// AUD weekly price history goes back to timestamp 1591833600 (June 11, 2020)
|
||||||
|
|
||||||
|
const priceHistory: any = {}; // map: timestamp -> Prices
|
||||||
|
|
||||||
|
for (const currency of this.currencies) {
|
||||||
|
const response = await query(this.urlHist.replace('{GRANULARITY}', '10080') + currency);
|
||||||
|
const priceHistoryRaw = response ? response['result'][this.getTicker(currency)] : [];
|
||||||
|
|
||||||
|
for (const price of priceHistoryRaw) {
|
||||||
|
if (existingPriceTimes.includes(parseInt(price[0]))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// prices[0] = kraken price timestamp
|
||||||
|
// prices[4] = closing price
|
||||||
|
if (priceHistory[price[0]] === undefined) {
|
||||||
|
priceHistory[price[0]] = priceUpdater.getEmptyPricesObj();
|
||||||
|
}
|
||||||
|
priceHistory[price[0]][currency] = price[4];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const time in priceHistory) {
|
||||||
|
await PricesRepository.$savePrices(parseInt(time, 10), priceHistory[time]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(priceHistory).length > 0) {
|
||||||
|
logger.notice(`Inserted ${Object.keys(priceHistory).length} Kraken EUR, USD, GBP, JPY, CAD, CHF and AUD weekly price history into db`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KrakenApi;
|
||||||
762
backend/src/tasks/price-feeds/mtgox-weekly.json
Normal file
762
backend/src/tasks/price-feeds/mtgox-weekly.json
Normal file
@@ -0,0 +1,762 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"ct": 1279497600,
|
||||||
|
"c": "0.08584"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1280102400,
|
||||||
|
"c": "0.0505"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1280707200,
|
||||||
|
"c": "0.0611"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1281312000,
|
||||||
|
"c": "0.0609"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1281916800,
|
||||||
|
"c": "0.06529"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1282521600,
|
||||||
|
"c": "0.066"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1283126400,
|
||||||
|
"c": "0.064"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1283731200,
|
||||||
|
"c": "0.06165"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1284336000,
|
||||||
|
"c": "0.0615"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1284940800,
|
||||||
|
"c": "0.0627"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1285545600,
|
||||||
|
"c": "0.0622"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1286150400,
|
||||||
|
"c": "0.06111"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1286755200,
|
||||||
|
"c": "0.0965"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1287360000,
|
||||||
|
"c": "0.102"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1287964800,
|
||||||
|
"c": "0.11501"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1288569600,
|
||||||
|
"c": "0.1925"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1289174400,
|
||||||
|
"c": "0.34"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1289779200,
|
||||||
|
"c": "0.27904"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1290384000,
|
||||||
|
"c": "0.27675"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1290988800,
|
||||||
|
"c": "0.27"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1291593600,
|
||||||
|
"c": "0.19"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1292198400,
|
||||||
|
"c": "0.2189"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1292803200,
|
||||||
|
"c": "0.2401"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1293408000,
|
||||||
|
"c": "0.263"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1294012800,
|
||||||
|
"c": "0.29997"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1294617600,
|
||||||
|
"c": "0.323"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1295222400,
|
||||||
|
"c": "0.38679"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1295827200,
|
||||||
|
"c": "0.4424"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1296432000,
|
||||||
|
"c": "0.4799"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1297036800,
|
||||||
|
"c": "0.8968"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1297641600,
|
||||||
|
"c": "1.05"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1298246400,
|
||||||
|
"c": "0.865"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1298851200,
|
||||||
|
"c": "0.89"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1299456000,
|
||||||
|
"c": "0.8999"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1300060800,
|
||||||
|
"c": "0.89249"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1300665600,
|
||||||
|
"c": "0.75218"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1301270400,
|
||||||
|
"c": "0.82754"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1301875200,
|
||||||
|
"c": "0.779"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1302480000,
|
||||||
|
"c": "0.7369"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1303084800,
|
||||||
|
"c": "1.1123"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1303689600,
|
||||||
|
"c": "1.6311"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1304294400,
|
||||||
|
"c": "3.03311"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1304899200,
|
||||||
|
"c": "3.8659"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1305504000,
|
||||||
|
"c": "6.98701"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1306108800,
|
||||||
|
"c": "6.6901"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1306713600,
|
||||||
|
"c": "8.4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1307318400,
|
||||||
|
"c": "16.7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1307923200,
|
||||||
|
"c": "18.5464"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1308528000,
|
||||||
|
"c": "17.51"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1309132800,
|
||||||
|
"c": "16.45001"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1309737600,
|
||||||
|
"c": "15.44049"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1310342400,
|
||||||
|
"c": "14.879"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1310947200,
|
||||||
|
"c": "13.16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1311552000,
|
||||||
|
"c": "13.98001"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1312156800,
|
||||||
|
"c": "13.35"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1312761600,
|
||||||
|
"c": "7.9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1313366400,
|
||||||
|
"c": "10.7957"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1313971200,
|
||||||
|
"c": "11.31125"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1314576000,
|
||||||
|
"c": "9.07011"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1315180800,
|
||||||
|
"c": "8.17798"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1315785600,
|
||||||
|
"c": "5.86436"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1316390400,
|
||||||
|
"c": "5.2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1316995200,
|
||||||
|
"c": "5.33"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1317600000,
|
||||||
|
"c": "5.02701"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1318204800,
|
||||||
|
"c": "4.10288"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1318809600,
|
||||||
|
"c": "3.5574"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1319414400,
|
||||||
|
"c": "3.12657"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1320019200,
|
||||||
|
"c": "3.27"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1320624000,
|
||||||
|
"c": "2.95959"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1321228800,
|
||||||
|
"c": "2.99626"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1321833600,
|
||||||
|
"c": "2.2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1322438400,
|
||||||
|
"c": "2.47991"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1323043200,
|
||||||
|
"c": "2.82809"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1323648000,
|
||||||
|
"c": "3.2511"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1324252800,
|
||||||
|
"c": "3.193"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1324857600,
|
||||||
|
"c": "4.225"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1325462400,
|
||||||
|
"c": "5.26766"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1326067200,
|
||||||
|
"c": "7.11358"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1326672000,
|
||||||
|
"c": "7.00177"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1327276800,
|
||||||
|
"c": "6.3097"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1327881600,
|
||||||
|
"c": "5.38191"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1328486400,
|
||||||
|
"c": "5.68881"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1329091200,
|
||||||
|
"c": "5.51468"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1329696000,
|
||||||
|
"c": "4.38669"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1330300800,
|
||||||
|
"c": "4.922"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1330905600,
|
||||||
|
"c": "4.8201"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1331510400,
|
||||||
|
"c": "4.90901"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1332115200,
|
||||||
|
"c": "5.27943"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1332720000,
|
||||||
|
"c": "4.55001"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1333324800,
|
||||||
|
"c": "4.81922"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1333929600,
|
||||||
|
"c": "4.79253"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1334534400,
|
||||||
|
"c": "4.96892"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1335139200,
|
||||||
|
"c": "5.20352"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1335744000,
|
||||||
|
"c": "4.90441"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1336348800,
|
||||||
|
"c": "5.04991"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1336953600,
|
||||||
|
"c": "4.92996"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1337558400,
|
||||||
|
"c": "5.09002"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1338163200,
|
||||||
|
"c": "5.13896"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1338768000,
|
||||||
|
"c": "5.2051"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1339372800,
|
||||||
|
"c": "5.46829"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1339977600,
|
||||||
|
"c": "6.16382"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1340582400,
|
||||||
|
"c": "6.35002"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1341187200,
|
||||||
|
"c": "6.62898"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1341792000,
|
||||||
|
"c": "6.79898"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1342396800,
|
||||||
|
"c": "7.62101"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1343001600,
|
||||||
|
"c": "8.4096"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1343606400,
|
||||||
|
"c": "8.71027"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1344211200,
|
||||||
|
"c": "10.86998"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1344816000,
|
||||||
|
"c": "11.6239"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1345420800,
|
||||||
|
"c": "7.98"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1346025600,
|
||||||
|
"c": "10.61"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1346630400,
|
||||||
|
"c": "10.2041"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1347235200,
|
||||||
|
"c": "11.02"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1347840000,
|
||||||
|
"c": "11.87"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1348444800,
|
||||||
|
"c": "12.19331"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1349049600,
|
||||||
|
"c": "12.4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1349654400,
|
||||||
|
"c": "11.8034"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1350259200,
|
||||||
|
"c": "11.7389"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1350864000,
|
||||||
|
"c": "11.63107"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1351468800,
|
||||||
|
"c": "10.69998"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1352073600,
|
||||||
|
"c": "10.80011"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1352678400,
|
||||||
|
"c": "10.84692"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1353283200,
|
||||||
|
"c": "11.65961"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1353888000,
|
||||||
|
"c": "12.4821"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1354492800,
|
||||||
|
"c": "12.50003"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1355097600,
|
||||||
|
"c": "13.388"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1355702400,
|
||||||
|
"c": "13.30002"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1356307200,
|
||||||
|
"c": "13.31202"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1356912000,
|
||||||
|
"c": "13.45001"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1357516800,
|
||||||
|
"c": "13.5199"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1358121600,
|
||||||
|
"c": "14.11601"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1358726400,
|
||||||
|
"c": "15.7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1359331200,
|
||||||
|
"c": "17.95"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1359936000,
|
||||||
|
"c": "20.59"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1360540800,
|
||||||
|
"c": "23.96975"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1361145600,
|
||||||
|
"c": "26.8146"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1361750400,
|
||||||
|
"c": "29.88999"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1362355200,
|
||||||
|
"c": "34.49999"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1362960000,
|
||||||
|
"c": "46"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1363564800,
|
||||||
|
"c": "47.4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1364169600,
|
||||||
|
"c": "71.93"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1364774400,
|
||||||
|
"c": "93.03001"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1365379200,
|
||||||
|
"c": "162.30102"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1365984000,
|
||||||
|
"c": "89.99999"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1366588800,
|
||||||
|
"c": "119.2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1367193600,
|
||||||
|
"c": "134.44444"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1367798400,
|
||||||
|
"c": "115.98"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1368403200,
|
||||||
|
"c": "114.82002"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1369008000,
|
||||||
|
"c": "122.49999"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1369612800,
|
||||||
|
"c": "133.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1370217600,
|
||||||
|
"c": "122.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1370822400,
|
||||||
|
"c": "100.43743"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1371427200,
|
||||||
|
"c": "99.9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1372032000,
|
||||||
|
"c": "107.90001"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1372636800,
|
||||||
|
"c": "97.51"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1373241600,
|
||||||
|
"c": "76.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1373846400,
|
||||||
|
"c": "94.41986"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1374451200,
|
||||||
|
"c": "91.998"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1375056000,
|
||||||
|
"c": "98.78008"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1375660800,
|
||||||
|
"c": "105.12"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1376265600,
|
||||||
|
"c": "105"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1376870400,
|
||||||
|
"c": "113.38"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1377475200,
|
||||||
|
"c": "122.11102"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1378080000,
|
||||||
|
"c": "146.01003"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1378684800,
|
||||||
|
"c": "126.31501"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1379289600,
|
||||||
|
"c": "138.3002"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1379894400,
|
||||||
|
"c": "134.00001"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1380499200,
|
||||||
|
"c": "143.88402"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1381104000,
|
||||||
|
"c": "137.8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1381708800,
|
||||||
|
"c": "147.53"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1382313600,
|
||||||
|
"c": "186.1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1382918400,
|
||||||
|
"c": "207.0001"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1383523200,
|
||||||
|
"c": "224.01001"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1384128000,
|
||||||
|
"c": "336.33101"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1384732800,
|
||||||
|
"c": "528"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1385337600,
|
||||||
|
"c": "795"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1385942400,
|
||||||
|
"c": "1004.42392"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1386547200,
|
||||||
|
"c": "804.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1387152000,
|
||||||
|
"c": "919.985"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1387756800,
|
||||||
|
"c": "639.48"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1388361600,
|
||||||
|
"c": "786.98"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1388966400,
|
||||||
|
"c": "1015"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1389571200,
|
||||||
|
"c": "940"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1390176000,
|
||||||
|
"c": "954.995"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1390780800,
|
||||||
|
"c": "1007.98999"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1391385600,
|
||||||
|
"c": "954"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1391990400,
|
||||||
|
"c": "659.49776"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1392595200,
|
||||||
|
"c": "299.702"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1393200000,
|
||||||
|
"c": "310.00001"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ct": 1393804800,
|
||||||
|
"c": "135"
|
||||||
|
}
|
||||||
|
]
|
||||||
262
backend/src/tasks/price-updater.ts
Normal file
262
backend/src/tasks/price-updater.ts
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import config from '../config';
|
||||||
|
import logger from '../logger';
|
||||||
|
import PricesRepository from '../repositories/PricesRepository';
|
||||||
|
import BitfinexApi from './price-feeds/bitfinex-api';
|
||||||
|
import BitflyerApi from './price-feeds/bitflyer-api';
|
||||||
|
import CoinbaseApi from './price-feeds/coinbase-api';
|
||||||
|
import FtxApi from './price-feeds/ftx-api';
|
||||||
|
import GeminiApi from './price-feeds/gemini-api';
|
||||||
|
import KrakenApi from './price-feeds/kraken-api';
|
||||||
|
|
||||||
|
export interface PriceFeed {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
urlHist: string;
|
||||||
|
currencies: string[];
|
||||||
|
|
||||||
|
$fetchPrice(currency): Promise<number>;
|
||||||
|
$fetchRecentHourlyPrice(currencies: string[]): Promise<PriceHistory>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PriceHistory {
|
||||||
|
[timestamp: number]: Prices;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Prices {
|
||||||
|
USD: number;
|
||||||
|
EUR: number;
|
||||||
|
GBP: number;
|
||||||
|
CAD: number;
|
||||||
|
CHF: number;
|
||||||
|
AUD: number;
|
||||||
|
JPY: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PriceUpdater {
|
||||||
|
historyInserted: boolean = false;
|
||||||
|
lastRun: number = 0;
|
||||||
|
lastHistoricalRun: number = 0;
|
||||||
|
running: boolean = false;
|
||||||
|
feeds: PriceFeed[] = [];
|
||||||
|
currencies: string[] = ['USD', 'EUR', 'GBP', 'CAD', 'CHF', 'AUD', 'JPY'];
|
||||||
|
latestPrices: Prices;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.latestPrices = this.getEmptyPricesObj();
|
||||||
|
|
||||||
|
this.feeds.push(new BitflyerApi()); // Does not have historical endpoint
|
||||||
|
this.feeds.push(new FtxApi());
|
||||||
|
this.feeds.push(new KrakenApi());
|
||||||
|
this.feeds.push(new CoinbaseApi());
|
||||||
|
this.feeds.push(new BitfinexApi());
|
||||||
|
this.feeds.push(new GeminiApi());
|
||||||
|
}
|
||||||
|
|
||||||
|
public getEmptyPricesObj(): Prices {
|
||||||
|
return {
|
||||||
|
USD: -1,
|
||||||
|
EUR: -1,
|
||||||
|
GBP: -1,
|
||||||
|
CAD: -1,
|
||||||
|
CHF: -1,
|
||||||
|
AUD: -1,
|
||||||
|
JPY: -1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $run(): Promise<void> {
|
||||||
|
if (this.running === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.running = true;
|
||||||
|
|
||||||
|
if ((Math.round(new Date().getTime() / 1000) - this.lastHistoricalRun) > 3600 * 24) {
|
||||||
|
// Once a day, look for missing prices (could happen due to network connectivity issues)
|
||||||
|
this.historyInserted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (this.historyInserted === false && config.DATABASE.ENABLED === true) {
|
||||||
|
await this.$insertHistoricalPrices();
|
||||||
|
} else {
|
||||||
|
await this.$updatePrice();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Cannot save BTC prices in db. Reason: ${e instanceof Error ? e.message : e}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch last BTC price from exchanges, average them, and save it in the database once every hour
|
||||||
|
*/
|
||||||
|
private async $updatePrice(): Promise<void> {
|
||||||
|
if (this.lastRun === 0 && config.DATABASE.ENABLED === true) {
|
||||||
|
this.lastRun = await PricesRepository.$getLatestPriceTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((Math.round(new Date().getTime() / 1000) - this.lastRun) < 3600) {
|
||||||
|
// Refresh only once every hour
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousRun = this.lastRun;
|
||||||
|
this.lastRun = new Date().getTime() / 1000;
|
||||||
|
|
||||||
|
for (const currency of this.currencies) {
|
||||||
|
let prices: number[] = [];
|
||||||
|
|
||||||
|
for (const feed of this.feeds) {
|
||||||
|
// Fetch prices from API which supports `currency`
|
||||||
|
if (feed.currencies.includes(currency)) {
|
||||||
|
try {
|
||||||
|
const price = await feed.$fetchPrice(currency);
|
||||||
|
if (price > 0) {
|
||||||
|
prices.push(price);
|
||||||
|
}
|
||||||
|
logger.debug(`${feed.name} BTC/${currency} price: ${price}`);
|
||||||
|
} catch (e) {
|
||||||
|
logger.debug(`Could not fetch BTC/${currency} price at ${feed.name}. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (prices.length === 1) {
|
||||||
|
logger.debug(`Only ${prices.length} feed available for BTC/${currency} price`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute average price, non weighted
|
||||||
|
prices = prices.filter(price => price > 0);
|
||||||
|
this.latestPrices[currency] = Math.round((prices.reduce((partialSum, a) => partialSum + a, 0)) / prices.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Latest BTC fiat averaged price: ${JSON.stringify(this.latestPrices)}`);
|
||||||
|
|
||||||
|
if (config.DATABASE.ENABLED === true) {
|
||||||
|
// Save everything in db
|
||||||
|
try {
|
||||||
|
const p = 60 * 60 * 1000; // milliseconds in an hour
|
||||||
|
const nowRounded = new Date(Math.round(new Date().getTime() / p) * p); // https://stackoverflow.com/a/28037042
|
||||||
|
await PricesRepository.$savePrices(nowRounded.getTime() / 1000, this.latestPrices);
|
||||||
|
} catch (e) {
|
||||||
|
this.lastRun = previousRun + 5 * 60;
|
||||||
|
logger.err(`Cannot save latest prices into db. Trying again in 5 minutes. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastRun = new Date().getTime() / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called once by the database migration to initialize historical prices data (weekly)
|
||||||
|
* We use MtGox weekly price from July 19, 2010 to September 30, 2013
|
||||||
|
* We use Kraken weekly price from October 3, 2013 up to last month
|
||||||
|
* We use Kraken hourly price for the past month
|
||||||
|
*/
|
||||||
|
private async $insertHistoricalPrices(): Promise<void> {
|
||||||
|
const existingPriceTimes = await PricesRepository.$getPricesTimes();
|
||||||
|
|
||||||
|
// Insert MtGox weekly prices
|
||||||
|
const pricesJson: any[] = JSON.parse(fs.readFileSync('./src/tasks/price-feeds/mtgox-weekly.json').toString());
|
||||||
|
const prices = this.getEmptyPricesObj();
|
||||||
|
let insertedCount: number = 0;
|
||||||
|
for (const price of pricesJson) {
|
||||||
|
if (existingPriceTimes.includes(price['ct'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// From 1380758400 we will use Kraken price as it follows closely MtGox, but was not affected as much
|
||||||
|
// by the MtGox exchange collapse a few months later
|
||||||
|
if (price['ct'] > 1380758400) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
prices.USD = price['c'];
|
||||||
|
await PricesRepository.$savePrices(price['ct'], prices);
|
||||||
|
++insertedCount;
|
||||||
|
}
|
||||||
|
if (insertedCount > 0) {
|
||||||
|
logger.notice(`Inserted ${insertedCount} MtGox USD weekly price history into db`);
|
||||||
|
} else {
|
||||||
|
logger.debug(`Inserted ${insertedCount} MtGox USD weekly price history into db`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert Kraken weekly prices
|
||||||
|
await new KrakenApi().$insertHistoricalPrice();
|
||||||
|
|
||||||
|
// Insert missing recent hourly prices
|
||||||
|
await this.$insertMissingRecentPrices();
|
||||||
|
|
||||||
|
this.historyInserted = true;
|
||||||
|
this.lastHistoricalRun = new Date().getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find missing hourly prices and insert them in the database
|
||||||
|
* It has a limited backward range and it depends on which API are available
|
||||||
|
*/
|
||||||
|
private async $insertMissingRecentPrices(): Promise<void> {
|
||||||
|
const existingPriceTimes = await PricesRepository.$getPricesTimes();
|
||||||
|
|
||||||
|
logger.info(`Fetching hourly price history from exchanges and saving missing ones into the database, this may take a while`);
|
||||||
|
|
||||||
|
const historicalPrices: PriceHistory[] = [];
|
||||||
|
|
||||||
|
// Fetch all historical hourly prices
|
||||||
|
for (const feed of this.feeds) {
|
||||||
|
try {
|
||||||
|
historicalPrices.push(await feed.$fetchRecentHourlyPrice(this.currencies));
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Cannot fetch hourly historical price from ${feed.name}. Ignoring this feed. Reason: ${e instanceof Error ? e.message : e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group them by timestamp and currency, for example
|
||||||
|
// grouped[123456789]['USD'] = [1, 2, 3, 4];
|
||||||
|
const grouped: Object = {};
|
||||||
|
for (const historicalEntry of historicalPrices) {
|
||||||
|
for (const time in historicalEntry) {
|
||||||
|
if (existingPriceTimes.includes(parseInt(time, 10))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (grouped[time] === undefined) {
|
||||||
|
grouped[time] = {
|
||||||
|
USD: [], EUR: [], GBP: [], CAD: [], CHF: [], AUD: [], JPY: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const currency of this.currencies) {
|
||||||
|
const price = historicalEntry[time][currency];
|
||||||
|
if (price > 0) {
|
||||||
|
grouped[time][currency].push(parseInt(price, 10));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Average prices and insert everything into the db
|
||||||
|
let totalInserted = 0;
|
||||||
|
for (const time in grouped) {
|
||||||
|
const prices: Prices = this.getEmptyPricesObj();
|
||||||
|
for (const currency in grouped[time]) {
|
||||||
|
if (grouped[time][currency].length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
prices[currency] = Math.round((grouped[time][currency].reduce(
|
||||||
|
(partialSum, a) => partialSum + a, 0)
|
||||||
|
) / grouped[time][currency].length);
|
||||||
|
}
|
||||||
|
await PricesRepository.$savePrices(parseInt(time, 10), prices);
|
||||||
|
++totalInserted;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalInserted > 0) {
|
||||||
|
logger.notice(`Inserted ${totalInserted} hourly historical prices into the db`);
|
||||||
|
} else {
|
||||||
|
logger.debug(`Inserted ${totalInserted} hourly historical prices into the db`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new PriceUpdater();
|
||||||
63
backend/src/utils/axios-query.ts
Normal file
63
backend/src/utils/axios-query.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import axios, { AxiosResponse } from 'axios';
|
||||||
|
import { SocksProxyAgent } from 'socks-proxy-agent';
|
||||||
|
import backendInfo from '../api/backend-info';
|
||||||
|
import config from '../config';
|
||||||
|
import logger from '../logger';
|
||||||
|
import * as https from 'https';
|
||||||
|
|
||||||
|
export async function query(path): Promise<object | undefined> {
|
||||||
|
type axiosOptions = {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': string
|
||||||
|
};
|
||||||
|
timeout: number;
|
||||||
|
httpsAgent?: https.Agent;
|
||||||
|
};
|
||||||
|
const setDelay = (secs: number = 1): Promise<void> => new Promise(resolve => setTimeout(() => resolve(), secs * 1000));
|
||||||
|
const axiosOptions: axiosOptions = {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': (config.MEMPOOL.USER_AGENT === 'mempool') ? `mempool/v${backendInfo.getBackendInfo().version}` : `${config.MEMPOOL.USER_AGENT}`
|
||||||
|
},
|
||||||
|
timeout: config.SOCKS5PROXY.ENABLED ? 30000 : 10000
|
||||||
|
};
|
||||||
|
let retry = 0;
|
||||||
|
|
||||||
|
while (retry < config.MEMPOOL.EXTERNAL_MAX_RETRY) {
|
||||||
|
try {
|
||||||
|
if (config.SOCKS5PROXY.ENABLED) {
|
||||||
|
const socksOptions: any = {
|
||||||
|
agentOptions: {
|
||||||
|
keepAlive: true,
|
||||||
|
},
|
||||||
|
hostname: config.SOCKS5PROXY.HOST,
|
||||||
|
port: config.SOCKS5PROXY.PORT
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config.SOCKS5PROXY.USERNAME && config.SOCKS5PROXY.PASSWORD) {
|
||||||
|
socksOptions.username = config.SOCKS5PROXY.USERNAME;
|
||||||
|
socksOptions.password = config.SOCKS5PROXY.PASSWORD;
|
||||||
|
} else {
|
||||||
|
// Retry with different tor circuits https://stackoverflow.com/a/64960234
|
||||||
|
socksOptions.username = `circuit${retry}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
axiosOptions.httpsAgent = new SocksProxyAgent(socksOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: AxiosResponse = await axios.get(path, axiosOptions);
|
||||||
|
if (data.statusText === 'error' || !data.data) {
|
||||||
|
throw new Error(`Could not fetch data from ${path}, Error: ${data.status}`);
|
||||||
|
}
|
||||||
|
return data.data;
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`Could not connect to ${path} (Attempt ${retry + 1}/${config.MEMPOOL.EXTERNAL_MAX_RETRY}). Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
retry++;
|
||||||
|
}
|
||||||
|
if (retry < config.MEMPOOL.EXTERNAL_MAX_RETRY) {
|
||||||
|
await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.err(`Could not connect to ${path}. All ${config.MEMPOOL.EXTERNAL_MAX_RETRY} attempts failed`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
@@ -3,14 +3,14 @@ import { BlockExtended } from '../mempool.interfaces';
|
|||||||
export function prepareBlock(block: any): BlockExtended {
|
export function prepareBlock(block: any): BlockExtended {
|
||||||
return <BlockExtended>{
|
return <BlockExtended>{
|
||||||
id: block.id ?? block.hash, // hash for indexed block
|
id: block.id ?? block.hash, // hash for indexed block
|
||||||
timestamp: block.timestamp ?? block.blockTimestamp, // blockTimestamp for indexed block
|
timestamp: block.timestamp ?? block.time ?? block.blockTimestamp, // blockTimestamp for indexed block
|
||||||
height: block.height,
|
height: block.height,
|
||||||
version: block.version,
|
version: block.version,
|
||||||
bits: block.bits,
|
bits: (typeof block.bits === 'string' ? parseInt(block.bits, 16): block.bits),
|
||||||
nonce: block.nonce,
|
nonce: block.nonce,
|
||||||
difficulty: block.difficulty,
|
difficulty: block.difficulty,
|
||||||
merkle_root: block.merkle_root,
|
merkle_root: block.merkle_root ?? block.merkleroot,
|
||||||
tx_count: block.tx_count,
|
tx_count: block.tx_count ?? block.nTx,
|
||||||
size: block.size,
|
size: block.size,
|
||||||
weight: block.weight,
|
weight: block.weight,
|
||||||
previousblockhash: block.previousblockhash,
|
previousblockhash: block.previousblockhash,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"types": ["node"],
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "esnext",
|
"target": "esnext",
|
||||||
"lib": ["es2019", "dom"],
|
"lib": ["es2019", "dom"],
|
||||||
@@ -11,7 +12,8 @@
|
|||||||
"typeRoots": [
|
"typeRoots": [
|
||||||
"node_modules/@types"
|
"node_modules/@types"
|
||||||
],
|
],
|
||||||
"allowSyntheticDefaultImports": true
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*.ts"
|
"src/**/*.ts"
|
||||||
|
|||||||
@@ -1,137 +0,0 @@
|
|||||||
{
|
|
||||||
"rules": {
|
|
||||||
"arrow-return-shorthand": true,
|
|
||||||
"callable-types": true,
|
|
||||||
"class-name": true,
|
|
||||||
"comment-format": [
|
|
||||||
true,
|
|
||||||
"check-space"
|
|
||||||
],
|
|
||||||
"curly": true,
|
|
||||||
"deprecation": {
|
|
||||||
"severity": "warn"
|
|
||||||
},
|
|
||||||
"eofline": true,
|
|
||||||
"forin": false,
|
|
||||||
"import-blacklist": [
|
|
||||||
true,
|
|
||||||
"rxjs",
|
|
||||||
"rxjs/Rx"
|
|
||||||
],
|
|
||||||
"import-spacing": true,
|
|
||||||
"indent": [
|
|
||||||
true,
|
|
||||||
"spaces"
|
|
||||||
],
|
|
||||||
"interface-over-type-literal": true,
|
|
||||||
"label-position": true,
|
|
||||||
"max-line-length": [
|
|
||||||
true,
|
|
||||||
140
|
|
||||||
],
|
|
||||||
"member-access": false,
|
|
||||||
"member-ordering": [
|
|
||||||
true,
|
|
||||||
{
|
|
||||||
"order": [
|
|
||||||
"static-field",
|
|
||||||
"instance-field",
|
|
||||||
"static-method",
|
|
||||||
"instance-method"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-arg": true,
|
|
||||||
"no-bitwise": true,
|
|
||||||
"no-console": [
|
|
||||||
true,
|
|
||||||
"debug",
|
|
||||||
"info",
|
|
||||||
"time",
|
|
||||||
"timeEnd",
|
|
||||||
"trace"
|
|
||||||
],
|
|
||||||
"no-construct": true,
|
|
||||||
"no-debugger": true,
|
|
||||||
"no-duplicate-super": true,
|
|
||||||
"no-empty": false,
|
|
||||||
"no-empty-interface": true,
|
|
||||||
"no-eval": true,
|
|
||||||
"no-inferrable-types": false,
|
|
||||||
"no-misused-new": true,
|
|
||||||
"no-non-null-assertion": true,
|
|
||||||
"no-shadowed-variable": true,
|
|
||||||
"no-string-literal": false,
|
|
||||||
"no-string-throw": true,
|
|
||||||
"no-switch-case-fall-through": true,
|
|
||||||
"no-trailing-whitespace": true,
|
|
||||||
"no-unnecessary-initializer": true,
|
|
||||||
"no-unused-expression": true,
|
|
||||||
"no-use-before-declare": true,
|
|
||||||
"no-var-keyword": true,
|
|
||||||
"object-literal-sort-keys": false,
|
|
||||||
"one-line": [
|
|
||||||
true,
|
|
||||||
"check-open-brace",
|
|
||||||
"check-catch",
|
|
||||||
"check-else",
|
|
||||||
"check-whitespace"
|
|
||||||
],
|
|
||||||
"prefer-const": true,
|
|
||||||
"quotemark": [
|
|
||||||
true,
|
|
||||||
"single"
|
|
||||||
],
|
|
||||||
"radix": true,
|
|
||||||
"semicolon": [
|
|
||||||
true,
|
|
||||||
"always"
|
|
||||||
],
|
|
||||||
"triple-equals": [
|
|
||||||
true,
|
|
||||||
"allow-null-check"
|
|
||||||
],
|
|
||||||
"typedef-whitespace": [
|
|
||||||
true,
|
|
||||||
{
|
|
||||||
"call-signature": "nospace",
|
|
||||||
"index-signature": "nospace",
|
|
||||||
"parameter": "nospace",
|
|
||||||
"property-declaration": "nospace",
|
|
||||||
"variable-declaration": "nospace"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"unified-signatures": true,
|
|
||||||
"variable-name": false,
|
|
||||||
"whitespace": [
|
|
||||||
true,
|
|
||||||
"check-branch",
|
|
||||||
"check-decl",
|
|
||||||
"check-operator",
|
|
||||||
"check-separator",
|
|
||||||
"check-type"
|
|
||||||
],
|
|
||||||
"directive-selector": [
|
|
||||||
true,
|
|
||||||
"attribute",
|
|
||||||
"app",
|
|
||||||
"camelCase"
|
|
||||||
],
|
|
||||||
"component-selector": [
|
|
||||||
true,
|
|
||||||
"element",
|
|
||||||
"app",
|
|
||||||
"kebab-case"
|
|
||||||
],
|
|
||||||
"no-output-on-prefix": true,
|
|
||||||
"use-input-property-decorator": true,
|
|
||||||
"use-output-property-decorator": true,
|
|
||||||
"use-host-property-decorator": true,
|
|
||||||
"no-input-rename": true,
|
|
||||||
"no-output-rename": true,
|
|
||||||
"use-life-cycle-interface": true,
|
|
||||||
"use-pipe-transform-interface": true,
|
|
||||||
"component-class-suffix": true,
|
|
||||||
"directive-class-suffix": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3
contributors/erikarvstedt.txt
Normal file
3
contributors/erikarvstedt.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of July 6, 2022.
|
||||||
|
|
||||||
|
Signed: erikarvstedt
|
||||||
@@ -4,6 +4,8 @@ This directory contains the Dockerfiles used to build and release the official i
|
|||||||
|
|
||||||
If you are looking to use these Docker images to deploy your own instance of Mempool, note that they only containerize Mempool's frontend and backend. You will still need to deploy and configure Bitcoin Core and an Electrum Server separately, along with any other utilities specific to your use case (e.g., a reverse proxy, etc). Such configuration is mostly beyond the scope of the Mempool project, so please only proceed if you know what you're doing.
|
If you are looking to use these Docker images to deploy your own instance of Mempool, note that they only containerize Mempool's frontend and backend. You will still need to deploy and configure Bitcoin Core and an Electrum Server separately, along with any other utilities specific to your use case (e.g., a reverse proxy, etc). Such configuration is mostly beyond the scope of the Mempool project, so please only proceed if you know what you're doing.
|
||||||
|
|
||||||
|
See a video guide of this installation method by k3tan [on BitcoinTV.com](https://bitcointv.com/w/8fpAx6rf5CQ16mMhospwjg).
|
||||||
|
|
||||||
Jump to a section in this doc:
|
Jump to a section in this doc:
|
||||||
- [Configure with Bitcoin Core Only](#configure-with-bitcoin-core-only)
|
- [Configure with Bitcoin Core Only](#configure-with-bitcoin-core-only)
|
||||||
- [Configure with Bitcoin Core + Electrum Server](#configure-with-bitcoin-core--electrum-server)
|
- [Configure with Bitcoin Core + Electrum Server](#configure-with-bitcoin-core--electrum-server)
|
||||||
@@ -233,7 +235,7 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
DATABASE_HOST: ""
|
DATABASE_HOST: ""
|
||||||
DATABASE_PORT: ""
|
DATABASE_PORT: ""
|
||||||
DATABASE_DATABASE: ""
|
DATABASE_DATABASE: ""
|
||||||
DATABASE_USERAME: ""
|
DATABASE_USERNAME: ""
|
||||||
DATABASE_PASSWORD: ""
|
DATABASE_PASSWORD: ""
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:16.15.0-buster-slim AS builder
|
FROM node:16.16.0-buster-slim AS builder
|
||||||
|
|
||||||
ARG commitHash
|
ARG commitHash
|
||||||
ENV DOCKER_COMMIT_HASH=${commitHash}
|
ENV DOCKER_COMMIT_HASH=${commitHash}
|
||||||
@@ -8,10 +8,10 @@ COPY . .
|
|||||||
|
|
||||||
RUN apt-get update
|
RUN apt-get update
|
||||||
RUN apt-get install -y build-essential python3 pkg-config
|
RUN apt-get install -y build-essential python3 pkg-config
|
||||||
RUN npm install
|
RUN npm install --omit=dev --omit=optional
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM node:16.15.0-buster-slim
|
FROM node:16.16.0-buster-slim
|
||||||
|
|
||||||
WORKDIR /backend
|
WORKDIR /backend
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,13 @@
|
|||||||
"PRICE_FEED_UPDATE_INTERVAL": __MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__,
|
"PRICE_FEED_UPDATE_INTERVAL": __MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__,
|
||||||
"USE_SECOND_NODE_FOR_MINFEE": __MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__,
|
"USE_SECOND_NODE_FOR_MINFEE": __MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__,
|
||||||
"EXTERNAL_ASSETS": __MEMPOOL_EXTERNAL_ASSETS__,
|
"EXTERNAL_ASSETS": __MEMPOOL_EXTERNAL_ASSETS__,
|
||||||
|
"EXTERNAL_MAX_RETRY": __MEMPOOL_EXTERNAL_MAX_RETRY__,
|
||||||
|
"EXTERNAL_RETRY_INTERVAL": __MEMPOOL_EXTERNAL_RETRY_INTERVAL__,
|
||||||
|
"USER_AGENT": "__MEMPOOL_USER_AGENT__",
|
||||||
"STDOUT_LOG_MIN_PRIORITY": "__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__",
|
"STDOUT_LOG_MIN_PRIORITY": "__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__",
|
||||||
"INDEXING_BLOCKS_AMOUNT": __MEMPOOL_INDEXING_BLOCKS_AMOUNT__
|
"INDEXING_BLOCKS_AMOUNT": __MEMPOOL_INDEXING_BLOCKS_AMOUNT__,
|
||||||
|
"BLOCKS_SUMMARIES_INDEXING": __MEMPOOL_BLOCKS_SUMMARIES_INDEXING__,
|
||||||
|
"AUTOMATIC_BLOCK_REINDEXING": __MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__
|
||||||
},
|
},
|
||||||
"CORE_RPC": {
|
"CORE_RPC": {
|
||||||
"HOST": "__CORE_RPC_HOST__",
|
"HOST": "__CORE_RPC_HOST__",
|
||||||
@@ -64,6 +69,7 @@
|
|||||||
},
|
},
|
||||||
"SOCKS5PROXY": {
|
"SOCKS5PROXY": {
|
||||||
"ENABLED": __SOCKS5PROXY_ENABLED__,
|
"ENABLED": __SOCKS5PROXY_ENABLED__,
|
||||||
|
"USE_ONION": __SOCKS5PROXY_USE_ONION__,
|
||||||
"HOST": "__SOCKS5PROXY_HOST__",
|
"HOST": "__SOCKS5PROXY_HOST__",
|
||||||
"PORT": "__SOCKS5PROXY_PORT__",
|
"PORT": "__SOCKS5PROXY_PORT__",
|
||||||
"USERNAME": "__SOCKS5PROXY_USERNAME__",
|
"USERNAME": "__SOCKS5PROXY_USERNAME__",
|
||||||
@@ -72,5 +78,13 @@
|
|||||||
"PRICE_DATA_SERVER": {
|
"PRICE_DATA_SERVER": {
|
||||||
"TOR_URL": "__PRICE_DATA_SERVER_TOR_URL__",
|
"TOR_URL": "__PRICE_DATA_SERVER_TOR_URL__",
|
||||||
"CLEARNET_URL": "__PRICE_DATA_SERVER_CLEARNET_URL__"
|
"CLEARNET_URL": "__PRICE_DATA_SERVER_CLEARNET_URL__"
|
||||||
|
},
|
||||||
|
"EXTERNAL_DATA_SERVER": {
|
||||||
|
"MEMPOOL_API": "__EXTERNAL_DATA_SERVER_MEMPOOL_API__",
|
||||||
|
"MEMPOOL_ONION": "__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__",
|
||||||
|
"LIQUID_API": "__EXTERNAL_DATA_SERVER_LIQUID_API__",
|
||||||
|
"LIQUID_ONION": "__EXTERNAL_DATA_SERVER_LIQUID_ONION__",
|
||||||
|
"BISQ_URL": "__EXTERNAL_DATA_SERVER_BISQ_URL__",
|
||||||
|
"BISQ_ONION": "__EXTERNAL_DATA_SERVER_BISQ_ONION__"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,16 @@ __MEMPOOL_BLOCK_WEIGHT_UNITS__=${MEMPOOL_BLOCK_WEIGHT_UNITS:=4000000}
|
|||||||
__MEMPOOL_INITIAL_BLOCKS_AMOUNT__=${MEMPOOL_INITIAL_BLOCKS_AMOUNT:=8}
|
__MEMPOOL_INITIAL_BLOCKS_AMOUNT__=${MEMPOOL_INITIAL_BLOCKS_AMOUNT:=8}
|
||||||
__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__=${MEMPOOL_MEMPOOL_BLOCKS_AMOUNT:=8}
|
__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__=${MEMPOOL_MEMPOOL_BLOCKS_AMOUNT:=8}
|
||||||
__MEMPOOL_INDEXING_BLOCKS_AMOUNT__=${MEMPOOL_INDEXING_BLOCKS_AMOUNT:=11000}
|
__MEMPOOL_INDEXING_BLOCKS_AMOUNT__=${MEMPOOL_INDEXING_BLOCKS_AMOUNT:=11000}
|
||||||
|
__MEMPOOL_BLOCKS_SUMMARIES_INDEXING__=${MEMPOOL_BLOCKS_SUMMARIES_INDEXING:=false}
|
||||||
__MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__=${MEMPOOL_PRICE_FEED_UPDATE_INTERVAL:=600}
|
__MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__=${MEMPOOL_PRICE_FEED_UPDATE_INTERVAL:=600}
|
||||||
__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__=${MEMPOOL_USE_SECOND_NODE_FOR_MINFEE:=false}
|
__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__=${MEMPOOL_USE_SECOND_NODE_FOR_MINFEE:=false}
|
||||||
__MEMPOOL_EXTERNAL_ASSETS__=${MEMPOOL_EXTERNAL_ASSETS:=[\"https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json\"]}
|
__MEMPOOL_EXTERNAL_ASSETS__=${MEMPOOL_EXTERNAL_ASSETS:=[]}
|
||||||
|
__MEMPOOL_EXTERNAL_MAX_RETRY__=${MEMPOOL_EXTERNAL_MAX_RETRY:=1}
|
||||||
|
__MEMPOOL_EXTERNAL_RETRY_INTERVAL__=${MEMPOOL_EXTERNAL_RETRY_INTERVAL:=0}
|
||||||
|
__MEMPOOL_USER_AGENT__=${MEMPOOL_USER_AGENT:=mempool}
|
||||||
__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info}
|
__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info}
|
||||||
|
__MEMPOOL_INDEXING_BLOCKS_AMOUNT__=${MEMPOOL_INDEXING_BLOCKS_AMOUNT:=false}
|
||||||
|
__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__=${MEMPOOL_AUTOMATIC_BLOCK_REINDEXING:=false}
|
||||||
|
|
||||||
# CORE_RPC
|
# CORE_RPC
|
||||||
__CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1}
|
__CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1}
|
||||||
@@ -65,6 +71,7 @@ __BISQ_DATA_PATH__=${BISQ_DATA_PATH:=/bisq/statsnode-data/btc_mainnet/db}
|
|||||||
|
|
||||||
# SOCKS5PROXY
|
# SOCKS5PROXY
|
||||||
__SOCKS5PROXY_ENABLED__=${SOCKS5PROXY_ENABLED:=false}
|
__SOCKS5PROXY_ENABLED__=${SOCKS5PROXY_ENABLED:=false}
|
||||||
|
__SOCKS5PROXY_USE_ONION__=${SOCKS5PROXY_USE_ONION:=true}
|
||||||
__SOCKS5PROXY_HOST__=${SOCKS5PROXY_HOST:=localhost}
|
__SOCKS5PROXY_HOST__=${SOCKS5PROXY_HOST:=localhost}
|
||||||
__SOCKS5PROXY_PORT__=${SOCKS5PROXY_PORT:=9050}
|
__SOCKS5PROXY_PORT__=${SOCKS5PROXY_PORT:=9050}
|
||||||
__SOCKS5PROXY_USERNAME__=${SOCKS5PROXY_USERNAME:=""}
|
__SOCKS5PROXY_USERNAME__=${SOCKS5PROXY_USERNAME:=""}
|
||||||
@@ -74,6 +81,14 @@ __SOCKS5PROXY_PASSWORD__=${SOCKS5PROXY_PASSWORD:=""}
|
|||||||
__PRICE_DATA_SERVER_TOR_URL__=${PRICE_DATA_SERVER_TOR_URL:=http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices}
|
__PRICE_DATA_SERVER_TOR_URL__=${PRICE_DATA_SERVER_TOR_URL:=http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices}
|
||||||
__PRICE_DATA_SERVER_CLEARNET_URL__=${PRICE_DATA_SERVER_CLEARNET_URL:=https://price.bisq.wiz.biz/getAllMarketPrices}
|
__PRICE_DATA_SERVER_CLEARNET_URL__=${PRICE_DATA_SERVER_CLEARNET_URL:=https://price.bisq.wiz.biz/getAllMarketPrices}
|
||||||
|
|
||||||
|
# EXTERNAL_DATA_SERVER
|
||||||
|
__EXTERNAL_DATA_SERVER_MEMPOOL_API__=${EXTERNAL_DATA_SERVER_MEMPOOL_API:=https://mempool.space/api/v1}
|
||||||
|
__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__=${EXTERNAL_DATA_SERVER_MEMPOOL_ONION:=http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1}
|
||||||
|
__EXTERNAL_DATA_SERVER_LIQUID_API__=${EXTERNAL_DATA_SERVER_LIQUID_API:=https://liquid.network/api/v1}
|
||||||
|
__EXTERNAL_DATA_SERVER_LIQUID_ONION__=${EXTERNAL_DATA_SERVER_LIQUID_ONION:=http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1}
|
||||||
|
__EXTERNAL_DATA_SERVER_BISQ_URL__=${EXTERNAL_DATA_SERVER_BISQ_URL:=https://bisq.markets/api}
|
||||||
|
__EXTERNAL_DATA_SERVER_BISQ_ONION__=${EXTERNAL_DATA_SERVER_BISQ_ONION:=http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api}
|
||||||
|
|
||||||
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
|
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
|
||||||
|
|
||||||
sed -i "s/__MEMPOOL_NETWORK__/${__MEMPOOL_NETWORK__}/g" mempool-config.json
|
sed -i "s/__MEMPOOL_NETWORK__/${__MEMPOOL_NETWORK__}/g" mempool-config.json
|
||||||
@@ -89,10 +104,16 @@ sed -i "s/__MEMPOOL_BLOCK_WEIGHT_UNITS__/${__MEMPOOL_BLOCK_WEIGHT_UNITS__}/g" me
|
|||||||
sed -i "s/__MEMPOOL_INITIAL_BLOCKS_AMOUNT__/${__MEMPOOL_INITIAL_BLOCKS_AMOUNT__}/g" mempool-config.json
|
sed -i "s/__MEMPOOL_INITIAL_BLOCKS_AMOUNT__/${__MEMPOOL_INITIAL_BLOCKS_AMOUNT__}/g" mempool-config.json
|
||||||
sed -i "s/__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__/${__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__}/g" mempool-config.json
|
sed -i "s/__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__/${__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__}/g" mempool-config.json
|
||||||
sed -i "s/__MEMPOOL_INDEXING_BLOCKS_AMOUNT__/${__MEMPOOL_INDEXING_BLOCKS_AMOUNT__}/g" mempool-config.json
|
sed -i "s/__MEMPOOL_INDEXING_BLOCKS_AMOUNT__/${__MEMPOOL_INDEXING_BLOCKS_AMOUNT__}/g" mempool-config.json
|
||||||
|
sed -i "s/__MEMPOOL_BLOCKS_SUMMARIES_INDEXING__/${__MEMPOOL_BLOCKS_SUMMARIES_INDEXING__}/g" mempool-config.json
|
||||||
sed -i "s/__MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__/${__MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__}/g" mempool-config.json
|
sed -i "s/__MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__/${__MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__}/g" mempool-config.json
|
||||||
sed -i "s/__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__/${__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__}/g" mempool-config.json
|
sed -i "s/__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__/${__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__}/g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_EXTERNAL_ASSETS__!${__MEMPOOL_EXTERNAL_ASSETS__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_EXTERNAL_ASSETS__!${__MEMPOOL_EXTERNAL_ASSETS__}!g" mempool-config.json
|
||||||
|
sed -i "s!__MEMPOOL_EXTERNAL_MAX_RETRY__!${__MEMPOOL_EXTERNAL_MAX_RETRY__}!g" mempool-config.json
|
||||||
|
sed -i "s!__MEMPOOL_EXTERNAL_RETRY_INTERVAL__!${__MEMPOOL_EXTERNAL_RETRY_INTERVAL__}!g" mempool-config.json
|
||||||
|
sed -i "s!__MEMPOOL_USER_AGENT__!${__MEMPOOL_USER_AGENT__}!g" mempool-config.json
|
||||||
sed -i "s/__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__/${__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__}/g" mempool-config.json
|
sed -i "s/__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__/${__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__}/g" mempool-config.json
|
||||||
|
sed -i "s/__MEMPOOL_INDEXING_BLOCKS_AMOUNT__/${__MEMPOOL_INDEXING_BLOCKS_AMOUNT__}/g" mempool-config.json
|
||||||
|
sed -i "s/__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__/${__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__}/g" mempool-config.json
|
||||||
|
|
||||||
sed -i "s/__CORE_RPC_HOST__/${__CORE_RPC_HOST__}/g" mempool-config.json
|
sed -i "s/__CORE_RPC_HOST__/${__CORE_RPC_HOST__}/g" mempool-config.json
|
||||||
sed -i "s/__CORE_RPC_PORT__/${__CORE_RPC_PORT__}/g" mempool-config.json
|
sed -i "s/__CORE_RPC_PORT__/${__CORE_RPC_PORT__}/g" mempool-config.json
|
||||||
@@ -132,6 +153,7 @@ sed -i "s/__BISQ_ENABLED__/${__BISQ_ENABLED__}/g" mempool-config.json
|
|||||||
sed -i "s!__BISQ_DATA_PATH__!${__BISQ_DATA_PATH__}!g" mempool-config.json
|
sed -i "s!__BISQ_DATA_PATH__!${__BISQ_DATA_PATH__}!g" mempool-config.json
|
||||||
|
|
||||||
sed -i "s/__SOCKS5PROXY_ENABLED__/${__SOCKS5PROXY_ENABLED__}/g" mempool-config.json
|
sed -i "s/__SOCKS5PROXY_ENABLED__/${__SOCKS5PROXY_ENABLED__}/g" mempool-config.json
|
||||||
|
sed -i "s/__SOCKS5PROXY_USE_ONION__/${__SOCKS5PROXY_USE_ONION__}/g" mempool-config.json
|
||||||
sed -i "s/__SOCKS5PROXY_HOST__/${__SOCKS5PROXY_HOST__}/g" mempool-config.json
|
sed -i "s/__SOCKS5PROXY_HOST__/${__SOCKS5PROXY_HOST__}/g" mempool-config.json
|
||||||
sed -i "s/__SOCKS5PROXY_PORT__/${__SOCKS5PROXY_PORT__}/g" mempool-config.json
|
sed -i "s/__SOCKS5PROXY_PORT__/${__SOCKS5PROXY_PORT__}/g" mempool-config.json
|
||||||
sed -i "s/__SOCKS5PROXY_USERNAME__/${__SOCKS5PROXY_USERNAME__}/g" mempool-config.json
|
sed -i "s/__SOCKS5PROXY_USERNAME__/${__SOCKS5PROXY_USERNAME__}/g" mempool-config.json
|
||||||
@@ -140,4 +162,11 @@ sed -i "s/__SOCKS5PROXY_PASSWORD__/${__SOCKS5PROXY_PASSWORD__}/g" mempool-config
|
|||||||
sed -i "s!__PRICE_DATA_SERVER_TOR_URL__!${__PRICE_DATA_SERVER_TOR_URL__}!g" mempool-config.json
|
sed -i "s!__PRICE_DATA_SERVER_TOR_URL__!${__PRICE_DATA_SERVER_TOR_URL__}!g" mempool-config.json
|
||||||
sed -i "s!__PRICE_DATA_SERVER_CLEARNET_URL__!${__PRICE_DATA_SERVER_CLEARNET_URL__}!g" mempool-config.json
|
sed -i "s!__PRICE_DATA_SERVER_CLEARNET_URL__!${__PRICE_DATA_SERVER_CLEARNET_URL__}!g" mempool-config.json
|
||||||
|
|
||||||
|
sed -i "s!__EXTERNAL_DATA_SERVER_MEMPOOL_API__!${__EXTERNAL_DATA_SERVER_MEMPOOL_API__}!g" mempool-config.json
|
||||||
|
sed -i "s!__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__!${__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__}!g" mempool-config.json
|
||||||
|
sed -i "s!__EXTERNAL_DATA_SERVER_LIQUID_API__!${__EXTERNAL_DATA_SERVER_LIQUID_API__}!g" mempool-config.json
|
||||||
|
sed -i "s!__EXTERNAL_DATA_SERVER_LIQUID_ONION__!${__EXTERNAL_DATA_SERVER_LIQUID_ONION__}!g" mempool-config.json
|
||||||
|
sed -i "s!__EXTERNAL_DATA_SERVER_BISQ_URL__!${__EXTERNAL_DATA_SERVER_BISQ_URL__}!g" mempool-config.json
|
||||||
|
sed -i "s!__EXTERNAL_DATA_SERVER_BISQ_ONION__!${__EXTERNAL_DATA_SERVER_BISQ_ONION__}!g" mempool-config.json
|
||||||
|
|
||||||
node /backend/dist/index.js
|
node /backend/dist/index.js
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:16.15.0-buster-slim AS builder
|
FROM node:16.16.0-buster-slim AS builder
|
||||||
|
|
||||||
ARG commitHash
|
ARG commitHash
|
||||||
ENV DOCKER_COMMIT_HASH=${commitHash}
|
ENV DOCKER_COMMIT_HASH=${commitHash}
|
||||||
@@ -8,7 +8,7 @@ WORKDIR /build
|
|||||||
COPY . .
|
COPY . .
|
||||||
RUN apt-get update
|
RUN apt-get update
|
||||||
RUN apt-get install -y build-essential rsync
|
RUN apt-get install -y build-essential rsync
|
||||||
RUN npm i
|
RUN npm install --omit=dev --omit=optional
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM nginx:1.17.8-alpine
|
FROM nginx:1.17.8-alpine
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ indent_size = 2
|
|||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.ts]
|
||||||
|
quote_type = single
|
||||||
|
|
||||||
[*.md]
|
[*.md]
|
||||||
max_line_length = off
|
max_line_length = off
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
|||||||
3
frontend/.eslintignore
Normal file
3
frontend/.eslintignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
frontend
|
||||||
34
frontend/.eslintrc
Normal file
34
frontend/.eslintrc
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"plugins": [
|
||||||
|
"@typescript-eslint"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/ban-ts-comment": 1,
|
||||||
|
"@typescript-eslint/ban-types": 1,
|
||||||
|
"@typescript-eslint/no-empty-function": 1,
|
||||||
|
"@typescript-eslint/no-explicit-any": 1,
|
||||||
|
"@typescript-eslint/no-inferrable-types": 1,
|
||||||
|
"@typescript-eslint/no-namespace": 1,
|
||||||
|
"@typescript-eslint/no-this-alias": 1,
|
||||||
|
"@typescript-eslint/no-var-requires": 1,
|
||||||
|
"no-case-declarations": 1,
|
||||||
|
"no-console": 1,
|
||||||
|
"no-constant-condition": 1,
|
||||||
|
"no-dupe-else-if": 1,
|
||||||
|
"no-empty": 1,
|
||||||
|
"no-extra-boolean-cast": 1,
|
||||||
|
"no-prototype-builtins": 1,
|
||||||
|
"no-self-assign": 1,
|
||||||
|
"no-useless-catch": 1,
|
||||||
|
"no-var": 1,
|
||||||
|
"prefer-const": 1,
|
||||||
|
"prefer-rest-params": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
2
frontend/.prettierignore
Normal file
2
frontend/.prettierignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
package-lock.json
|
||||||
6
frontend/.prettierrc
Normal file
6
frontend/.prettierrc
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"printWidth": 80,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5"
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ $ npm run config:defaults:bisq
|
|||||||
|
|
||||||
### 3. Run the Frontend
|
### 3. Run the Frontend
|
||||||
|
|
||||||
_Make sure to use Node.js 16.15 and npm 7._
|
_Make sure to use Node.js 16.10 and npm 7._
|
||||||
|
|
||||||
Install project dependencies and run the frontend server:
|
Install project dependencies and run the frontend server:
|
||||||
|
|
||||||
@@ -71,13 +71,13 @@ Set up the [Mempool backend](../backend/) first, if you haven't already.
|
|||||||
|
|
||||||
### 1. Build the Frontend
|
### 1. Build the Frontend
|
||||||
|
|
||||||
_Node.js 16 and npm 7 are recommended._
|
_Make sure to use Node.js 16.10 and npm 7._
|
||||||
|
|
||||||
Build the frontend:
|
Build the frontend:
|
||||||
|
|
||||||
```
|
```
|
||||||
cd frontend
|
cd frontend
|
||||||
npm install # add --prod for production
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -248,23 +248,6 @@
|
|||||||
"browserTarget": "mempool:build"
|
"browserTarget": "mempool:build"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"test": {
|
|
||||||
"builder": "@angular-devkit/build-angular:karma",
|
|
||||||
"options": {
|
|
||||||
"main": "src/test.ts",
|
|
||||||
"polyfills": "src/polyfills.ts",
|
|
||||||
"tsConfig": "tsconfig.spec.json",
|
|
||||||
"karmaConfig": "karma.conf.js",
|
|
||||||
"assets": [
|
|
||||||
"src/favicon.ico",
|
|
||||||
"src/resources"
|
|
||||||
],
|
|
||||||
"styles": [
|
|
||||||
"src/styles.scss"
|
|
||||||
],
|
|
||||||
"scripts": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"e2e": {
|
"e2e": {
|
||||||
"builder": "@cypress/schematic:cypress",
|
"builder": "@cypress/schematic:cypress",
|
||||||
"options": {
|
"options": {
|
||||||
|
|||||||
23
frontend/cypress.config.ts
Normal file
23
frontend/cypress.config.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { defineConfig } from 'cypress'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
projectId: 'ry4br7',
|
||||||
|
videosFolder: 'cypress/videos',
|
||||||
|
screenshotsFolder: 'cypress/screenshots',
|
||||||
|
fixturesFolder: 'cypress/fixtures',
|
||||||
|
video: false,
|
||||||
|
retries: {
|
||||||
|
runMode: 3,
|
||||||
|
openMode: 0,
|
||||||
|
},
|
||||||
|
chromeWebSecurity: false,
|
||||||
|
e2e: {
|
||||||
|
// We've imported your old cypress plugins here.
|
||||||
|
// You may want to clean this up later by importing these.
|
||||||
|
setupNodeEvents(on, config) {
|
||||||
|
return require('./cypress/plugins/index.js')(on, config)
|
||||||
|
},
|
||||||
|
baseUrl: 'http://localhost:4200',
|
||||||
|
specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"projectId": "ry4br7",
|
|
||||||
"integrationFolder": "cypress/integration",
|
|
||||||
"supportFile": "cypress/support/index.ts",
|
|
||||||
"videosFolder": "cypress/videos",
|
|
||||||
"screenshotsFolder": "cypress/screenshots",
|
|
||||||
"pluginsFile": "cypress/plugins/index.js",
|
|
||||||
"fixturesFolder": "cypress/fixtures",
|
|
||||||
"baseUrl": "http://localhost:4200",
|
|
||||||
"video": false,
|
|
||||||
"retries": {
|
|
||||||
"runMode": 3,
|
|
||||||
"openMode": 0
|
|
||||||
},
|
|
||||||
"chromeWebSecurity": false
|
|
||||||
}
|
|
||||||
@@ -35,13 +35,14 @@ describe('Bisq', () => {
|
|||||||
"Proposal", "Reimbursement request", "Transfer BSQ", "Unlock", "Vote reveal"
|
"Proposal", "Reimbursement request", "Transfer BSQ", "Unlock", "Vote reveal"
|
||||||
];
|
];
|
||||||
filters.forEach((filter) => {
|
filters.forEach((filter) => {
|
||||||
it(`filters the transaction screen by ${filter}`, () => {
|
it.only(`filters the transaction screen by ${filter}`, () => {
|
||||||
cy.visit(`${basePath}/transactions`);
|
cy.visit(`${basePath}/transactions`);
|
||||||
|
cy.wait('@txs');
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.get('#filter').click();
|
cy.get('#filter').click();
|
||||||
cy.contains(filter).find('input').click();
|
cy.contains(filter).find('input').click();
|
||||||
//TODO: change this waiter
|
cy.wait('@txs');
|
||||||
cy.wait(1000);
|
cy.wait(500);
|
||||||
cy.get('td:nth-of-type(2)').each(($td) => {
|
cy.get('td:nth-of-type(2)').each(($td) => {
|
||||||
expect($td.text().trim()).to.eq(filter);
|
expect($td.text().trim()).to.eq(filter);
|
||||||
});
|
});
|
||||||
@@ -56,7 +57,7 @@ describe('Bisq', () => {
|
|||||||
filters.forEach((filter) => {
|
filters.forEach((filter) => {
|
||||||
cy.contains(filter).find('input').click();
|
cy.contains(filter).find('input').click();
|
||||||
//TODO: change this waiter
|
//TODO: change this waiter
|
||||||
cy.wait(1000);
|
cy.wait(1500);
|
||||||
});
|
});
|
||||||
cy.get('td:nth-of-type(2)').each(($td) => {
|
cy.get('td:nth-of-type(2)').each(($td) => {
|
||||||
const regex = new RegExp(`${filters.join('|')}`, 'g');
|
const regex = new RegExp(`${filters.join('|')}`, 'g');
|
||||||
@@ -124,7 +124,7 @@ describe('Liquid', () => {
|
|||||||
cy.visit(`${basePath}/assets`);
|
cy.visit(`${basePath}/assets`);
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.get('.container-xl input').click().type('Liquid Bitcoin').then(() => {
|
cy.get('.container-xl input').click().type('Liquid Bitcoin').then(() => {
|
||||||
cy.get('ngb-typeahead-window').should('have.length', 1);
|
cy.get('ngb-typeahead-window', { timeout: 30000 }).should('have.length', 1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@ describe('Liquid', () => {
|
|||||||
cy.visit(`${basePath}/assets`);
|
cy.visit(`${basePath}/assets`);
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.get('.container-xl input').click().type('Liquid AUD').then(() => {
|
cy.get('.container-xl input').click().type('Liquid AUD').then(() => {
|
||||||
cy.get('ngb-typeahead-window:nth-of-type(1) button').click();
|
cy.get('ngb-typeahead-window:nth-of-type(1) button', { timeout: 30000 }).click();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -189,7 +189,7 @@ describe('Mainnet', () => {
|
|||||||
cy.get('[data-cy="tx-2"] .table-tx-vin .highlight').invoke('text').should('contain', `${address}`);
|
cy.get('[data-cy="tx-2"] .table-tx-vin .highlight').invoke('text').should('contain', `${address}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.only('highlights both input and output addresses in the same transaction', () => {
|
it('highlights both input and output addresses in the same transaction', () => {
|
||||||
const address = 'bc1q03u63r6hm7a3v6em58zdqtp446w2pw30nm63mv';
|
const address = 'bc1q03u63r6hm7a3v6em58zdqtp446w2pw30nm63mv';
|
||||||
cy.visit(`/address/${address}`);
|
cy.visit(`/address/${address}`);
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
@@ -241,7 +241,7 @@ describe('Mainnet', () => {
|
|||||||
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('not.exist');
|
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('not.exist');
|
||||||
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
cy.document().left();
|
cy.document().left();
|
||||||
cy.get('.title-block h1').invoke('text').should('equal', 'Next block');
|
cy.get('.title-block h1').invoke('text').should('equal', 'Next Block');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
171
frontend/cypress/e2e/mainnet/mining.spec.ts
Normal file
171
frontend/cypress/e2e/mainnet/mining.spec.ts
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
const baseModule = Cypress.env("BASE_MODULE");
|
||||||
|
|
||||||
|
describe('Mainnet - Mining Features', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
//https://github.com/cypress-io/cypress/issues/14459
|
||||||
|
if (Cypress.browser.family === 'chromium') {
|
||||||
|
Cypress.automation('remote:debugger:protocol', {
|
||||||
|
command: 'Network.enable',
|
||||||
|
params: {}
|
||||||
|
});
|
||||||
|
Cypress.automation('remote:debugger:protocol', {
|
||||||
|
command: 'Network.setCacheDisabled',
|
||||||
|
params: { cacheDisabled: true }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (baseModule === 'mempool') {
|
||||||
|
|
||||||
|
describe('Miner page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.intercept('/api/v1/mining/pool/**').as('pool');
|
||||||
|
cy.intercept('/api/v1/mining/hashrate/pools/**').as('hashrate');
|
||||||
|
cy.intercept('/api/tx/**').as('tx');
|
||||||
|
cy.intercept('/api/v1/outpends/**').as('outspends');
|
||||||
|
});
|
||||||
|
it('loads the mining pool page from the dashboard', () => {
|
||||||
|
cy.visit('/mining');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('[data-cy="bitcoin-block-0-pool"]').click().then(() => {
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.wait('@pool');
|
||||||
|
cy.url().should('match', /\/mining\/pool\/(\w+)/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the mining pool page from the blocks page', () => {
|
||||||
|
cy.visit('/mining');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('[data-cy="bitcoin-block-0-height"]').click().then(() => {
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('[data-cy="block-details-miner-badge"]').click().then(() => {
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.wait('@pool');
|
||||||
|
cy.url().should('match', /\/mining\/pool\/(\w+)/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Mining Dashboard Landing page widgets', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/mining');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the mempool blocks', () => {
|
||||||
|
cy.get('[data-cy="mempool-block-0-fees"]').invoke('text').should('match', /~(.*) sat\/vB/);
|
||||||
|
cy.get('[data-cy="mempool-block-0-fee-span"]').invoke('text').should('match', /(.*) - (.*) sat\/vB/);
|
||||||
|
cy.get('[data-cy="mempool-block-0-total-fees"]').invoke('text').should('match', /(.*) BTC/);
|
||||||
|
cy.get('[data-cy="mempool-block-0-transaction-count"]').invoke('text').should('match', /(.*) transactions/);
|
||||||
|
cy.get('[data-cy="mempool-block-0-time"]').invoke('text').should('match', /In ~(.*) minutes/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the mined blocks', () => {
|
||||||
|
cy.get('[data-cy="bitcoin-block-0-height"]').invoke('text').should('match', /(\d)/);
|
||||||
|
cy.get('[data-cy="bitcoin-block-0-fees"]').invoke('text').should('match', /~(.*) sat\/vB/);
|
||||||
|
cy.get('[data-cy="bitcoin-block-0-fee-span"]').invoke('text').should('match', /(.*) - (.*) sat\/vB/);
|
||||||
|
cy.get('[data-cy="bitcoin-block-0-total-fees"]').invoke('text').should('match', /(.*) BTC/);
|
||||||
|
cy.get('[data-cy="bitcoin-block-0-transactions"]').invoke('text').should('match', /(.*) transactions/);
|
||||||
|
cy.get('[data-cy="bitcoin-block-0-time"]').invoke('text').should('match', /((.*) ago|Just now)/);
|
||||||
|
cy.get('[data-cy="bitcoin-block-0-pool"]').invoke('text').should('match', /(\w)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the reward stats for the last 144 blocks', () => {
|
||||||
|
cy.get('[data-cy="reward-stats"]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the difficulty adjustment stats', () => {
|
||||||
|
cy.get('[data-cy="difficulty-adjustment"]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the latest blocks', () => {
|
||||||
|
cy.get('[data-cy="latest-blocks"]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the pools pie chart', () => {
|
||||||
|
cy.get('[data-cy="pool-distribution"]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the hashrate graph', () => {
|
||||||
|
cy.get('[data-cy="hashrate-graph"]');
|
||||||
|
});
|
||||||
|
it('shows the latest blocks', () => {
|
||||||
|
cy.get('[data-cy="latest-blocks"]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the latest adjustments', () => {
|
||||||
|
cy.get('[data-cy="difficulty-adjustments-table"]');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.only('mining graphs', () => {
|
||||||
|
describe('pools ranking', () => {
|
||||||
|
it('loads the graph', () => {
|
||||||
|
cy.visit('/graphs/mining/pools');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
cy.get('.spinner-border').should('not.exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('pools dominance', () => {
|
||||||
|
it('loads the graph', () => {
|
||||||
|
cy.visit('/graphs/mining/pools-dominance');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
cy.get('.spinner-border').should('not.exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hashrate & difficulty', () => {
|
||||||
|
it('loads the graph', () => {
|
||||||
|
cy.visit('/graphs/mining/hashrate-difficulty');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
cy.get('.spinner-border').should('not.exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('block fee rates', () => {
|
||||||
|
it('loads the graph', () => {
|
||||||
|
cy.visit('/graphs/mining/block-fee-rates');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
cy.get('.spinner-border').should('not.exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('block fees', () => {
|
||||||
|
it('loads the graph', () => {
|
||||||
|
cy.visit('/graphs/mining/block-fees');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
cy.get('.spinner-border').should('not.exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('block rewards', () => {
|
||||||
|
it('loads the graph', () => {
|
||||||
|
cy.visit('/graphs/mining/block-rewards');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
cy.get('.spinner-border').should('not.exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('block sizes and weights', () => {
|
||||||
|
it('loads the graph', () => {
|
||||||
|
cy.visit('/graphs/mining/block-sizes-weights');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
cy.get('.spinner-border').should('not.exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
it.skip(`Tests cannot be run on the selected BASE_MODULE ${baseModule}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -50,98 +50,98 @@ import { mockWebSocket } from './websocket';
|
|||||||
|
|
||||||
/* global Cypress */
|
/* global Cypress */
|
||||||
const codes = {
|
const codes = {
|
||||||
ArrowLeft: 37,
|
ArrowLeft: 37,
|
||||||
ArrowUp: 38,
|
ArrowUp: 38,
|
||||||
ArrowRight: 39,
|
ArrowRight: 39,
|
||||||
ArrowDown: 40
|
ArrowDown: 40
|
||||||
}
|
}
|
||||||
|
|
||||||
Cypress.Commands.add('waitForSkeletonGone', () => {
|
Cypress.Commands.add('waitForSkeletonGone', () => {
|
||||||
cy.waitUntil(() => {
|
cy.waitUntil(() => {
|
||||||
return Cypress.$('.skeleton-loader').length === 0;
|
return Cypress.$('.skeleton-loader').length === 0;
|
||||||
}, { verbose: true, description: "waitForSkeletonGone", errorMsg: "skeleton loaders never went away", timeout: 15000, interval: 50});
|
}, { verbose: true, description: "waitForSkeletonGone", errorMsg: "skeleton loaders never went away", timeout: 15000, interval: 50 });
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add(
|
Cypress.Commands.add(
|
||||||
"waitForPageIdle",
|
"waitForPageIdle",
|
||||||
() => {
|
() => {
|
||||||
console.warn("Waiting for page idle state");
|
console.warn("Waiting for page idle state");
|
||||||
const pageIdleDetector = new PageIdleDetector();
|
const pageIdleDetector = new PageIdleDetector();
|
||||||
pageIdleDetector.WaitForPageToBeIdle();
|
pageIdleDetector.WaitForPageToBeIdle();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
Cypress.Commands.add('mockMempoolSocket', () => {
|
Cypress.Commands.add('mockMempoolSocket', () => {
|
||||||
mockWebSocket();
|
mockWebSocket();
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add('changeNetwork', (network: "testnet"|"signet"|"liquid"|"bisq"|"mainnet" ) => {
|
Cypress.Commands.add('changeNetwork', (network: "testnet" | "signet" | "liquid" | "bisq" | "mainnet") => {
|
||||||
cy.get('.dropdown-toggle').click().then(() => {
|
cy.get('.dropdown-toggle').click().then(() => {
|
||||||
cy.get(`.${network}`).click().then(() => {
|
cy.get(`a.${network}`).click().then(() => {
|
||||||
cy.waitForPageIdle();
|
cy.waitForPageIdle();
|
||||||
if(network !== 'bisq'){
|
if (network !== 'bisq') {
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// https://github.com/bahmutov/cypress-arrows/blob/8f0303842a343550fbeaf01528d01d1ff213b70c/src/index.js
|
// https://github.com/bahmutov/cypress-arrows/blob/8f0303842a343550fbeaf01528d01d1ff213b70c/src/index.js
|
||||||
function keydownCommand ($el, key) {
|
function keydownCommand($el, key) {
|
||||||
const message = `sending the "${key}" keydown event`
|
const message = `sending the "${key}" keydown event`
|
||||||
const log = Cypress.log({
|
const log = Cypress.log({
|
||||||
name: `keydown: ${key}`,
|
name: `keydown: ${key}`,
|
||||||
message: message,
|
message: message,
|
||||||
consoleProps: function () {
|
consoleProps: function () {
|
||||||
return {
|
return {
|
||||||
Subject: $el
|
Subject: $el
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
})
|
||||||
const e = $el.createEvent('KeyboardEvent')
|
|
||||||
|
const e = $el.createEvent('KeyboardEvent')
|
||||||
Object.defineProperty(e, 'key', {
|
|
||||||
get: function () {
|
Object.defineProperty(e, 'key', {
|
||||||
return key
|
get: function () {
|
||||||
}
|
return key
|
||||||
})
|
}
|
||||||
|
})
|
||||||
Object.defineProperty(e, 'keyCode', {
|
|
||||||
get: function () {
|
Object.defineProperty(e, 'keyCode', {
|
||||||
return this.keyCodeVal
|
get: function () {
|
||||||
}
|
return this.keyCodeVal
|
||||||
})
|
}
|
||||||
Object.defineProperty(e, 'which', {
|
})
|
||||||
get: function () {
|
Object.defineProperty(e, 'which', {
|
||||||
return this.keyCodeVal
|
get: function () {
|
||||||
}
|
return this.keyCodeVal
|
||||||
})
|
}
|
||||||
var metaKey = false
|
})
|
||||||
|
var metaKey = false
|
||||||
Object.defineProperty(e, 'metaKey', {
|
|
||||||
get: function () {
|
Object.defineProperty(e, 'metaKey', {
|
||||||
return metaKey
|
get: function () {
|
||||||
}
|
return metaKey
|
||||||
})
|
}
|
||||||
|
})
|
||||||
Object.defineProperty(e, 'shiftKey', {
|
|
||||||
get: function () {
|
Object.defineProperty(e, 'shiftKey', {
|
||||||
return false
|
get: function () {
|
||||||
}
|
return false
|
||||||
})
|
}
|
||||||
e.keyCodeVal = codes[key]
|
})
|
||||||
|
e.keyCodeVal = codes[key]
|
||||||
e.initKeyboardEvent('keydown', true, true,
|
|
||||||
$el.defaultView, false, false, false, false, e.keyCodeVal, e.keyCodeVal)
|
e.initKeyboardEvent('keydown', true, true,
|
||||||
|
$el.defaultView, false, false, false, false, e.keyCodeVal, e.keyCodeVal)
|
||||||
$el.dispatchEvent(e)
|
|
||||||
log.snapshot().end()
|
$el.dispatchEvent(e)
|
||||||
return $el
|
log.snapshot().end()
|
||||||
}
|
return $el
|
||||||
|
}
|
||||||
Cypress.Commands.add('keydown', { prevSubject: "dom" }, keydownCommand)
|
|
||||||
Cypress.Commands.add('left', { prevSubject: "dom" }, $el => keydownCommand($el, 'ArrowLeft'))
|
Cypress.Commands.add('keydown', { prevSubject: "dom" }, keydownCommand)
|
||||||
Cypress.Commands.add('right', { prevSubject: "dom" }, $el => keydownCommand($el, 'ArrowRight'))
|
Cypress.Commands.add('left', { prevSubject: "dom" }, $el => keydownCommand($el, 'ArrowLeft'))
|
||||||
Cypress.Commands.add('up', { prevSubject: "dom" }, $el => keydownCommand($el, 'ArrowUp'))
|
Cypress.Commands.add('right', { prevSubject: "dom" }, $el => keydownCommand($el, 'ArrowRight'))
|
||||||
Cypress.Commands.add('down', { prevSubject: "dom" }, $el => keydownCommand($el, 'ArrowDown'))
|
Cypress.Commands.add('up', { prevSubject: "dom" }, $el => keydownCommand($el, 'ArrowUp'))
|
||||||
|
Cypress.Commands.add('down', { prevSubject: "dom" }, $el => keydownCommand($el, 'ArrowDown'))
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
// Karma configuration file, see link for more information
|
|
||||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
|
||||||
|
|
||||||
module.exports = function (config) {
|
|
||||||
config.set({
|
|
||||||
basePath: '',
|
|
||||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
|
||||||
plugins: [
|
|
||||||
require('karma-jasmine'),
|
|
||||||
require('karma-chrome-launcher'),
|
|
||||||
require('karma-jasmine-html-reporter'),
|
|
||||||
require('karma-coverage-istanbul-reporter'),
|
|
||||||
require('@angular-devkit/build-angular/plugins/karma')
|
|
||||||
],
|
|
||||||
client: {
|
|
||||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
|
||||||
},
|
|
||||||
coverageIstanbulReporter: {
|
|
||||||
dir: require('path').join(__dirname, './coverage/mempool'),
|
|
||||||
reports: ['html', 'lcovonly', 'text-summary'],
|
|
||||||
fixWebpackSourcePaths: true
|
|
||||||
},
|
|
||||||
reporters: ['progress', 'kjhtml'],
|
|
||||||
port: 9876,
|
|
||||||
colors: true,
|
|
||||||
logLevel: config.LOG_INFO,
|
|
||||||
autoWatch: true,
|
|
||||||
browsers: ['Chrome'],
|
|
||||||
singleRun: false,
|
|
||||||
restartOnFileChange: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
4533
frontend/package-lock.json
generated
4533
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mempool-frontend",
|
"name": "mempool-frontend",
|
||||||
"version": "2.4.0-dev",
|
"version": "2.4.2-dev",
|
||||||
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
||||||
"license": "GNU Affero General Public License v3.0",
|
"license": "GNU Affero General Public License v3.0",
|
||||||
"homepage": "https://mempool.space",
|
"homepage": "https://mempool.space",
|
||||||
@@ -20,20 +20,20 @@
|
|||||||
],
|
],
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "./node_modules/@angular/cli/bin/ng",
|
"ng": "./node_modules/@angular/cli/bin/ng.js",
|
||||||
"tsc": "./node_modules/typescript/bin/tsc",
|
"tsc": "./node_modules/typescript/bin/tsc",
|
||||||
"i18n-extract-from-source": "./node_modules/@angular/cli/bin/ng extract-i18n --out-file ./src/locale/messages.xlf",
|
"i18n-extract-from-source": "./node_modules/@angular/cli/bin/ng extract-i18n --out-file ./src/locale/messages.xlf",
|
||||||
"i18n-pull-from-transifex": "tx pull -a --parallel --minimum-perc 1 --force",
|
"i18n-pull-from-transifex": "tx pull -a --parallel --minimum-perc 1 --force",
|
||||||
"serve": "npm run generate-config && ng serve -c local",
|
"serve": "npm run generate-config && npm run ng -- serve -c local",
|
||||||
"serve:stg": "npm run generate-config && ng serve -c staging",
|
"serve:stg": "npm run generate-config && npm run ng -- serve -c staging",
|
||||||
"serve:local-prod": "npm run generate-config && ng serve -c local-prod",
|
"serve:local-prod": "npm run generate-config && npm run ng -- serve -c local-prod",
|
||||||
"serve:local-staging": "npm run generate-config && ng serve -c local-staging",
|
"serve:local-staging": "npm run generate-config && npm run ng -- serve -c local-staging",
|
||||||
"start": "npm run generate-config && npm run sync-assets-dev && ng serve -c local",
|
"start": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local",
|
||||||
"start:stg": "npm run generate-config && npm run sync-assets-dev && ng serve -c staging",
|
"start:stg": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c staging",
|
||||||
"start:local-prod": "npm run generate-config && npm run sync-assets-dev && ng serve -c local-prod",
|
"start:local-prod": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-prod",
|
||||||
"start:local-staging": "npm run generate-config && npm run sync-assets-dev && ng serve -c local-staging",
|
"start:local-staging": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-staging",
|
||||||
"start:mixed": "npm run generate-config && npm run sync-assets-dev && ng serve -c mixed",
|
"start:mixed": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c mixed",
|
||||||
"build": "npm run generate-config && ng build --configuration production --localize && npm run sync-assets && npm run build-mempool.js",
|
"build": "npm run generate-config && npm run ng -- build --configuration production --localize && npm run sync-assets && npm run build-mempool.js",
|
||||||
"sync-assets": "node sync-assets.js && rsync -av ./dist/mempool/browser/en-US/resources ./dist/mempool/browser/resources",
|
"sync-assets": "node sync-assets.js && rsync -av ./dist/mempool/browser/en-US/resources ./dist/mempool/browser/resources",
|
||||||
"sync-assets-dev": "node sync-assets.js dev",
|
"sync-assets-dev": "node sync-assets.js dev",
|
||||||
"generate-config": "node generate-config.js",
|
"generate-config": "node generate-config.js",
|
||||||
@@ -41,17 +41,19 @@
|
|||||||
"build-mempool-js": "browserify -p tinyify ./node_modules/@mempool/mempool.js/lib/index.js --standalone mempoolJS > ./dist/mempool/browser/en-US/mempool.js",
|
"build-mempool-js": "browserify -p tinyify ./node_modules/@mempool/mempool.js/lib/index.js --standalone mempoolJS > ./dist/mempool/browser/en-US/mempool.js",
|
||||||
"build-mempool-bisq-js": "browserify -p tinyify ./node_modules/@mempool/mempool.js/lib/index-bisq.js --standalone bisqJS > ./dist/mempool/browser/en-US/bisq.js",
|
"build-mempool-bisq-js": "browserify -p tinyify ./node_modules/@mempool/mempool.js/lib/index-bisq.js --standalone bisqJS > ./dist/mempool/browser/en-US/bisq.js",
|
||||||
"build-mempool-liquid-js": "browserify -p tinyify ./node_modules/@mempool/mempool.js/lib/index-liquid.js --standalone liquidJS > ./dist/mempool/browser/en-US/liquid.js",
|
"build-mempool-liquid-js": "browserify -p tinyify ./node_modules/@mempool/mempool.js/lib/index-liquid.js --standalone liquidJS > ./dist/mempool/browser/en-US/liquid.js",
|
||||||
"test": "ng test",
|
"test": "npm run ng -- test",
|
||||||
"lint": "ng lint",
|
"lint": "./node_modules/.bin/eslint . --ext .ts",
|
||||||
"e2e": "npm run generate-config && ng e2e",
|
"lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix",
|
||||||
|
"prettier": "prettier --write \"src/app/**/*.{js,json,css,scss,less,md,ts,html,component.html}\"",
|
||||||
|
"e2e": "npm run generate-config && npm run ng -- e2e",
|
||||||
"e2e:ci": "npm run cypress:run:ci",
|
"e2e:ci": "npm run cypress:run:ci",
|
||||||
"config:defaults:mempool": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=mempool BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
|
"config:defaults:mempool": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=mempool BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
|
||||||
"config:defaults:liquid": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=liquid BLOCK_WEIGHT_UNITS=300000 && npm run generate-config",
|
"config:defaults:liquid": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=liquid BLOCK_WEIGHT_UNITS=300000 && npm run generate-config",
|
||||||
"config:defaults:bisq": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=bisq BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
|
"config:defaults:bisq": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=bisq BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
|
||||||
"dev:ssr": "npm run generate-config && ng run mempool:serve-ssr",
|
"dev:ssr": "npm run generate-config && npm run ng -- run mempool:serve-ssr",
|
||||||
"serve:ssr": "node server.run.js",
|
"serve:ssr": "node server.run.js",
|
||||||
"build:ssr": "npm run build && ng run mempool:server:production && ./node_modules/typescript/bin/tsc server.run.ts",
|
"build:ssr": "npm run build && npm run ng -- run mempool:server:production && npm run tsc -- server.run.ts",
|
||||||
"prerender": "ng run mempool:prerender",
|
"prerender": "npm run ng -- run mempool:prerender",
|
||||||
"cypress:open": "cypress open",
|
"cypress:open": "cypress open",
|
||||||
"cypress:run": "cypress run",
|
"cypress:run": "cypress run",
|
||||||
"cypress:run:record": "cypress run --record",
|
"cypress:run:record": "cypress run --record",
|
||||||
@@ -77,7 +79,6 @@
|
|||||||
"@fortawesome/fontawesome-common-types": "~6.1.1",
|
"@fortawesome/fontawesome-common-types": "~6.1.1",
|
||||||
"@fortawesome/fontawesome-svg-core": "~6.1.1",
|
"@fortawesome/fontawesome-svg-core": "~6.1.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "~6.1.1",
|
"@fortawesome/free-solid-svg-icons": "~6.1.1",
|
||||||
"@juggle/resize-observer": "^3.3.1",
|
|
||||||
"@mempool/mempool.js": "2.3.0",
|
"@mempool/mempool.js": "2.3.0",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^11.0.0",
|
"@ng-bootstrap/ng-bootstrap": "^11.0.0",
|
||||||
"@nguniversal/express-engine": "~13.1.1",
|
"@nguniversal/express-engine": "~13.1.1",
|
||||||
@@ -88,8 +89,7 @@
|
|||||||
"domino": "^2.1.6",
|
"domino": "^2.1.6",
|
||||||
"echarts": "~5.3.2",
|
"echarts": "~5.3.2",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"lightweight-charts": "^3.3.0",
|
"lightweight-charts": "~3.8.0",
|
||||||
"ngx-bootrap-multiselect": "^2.0.0",
|
|
||||||
"ngx-echarts": "8.0.1",
|
"ngx-echarts": "8.0.1",
|
||||||
"ngx-infinite-scroll": "^10.0.1",
|
"ngx-infinite-scroll": "^10.0.1",
|
||||||
"qrcode": "1.5.0",
|
"qrcode": "1.5.0",
|
||||||
@@ -104,28 +104,24 @@
|
|||||||
"@angular/language-service": "~13.3.10",
|
"@angular/language-service": "~13.3.10",
|
||||||
"@nguniversal/builders": "~13.1.1",
|
"@nguniversal/builders": "~13.1.1",
|
||||||
"@types/express": "^4.17.0",
|
"@types/express": "^4.17.0",
|
||||||
"@types/jasmine": "~4.0.3",
|
|
||||||
"@types/jasminewd2": "~2.0.10",
|
|
||||||
"@types/node": "^12.11.1",
|
"@types/node": "^12.11.1",
|
||||||
"codelyzer": "~6.0.2",
|
"@typescript-eslint/eslint-plugin": "^5.30.5",
|
||||||
"http-proxy-middleware": "^1.0.5",
|
"@typescript-eslint/parser": "^5.30.5",
|
||||||
"jasmine-core": "~4.1.0",
|
"eslint": "^8.19.0",
|
||||||
"jasmine-spec-reporter": "~7.0.0",
|
"http-proxy-middleware": "~2.0.6",
|
||||||
"karma": "~6.3.19",
|
"prettier": "^2.7.1",
|
||||||
"karma-chrome-launcher": "~3.1.0",
|
"ts-node": "~10.8.1",
|
||||||
"karma-coverage": "~2.0.3",
|
|
||||||
"karma-jasmine": "~5.0.0",
|
|
||||||
"karma-jasmine-html-reporter": "^1.5.0",
|
|
||||||
"ts-node": "~8.3.0",
|
|
||||||
"tslint": "~6.1.0",
|
|
||||||
"typescript": "~4.6.4"
|
"typescript": "~4.6.4"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@cypress/schematic": "^1.3.0",
|
"@cypress/schematic": "~2.0.0",
|
||||||
"cypress": "^9.6.1",
|
"cypress": "^10.3.0",
|
||||||
"cypress-fail-on-console-error": "^2.1.3",
|
"cypress-fail-on-console-error": "~3.0.0",
|
||||||
"cypress-wait-until": "^1.7.1",
|
"cypress-wait-until": "^1.7.2",
|
||||||
"mock-socket": "^9.0.3",
|
"mock-socket": "~9.1.4",
|
||||||
"start-server-and-test": "^1.12.6"
|
"start-server-and-test": "~1.14.0"
|
||||||
|
},
|
||||||
|
"scarfSettings": {
|
||||||
|
"enabled": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ let routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'block',
|
path: 'block',
|
||||||
component: StartComponent,
|
component: StartComponent,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: ':id',
|
path: ':id',
|
||||||
component: BlockComponent
|
component: BlockComponent
|
||||||
@@ -258,7 +258,7 @@ let routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'block',
|
path: 'block',
|
||||||
component: StartComponent,
|
component: StartComponent,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: ':id',
|
path: ':id',
|
||||||
component: BlockComponent
|
component: BlockComponent
|
||||||
@@ -361,7 +361,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
|||||||
{
|
{
|
||||||
path: 'block',
|
path: 'block',
|
||||||
component: StartComponent,
|
component: StartComponent,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: ':id',
|
path: ':id',
|
||||||
component: BlockComponent
|
component: BlockComponent
|
||||||
@@ -465,7 +465,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
|||||||
{
|
{
|
||||||
path: 'block',
|
path: 'block',
|
||||||
component: StartComponent,
|
component: StartComponent,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: ':id',
|
path: ':id',
|
||||||
component: BlockComponent
|
component: BlockComponent
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<div class="d-block float-right" id="filter">
|
<div class="d-block float-right" id="filter">
|
||||||
<form [formGroup]="radioGroupForm">
|
<form [formGroup]="radioGroupForm">
|
||||||
<ngx-bootrap-multiselect [options]="txTypeOptions" [settings]="txTypeDropdownSettings" [texts]="txTypeDropdownTexts" formControlName="txTypes"></ngx-bootrap-multiselect>
|
<ngx-bootstrap-multiselect [options]="txTypeOptions" [settings]="txTypeDropdownSettings" [texts]="txTypeDropdownTexts" formControlName="txTypes"></ngx-bootstrap-multiselect>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { BisqApiService } from '../bisq-api.service';
|
|||||||
import { SeoService } from 'src/app/services/seo.service';
|
import { SeoService } from 'src/app/services/seo.service';
|
||||||
import { FormGroup, FormBuilder } from '@angular/forms';
|
import { FormGroup, FormBuilder } from '@angular/forms';
|
||||||
import { Router, ActivatedRoute } from '@angular/router';
|
import { Router, ActivatedRoute } from '@angular/router';
|
||||||
import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts } from 'ngx-bootrap-multiselect';
|
import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts } from 'src/app/components/ngx-bootstrap-multiselect/types'
|
||||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export class BisqTransfersComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges() {
|
ngOnChanges() {
|
||||||
this.totalOutput = this.tx.outputs.filter((output) => output.isVerified).reduce((acc, output) => acc + output.bsqAmount, 0);;
|
this.totalOutput = this.tx.outputs.filter((output) => output.isVerified).reduce((acc, output) => acc + output.bsqAmount, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
switchCurrency() {
|
switchCurrency() {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { BisqRoutingModule } from './bisq.routing.module';
|
import { BisqRoutingModule } from './bisq.routing.module';
|
||||||
import { SharedModule } from '../shared/shared.module';
|
import { SharedModule } from '../shared/shared.module';
|
||||||
import { NgxBootstrapMultiselectModule } from 'ngx-bootrap-multiselect';
|
|
||||||
|
|
||||||
import { LightweightChartsComponent } from './lightweight-charts/lightweight-charts.component';
|
import { LightweightChartsComponent } from './lightweight-charts/lightweight-charts.component';
|
||||||
import { LightweightChartsAreaComponent } from './lightweight-charts-area/lightweight-charts-area.component';
|
import { LightweightChartsAreaComponent } from './lightweight-charts-area/lightweight-charts-area.component';
|
||||||
@@ -24,6 +23,10 @@ import { BisqStatsComponent } from './bisq-stats/bisq-stats.component';
|
|||||||
import { BsqAmountComponent } from './bsq-amount/bsq-amount.component';
|
import { BsqAmountComponent } from './bsq-amount/bsq-amount.component';
|
||||||
import { BisqTradesComponent } from './bisq-trades/bisq-trades.component';
|
import { BisqTradesComponent } from './bisq-trades/bisq-trades.component';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { AutofocusDirective } from '../components/ngx-bootstrap-multiselect/autofocus.directive';
|
||||||
|
import { MultiSelectSearchFilter } from '../components/ngx-bootstrap-multiselect/search-filter.pipe';
|
||||||
|
import { OffClickDirective } from '../components/ngx-bootstrap-multiselect/off-click.directive';
|
||||||
|
import { NgxDropdownMultiselectComponent } from '../components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@@ -44,16 +47,21 @@ import { CommonModule } from '@angular/common';
|
|||||||
BisqMarketComponent,
|
BisqMarketComponent,
|
||||||
BisqTradesComponent,
|
BisqTradesComponent,
|
||||||
BisqMainDashboardComponent,
|
BisqMainDashboardComponent,
|
||||||
|
NgxDropdownMultiselectComponent,
|
||||||
|
AutofocusDirective,
|
||||||
|
OffClickDirective,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
BisqRoutingModule,
|
BisqRoutingModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
FontAwesomeModule,
|
FontAwesomeModule,
|
||||||
NgxBootstrapMultiselectModule,
|
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
BisqApiService,
|
BisqApiService,
|
||||||
|
MultiSelectSearchFilter,
|
||||||
|
AutofocusDirective,
|
||||||
|
OffClickDirective,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class BisqModule {
|
export class BisqModule {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createChart, CrosshairMode, isBusinessDay } from 'lightweight-charts';
|
import { createChart, CrosshairMode, isBusinessDay } from 'lightweight-charts';
|
||||||
import { ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, ElementRef, HostListener, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-lightweight-charts-area',
|
selector: 'app-lightweight-charts-area',
|
||||||
@@ -25,6 +25,15 @@ export class LightweightChartsAreaComponent implements OnInit, OnChanges, OnDest
|
|||||||
private element: ElementRef,
|
private element: ElementRef,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
|
@HostListener('window:resize', ['$event'])
|
||||||
|
resizeCanvas(): void {
|
||||||
|
this.width = this.element.nativeElement.parentElement.offsetWidth;
|
||||||
|
this.chart.applyOptions({
|
||||||
|
width: this.width,
|
||||||
|
height: this.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.width = this.element.nativeElement.parentElement.offsetWidth;
|
this.width = this.element.nativeElement.parentElement.offsetWidth;
|
||||||
this.container = document.createElement('div');
|
this.container = document.createElement('div');
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createChart, CrosshairMode } from 'lightweight-charts';
|
import { createChart, CrosshairMode } from 'lightweight-charts';
|
||||||
import { ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, ElementRef, HostListener, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-lightweight-charts',
|
selector: 'app-lightweight-charts',
|
||||||
@@ -21,6 +21,14 @@ export class LightweightChartsComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
private element: ElementRef,
|
private element: ElementRef,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
|
@HostListener('window:resize', ['$event'])
|
||||||
|
resizeCanvas(): void {
|
||||||
|
this.chart.applyOptions({
|
||||||
|
width: this.element.nativeElement.parentElement.offsetWidth,
|
||||||
|
height: this.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.chart = createChart(this.element.nativeElement, {
|
this.chart = createChart(this.element.nativeElement, {
|
||||||
width: this.element.nativeElement.parentElement.offsetWidth,
|
width: this.element.nativeElement.parentElement.offsetWidth,
|
||||||
|
|||||||
@@ -48,11 +48,13 @@
|
|||||||
<span>Spiral</span>
|
<span>Spiral</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://foundrydigital.com/" target="_blank" title="Foundry">
|
<a href="https://foundrydigital.com/" target="_blank" title="Foundry">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="400px" height="400px" viewBox="0 0 400 400" class="image">
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="-10 -10 100 100" class="image">
|
||||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
<g>
|
<g transform="translate(-186.000000, -2316.000000)">
|
||||||
<rect fill="#87E1A1" fill-rule="nonzero" x="0" y="0" width="400" height="400"></rect>
|
<g transform="translate(186.000000, 2316.000000)">
|
||||||
<path d="M124,149.256434 L169.106586,149.256434 L169.106586,128.378728 C169.106586,102.958946 183.316852,90 207.489341,90 L276.773787,90 L276.773787,119.404671 L222.192348,119.404671 C216.458028,119.404671 213.968815,122.397366 213.968815,127.633575 L213.968815,149.256434 L276.023264,149.256434 L276.023264,181.902184 L213.968815,181.902184 L213.968815,310 L169.106586,310 L169.106586,181.902184 L124,181.902184 L124,149.256434" fill="#000000"></path>
|
<rect id="" fill="#023D32" x="-10" y="-10" width="100" height="100" rx="8"></rect>
|
||||||
|
<path d="M61.6666667,9.16666667 L61.6666667,17.0041667 L46.2625,17.0041667 C46.2625,17.0041667 44.1666667,16.6666667 44.1666667,18.3333333 L44.1666667,25.8025 L61.6666667,25.8025 L61.6666667,34.7391667 L44.1666667,34.7391667 L44.1666667,70.5575 L31.7825,70.5575 L31.7825,35 L19.1666667,35 L19.1666667,25.595 L31.6666667,25.595 L31.6666667,17.5 C31.6666667,17.5 32.5,9.16666667 40.4166667,9.16666667 L61.6666667,9.16666667 Z" id="Fill-1" fill="#86E2A0"></path>
|
||||||
|
</g>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { AddressLabelsComponent } from './address-labels.component';
|
|
||||||
|
|
||||||
describe('AddressLabelsComponent', () => {
|
|
||||||
let component: AddressLabelsComponent;
|
|
||||||
let fixture: ComponentFixture<AddressLabelsComponent>;
|
|
||||||
|
|
||||||
beforeEach(async(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
declarations: [ AddressLabelsComponent ]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(AddressLabelsComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -85,6 +85,8 @@ export class AddressLabelsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.detectMultisig(this.vin.inner_redeemscript_asm);
|
this.detectMultisig(this.vin.inner_redeemscript_asm);
|
||||||
|
|
||||||
|
this.detectMultisig(this.vin.prevout.scriptpubkey_asm);
|
||||||
}
|
}
|
||||||
|
|
||||||
detectMultisig(script: string) {
|
detectMultisig(script: string) {
|
||||||
@@ -118,7 +120,11 @@ export class AddressLabelsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
const m = parseInt(opM.match(/[0-9]+/)[0], 10);
|
const m = parseInt(opM.match(/[0-9]+/)[0], 10);
|
||||||
|
|
||||||
this.label = $localize`:@@address-label.multisig:Multisig ${m}:multisigM: of ${n}:multisigN:`
|
if (ops.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.label = $localize`:@@address-label.multisig:Multisig ${m}:multisigM: of ${n}:multisigN:`;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleVout() {
|
handleVout() {
|
||||||
|
|||||||
@@ -130,7 +130,7 @@
|
|||||||
<span i18n="address.error.loading-address-data">Error loading address data.</span>
|
<span i18n="address.error.loading-address-data">Error loading address data.</span>
|
||||||
<br>
|
<br>
|
||||||
<ng-template #displayServerError><i class="small">({{ error.error }})</i></ng-template>
|
<ng-template #displayServerError><i class="small">({{ error.error }})</i></ng-template>
|
||||||
<ng-template [ngIf]="error.status === 413 || error.status === 405" [ngIfElse]="displayServerError">
|
<ng-template [ngIf]="error.status === 413 || error.status === 405 || error.status === 504" [ngIfElse]="displayServerError">
|
||||||
<ng-container i18n="Electrum server limit exceeded error">
|
<ng-container i18n="Electrum server limit exceeded error">
|
||||||
<i>There many transactions on this address, more than your backend can handle. See more on <a href="/docs/faq#address-lookup-issues">setting up a stronger backend</a>.</i>
|
<i>There many transactions on this address, more than your backend can handle. See more on <a href="/docs/faq#address-lookup-issues">setting up a stronger backend</a>.</i>
|
||||||
<br><br>
|
<br><br>
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { AmountComponent } from './amount.component';
|
|
||||||
|
|
||||||
describe('AmountComponent', () => {
|
|
||||||
let component: AmountComponent;
|
|
||||||
let fixture: ComponentFixture<AmountComponent>;
|
|
||||||
|
|
||||||
beforeEach(async(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
declarations: [ AmountComponent ]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(AmountComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { TestBed, async } from '@angular/core/testing';
|
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
|
||||||
import { AppComponent } from './app.component';
|
|
||||||
|
|
||||||
describe('AppComponent', () => {
|
|
||||||
beforeEach(async(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
imports: [
|
|
||||||
RouterTestingModule
|
|
||||||
],
|
|
||||||
declarations: [
|
|
||||||
AppComponent
|
|
||||||
],
|
|
||||||
}).compileComponents();
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should create the app', () => {
|
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
|
||||||
const app = fixture.debugElement.componentInstance;
|
|
||||||
expect(app).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`should have as title 'mempool'`, () => {
|
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
|
||||||
const app = fixture.debugElement.componentInstance;
|
|
||||||
expect(app.title).toEqual('mempool');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render title in a h1 tag', () => {
|
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
|
||||||
fixture.detectChanges();
|
|
||||||
const compiled = fixture.debugElement.nativeElement;
|
|
||||||
expect(compiled.querySelector('h1').textContent).toContain('Welcome to mempool!');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -9,34 +9,34 @@
|
|||||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
|
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
|
||||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 144">
|
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 144">
|
||||||
<input ngbButton type="radio" [value]="'24h'" fragment="24h"> 24h
|
<input ngbButton type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 24h
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 432">
|
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 432">
|
||||||
<input ngbButton type="radio" [value]="'3d'" fragment="3d"> 3D
|
<input ngbButton type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 3D
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 1008">
|
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 1008">
|
||||||
<input ngbButton type="radio" [value]="'1w'" fragment="1w"> 1W
|
<input ngbButton type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 1W
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 4320">
|
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 4320">
|
||||||
<input ngbButton type="radio" [value]="'1m'" fragment="1m"> 1M
|
<input ngbButton type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 1M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 12960">
|
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 12960">
|
||||||
<input ngbButton type="radio" [value]="'3m'" fragment="3m"> 3M
|
<input ngbButton type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 3M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 25920">
|
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 25920">
|
||||||
<input ngbButton type="radio" [value]="'6m'" fragment="6m"> 6M
|
<input ngbButton type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 6M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 52560">
|
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 52560">
|
||||||
<input ngbButton type="radio" [value]="'1y'" fragment="1y"> 1Y
|
<input ngbButton type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 1Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 105120">
|
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 105120">
|
||||||
<input ngbButton type="radio" [value]="'2y'" fragment="2y"> 2Y
|
<input ngbButton type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 2Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680">
|
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680">
|
||||||
<input ngbButton type="radio" [value]="'3y'" fragment="3y"> 3Y
|
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 3Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount > 157680">
|
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||||
<input ngbButton type="radio" [value]="'all'" fragment="all"> ALL
|
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> ALL
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -79,57 +79,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pool-distribution {
|
|
||||||
min-height: 56px;
|
|
||||||
display: block;
|
|
||||||
@media (min-width: 485px) {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
h5 {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.item {
|
|
||||||
width: 50%;
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0px auto 20px;
|
|
||||||
&:nth-child(2) {
|
|
||||||
order: 2;
|
|
||||||
@media (min-width: 485px) {
|
|
||||||
order: 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&:nth-child(3) {
|
|
||||||
order: 3;
|
|
||||||
@media (min-width: 485px) {
|
|
||||||
order: 2;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
@media (min-width: 992px) {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.card-title {
|
|
||||||
font-size: 1rem;
|
|
||||||
color: #4a68b9;
|
|
||||||
}
|
|
||||||
.card-text {
|
|
||||||
font-size: 18px;
|
|
||||||
span {
|
|
||||||
color: #ffffff66;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-loader {
|
|
||||||
width: 100%;
|
|
||||||
display: block;
|
|
||||||
max-width: 80px;
|
|
||||||
margin: 15px auto 3px;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { MiningService } from 'src/app/services/mining.service';
|
|||||||
import { selectPowerOfTen } from 'src/app/bitcoin.utils';
|
import { selectPowerOfTen } from 'src/app/bitcoin.utils';
|
||||||
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
|
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
|
||||||
import { StateService } from 'src/app/services/state.service';
|
import { StateService } from 'src/app/services/state.service';
|
||||||
import { Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-block-fee-rates-graph',
|
selector: 'app-block-fee-rates-graph',
|
||||||
@@ -56,6 +56,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
|
|||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
|
private route: ActivatedRoute,
|
||||||
) {
|
) {
|
||||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
|
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
|
||||||
this.radioGroupForm.controls.dateSpan.setValue('1y');
|
this.radioGroupForm.controls.dateSpan.setValue('1y');
|
||||||
@@ -67,9 +68,17 @@ export class BlockFeeRatesGraphComponent implements OnInit {
|
|||||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
|
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
|
||||||
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
|
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
|
||||||
|
|
||||||
|
this.route
|
||||||
|
.fragment
|
||||||
|
.subscribe((fragment) => {
|
||||||
|
if (['24h', '3d', '1w', '1m', '3m', '6m', '1y', '2y', '3y', 'all'].indexOf(fragment) > -1) {
|
||||||
|
this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.statsObservable$ = this.radioGroupForm.get('dateSpan').valueChanges
|
this.statsObservable$ = this.radioGroupForm.get('dateSpan').valueChanges
|
||||||
.pipe(
|
.pipe(
|
||||||
startWith(this.miningWindowPreference),
|
startWith(this.radioGroupForm.controls.dateSpan.value),
|
||||||
switchMap((timespan) => {
|
switchMap((timespan) => {
|
||||||
this.storageService.setValue('miningWindowPreference', timespan);
|
this.storageService.setValue('miningWindowPreference', timespan);
|
||||||
this.timespan = timespan;
|
this.timespan = timespan;
|
||||||
@@ -165,21 +174,20 @@ export class BlockFeeRatesGraphComponent implements OnInit {
|
|||||||
align: 'left',
|
align: 'left',
|
||||||
},
|
},
|
||||||
borderColor: '#000',
|
borderColor: '#000',
|
||||||
formatter: function (data) {
|
formatter: function(data) {
|
||||||
if (data.length <= 0) {
|
if (data.length <= 0) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
let tooltip = `<b style="color: white; margin-left: 2px">
|
let tooltip = `<b style="color: white; margin-left: 2px">${formatterXAxis(this.locale, this.timespan, parseInt(data[0].axisValue, 10))}</b><br>`;
|
||||||
${formatterXAxis(this.locale, this.timespan, parseInt(data[0].axisValue, 10))}</b><br>`;
|
|
||||||
|
|
||||||
for (const pool of data.reverse()) {
|
for (const pool of data.reverse()) {
|
||||||
tooltip += `${pool.marker} ${pool.seriesName}: ${pool.data[1]} sats/vByte<br>`;
|
tooltip += `${pool.marker} ${pool.seriesName}: ${pool.data[1]} sats/vByte<br>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['24h', '3d'].includes(this.timespan)) {
|
if (['24h', '3d'].includes(this.timespan)) {
|
||||||
tooltip += `<small>At block: ${data[0].data[2]}</small>`;
|
tooltip += `<small>` + $localize`At block: ${data[0].data[2]}` + `</small>`;
|
||||||
} else {
|
} else {
|
||||||
tooltip += `<small>Around block ${data[0].data[2]}</small>`;
|
tooltip += `<small>` + $localize`Around block: ${data[0].data[2]}` + `</small>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return tooltip;
|
return tooltip;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user