Compare commits
390 Commits
v2.2
...
v2.3.0-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3e47e1438 | ||
|
|
6f9762d50b | ||
|
|
9bf475bc97 | ||
|
|
e59a318cad | ||
|
|
57b64f64ad | ||
|
|
af3af5f099 | ||
|
|
fec603d5c5 | ||
|
|
ed2ebb1c70 | ||
|
|
14d2f8dd97 | ||
|
|
bf563cc195 | ||
|
|
f66e0a2c12 | ||
|
|
a43cd48795 | ||
|
|
44339daedf | ||
|
|
14b7b6427a | ||
|
|
a2e866d15a | ||
|
|
c2f288a861 | ||
|
|
e1c943d0a7 | ||
|
|
fa2d2e60b5 | ||
|
|
c919980652 | ||
|
|
b48389ae7d | ||
|
|
2bac7f9987 | ||
|
|
acf6fd9db5 | ||
|
|
74a9b65e81 | ||
|
|
822c840e54 | ||
|
|
6e93ef68fe | ||
|
|
3006deae6e | ||
|
|
740f5c2003 | ||
|
|
5c9d44e9eb | ||
|
|
88527b41e7 | ||
|
|
83cce0c3a7 | ||
|
|
e144d0c8e5 | ||
|
|
d72dbc1415 | ||
|
|
b857a7c37f | ||
|
|
c72c287b27 | ||
|
|
18e0a17d26 | ||
|
|
87eeef5d41 | ||
|
|
76a2fdeea7 | ||
|
|
792eb3727c | ||
|
|
2e0845847d | ||
|
|
8f774e55a8 | ||
|
|
28418640bb | ||
|
|
8aae5c1c9c | ||
|
|
92048964d1 | ||
|
|
b2140c2abe | ||
|
|
d0a8509194 | ||
|
|
aa0c3e6fed | ||
|
|
f0462114f3 | ||
|
|
9e0f9840aa | ||
|
|
d763c30f6a | ||
|
|
92b69657da | ||
|
|
d9ec0c1a36 | ||
|
|
4bf9f8b062 | ||
|
|
eefd8104bb | ||
|
|
16e807c4b0 | ||
|
|
461296e002 | ||
|
|
86c877c8e9 | ||
|
|
80fcceef73 | ||
|
|
b0e54818ae | ||
|
|
acbd7f0bde | ||
|
|
9a6efceb34 | ||
|
|
5ce7b55441 | ||
|
|
a3edaf17cc | ||
|
|
5695019216 | ||
|
|
7ab1ce8fc4 | ||
|
|
1f8ec2bd8e | ||
|
|
78b488466e | ||
|
|
66630743f6 | ||
|
|
ffa18bbe71 | ||
|
|
8cb1c5c88c | ||
|
|
bb07031362 | ||
|
|
31a0d44543 | ||
|
|
f90e19c767 | ||
|
|
800625d80e | ||
|
|
552540f510 | ||
|
|
bbee2dcb5b | ||
|
|
e4e338b05a | ||
|
|
061a55b236 | ||
|
|
9f0f9230fb | ||
|
|
40956c0a23 | ||
|
|
f29e35b325 | ||
|
|
d88efb8b0d | ||
|
|
b9489525c6 | ||
|
|
8ddcd298b0 | ||
|
|
69df6e4dcb | ||
|
|
f3c8e2134b | ||
|
|
0e25c52e67 | ||
|
|
60f41d3181 | ||
|
|
50c5244abf | ||
|
|
605c1a980c | ||
|
|
0d67bc36ee | ||
|
|
aa39bbd091 | ||
|
|
641d2ad028 | ||
|
|
d602b20f56 | ||
|
|
138f6e4e39 | ||
|
|
3e788ecbf9 | ||
|
|
2236c6d9a6 | ||
|
|
2a0a1b0213 | ||
|
|
e6e49fd5d6 | ||
|
|
f6d5f44469 | ||
|
|
51e09ff64f | ||
|
|
07ba2f6ecc | ||
|
|
bc42552bec | ||
|
|
c6b44a3be9 | ||
|
|
e1f4de0de3 | ||
|
|
d22dc0888a | ||
|
|
75fb27c690 | ||
|
|
401506a103 | ||
|
|
ab27ea28f0 | ||
|
|
6e579ce0b6 | ||
|
|
e7030cca32 | ||
|
|
2c496e9a50 | ||
|
|
014d6dee66 | ||
|
|
47ae306a75 | ||
|
|
8b8b06e6ab | ||
|
|
fa7a45421e | ||
|
|
d376ba1c61 | ||
|
|
388aa7fbe3 | ||
|
|
34695146ee | ||
|
|
9020c618f0 | ||
|
|
584d091d4e | ||
|
|
f434e50a2c | ||
|
|
1a7decb91d | ||
|
|
3574f8639e | ||
|
|
9b956ff88d | ||
|
|
1a98a14541 | ||
|
|
0088e58c14 | ||
|
|
3fad765269 | ||
|
|
2e122a9be1 | ||
|
|
2978d16148 | ||
|
|
fc58bcb3bc | ||
|
|
1696623e2f | ||
|
|
251a1af442 | ||
|
|
9bdf42530a | ||
|
|
8525fbb177 | ||
|
|
63a3568481 | ||
|
|
e4941740de | ||
|
|
25bd33f7da | ||
|
|
2d007b9100 | ||
|
|
b71330c606 | ||
|
|
844b640c8c | ||
|
|
1277e58e68 | ||
|
|
fde6fe324a | ||
|
|
4ed114a4d5 | ||
|
|
8c2dfea6a6 | ||
|
|
a0624df06b | ||
|
|
1eedcf900b | ||
|
|
0b077d6fda | ||
|
|
80047313e7 | ||
|
|
71229b94c8 | ||
|
|
c256daf8c8 | ||
|
|
ba0fb996d2 | ||
|
|
5977e96034 | ||
|
|
a151c5cddd | ||
|
|
0323fd966d | ||
|
|
beb834bc30 | ||
|
|
ad6503c7b3 | ||
|
|
f8c11c8b6b | ||
|
|
ba5421e77b | ||
|
|
20fa803cee | ||
|
|
393fa78a43 | ||
|
|
3f290dae06 | ||
|
|
24d18b9f2f | ||
|
|
79ef8ca371 | ||
|
|
ec12f21113 | ||
|
|
2e8ecc7277 | ||
|
|
fc28b06a0f | ||
|
|
8fdbfdc04c | ||
|
|
bdfcfc96a8 | ||
|
|
bb8649bc81 | ||
|
|
777e3d58b7 | ||
|
|
c552f1aab6 | ||
|
|
c0f2fa3042 | ||
|
|
05936f82bd | ||
|
|
c7db81c97c | ||
|
|
bd1a37b8ef | ||
|
|
efc4e6a8ed | ||
|
|
dd5d87e91e | ||
|
|
ca6df488c5 | ||
|
|
1e018a6aa5 | ||
|
|
0a627f96be | ||
|
|
17a8e67d8a | ||
|
|
815c2c5ad5 | ||
|
|
4376de85ff | ||
|
|
7e89de4612 | ||
|
|
4b72a14706 | ||
|
|
b34f6fedb6 | ||
|
|
58af0d78af | ||
|
|
ca13d9109c | ||
|
|
e103fb5876 | ||
|
|
58178f4563 | ||
|
|
04f1879fd1 | ||
|
|
f5bc9ced0a | ||
|
|
7fe9993f91 | ||
|
|
7c95339324 | ||
|
|
006442f9de | ||
|
|
e20100e437 | ||
|
|
d7cf2b37d5 | ||
|
|
278c2b9aae | ||
|
|
944246fcf5 | ||
|
|
03d87f4993 | ||
|
|
cf8cab5f77 | ||
|
|
49d1376647 | ||
|
|
de5518d262 | ||
|
|
7e4c51f47f | ||
|
|
fe1d153632 | ||
|
|
a98f9ab80e | ||
|
|
867afaf265 | ||
|
|
3d2ec64b14 | ||
|
|
bb407c0b42 | ||
|
|
83c3d901c7 | ||
|
|
901cee903c | ||
|
|
250ea09c7e | ||
|
|
648d59631b | ||
|
|
ed06e3c491 | ||
|
|
3e8d646edd | ||
|
|
9c2c698575 | ||
|
|
e2b0a286a4 | ||
|
|
154809f0f9 | ||
|
|
8d9a51a7c4 | ||
|
|
b3294369d4 | ||
|
|
53730920e3 | ||
|
|
d73b814277 | ||
|
|
dd0050c066 | ||
|
|
ae51ee3e26 | ||
|
|
4b16e5d65f | ||
|
|
4f73bba132 | ||
|
|
3c229602e4 | ||
|
|
c74c902ebc | ||
|
|
8bfd315ba3 | ||
|
|
9d75c47792 | ||
|
|
e183be1a5c | ||
|
|
7e273ce63d | ||
|
|
6d070e75b0 | ||
|
|
af5e0d7cd6 | ||
|
|
a2f1003916 | ||
|
|
f4f96fd18e | ||
|
|
f3b470b63e | ||
|
|
398a72c1a6 | ||
|
|
ddd6420d9b | ||
|
|
d76f42296a | ||
|
|
45af88774f | ||
|
|
71c6b0e11d | ||
|
|
47a6118ffb | ||
|
|
994eb378af | ||
|
|
34d46e8ca5 | ||
|
|
a940f7e3b4 | ||
|
|
8c29395533 | ||
|
|
8208bbf0b7 | ||
|
|
dbd205b73f | ||
|
|
7ef4be26ed | ||
|
|
1223c58a98 | ||
|
|
7d3757676f | ||
|
|
073bd60ed8 | ||
|
|
18c38fc1c1 | ||
|
|
0eb95447bb | ||
|
|
c6b1979391 | ||
|
|
0f390e65a4 | ||
|
|
5dc0f4e270 | ||
|
|
223288cc52 | ||
|
|
72a35200b3 | ||
|
|
11817c04f7 | ||
|
|
7a8b2db3fb | ||
|
|
6d910a5e24 | ||
|
|
e1f07884b9 | ||
|
|
e00e61edfa | ||
|
|
4f988e186a | ||
|
|
1aa54faa35 | ||
|
|
99adccf43c | ||
|
|
0bb9247609 | ||
|
|
d841933b21 | ||
|
|
bc8b78a01b | ||
|
|
b0c708659b | ||
|
|
e31b906084 | ||
|
|
7249620471 | ||
|
|
dc9d5d0be3 | ||
|
|
a9009d4de2 | ||
|
|
a265787cd4 | ||
|
|
c6e72be483 | ||
|
|
4680519d2e | ||
|
|
5b17f88de2 | ||
|
|
a6d34ba4f1 | ||
|
|
508c8b0be3 | ||
|
|
ef7dd6c8fb | ||
|
|
f03249761b | ||
|
|
cb5877ba0a | ||
|
|
96f14d2781 | ||
|
|
8eb70416da | ||
|
|
b9246a72f2 | ||
|
|
43e222b9df | ||
|
|
5548d08a9e | ||
|
|
10fa39634e | ||
|
|
e6b90385b2 | ||
|
|
61181c6791 | ||
|
|
d2cccd2422 | ||
|
|
b05ebe1598 | ||
|
|
d92827a411 | ||
|
|
d061f7589c | ||
|
|
1c01094e07 | ||
|
|
f28a85f91b | ||
|
|
15903faf49 | ||
|
|
4895343d4e | ||
|
|
a0559cbb24 | ||
|
|
0293ba4a52 | ||
|
|
8b0d1db776 | ||
|
|
1908b1a5a6 | ||
|
|
037f472f8c | ||
|
|
a32c1f40b1 | ||
|
|
837e714b1f | ||
|
|
91a37d8fe8 | ||
|
|
a00aa27ae4 | ||
|
|
226e72451c | ||
|
|
544be77bdc | ||
|
|
b8a110a772 | ||
|
|
7788a2d6bd | ||
|
|
da17fd16fa | ||
|
|
e670f80fed | ||
|
|
857a5ff6fc | ||
|
|
2de28b9926 | ||
|
|
e6f8cf6cc8 | ||
|
|
d7586af392 | ||
|
|
35881b2457 | ||
|
|
59cd80b6d1 | ||
|
|
735c2ba587 | ||
|
|
be1ef43cd1 | ||
|
|
34ad88d3d0 | ||
|
|
751c7d6e69 | ||
|
|
60d8697b09 | ||
|
|
41aa1248be | ||
|
|
cedd94c654 | ||
|
|
bf13994d28 | ||
|
|
8a44ccc55d | ||
|
|
81df40681f | ||
|
|
9e46cde9b7 | ||
|
|
723034b3d3 | ||
|
|
59898f1269 | ||
|
|
195b9bf542 | ||
|
|
0333d91b15 | ||
|
|
f0bd487ea9 | ||
|
|
cd8e308870 | ||
|
|
f6a889298c | ||
|
|
11f5e99187 | ||
|
|
334f9358b0 | ||
|
|
820561610a | ||
|
|
2c895e7b03 | ||
|
|
f36f48b11c | ||
|
|
f12f1b4a4e | ||
|
|
037d6a75ea | ||
|
|
775323de3e | ||
|
|
d91dfa2f41 | ||
|
|
3ac06bb983 | ||
|
|
1ba0075829 | ||
|
|
95436d398d | ||
|
|
f2f5749769 | ||
|
|
cb90b09a0e | ||
|
|
2e54f4ca94 | ||
|
|
853e2fcb8f | ||
|
|
9e0a5300b0 | ||
|
|
1b5930887c | ||
|
|
5b39c018db | ||
|
|
ad08c3a907 | ||
|
|
08328cbf0f | ||
|
|
03ce592ab0 | ||
|
|
21db5a4102 | ||
|
|
7234734056 | ||
|
|
7bf9d604b9 | ||
|
|
08fd4a4835 | ||
|
|
9a715871c5 | ||
|
|
d405334109 | ||
|
|
38aee1a897 | ||
|
|
52aea12f22 | ||
|
|
ecbd18087b | ||
|
|
d13e18a72a | ||
|
|
8749b8b0fa | ||
|
|
f2e0a71b01 | ||
|
|
b4eea3dc72 | ||
|
|
cdfc03f352 | ||
|
|
2c5ccab77c | ||
|
|
80d76ad1f4 | ||
|
|
9a2428ad79 | ||
|
|
71cf41362f | ||
|
|
652f88770e | ||
|
|
7de2cf89f4 | ||
|
|
d7a827ba7f | ||
|
|
9dae7020c8 | ||
|
|
3ae3df6722 | ||
|
|
2e2e6aa01f | ||
|
|
1e9f131a2a | ||
|
|
5197a15e31 | ||
|
|
1d29fad986 | ||
|
|
f6e4907128 |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -9,4 +9,4 @@ community_bridge: # Replace with a single Community Bridge project-name e.g., cl
|
|||||||
liberapay: # Replace with a single Liberapay username
|
liberapay: # Replace with a single Liberapay username
|
||||||
issuehunt: # Replace with a single IssueHunt username
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
otechie: # Replace with a single Otechie username
|
otechie: # Replace with a single Otechie username
|
||||||
custom: ['https://mempool.space/about'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
custom: ['https://mempool.space/sponsor'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
|
|||||||
77
.github/workflows/cypress.yml
vendored
Normal file
77
.github/workflows/cypress.yml
vendored
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
name: Cypress Tests
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cypress:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
containers: [1, 2, 3, 4, 5]
|
||||||
|
os: ["ubuntu-latest"]
|
||||||
|
browser: [chrome]
|
||||||
|
name: E2E tests on ${{ matrix.browser }} - ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: ${{ matrix.browser }} browser tests (Mempool)
|
||||||
|
uses: cypress-io/github-action@v2
|
||||||
|
with:
|
||||||
|
working-directory: frontend
|
||||||
|
build: npm run config:defaults:mempool
|
||||||
|
start: npm run start:local-prod
|
||||||
|
wait-on: 'http://localhost:4200'
|
||||||
|
wait-on-timeout: 120
|
||||||
|
record: true
|
||||||
|
parallel: true
|
||||||
|
env: BASE_MODULE=mempool
|
||||||
|
group: Tests on ${{ matrix.browser }} (Mempool)
|
||||||
|
browser: ${{ matrix.browser }}
|
||||||
|
ci-build-id: '${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}'
|
||||||
|
env:
|
||||||
|
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
|
||||||
|
|
||||||
|
- name: ${{ matrix.browser }} browser tests (Liquid)
|
||||||
|
uses: cypress-io/github-action@v2
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
working-directory: frontend
|
||||||
|
build: npm run config:defaults:liquid
|
||||||
|
start: npm run start:local-prod
|
||||||
|
wait-on: 'http://localhost:4200'
|
||||||
|
wait-on-timeout: 120
|
||||||
|
record: true
|
||||||
|
parallel: true
|
||||||
|
spec: cypress/integration/liquid/liquid.spec.ts
|
||||||
|
env: BASE_MODULE=liquid
|
||||||
|
group: Tests on ${{ matrix.browser }} (Liquid)
|
||||||
|
browser: ${{ matrix.browser }}
|
||||||
|
ci-build-id: '${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}'
|
||||||
|
env:
|
||||||
|
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
|
||||||
|
|
||||||
|
- name: ${{ matrix.browser }} browser tests (Bisq)
|
||||||
|
uses: cypress-io/github-action@v2
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
working-directory: frontend
|
||||||
|
build: npm run config:defaults:bisq
|
||||||
|
start: npm run start:local-prod
|
||||||
|
wait-on: 'http://localhost:4200'
|
||||||
|
wait-on-timeout: 120
|
||||||
|
record: true
|
||||||
|
parallel: true
|
||||||
|
spec: cypress/integration/bisq/bisq.spec.ts
|
||||||
|
env: BASE_MODULE=bisq
|
||||||
|
group: Tests on ${{ matrix.browser }} (Bisq)
|
||||||
|
browser: ${{ matrix.browser }}
|
||||||
|
ci-build-id: '${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}'
|
||||||
|
env:
|
||||||
|
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
|
||||||
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"typescript.tsdk": "./backend/node_modules/typescript/lib"
|
||||||
|
}
|
||||||
9
LICENSE
9
LICENSE
@@ -12,8 +12,13 @@ the terms of (at your option) either:
|
|||||||
Foundation, either version 3 of the License or any later version approved by a
|
Foundation, either version 3 of the License or any later version approved by a
|
||||||
proxy statement published on <https://mempool.space/about>.
|
proxy statement published on <https://mempool.space/about>.
|
||||||
|
|
||||||
However, these licenses do not grant you any rights to use the "mempool.space"
|
However, this copyright license does not include an implied right or license to
|
||||||
trademarks or logos, or any other trademarks of Mempool Space K.K.
|
use our trademarks: The Mempool Open Source Project™, mempool.space™, the
|
||||||
|
mempool Logo™, the mempool.space Vertical Logo™, the mempool.space Horizontal
|
||||||
|
Logo™, the mempool Square Logo™, and the mempool Blocks logo™ are registered
|
||||||
|
trademarks or trademarks of Mempool Space K.K in Japan, the United States,
|
||||||
|
and/or other countries. See our full Trademark Policy and Guidelines for more
|
||||||
|
details, published on <https://mempool.space/trademark-policy>.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY
|
This program is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Mempool is the fully featured visualizer, explorer, and API service running on [mempool.space](https://mempool.space/), an open source project developed and operated for the benefit of the Bitcoin community, with a focus on the emerging transaction fee market to help our transition into a multi-layer ecosystem.
|
Mempool is the fully featured visualizer, explorer, and API service running on [mempool.space](https://mempool.space/), an open source project developed and operated for the benefit of the Bitcoin community, with a focus on the emerging transaction fee market to help our transition into a multi-layer ecosystem.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Installation Methods
|
## Installation Methods
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ Install mempool dependencies from npm and build the frontend static HTML/CSS/JS:
|
|||||||
Install the output into nginx webroot folder:
|
Install the output into nginx webroot folder:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo rsync -av --delete dist/mempool/ /var/www/html/
|
sudo rsync -av --delete dist/mempool /var/www/
|
||||||
```
|
```
|
||||||
|
|
||||||
## nginx + certbot
|
## nginx + certbot
|
||||||
@@ -179,7 +179,7 @@ Install the supplied nginx.conf and nginx-mempool.conf in /etc/nginx
|
|||||||
apt-get install -y nginx python-certbot-nginx
|
apt-get install -y nginx python-certbot-nginx
|
||||||
|
|
||||||
# install the mempool configuration for nginx
|
# install the mempool configuration for nginx
|
||||||
cp nginx.conf nginx-mempool.conf /etc/nginx/nginx.conf
|
cp nginx.conf nginx-mempool.conf /etc/nginx/
|
||||||
|
|
||||||
# replace example.com with your domain name
|
# replace example.com with your domain name
|
||||||
certbot --nginx -d example.com
|
certbot --nginx -d example.com
|
||||||
|
|||||||
4
backend/.vscode/settings.json
vendored
Normal file
4
backend/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"typescript.tsdk": "../backend/node_modules/typescript/lib"
|
||||||
|
}
|
||||||
@@ -8,7 +8,12 @@
|
|||||||
"POLL_RATE_MS": 2000,
|
"POLL_RATE_MS": 2000,
|
||||||
"CACHE_DIR": "./cache",
|
"CACHE_DIR": "./cache",
|
||||||
"CLEAR_PROTECTION_MINUTES": 20,
|
"CLEAR_PROTECTION_MINUTES": 20,
|
||||||
"RECOMMENDED_FEE_PERCENTILE": 50
|
"RECOMMENDED_FEE_PERCENTILE": 50,
|
||||||
|
"BLOCK_WEIGHT_UNITS": 4000000,
|
||||||
|
"INITIAL_BLOCKS_AMOUNT": 8,
|
||||||
|
"MEMPOOL_BLOCKS_AMOUNT": 8,
|
||||||
|
"PRICE_FEED_UPDATE_INTERVAL": 3600,
|
||||||
|
"USE_SECOND_NODE_FOR_MINFEE": false
|
||||||
},
|
},
|
||||||
"CORE_RPC": {
|
"CORE_RPC": {
|
||||||
"HOST": "127.0.0.1",
|
"HOST": "127.0.0.1",
|
||||||
@@ -24,8 +29,7 @@
|
|||||||
"ESPLORA": {
|
"ESPLORA": {
|
||||||
"REST_API_URL": "http://127.0.0.1:3000"
|
"REST_API_URL": "http://127.0.0.1:3000"
|
||||||
},
|
},
|
||||||
"CORE_RPC_MINFEE": {
|
"SECOND_CORE_RPC": {
|
||||||
"ENABLED": false,
|
|
||||||
"HOST": "127.0.0.1",
|
"HOST": "127.0.0.1",
|
||||||
"PORT": 8332,
|
"PORT": 8332,
|
||||||
"USERNAME": "mempool",
|
"USERNAME": "mempool",
|
||||||
|
|||||||
33
backend/package-lock.json
generated
33
backend/package-lock.json
generated
@@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "mempool-backend",
|
"name": "mempool-backend",
|
||||||
"version": "2.2.0",
|
"version": "2.3.0-dev",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mempool-backend",
|
"name": "mempool-backend",
|
||||||
"version": "2.2.0",
|
"version": "2.3.0-dev",
|
||||||
"license": "GNU Affero General Public License v3.0",
|
"license": "GNU Affero General Public License v3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mempool/bitcoin": "^3.0.2",
|
"@mempool/bitcoin": "^3.0.3",
|
||||||
"@mempool/electrum-client": "^1.1.7",
|
"@mempool/electrum-client": "^1.1.7",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"bitcoinjs-lib": "^5.2.0",
|
"bitcoinjs-lib": "^5.2.0",
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
"@types/locutus": "^0.0.6",
|
"@types/locutus": "^0.0.6",
|
||||||
"@types/ws": "^7.4.4",
|
"@types/ws": "^7.4.4",
|
||||||
"tslint": "^6.1.0",
|
"tslint": "^6.1.0",
|
||||||
"typescript": "^4.1.5"
|
"typescript": "4.4.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
@@ -56,9 +56,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mempool/bitcoin": {
|
"node_modules/@mempool/bitcoin": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@mempool/bitcoin/-/bitcoin-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@mempool/bitcoin/-/bitcoin-3.0.3.tgz",
|
||||||
"integrity": "sha512-WNHFTDJEEBmakSPAbrJ933mGgm1uYxmOElyQYZVW7D7CRUd8mKek+QlViin63e71vyfMVOGXtWwSb87dxghggQ==",
|
"integrity": "sha512-10UdbwchnevlebDTN+Xhv75AEhDmTMy9UgWHlqx5MG2mheFG6+eqmtHsdxeYnv3IAtTtlRfA6fY0RbV/x4TNFQ==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.10.0"
|
"node": ">= 0.10.0"
|
||||||
}
|
}
|
||||||
@@ -1473,10 +1473,11 @@
|
|||||||
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
|
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "4.2.3",
|
"version": "4.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.2.tgz",
|
||||||
"integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==",
|
"integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -1590,9 +1591,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@mempool/bitcoin": {
|
"@mempool/bitcoin": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@mempool/bitcoin/-/bitcoin-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@mempool/bitcoin/-/bitcoin-3.0.3.tgz",
|
||||||
"integrity": "sha512-WNHFTDJEEBmakSPAbrJ933mGgm1uYxmOElyQYZVW7D7CRUd8mKek+QlViin63e71vyfMVOGXtWwSb87dxghggQ=="
|
"integrity": "sha512-10UdbwchnevlebDTN+Xhv75AEhDmTMy9UgWHlqx5MG2mheFG6+eqmtHsdxeYnv3IAtTtlRfA6fY0RbV/x4TNFQ=="
|
||||||
},
|
},
|
||||||
"@mempool/electrum-client": {
|
"@mempool/electrum-client": {
|
||||||
"version": "1.1.8",
|
"version": "1.1.8",
|
||||||
@@ -2770,9 +2771,9 @@
|
|||||||
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
|
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
|
||||||
},
|
},
|
||||||
"typescript": {
|
"typescript": {
|
||||||
"version": "4.2.3",
|
"version": "4.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.2.tgz",
|
||||||
"integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==",
|
"integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"unpipe": {
|
"unpipe": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mempool-backend",
|
"name": "mempool-backend",
|
||||||
"version": "2.2.0",
|
"version": "2.3.0-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",
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mempool/bitcoin": "^3.0.2",
|
"@mempool/bitcoin": "^3.0.3",
|
||||||
"@mempool/electrum-client": "^1.1.7",
|
"@mempool/electrum-client": "^1.1.7",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"bitcoinjs-lib": "^5.2.0",
|
"bitcoinjs-lib": "^5.2.0",
|
||||||
@@ -45,6 +45,6 @@
|
|||||||
"@types/locutus": "^0.0.6",
|
"@types/locutus": "^0.0.6",
|
||||||
"@types/ws": "^7.4.4",
|
"@types/ws": "^7.4.4",
|
||||||
"tslint": "^6.1.0",
|
"tslint": "^6.1.0",
|
||||||
"typescript": "^4.1.5"
|
"typescript": "4.4.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class BackendInfo {
|
|||||||
try {
|
try {
|
||||||
this.gitCommitHash = fs.readFileSync('../.git/refs/heads/master').toString().trim();
|
this.gitCommitHash = fs.readFileSync('../.git/refs/heads/master').toString().trim();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('Could not load git commit info: ' + e.message || e);
|
logger.err('Could not load git commit info: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ class BackendInfo {
|
|||||||
const packageJson = fs.readFileSync('package.json').toString();
|
const packageJson = fs.readFileSync('package.json').toString();
|
||||||
this.version = JSON.parse(packageJson).version;
|
this.version = JSON.parse(packageJson).version;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(e);
|
throw new Error(e instanceof Error ? e.message : 'Error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ class Bisq {
|
|||||||
this.buildIndex();
|
this.buildIndex();
|
||||||
this.calculateStats();
|
this.calculateStats();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('loadBisqDumpFile() error.' + e.message || e);
|
logger.info('loadBisqDumpFile() error.' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ interface BisqScriptPubKey {
|
|||||||
addresses: string[];
|
addresses: string[];
|
||||||
asm: string;
|
asm: string;
|
||||||
hex: string;
|
hex: string;
|
||||||
reqSigs: number;
|
reqSigs?: number;
|
||||||
type: string;
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ class Bisq {
|
|||||||
logger.debug('Bisq market data updated in ' + time + ' ms');
|
logger.debug('Bisq market data updated in ' + time + ' ms');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('loadBisqMarketDataDumpFile() error.' + e.message || e);
|
logger.err('loadBisqMarketDataDumpFile() error.' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,17 @@ export interface AbstractBitcoinApi {
|
|||||||
$getBlockHeightTip(): Promise<number>;
|
$getBlockHeightTip(): Promise<number>;
|
||||||
$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>;
|
||||||
$getBlock(hash: string): Promise<IEsploraApi.Block>;
|
$getBlock(hash: string): Promise<IEsploraApi.Block>;
|
||||||
$getAddress(address: string): Promise<IEsploraApi.Address>;
|
$getAddress(address: string): Promise<IEsploraApi.Address>;
|
||||||
$getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
|
$getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
|
||||||
$getAddressPrefix(prefix: string): string[];
|
$getAddressPrefix(prefix: string): string[];
|
||||||
|
$sendRawTransaction(rawTransaction: string): Promise<string>;
|
||||||
|
}
|
||||||
|
export interface BitcoinRpcCredentials {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
user: string;
|
||||||
|
pass: string;
|
||||||
|
timeout: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,17 @@ import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
|
|||||||
import EsploraApi from './esplora-api';
|
import EsploraApi from './esplora-api';
|
||||||
import BitcoinApi from './bitcoin-api';
|
import BitcoinApi from './bitcoin-api';
|
||||||
import ElectrumApi from './electrum-api';
|
import ElectrumApi from './electrum-api';
|
||||||
|
import bitcoinClient from './bitcoin-client';
|
||||||
|
|
||||||
function bitcoinApiFactory(): AbstractBitcoinApi {
|
function bitcoinApiFactory(): AbstractBitcoinApi {
|
||||||
switch (config.MEMPOOL.BACKEND) {
|
switch (config.MEMPOOL.BACKEND) {
|
||||||
case 'esplora':
|
case 'esplora':
|
||||||
return new EsploraApi();
|
return new EsploraApi();
|
||||||
case 'electrum':
|
case 'electrum':
|
||||||
return new ElectrumApi();
|
return new ElectrumApi(bitcoinClient);
|
||||||
case 'none':
|
case 'none':
|
||||||
default:
|
default:
|
||||||
return new BitcoinApi();
|
return new BitcoinApi(bitcoinClient);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export namespace IBitcoinApi {
|
|||||||
time: number; // (numeric) Same as blocktime
|
time: number; // (numeric) Same as blocktime
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Vin {
|
export interface Vin {
|
||||||
txid?: string; // (string) The transaction id
|
txid?: string; // (string) The transaction id
|
||||||
vout?: number; // (string)
|
vout?: number; // (string)
|
||||||
scriptSig?: { // (json object) The script
|
scriptSig?: { // (json object) The script
|
||||||
@@ -82,28 +82,36 @@ export namespace IBitcoinApi {
|
|||||||
sequence: number; // (numeric) The script sequence number
|
sequence: number; // (numeric) The script sequence number
|
||||||
txinwitness?: string[]; // (string) hex-encoded witness data
|
txinwitness?: string[]; // (string) hex-encoded witness data
|
||||||
coinbase?: string;
|
coinbase?: string;
|
||||||
|
is_pegin?: boolean; // (boolean) Elements peg-in
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Vout {
|
export interface Vout {
|
||||||
value: number; // (numeric) The value in BTC
|
value: number; // (numeric) The value in BTC
|
||||||
n: number; // (numeric) index
|
n: number; // (numeric) index
|
||||||
|
asset?: string; // (string) Elements asset id
|
||||||
scriptPubKey: { // (json object)
|
scriptPubKey: { // (json object)
|
||||||
asm: string; // (string) the asm
|
asm: string; // (string) the asm
|
||||||
hex: string; // (string) the hex
|
hex: string; // (string) the hex
|
||||||
reqSigs: number; // (numeric) The required sigs
|
reqSigs?: number; // (numeric) The required sigs
|
||||||
type: string; // (string) The type, eg 'pubkeyhash'
|
type: string; // (string) The type, eg 'pubkeyhash'
|
||||||
addresses: string[] // (string) bitcoin address
|
address?: string; // (string) bitcoin address
|
||||||
|
addresses?: string[]; // (string) bitcoin addresses
|
||||||
|
pegout_chain?: string; // (string) Elements peg-out chain
|
||||||
|
pegout_addresses?: string[]; // (string) Elements peg-out addresses
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddressInformation {
|
export interface AddressInformation {
|
||||||
isvalid: boolean; // (boolean) If the address is valid or not. If not, this is the only property returned.
|
isvalid: boolean; // (boolean) If the address is valid or not. If not, this is the only property returned.
|
||||||
|
isvalid_parent?: boolean; // (boolean) Elements only
|
||||||
address: string; // (string) The bitcoin address validated
|
address: string; // (string) The bitcoin address validated
|
||||||
scriptPubKey: string; // (string) The hex-encoded scriptPubKey generated by the address
|
scriptPubKey: string; // (string) The hex-encoded scriptPubKey generated by the address
|
||||||
isscript: boolean; // (boolean) If the key is a script
|
isscript: boolean; // (boolean) If the key is a script
|
||||||
iswitness: boolean; // (boolean) If the address is a witness
|
iswitness: boolean; // (boolean) If the address is a witness
|
||||||
witness_version?: boolean; // (numeric, optional) The version number of the witness program
|
witness_version?: boolean; // (numeric, optional) The version number of the witness program
|
||||||
witness_program: string; // (string, optional) The hex value of the witness program
|
witness_program: string; // (string, optional) The hex value of the witness program
|
||||||
|
confidential_key?: string; // (string) Elements only
|
||||||
|
unconfidential?: string; // (string) Elements only
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChainTips {
|
export interface ChainTips {
|
||||||
@@ -113,4 +121,46 @@ export namespace IBitcoinApi {
|
|||||||
status: 'invalid' | 'headers-only' | 'valid-headers' | 'valid-fork' | 'active';
|
status: 'invalid' | 'headers-only' | 'valid-headers' | 'valid-fork' | 'active';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BlockchainInfo {
|
||||||
|
chain: number; // (string) current network name as defined in BIP70 (main, test, regtest)
|
||||||
|
blocks: number; // (numeric) the current number of blocks processed in the server
|
||||||
|
headers: number; // (numeric) the current number of headers we have validated
|
||||||
|
bestblockhash: string, // (string) the hash of the currently best block
|
||||||
|
difficulty: number; // (numeric) the current difficulty
|
||||||
|
mediantime: number; // (numeric) median time for the current best block
|
||||||
|
verificationprogress: number; // (numeric) estimate of verification progress [0..1]
|
||||||
|
initialblockdownload: boolean; // (bool) (debug information) estimate of whether this node is in Initial Block Download mode.
|
||||||
|
chainwork: string // (string) total amount of work in active chain, in hexadecimal
|
||||||
|
size_on_disk: number; // (numeric) the estimated size of the block and undo files on disk
|
||||||
|
pruned: number; // (boolean) if the blocks are subject to pruning
|
||||||
|
pruneheight: number; // (numeric) lowest-height complete block stored (only present if pruning is enabled)
|
||||||
|
automatic_pruning: number; // (boolean) whether automatic pruning is enabled (only present if pruning is enabled)
|
||||||
|
prune_target_size: number; // (numeric) the target size used by pruning (only present if automatic pruning is enabled)
|
||||||
|
softforks: SoftFork[]; // (array) status of softforks in progress
|
||||||
|
bip9_softforks: { [name: string]: Bip9SoftForks[] } // (object) status of BIP9 softforks in progress
|
||||||
|
warnings: string; // (string) any network and blockchain warnings.
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SoftFork {
|
||||||
|
id: string; // (string) name of softfork
|
||||||
|
version: number; // (numeric) block version
|
||||||
|
reject: { // (object) progress toward rejecting pre-softfork blocks
|
||||||
|
status: boolean; // (boolean) true if threshold reached
|
||||||
|
},
|
||||||
|
}
|
||||||
|
interface Bip9SoftForks {
|
||||||
|
status: number; // (string) one of defined, started, locked_in, active, failed
|
||||||
|
bit: number; // (numeric) the bit (0-28) in the block version field used to signal this softfork (only for started status)
|
||||||
|
startTime: number; // (numeric) the minimum median time past of a block at which the bit gains its meaning
|
||||||
|
timeout: number; // (numeric) the median time past of a block at which the deployment is considered failed if not yet locked in
|
||||||
|
since: number; // (numeric) height of the first block to which the status applies
|
||||||
|
statistics: { // (object) numeric statistics about BIP9 signalling for a softfork (only for started status)
|
||||||
|
period: number; // (numeric) the length in blocks of the BIP9 signalling period
|
||||||
|
threshold: number; // (numeric) the number of blocks with the version bit set required to activate the feature
|
||||||
|
elapsed: number; // (numeric) the number of blocks elapsed since the beginning of the current period
|
||||||
|
count: number; // (numeric) the number of blocks with the version bit set in the current period
|
||||||
|
possible: boolean; // (boolean) returns false if there are not enough blocks left in this period to pass activation threshold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import config from '../../config';
|
|
||||||
import * as bitcoin from '@mempool/bitcoin';
|
|
||||||
import * as bitcoinjs from 'bitcoinjs-lib';
|
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
|
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
|
||||||
import { IBitcoinApi } from './bitcoin-api.interface';
|
import { IBitcoinApi } from './bitcoin-api.interface';
|
||||||
@@ -10,16 +8,10 @@ import { TransactionExtended } from '../../mempool.interfaces';
|
|||||||
|
|
||||||
class BitcoinApi implements AbstractBitcoinApi {
|
class BitcoinApi implements AbstractBitcoinApi {
|
||||||
private rawMempoolCache: IBitcoinApi.RawMempool | null = null;
|
private rawMempoolCache: IBitcoinApi.RawMempool | null = null;
|
||||||
private bitcoindClient: any;
|
protected bitcoindClient: any;
|
||||||
|
|
||||||
constructor() {
|
constructor(bitcoinClient: any) {
|
||||||
this.bitcoindClient = new bitcoin.Client({
|
this.bitcoindClient = bitcoinClient;
|
||||||
host: config.CORE_RPC.HOST,
|
|
||||||
port: config.CORE_RPC.PORT,
|
|
||||||
user: config.CORE_RPC.USERNAME,
|
|
||||||
pass: config.CORE_RPC.PASSWORD,
|
|
||||||
timeout: 60000,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$getRawTransaction(txId: string, skipConversion = false, addPrevout = false): Promise<IEsploraApi.Transaction> {
|
$getRawTransaction(txId: string, skipConversion = false, addPrevout = false): Promise<IEsploraApi.Transaction> {
|
||||||
@@ -56,10 +48,18 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
.then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx);
|
.then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$getRawBlock(hash: string): Promise<string> {
|
||||||
|
return this.bitcoindClient.getBlock(hash, 0);
|
||||||
|
}
|
||||||
|
|
||||||
$getBlockHash(height: number): Promise<string> {
|
$getBlockHash(height: number): Promise<string> {
|
||||||
return this.bitcoindClient.getBlockHash(height);
|
return this.bitcoindClient.getBlockHash(height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$getBlockHeader(hash: string): Promise<string> {
|
||||||
|
return this.bitcoindClient.getBlockHeader(hash, false);
|
||||||
|
}
|
||||||
|
|
||||||
async $getBlock(hash: string): Promise<IEsploraApi.Block> {
|
async $getBlock(hash: string): Promise<IEsploraApi.Block> {
|
||||||
const foundBlock = blocks.getBlocks().find((block) => block.id === hash);
|
const foundBlock = blocks.getBlocks().find((block) => block.id === hash);
|
||||||
if (foundBlock) {
|
if (foundBlock) {
|
||||||
@@ -98,6 +98,10 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
return found;
|
return found;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$sendRawTransaction(rawTransaction: string): Promise<string> {
|
||||||
|
return this.bitcoindClient.sendRawTransaction(rawTransaction);
|
||||||
|
}
|
||||||
|
|
||||||
protected async $convertTransaction(transaction: IBitcoinApi.Transaction, addPrevout: boolean): Promise<IEsploraApi.Transaction> {
|
protected async $convertTransaction(transaction: IBitcoinApi.Transaction, addPrevout: boolean): Promise<IEsploraApi.Transaction> {
|
||||||
let esploraTransaction: IEsploraApi.Transaction = {
|
let esploraTransaction: IEsploraApi.Transaction = {
|
||||||
txid: transaction.txid,
|
txid: transaction.txid,
|
||||||
@@ -115,7 +119,8 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
return {
|
return {
|
||||||
value: vout.value * 100000000,
|
value: vout.value * 100000000,
|
||||||
scriptpubkey: vout.scriptPubKey.hex,
|
scriptpubkey: vout.scriptPubKey.hex,
|
||||||
scriptpubkey_address: vout.scriptPubKey && vout.scriptPubKey.addresses ? vout.scriptPubKey.addresses[0] : '',
|
scriptpubkey_address: vout.scriptPubKey && vout.scriptPubKey.address ? vout.scriptPubKey.address
|
||||||
|
: vout.scriptPubKey.addresses ? vout.scriptPubKey.addresses[0] : '',
|
||||||
scriptpubkey_asm: vout.scriptPubKey.asm ? this.convertScriptSigAsm(vout.scriptPubKey.asm) : '',
|
scriptpubkey_asm: vout.scriptPubKey.asm ? this.convertScriptSigAsm(vout.scriptPubKey.asm) : '',
|
||||||
scriptpubkey_type: this.translateScriptPubKeyType(vout.scriptPubKey.type),
|
scriptpubkey_type: this.translateScriptPubKeyType(vout.scriptPubKey.type),
|
||||||
};
|
};
|
||||||
@@ -229,10 +234,6 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected $validateAddress(address: string): Promise<IBitcoinApi.AddressInformation> {
|
|
||||||
return this.bitcoindClient.validateAddress(address);
|
|
||||||
}
|
|
||||||
|
|
||||||
private $getMempoolEntry(txid: string): Promise<IBitcoinApi.MempoolEntry> {
|
private $getMempoolEntry(txid: string): Promise<IBitcoinApi.MempoolEntry> {
|
||||||
return this.bitcoindClient.getMempoolEntry(txid);
|
return this.bitcoindClient.getMempoolEntry(txid);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
import config from '../../config';
|
|
||||||
import * as bitcoin from '@mempool/bitcoin';
|
|
||||||
import { IBitcoinApi } from './bitcoin-api.interface';
|
|
||||||
|
|
||||||
class BitcoinBaseApi {
|
|
||||||
bitcoindClient: any;
|
|
||||||
bitcoindClientMempoolInfo: any;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.bitcoindClient = new bitcoin.Client({
|
|
||||||
host: config.CORE_RPC.HOST,
|
|
||||||
port: config.CORE_RPC.PORT,
|
|
||||||
user: config.CORE_RPC.USERNAME,
|
|
||||||
pass: config.CORE_RPC.PASSWORD,
|
|
||||||
timeout: 60000,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (config.CORE_RPC_MINFEE.ENABLED) {
|
|
||||||
this.bitcoindClientMempoolInfo = new bitcoin.Client({
|
|
||||||
host: config.CORE_RPC_MINFEE.HOST,
|
|
||||||
port: config.CORE_RPC_MINFEE.PORT,
|
|
||||||
user: config.CORE_RPC_MINFEE.USERNAME,
|
|
||||||
pass: config.CORE_RPC_MINFEE.PASSWORD,
|
|
||||||
timeout: 60000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$getMempoolInfo(): Promise<IBitcoinApi.MempoolInfo> {
|
|
||||||
if (config.CORE_RPC_MINFEE.ENABLED) {
|
|
||||||
return Promise.all([
|
|
||||||
this.bitcoindClient.getMempoolInfo(),
|
|
||||||
this.bitcoindClientMempoolInfo.getMempoolInfo()
|
|
||||||
]).then(([mempoolInfo, secondMempoolInfo]) => {
|
|
||||||
mempoolInfo.maxmempool = secondMempoolInfo.maxmempool;
|
|
||||||
mempoolInfo.mempoolminfee = secondMempoolInfo.mempoolminfee;
|
|
||||||
mempoolInfo.minrelaytxfee = secondMempoolInfo.minrelaytxfee;
|
|
||||||
return mempoolInfo;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return this.bitcoindClient.getMempoolInfo();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new BitcoinBaseApi();
|
|
||||||
13
backend/src/api/bitcoin/bitcoin-client.ts
Normal file
13
backend/src/api/bitcoin/bitcoin-client.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import config from '../../config';
|
||||||
|
import * as bitcoin from '@mempool/bitcoin';
|
||||||
|
import { BitcoinRpcCredentials } from './bitcoin-api-abstract-factory';
|
||||||
|
|
||||||
|
const nodeRpcCredentials: BitcoinRpcCredentials = {
|
||||||
|
host: config.CORE_RPC.HOST,
|
||||||
|
port: config.CORE_RPC.PORT,
|
||||||
|
user: config.CORE_RPC.USERNAME,
|
||||||
|
pass: config.CORE_RPC.PASSWORD,
|
||||||
|
timeout: 60000,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default new bitcoin.Client(nodeRpcCredentials);
|
||||||
13
backend/src/api/bitcoin/bitcoin-second-client.ts
Normal file
13
backend/src/api/bitcoin/bitcoin-second-client.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import config from '../../config';
|
||||||
|
import * as bitcoin from '@mempool/bitcoin';
|
||||||
|
import { BitcoinRpcCredentials } from './bitcoin-api-abstract-factory';
|
||||||
|
|
||||||
|
const nodeRpcCredentials: BitcoinRpcCredentials = {
|
||||||
|
host: config.SECOND_CORE_RPC.HOST,
|
||||||
|
port: config.SECOND_CORE_RPC.PORT,
|
||||||
|
user: config.SECOND_CORE_RPC.USERNAME,
|
||||||
|
pass: config.SECOND_CORE_RPC.PASSWORD,
|
||||||
|
timeout: 60000,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default new bitcoin.Client(nodeRpcCredentials);
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
|
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
|
||||||
import { IBitcoinApi } from './bitcoin-api.interface';
|
|
||||||
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 mempool from '../mempool';
|
|
||||||
import logger from '../../logger';
|
import logger from '../../logger';
|
||||||
import * as ElectrumClient from '@mempool/electrum-client';
|
import * as ElectrumClient from '@mempool/electrum-client';
|
||||||
import * as sha256 from 'crypto-js/sha256';
|
import * as sha256 from 'crypto-js/sha256';
|
||||||
@@ -15,8 +13,8 @@ import memoryCache from '../memory-cache';
|
|||||||
class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
|
class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
|
||||||
private electrumClient: any;
|
private electrumClient: any;
|
||||||
|
|
||||||
constructor() {
|
constructor(bitcoinClient: any) {
|
||||||
super();
|
super(bitcoinClient);
|
||||||
|
|
||||||
const electrumConfig = { client: 'mempool-v2', version: '1.4' };
|
const electrumConfig = { client: 'mempool-v2', version: '1.4' };
|
||||||
const electrumPersistencePolicy = { retryPeriod: 10000, maxRetry: 1000, callback: null };
|
const electrumPersistencePolicy = { retryPeriod: 10000, maxRetry: 1000, callback: null };
|
||||||
@@ -44,7 +42,7 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async $getAddress(address: string): Promise<IEsploraApi.Address> {
|
async $getAddress(address: string): Promise<IEsploraApi.Address> {
|
||||||
const addressInfo = await this.$validateAddress(address);
|
const addressInfo = await this.bitcoindClient.validateAddress(address);
|
||||||
if (!addressInfo || !addressInfo.isvalid) {
|
if (!addressInfo || !addressInfo.isvalid) {
|
||||||
return ({
|
return ({
|
||||||
'address': address,
|
'address': address,
|
||||||
@@ -93,12 +91,12 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
|
|||||||
if (e === 'failed to get confirmed status') {
|
if (e === 'failed to get confirmed status') {
|
||||||
e = 'The number of transactions on this address exceeds the Electrum server limit';
|
e = 'The number of transactions on this address exceeds the Electrum server limit';
|
||||||
}
|
}
|
||||||
throw new Error(e);
|
throw new Error(typeof e === 'string' ? e : 'Error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async $getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]> {
|
async $getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]> {
|
||||||
const addressInfo = await this.$validateAddress(address);
|
const addressInfo = await this.bitcoindClient.validateAddress(address);
|
||||||
if (!addressInfo || !addressInfo.isvalid) {
|
if (!addressInfo || !addressInfo.isvalid) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -131,7 +129,7 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
|
|||||||
if (e === 'failed to get confirmed status') {
|
if (e === 'failed to get confirmed status') {
|
||||||
e = 'The number of transactions on this address exceeds the Electrum server limit';
|
e = 'The number of transactions on this address exceeds the Electrum server limit';
|
||||||
}
|
}
|
||||||
throw new Error(e);
|
throw new Error(typeof e === 'string' ? e : 'Error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export namespace IEsploraApi {
|
|||||||
vin: Vin[];
|
vin: Vin[];
|
||||||
vout: Vout[];
|
vout: Vout[];
|
||||||
status: Status;
|
status: Status;
|
||||||
|
hex?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Recent {
|
export interface Recent {
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ class ElectrsApi implements AbstractBitcoinApi {
|
|||||||
.then((response) => response.data);
|
.then((response) => response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$getBlockHeader(hash: string): Promise<string> {
|
||||||
|
return axios.get<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/header', this.axiosConfig)
|
||||||
|
.then((response) => response.data);
|
||||||
|
}
|
||||||
|
|
||||||
$getBlock(hash: string): Promise<IEsploraApi.Block> {
|
$getBlock(hash: string): Promise<IEsploraApi.Block> {
|
||||||
return axios.get<IEsploraApi.Block>(config.ESPLORA.REST_API_URL + '/block/' + hash, this.axiosConfig)
|
return axios.get<IEsploraApi.Block>(config.ESPLORA.REST_API_URL + '/block/' + hash, this.axiosConfig)
|
||||||
.then((response) => response.data);
|
.then((response) => response.data);
|
||||||
@@ -51,6 +56,10 @@ class ElectrsApi implements AbstractBitcoinApi {
|
|||||||
$getAddressPrefix(prefix: string): string[] {
|
$getAddressPrefix(prefix: string): string[] {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$sendRawTransaction(rawTransaction: string): Promise<string> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ElectrsApi;
|
export default ElectrsApi;
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import { BlockExtended, TransactionExtended } 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';
|
||||||
|
|
||||||
class Blocks {
|
class Blocks {
|
||||||
private static INITIAL_BLOCK_AMOUNT = 8;
|
|
||||||
private blocks: BlockExtended[] = [];
|
private blocks: BlockExtended[] = [];
|
||||||
private currentBlockHeight = 0;
|
private currentBlockHeight = 0;
|
||||||
|
private currentDifficulty = 0;
|
||||||
private lastDifficultyAdjustmentTime = 0;
|
private lastDifficultyAdjustmentTime = 0;
|
||||||
|
private previousDifficultyRetarget = 0;
|
||||||
private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
|
private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
|
||||||
|
|
||||||
constructor() { }
|
constructor() { }
|
||||||
@@ -32,21 +34,32 @@ class Blocks {
|
|||||||
const blockHeightTip = await bitcoinApi.$getBlockHeightTip();
|
const blockHeightTip = await bitcoinApi.$getBlockHeightTip();
|
||||||
|
|
||||||
if (this.blocks.length === 0) {
|
if (this.blocks.length === 0) {
|
||||||
this.currentBlockHeight = blockHeightTip - Blocks.INITIAL_BLOCK_AMOUNT;
|
this.currentBlockHeight = blockHeightTip - config.MEMPOOL.INITIAL_BLOCKS_AMOUNT;
|
||||||
} else {
|
} else {
|
||||||
this.currentBlockHeight = this.blocks[this.blocks.length - 1].height;
|
this.currentBlockHeight = this.blocks[this.blocks.length - 1].height;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blockHeightTip - this.currentBlockHeight > Blocks.INITIAL_BLOCK_AMOUNT * 2) {
|
if (blockHeightTip - this.currentBlockHeight > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 2) {
|
||||||
logger.info(`${blockHeightTip - this.currentBlockHeight} blocks since tip. Fast forwarding to the ${Blocks.INITIAL_BLOCK_AMOUNT} recent blocks`);
|
logger.info(`${blockHeightTip - this.currentBlockHeight} blocks since tip. Fast forwarding to the ${config.MEMPOOL.INITIAL_BLOCKS_AMOUNT} recent blocks`);
|
||||||
this.currentBlockHeight = blockHeightTip - Blocks.INITIAL_BLOCK_AMOUNT;
|
this.currentBlockHeight = blockHeightTip - config.MEMPOOL.INITIAL_BLOCKS_AMOUNT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.lastDifficultyAdjustmentTime) {
|
if (!this.lastDifficultyAdjustmentTime) {
|
||||||
const heightDiff = blockHeightTip % 2016;
|
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
|
||||||
const blockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff);
|
if (blockchainInfo.blocks === blockchainInfo.headers) {
|
||||||
const block = await bitcoinApi.$getBlock(blockHash);
|
const heightDiff = blockHeightTip % 2016;
|
||||||
this.lastDifficultyAdjustmentTime = block.timestamp;
|
const blockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff);
|
||||||
|
const block = await bitcoinApi.$getBlock(blockHash);
|
||||||
|
this.lastDifficultyAdjustmentTime = block.timestamp;
|
||||||
|
this.currentDifficulty = block.difficulty;
|
||||||
|
|
||||||
|
const previousPeriodBlockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff - 2016);
|
||||||
|
const previousPeriodBlock = await bitcoinApi.$getBlock(previousPeriodBlockHash);
|
||||||
|
this.previousDifficultyRetarget = (block.difficulty - previousPeriodBlock.difficulty) / previousPeriodBlock.difficulty * 100;
|
||||||
|
logger.debug(`Initial difficulty adjustment data set.`);
|
||||||
|
} else {
|
||||||
|
logger.debug(`Blockchain headers (${blockchainInfo.headers}) and blocks (${blockchainInfo.blocks}) not in sync. Waiting...`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
while (this.currentBlockHeight < blockHeightTip) {
|
while (this.currentBlockHeight < blockHeightTip) {
|
||||||
@@ -76,7 +89,7 @@ class Blocks {
|
|||||||
const tx = await transactionUtils.$getTransactionExtended(txIds[i]);
|
const tx = await transactionUtils.$getTransactionExtended(txIds[i]);
|
||||||
transactions.push(tx);
|
transactions.push(tx);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Error fetching block tx: ' + e.message || e);
|
logger.debug('Error fetching block tx: ' + (e instanceof Error ? e.message : e));
|
||||||
if (i === 0) {
|
if (i === 0) {
|
||||||
throw new Error('Failed to fetch Coinbase transaction: ' + txIds[i]);
|
throw new Error('Failed to fetch Coinbase transaction: ' + txIds[i]);
|
||||||
}
|
}
|
||||||
@@ -97,16 +110,18 @@ class Blocks {
|
|||||||
blockExtended.coinbaseTx = transactionUtils.stripCoinbaseTransaction(transactions[0]);
|
blockExtended.coinbaseTx = transactionUtils.stripCoinbaseTransaction(transactions[0]);
|
||||||
transactions.shift();
|
transactions.shift();
|
||||||
transactions.sort((a, b) => b.effectiveFeePerVsize - a.effectiveFeePerVsize);
|
transactions.sort((a, b) => b.effectiveFeePerVsize - a.effectiveFeePerVsize);
|
||||||
blockExtended.medianFee = transactions.length > 1 ? Common.median(transactions.map((tx) => tx.effectiveFeePerVsize)) : 0;
|
blockExtended.medianFee = transactions.length > 0 ? Common.median(transactions.map((tx) => tx.effectiveFeePerVsize)) : 0;
|
||||||
blockExtended.feeRange = transactions.length > 1 ? Common.getFeesInRange(transactions, 8) : [0, 0];
|
blockExtended.feeRange = transactions.length > 0 ? Common.getFeesInRange(transactions, 8) : [0, 0];
|
||||||
|
|
||||||
if (block.height % 2016 === 0) {
|
if (block.height % 2016 === 0) {
|
||||||
|
this.previousDifficultyRetarget = (block.difficulty - this.currentDifficulty) / this.currentDifficulty * 100;
|
||||||
this.lastDifficultyAdjustmentTime = block.timestamp;
|
this.lastDifficultyAdjustmentTime = block.timestamp;
|
||||||
|
this.currentDifficulty = block.difficulty;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.blocks.push(blockExtended);
|
this.blocks.push(blockExtended);
|
||||||
if (this.blocks.length > Blocks.INITIAL_BLOCK_AMOUNT * 4) {
|
if (this.blocks.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) {
|
||||||
this.blocks = this.blocks.slice(-Blocks.INITIAL_BLOCK_AMOUNT * 4);
|
this.blocks = this.blocks.slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.newBlockCallbacks.length) {
|
if (this.newBlockCallbacks.length) {
|
||||||
@@ -122,6 +137,10 @@ class Blocks {
|
|||||||
return this.lastDifficultyAdjustmentTime;
|
return this.lastDifficultyAdjustmentTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getPreviousDifficultyRetarget(): number {
|
||||||
|
return this.previousDifficultyRetarget;
|
||||||
|
}
|
||||||
|
|
||||||
public getCurrentBlockHeight(): number {
|
public getCurrentBlockHeight(): number {
|
||||||
return this.currentBlockHeight;
|
return this.currentBlockHeight;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { CpfpInfo, TransactionExtended, TransactionStripped } from '../mempool.interfaces';
|
import { CpfpInfo, TransactionExtended, TransactionStripped } from '../mempool.interfaces';
|
||||||
|
import config from '../config';
|
||||||
export class Common {
|
export class Common {
|
||||||
|
static nativeAssetId = '6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d';
|
||||||
|
|
||||||
static median(numbers: number[]) {
|
static median(numbers: number[]) {
|
||||||
let medianNr = 0;
|
let medianNr = 0;
|
||||||
const numsLen = numbers.length;
|
const numsLen = numbers.length;
|
||||||
@@ -105,7 +107,7 @@ export class Common {
|
|||||||
totalFees += tx.bestDescendant.fee;
|
totalFees += tx.bestDescendant.fee;
|
||||||
}
|
}
|
||||||
|
|
||||||
tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
|
tx.effectiveFeePerVsize = Math.max(config.MEMPOOL.NETWORK === 'liquid' ? 0.1 : 1, totalFees / (totalWeight / 4));
|
||||||
tx.cpfpChecked = true;
|
tx.cpfpChecked = true;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class DiskCache {
|
|||||||
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;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn('Error writing to cache file: ' + e.message || e);
|
logger.warn('Error writing to cache file: ' + (e instanceof Error ? e.message : e));
|
||||||
this.isWritingCache = false;
|
this.isWritingCache = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { IConversionRates } from '../mempool.interfaces';
|
import { IConversionRates } from '../mempool.interfaces';
|
||||||
|
import config from '../config';
|
||||||
|
|
||||||
class FiatConversion {
|
class FiatConversion {
|
||||||
private conversionRates: IConversionRates = {
|
private conversionRates: IConversionRates = {
|
||||||
@@ -16,7 +17,7 @@ class FiatConversion {
|
|||||||
|
|
||||||
public startService() {
|
public startService() {
|
||||||
logger.info('Starting currency rates service');
|
logger.info('Starting currency rates service');
|
||||||
setInterval(this.updateCurrency.bind(this), 1000 * 60);
|
setInterval(this.updateCurrency.bind(this), 1000 * config.MEMPOOL.PRICE_FEED_UPDATE_INTERVAL);
|
||||||
this.updateCurrency();
|
this.updateCurrency();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ class FiatConversion {
|
|||||||
this.ratesChangedCallback(this.conversionRates);
|
this.ratesChangedCallback(this.conversionRates);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('Error updating fiat conversion rates: ' + e);
|
logger.err('Error updating fiat conversion rates: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
111
backend/src/api/liquid/elements-parser.ts
Normal file
111
backend/src/api/liquid/elements-parser.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { IBitcoinApi } from '../bitcoin/bitcoin-api.interface';
|
||||||
|
import bitcoinClient from '../bitcoin/bitcoin-client';
|
||||||
|
import bitcoinSecondClient from '../bitcoin/bitcoin-second-client';
|
||||||
|
import { Common } from '../common';
|
||||||
|
import { DB } from '../../database';
|
||||||
|
import logger from '../../logger';
|
||||||
|
|
||||||
|
class ElementsParser {
|
||||||
|
private isRunning = false;
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
public async $parse() {
|
||||||
|
if (this.isRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.isRunning = true;
|
||||||
|
const result = await bitcoinClient.getChainTips();
|
||||||
|
const tip = result[0].height;
|
||||||
|
const latestBlock = await this.$getLatestBlockFromDatabase();
|
||||||
|
for (let height = latestBlock.block + 1; height <= tip; height++) {
|
||||||
|
const blockHash: IBitcoinApi.ChainTips = await bitcoinClient.getBlockHash(height);
|
||||||
|
const block: IBitcoinApi.Block = await bitcoinClient.getBlock(blockHash, 2);
|
||||||
|
await this.$parseBlock(block);
|
||||||
|
await this.$saveLatestBlockToDatabase(block.height, block.time, block.hash);
|
||||||
|
}
|
||||||
|
this.isRunning = false;
|
||||||
|
} catch (e) {
|
||||||
|
this.isRunning = false;
|
||||||
|
throw new Error(e instanceof Error ? e.message : 'Error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getPegDataByMonth(): Promise<any> {
|
||||||
|
const connection = await DB.pool.getConnection();
|
||||||
|
const query = `SELECT SUM(amount) AS amount, DATE_FORMAT(FROM_UNIXTIME(datetime), '%Y-%m-01') AS date FROM elements_pegs GROUP BY DATE_FORMAT(FROM_UNIXTIME(datetime), '%Y%m')`;
|
||||||
|
const [rows] = await connection.query<any>(query);
|
||||||
|
connection.release();
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async $parseBlock(block: IBitcoinApi.Block) {
|
||||||
|
for (const tx of block.tx) {
|
||||||
|
await this.$parseInputs(tx, block);
|
||||||
|
await this.$parseOutputs(tx, block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async $parseInputs(tx: IBitcoinApi.Transaction, block: IBitcoinApi.Block) {
|
||||||
|
for (const [index, input] of tx.vin.entries()) {
|
||||||
|
if (input.is_pegin) {
|
||||||
|
await this.$parsePegIn(input, index, tx.txid, block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async $parsePegIn(input: IBitcoinApi.Vin, vindex: number, txid: string, block: IBitcoinApi.Block) {
|
||||||
|
const bitcoinTx: IBitcoinApi.Transaction = await bitcoinSecondClient.getRawTransaction(input.txid, true);
|
||||||
|
const prevout = bitcoinTx.vout[input.vout || 0];
|
||||||
|
const outputAddress = prevout.scriptPubKey.address || (prevout.scriptPubKey.addresses && prevout.scriptPubKey.addresses[0]) || '';
|
||||||
|
await this.$savePegToDatabase(block.height, block.time, prevout.value * 100000000, txid, vindex,
|
||||||
|
outputAddress, bitcoinTx.txid, prevout.n, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async $parseOutputs(tx: IBitcoinApi.Transaction, block: IBitcoinApi.Block) {
|
||||||
|
for (const output of tx.vout) {
|
||||||
|
if (output.scriptPubKey.pegout_chain) {
|
||||||
|
await this.$savePegToDatabase(block.height, block.time, 0 - output.value * 100000000, tx.txid, output.n,
|
||||||
|
(output.scriptPubKey.pegout_addresses && output.scriptPubKey.pegout_addresses[0] || ''), '', 0, 0);
|
||||||
|
}
|
||||||
|
if (!output.scriptPubKey.pegout_chain && output.scriptPubKey.type === 'nulldata'
|
||||||
|
&& output.value && output.value > 0 && output.asset && output.asset === Common.nativeAssetId) {
|
||||||
|
await this.$savePegToDatabase(block.height, block.time, 0 - output.value * 100000000, tx.txid, output.n,
|
||||||
|
(output.scriptPubKey.pegout_addresses && output.scriptPubKey.pegout_addresses[0] || ''), '', 0, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async $savePegToDatabase(height: number, blockTime: number, amount: number, txid: string,
|
||||||
|
txindex: number, bitcoinaddress: string, bitcointxid: string, bitcoinindex: number, final_tx: number): Promise<void> {
|
||||||
|
const connection = await DB.pool.getConnection();
|
||||||
|
const query = `INSERT INTO elements_pegs(
|
||||||
|
block, datetime, amount, txid, txindex, bitcoinaddress, bitcointxid, bitcoinindex, final_tx
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
||||||
|
|
||||||
|
const params: (string | number)[] = [
|
||||||
|
height, blockTime, amount, txid, txindex, bitcoinaddress, bitcointxid, bitcoinindex, final_tx
|
||||||
|
];
|
||||||
|
await connection.query(query, params);
|
||||||
|
connection.release();
|
||||||
|
logger.debug(`Saved L-BTC peg from block height #${height} with TXID ${txid}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async $getLatestBlockFromDatabase(): Promise<any> {
|
||||||
|
const connection = await DB.pool.getConnection();
|
||||||
|
const query = `SELECT block, datetime, block_hash FROM last_elements_block`;
|
||||||
|
const [rows] = await connection.query<any>(query);
|
||||||
|
connection.release();
|
||||||
|
return rows[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async $saveLatestBlockToDatabase(blockHeight: number, datetime: number, blockHash: string) {
|
||||||
|
const connection = await DB.pool.getConnection();
|
||||||
|
const query = `UPDATE last_elements_block SET block = ?, datetime = ?, block_hash = ?`;
|
||||||
|
await connection.query<any>(query, [blockHeight, datetime, blockHash]);
|
||||||
|
connection.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new ElementsParser();
|
||||||
@@ -4,7 +4,6 @@ import { Common } from './common';
|
|||||||
import config from '../config';
|
import config from '../config';
|
||||||
|
|
||||||
class MempoolBlocks {
|
class MempoolBlocks {
|
||||||
private static DEFAULT_PROJECTED_BLOCKS_AMOUNT = 8;
|
|
||||||
private mempoolBlocks: MempoolBlockWithTransactions[] = [];
|
private mempoolBlocks: MempoolBlockWithTransactions[] = [];
|
||||||
|
|
||||||
constructor() {}
|
constructor() {}
|
||||||
@@ -72,29 +71,30 @@ class MempoolBlocks {
|
|||||||
|
|
||||||
private calculateMempoolBlocks(transactionsSorted: TransactionExtended[]): MempoolBlockWithTransactions[] {
|
private calculateMempoolBlocks(transactionsSorted: TransactionExtended[]): MempoolBlockWithTransactions[] {
|
||||||
const mempoolBlocks: MempoolBlockWithTransactions[] = [];
|
const mempoolBlocks: MempoolBlockWithTransactions[] = [];
|
||||||
let blockVSize = 0;
|
let blockWeight = 0;
|
||||||
let blockSize = 0;
|
let blockSize = 0;
|
||||||
let transactions: TransactionExtended[] = [];
|
let transactions: TransactionExtended[] = [];
|
||||||
transactionsSorted.forEach((tx) => {
|
transactionsSorted.forEach((tx) => {
|
||||||
if (blockVSize + tx.vsize <= 1000000 || mempoolBlocks.length === MempoolBlocks.DEFAULT_PROJECTED_BLOCKS_AMOUNT - 1) {
|
if (blockWeight + tx.weight <= config.MEMPOOL.BLOCK_WEIGHT_UNITS
|
||||||
blockVSize += tx.vsize;
|
|| mempoolBlocks.length === config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT - 1) {
|
||||||
|
blockWeight += tx.weight;
|
||||||
blockSize += tx.size;
|
blockSize += tx.size;
|
||||||
transactions.push(tx);
|
transactions.push(tx);
|
||||||
} else {
|
} else {
|
||||||
mempoolBlocks.push(this.dataToMempoolBlocks(transactions, blockSize, blockVSize, mempoolBlocks.length));
|
mempoolBlocks.push(this.dataToMempoolBlocks(transactions, blockSize, blockWeight, mempoolBlocks.length));
|
||||||
blockVSize = tx.vsize;
|
blockWeight = tx.weight;
|
||||||
blockSize = tx.size;
|
blockSize = tx.size;
|
||||||
transactions = [tx];
|
transactions = [tx];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (transactions.length) {
|
if (transactions.length) {
|
||||||
mempoolBlocks.push(this.dataToMempoolBlocks(transactions, blockSize, blockVSize, mempoolBlocks.length));
|
mempoolBlocks.push(this.dataToMempoolBlocks(transactions, blockSize, blockWeight, mempoolBlocks.length));
|
||||||
}
|
}
|
||||||
return mempoolBlocks;
|
return mempoolBlocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
private dataToMempoolBlocks(transactions: TransactionExtended[],
|
private dataToMempoolBlocks(transactions: TransactionExtended[],
|
||||||
blockSize: number, blockVSize: number, blocksIndex: number): MempoolBlockWithTransactions {
|
blockSize: number, blockWeight: number, blocksIndex: number): MempoolBlockWithTransactions {
|
||||||
let rangeLength = 4;
|
let rangeLength = 4;
|
||||||
if (blocksIndex === 0) {
|
if (blocksIndex === 0) {
|
||||||
rangeLength = 8;
|
rangeLength = 8;
|
||||||
@@ -106,7 +106,7 @@ class MempoolBlocks {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
blockSize: blockSize,
|
blockSize: blockSize,
|
||||||
blockVSize: blockVSize,
|
blockVSize: blockWeight / 4,
|
||||||
nTx: transactions.length,
|
nTx: transactions.length,
|
||||||
totalFees: transactions.reduce((acc, cur) => acc + cur.fee, 0),
|
totalFees: transactions.reduce((acc, cur) => acc + cur.fee, 0),
|
||||||
medianFee: Common.percentile(transactions.map((tx) => tx.effectiveFeePerVsize), config.MEMPOOL.RECOMMENDED_FEE_PERCENTILE),
|
medianFee: Common.percentile(transactions.map((tx) => tx.effectiveFeePerVsize), config.MEMPOOL.RECOMMENDED_FEE_PERCENTILE),
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import logger from '../logger';
|
|||||||
import { Common } from './common';
|
import { Common } from './common';
|
||||||
import transactionUtils from './transaction-utils';
|
import transactionUtils from './transaction-utils';
|
||||||
import { IBitcoinApi } from './bitcoin/bitcoin-api.interface';
|
import { IBitcoinApi } from './bitcoin/bitcoin-api.interface';
|
||||||
import bitcoinBaseApi from './bitcoin/bitcoin-base.api';
|
|
||||||
import loadingIndicators from './loading-indicators';
|
import loadingIndicators from './loading-indicators';
|
||||||
|
import bitcoinClient from './bitcoin/bitcoin-client';
|
||||||
|
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
||||||
|
|
||||||
class Mempool {
|
class Mempool {
|
||||||
private static WEBSOCKET_REFRESH_RATE_MS = 10000;
|
private static WEBSOCKET_REFRESH_RATE_MS = 10000;
|
||||||
@@ -61,7 +62,7 @@ class Mempool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async $updateMemPoolInfo() {
|
public async $updateMemPoolInfo() {
|
||||||
this.mempoolInfo = await bitcoinBaseApi.$getMempoolInfo();
|
this.mempoolInfo = await this.$getMempoolInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getMempoolInfo(): IBitcoinApi.MempoolInfo {
|
public getMempoolInfo(): IBitcoinApi.MempoolInfo {
|
||||||
@@ -124,7 +125,7 @@ class Mempool {
|
|||||||
}
|
}
|
||||||
newTransactions.push(transaction);
|
newTransactions.push(transaction);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Error finding transaction in mempool: ' + e.message || e);
|
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,6 +206,21 @@ class Mempool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private $getMempoolInfo() {
|
||||||
|
if (config.MEMPOOL.USE_SECOND_NODE_FOR_MINFEE) {
|
||||||
|
return Promise.all([
|
||||||
|
bitcoinClient.getMempoolInfo(),
|
||||||
|
bitcoinSecondClient.getMempoolInfo()
|
||||||
|
]).then(([mempoolInfo, secondMempoolInfo]) => {
|
||||||
|
mempoolInfo.maxmempool = secondMempoolInfo.maxmempool;
|
||||||
|
mempoolInfo.mempoolminfee = secondMempoolInfo.mempoolminfee;
|
||||||
|
mempoolInfo.minrelaytxfee = secondMempoolInfo.minrelaytxfee;
|
||||||
|
return mempoolInfo;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return bitcoinClient.getMempoolInfo();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Mempool();
|
export default new Mempool();
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ class Statistics {
|
|||||||
connection.release();
|
connection.release();
|
||||||
return result.insertId;
|
return result.insertId;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$create() error' + e.message || e);
|
logger.err('$create() error' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,7 +313,7 @@ class Statistics {
|
|||||||
return this.mapStatisticToOptimizedStatistic([rows[0]])[0];
|
return this.mapStatisticToOptimizedStatistic([rows[0]])[0];
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$list2H() error' + e.message || e);
|
logger.err('$list2H() error' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,7 +325,7 @@ class Statistics {
|
|||||||
connection.release();
|
connection.release();
|
||||||
return this.mapStatisticToOptimizedStatistic(rows);
|
return this.mapStatisticToOptimizedStatistic(rows);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$list2H() error' + e.message || e);
|
logger.err('$list2H() error' + (e instanceof Error ? e.message : e));
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -338,7 +338,7 @@ class Statistics {
|
|||||||
connection.release();
|
connection.release();
|
||||||
return this.mapStatisticToOptimizedStatistic(rows);
|
return this.mapStatisticToOptimizedStatistic(rows);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$list24h() error' + e.message || e);
|
logger.err('$list24h() error' + (e instanceof Error ? e.message : e));
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -351,7 +351,7 @@ class Statistics {
|
|||||||
connection.release();
|
connection.release();
|
||||||
return this.mapStatisticToOptimizedStatistic(rows);
|
return this.mapStatisticToOptimizedStatistic(rows);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$list1W() error' + e);
|
logger.err('$list1W() error' + (e instanceof Error ? e.message : e));
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -364,7 +364,7 @@ class Statistics {
|
|||||||
connection.release();
|
connection.release();
|
||||||
return this.mapStatisticToOptimizedStatistic(rows);
|
return this.mapStatisticToOptimizedStatistic(rows);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$list1M() error' + e);
|
logger.err('$list1M() error' + (e instanceof Error ? e.message : e));
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -377,7 +377,7 @@ class Statistics {
|
|||||||
connection.release();
|
connection.release();
|
||||||
return this.mapStatisticToOptimizedStatistic(rows);
|
return this.mapStatisticToOptimizedStatistic(rows);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$list3M() error' + e);
|
logger.err('$list3M() error' + (e instanceof Error ? e.message : e));
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -390,7 +390,7 @@ class Statistics {
|
|||||||
connection.release();
|
connection.release();
|
||||||
return this.mapStatisticToOptimizedStatistic(rows);
|
return this.mapStatisticToOptimizedStatistic(rows);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$list6M() error' + e);
|
logger.err('$list6M() error' + (e instanceof Error ? e.message : e));
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -403,7 +403,7 @@ class Statistics {
|
|||||||
connection.release();
|
connection.release();
|
||||||
return this.mapStatisticToOptimizedStatistic(rows);
|
return this.mapStatisticToOptimizedStatistic(rows);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$list6M() error' + e);
|
logger.err('$list6M() error' + (e instanceof Error ? e.message : e));
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||||
import logger from '../logger';
|
|
||||||
import { TransactionExtended, TransactionMinerInfo } from '../mempool.interfaces';
|
import { TransactionExtended, TransactionMinerInfo } from '../mempool.interfaces';
|
||||||
import { IEsploraApi } from './bitcoin/esplora-api.interface';
|
import { IEsploraApi } from './bitcoin/esplora-api.interface';
|
||||||
|
import config from '../config';
|
||||||
|
|
||||||
class TransactionUtils {
|
class TransactionUtils {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
@@ -31,7 +31,7 @@ class TransactionUtils {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return transaction;
|
return transaction;
|
||||||
}
|
}
|
||||||
const feePerVbytes = Math.max(1, (transaction.fee || 0) / (transaction.weight / 4));
|
const feePerVbytes = Math.max(config.MEMPOOL.NETWORK === 'liquid' ? 0.1 : 1, (transaction.fee || 0) / (transaction.weight / 4));
|
||||||
const transactionExtended: TransactionExtended = Object.assign({
|
const transactionExtended: TransactionExtended = Object.assign({
|
||||||
vsize: Math.round(transaction.weight / 4),
|
vsize: Math.round(transaction.weight / 4),
|
||||||
feePerVsize: feePerVbytes,
|
feePerVsize: feePerVbytes,
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import transactionUtils from './transaction-utils';
|
|||||||
|
|
||||||
class WebsocketHandler {
|
class WebsocketHandler {
|
||||||
private wss: WebSocket.Server | undefined;
|
private wss: WebSocket.Server | undefined;
|
||||||
private nativeAssetId = '6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d';
|
|
||||||
private extraInitProperties = {};
|
private extraInitProperties = {};
|
||||||
|
|
||||||
constructor() { }
|
constructor() { }
|
||||||
@@ -53,18 +52,25 @@ class WebsocketHandler {
|
|||||||
if (parsedMessage['watch-mempool']) {
|
if (parsedMessage['watch-mempool']) {
|
||||||
const tx = memPool.getMempool()[client['track-tx']];
|
const tx = memPool.getMempool()[client['track-tx']];
|
||||||
if (tx) {
|
if (tx) {
|
||||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||||
|
response['tx'] = tx;
|
||||||
|
} else {
|
||||||
|
// tx.prevouts is missing from transactions when in bitcoind mode
|
||||||
try {
|
try {
|
||||||
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
|
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
|
||||||
response['tx'] = fullTx;
|
response['tx'] = fullTx;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Error finding transaction in mempool: ' + e.message || e);
|
logger.debug('Error finding transaction: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
response['tx'] = tx;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
client['track-mempool-tx'] = parsedMessage['track-tx'];
|
try {
|
||||||
|
const fullTx = await transactionUtils.$getTransactionExtended(client['track-tx'], true);
|
||||||
|
response['tx'] = fullTx;
|
||||||
|
} catch (e) {
|
||||||
|
logger.debug('Error finding transaction. ' + (e instanceof Error ? e.message : e));
|
||||||
|
client['track-mempool-tx'] = parsedMessage['track-tx'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -73,9 +79,13 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (parsedMessage && parsedMessage['track-address']) {
|
if (parsedMessage && parsedMessage['track-address']) {
|
||||||
if (/^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,87})$/
|
if (/^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100})$/
|
||||||
.test(parsedMessage['track-address'])) {
|
.test(parsedMessage['track-address'])) {
|
||||||
client['track-address'] = parsedMessage['track-address'];
|
let matchedAddress = parsedMessage['track-address'];
|
||||||
|
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(parsedMessage['track-address'])) {
|
||||||
|
matchedAddress = matchedAddress.toLowerCase();
|
||||||
|
}
|
||||||
|
client['track-address'] = matchedAddress;
|
||||||
} else {
|
} else {
|
||||||
client['track-address'] = null;
|
client['track-address'] = null;
|
||||||
}
|
}
|
||||||
@@ -90,7 +100,7 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (parsedMessage.action === 'init') {
|
if (parsedMessage.action === 'init') {
|
||||||
const _blocks = blocks.getBlocks().slice(-8);
|
const _blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT);
|
||||||
if (!_blocks) {
|
if (!_blocks) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -117,7 +127,7 @@ class WebsocketHandler {
|
|||||||
client.send(JSON.stringify(response));
|
client.send(JSON.stringify(response));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Error parsing websocket message: ' + e.message || e);
|
logger.debug('Error parsing websocket message: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -166,12 +176,13 @@ class WebsocketHandler {
|
|||||||
|
|
||||||
getInitData(_blocks?: BlockExtended[]) {
|
getInitData(_blocks?: BlockExtended[]) {
|
||||||
if (!_blocks) {
|
if (!_blocks) {
|
||||||
_blocks = blocks.getBlocks().slice(-8);
|
_blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
'mempoolInfo': memPool.getMempoolInfo(),
|
'mempoolInfo': memPool.getMempoolInfo(),
|
||||||
'vBytesPerSecond': memPool.getVBytesPerSecond(),
|
'vBytesPerSecond': memPool.getVBytesPerSecond(),
|
||||||
'lastDifficultyAdjustment': blocks.getLastDifficultyAdjustmentTime(),
|
'lastDifficultyAdjustment': blocks.getLastDifficultyAdjustmentTime(),
|
||||||
|
'previousRetarget': blocks.getPreviousDifficultyRetarget(),
|
||||||
'blocks': _blocks,
|
'blocks': _blocks,
|
||||||
'conversions': fiatConversion.getConversionRates(),
|
'conversions': fiatConversion.getConversionRates(),
|
||||||
'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
|
'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
|
||||||
@@ -244,7 +255,7 @@ class WebsocketHandler {
|
|||||||
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
|
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
|
||||||
response['tx'] = fullTx;
|
response['tx'] = fullTx;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Error finding transaction in mempool: ' + e.message || e);
|
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
response['tx'] = tx;
|
response['tx'] = tx;
|
||||||
@@ -264,7 +275,7 @@ class WebsocketHandler {
|
|||||||
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
|
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
|
||||||
foundTransactions.push(fullTx);
|
foundTransactions.push(fullTx);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Error finding transaction in mempool: ' + e.message || e);
|
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
foundTransactions.push(tx);
|
foundTransactions.push(tx);
|
||||||
@@ -278,7 +289,7 @@ class WebsocketHandler {
|
|||||||
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
|
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
|
||||||
foundTransactions.push(fullTx);
|
foundTransactions.push(fullTx);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Error finding transaction in mempool: ' + e.message || e);
|
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
foundTransactions.push(tx);
|
foundTransactions.push(tx);
|
||||||
@@ -296,7 +307,7 @@ class WebsocketHandler {
|
|||||||
|
|
||||||
newTransactions.forEach((tx) => {
|
newTransactions.forEach((tx) => {
|
||||||
|
|
||||||
if (client['track-asset'] === this.nativeAssetId) {
|
if (client['track-asset'] === Common.nativeAssetId) {
|
||||||
if (tx.vin.some((vin) => !!vin.is_pegin)) {
|
if (tx.vin.some((vin) => !!vin.is_pegin)) {
|
||||||
foundTransactions.push(tx);
|
foundTransactions.push(tx);
|
||||||
return;
|
return;
|
||||||
@@ -329,7 +340,7 @@ class WebsocketHandler {
|
|||||||
const fullTx = await transactionUtils.$getTransactionExtended(rbfTransaction, true);
|
const fullTx = await transactionUtils.$getTransactionExtended(rbfTransaction, true);
|
||||||
response['rbfTransaction'] = fullTx;
|
response['rbfTransaction'] = fullTx;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Error finding transaction in mempool: ' + e.message || e);
|
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
response['rbfTransaction'] = rbfTx;
|
response['rbfTransaction'] = rbfTx;
|
||||||
@@ -384,6 +395,7 @@ class WebsocketHandler {
|
|||||||
'block': block,
|
'block': block,
|
||||||
'mempoolInfo': memPool.getMempoolInfo(),
|
'mempoolInfo': memPool.getMempoolInfo(),
|
||||||
'lastDifficultyAdjustment': blocks.getLastDifficultyAdjustmentTime(),
|
'lastDifficultyAdjustment': blocks.getLastDifficultyAdjustmentTime(),
|
||||||
|
'previousRetarget': blocks.getPreviousDifficultyRetarget(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (mBlocks && client['want-mempool-blocks']) {
|
if (mBlocks && client['want-mempool-blocks']) {
|
||||||
@@ -426,7 +438,7 @@ class WebsocketHandler {
|
|||||||
const foundTransactions: TransactionExtended[] = [];
|
const foundTransactions: TransactionExtended[] = [];
|
||||||
|
|
||||||
transactions.forEach((tx) => {
|
transactions.forEach((tx) => {
|
||||||
if (client['track-asset'] === this.nativeAssetId) {
|
if (client['track-asset'] === Common.nativeAssetId) {
|
||||||
if (tx.vin && tx.vin.some((vin) => !!vin.is_pegin)) {
|
if (tx.vin && tx.vin.some((vin) => !!vin.is_pegin)) {
|
||||||
foundTransactions.push(tx);
|
foundTransactions.push(tx);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ interface IConfig {
|
|||||||
CACHE_DIR: string;
|
CACHE_DIR: string;
|
||||||
CLEAR_PROTECTION_MINUTES: number;
|
CLEAR_PROTECTION_MINUTES: number;
|
||||||
RECOMMENDED_FEE_PERCENTILE: number;
|
RECOMMENDED_FEE_PERCENTILE: number;
|
||||||
|
BLOCK_WEIGHT_UNITS: number;
|
||||||
|
INITIAL_BLOCKS_AMOUNT: number;
|
||||||
|
MEMPOOL_BLOCKS_AMOUNT: number;
|
||||||
|
PRICE_FEED_UPDATE_INTERVAL: number;
|
||||||
|
USE_SECOND_NODE_FOR_MINFEE: boolean;
|
||||||
};
|
};
|
||||||
ESPLORA: {
|
ESPLORA: {
|
||||||
REST_API_URL: string;
|
REST_API_URL: string;
|
||||||
@@ -26,8 +31,7 @@ interface IConfig {
|
|||||||
USERNAME: string;
|
USERNAME: string;
|
||||||
PASSWORD: string;
|
PASSWORD: string;
|
||||||
};
|
};
|
||||||
CORE_RPC_MINFEE: {
|
SECOND_CORE_RPC: {
|
||||||
ENABLED: boolean;
|
|
||||||
HOST: string;
|
HOST: string;
|
||||||
PORT: number;
|
PORT: number;
|
||||||
USERNAME: string;
|
USERNAME: string;
|
||||||
@@ -69,6 +73,11 @@ const defaults: IConfig = {
|
|||||||
'CACHE_DIR': './cache',
|
'CACHE_DIR': './cache',
|
||||||
'CLEAR_PROTECTION_MINUTES': 20,
|
'CLEAR_PROTECTION_MINUTES': 20,
|
||||||
'RECOMMENDED_FEE_PERCENTILE': 50,
|
'RECOMMENDED_FEE_PERCENTILE': 50,
|
||||||
|
'BLOCK_WEIGHT_UNITS': 4000000,
|
||||||
|
'INITIAL_BLOCKS_AMOUNT': 8,
|
||||||
|
'MEMPOOL_BLOCKS_AMOUNT': 8,
|
||||||
|
'PRICE_FEED_UPDATE_INTERVAL': 3600,
|
||||||
|
'USE_SECOND_NODE_FOR_MINFEE': false,
|
||||||
},
|
},
|
||||||
'ESPLORA': {
|
'ESPLORA': {
|
||||||
'REST_API_URL': 'http://127.0.0.1:3000',
|
'REST_API_URL': 'http://127.0.0.1:3000',
|
||||||
@@ -84,8 +93,7 @@ const defaults: IConfig = {
|
|||||||
'USERNAME': 'mempool',
|
'USERNAME': 'mempool',
|
||||||
'PASSWORD': 'mempool'
|
'PASSWORD': 'mempool'
|
||||||
},
|
},
|
||||||
'CORE_RPC_MINFEE': {
|
'SECOND_CORE_RPC': {
|
||||||
'ENABLED': false,
|
|
||||||
'HOST': '127.0.0.1',
|
'HOST': '127.0.0.1',
|
||||||
'PORT': 8332,
|
'PORT': 8332,
|
||||||
'USERNAME': 'mempool',
|
'USERNAME': 'mempool',
|
||||||
@@ -121,7 +129,7 @@ class Config implements IConfig {
|
|||||||
ESPLORA: IConfig['ESPLORA'];
|
ESPLORA: IConfig['ESPLORA'];
|
||||||
ELECTRUM: IConfig['ELECTRUM'];
|
ELECTRUM: IConfig['ELECTRUM'];
|
||||||
CORE_RPC: IConfig['CORE_RPC'];
|
CORE_RPC: IConfig['CORE_RPC'];
|
||||||
CORE_RPC_MINFEE: IConfig['CORE_RPC_MINFEE'];
|
SECOND_CORE_RPC: IConfig['SECOND_CORE_RPC'];
|
||||||
DATABASE: IConfig['DATABASE'];
|
DATABASE: IConfig['DATABASE'];
|
||||||
SYSLOG: IConfig['SYSLOG'];
|
SYSLOG: IConfig['SYSLOG'];
|
||||||
STATISTICS: IConfig['STATISTICS'];
|
STATISTICS: IConfig['STATISTICS'];
|
||||||
@@ -133,7 +141,7 @@ class Config implements IConfig {
|
|||||||
this.ESPLORA = configs.ESPLORA;
|
this.ESPLORA = configs.ESPLORA;
|
||||||
this.ELECTRUM = configs.ELECTRUM;
|
this.ELECTRUM = configs.ELECTRUM;
|
||||||
this.CORE_RPC = configs.CORE_RPC;
|
this.CORE_RPC = configs.CORE_RPC;
|
||||||
this.CORE_RPC_MINFEE = configs.CORE_RPC_MINFEE;
|
this.SECOND_CORE_RPC = configs.SECOND_CORE_RPC;
|
||||||
this.DATABASE = configs.DATABASE;
|
this.DATABASE = configs.DATABASE;
|
||||||
this.SYSLOG = configs.SYSLOG;
|
this.SYSLOG = configs.SYSLOG;
|
||||||
this.STATISTICS = configs.STATISTICS;
|
this.STATISTICS = configs.STATISTICS;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export async function checkDbConnection() {
|
|||||||
logger.info('Database connection established.');
|
logger.info('Database connection established.');
|
||||||
connection.release();
|
connection.release();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('Could not connect to database: ' + e.message || e);
|
logger.err('Could not connect to database: ' + (e instanceof Error ? e.message : e));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import logger from './logger';
|
|||||||
import backendInfo from './api/backend-info';
|
import backendInfo from './api/backend-info';
|
||||||
import loadingIndicators from './api/loading-indicators';
|
import loadingIndicators from './api/loading-indicators';
|
||||||
import mempool from './api/mempool';
|
import mempool from './api/mempool';
|
||||||
|
import elementsParser from './api/liquid/elements-parser';
|
||||||
|
|
||||||
class Server {
|
class Server {
|
||||||
private wss: WebSocket.Server | undefined;
|
private wss: WebSocket.Server | undefined;
|
||||||
@@ -111,8 +112,8 @@ class Server {
|
|||||||
try {
|
try {
|
||||||
await memPool.$updateMemPoolInfo();
|
await memPool.$updateMemPoolInfo();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = `updateMempoolInfo: ${(e.message || e)}`;
|
const msg = `updateMempoolInfo: ${(e instanceof Error ? e.message : e)}`;
|
||||||
if (config.CORE_RPC_MINFEE.ENABLED) {
|
if (config.MEMPOOL.USE_SECOND_NODE_FOR_MINFEE) {
|
||||||
logger.warn(msg);
|
logger.warn(msg);
|
||||||
} else {
|
} else {
|
||||||
logger.debug(msg);
|
logger.debug(msg);
|
||||||
@@ -123,7 +124,7 @@ class Server {
|
|||||||
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;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const loggerMsg = `runMainLoop error: ${(e.message || e)}. Retrying in ${this.currentBackendRetryInterval} sec.`;
|
const loggerMsg = `runMainLoop error: ${(e instanceof Error ? e.message : e)}. Retrying in ${this.currentBackendRetryInterval} sec.`;
|
||||||
if (this.currentBackendRetryInterval > 5) {
|
if (this.currentBackendRetryInterval > 5) {
|
||||||
logger.warn(loggerMsg);
|
logger.warn(loggerMsg);
|
||||||
mempool.setOutOfSync();
|
mempool.setOutOfSync();
|
||||||
@@ -141,6 +142,15 @@ class Server {
|
|||||||
if (this.wss) {
|
if (this.wss) {
|
||||||
websocketHandler.setWebsocketServer(this.wss);
|
websocketHandler.setWebsocketServer(this.wss);
|
||||||
}
|
}
|
||||||
|
if (config.MEMPOOL.NETWORK === 'liquid') {
|
||||||
|
blocks.setNewBlockCallback(async () => {
|
||||||
|
try {
|
||||||
|
await elementsParser.$parse();
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('Elements parsing error: ' + (e instanceof Error ? e.message : e));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
websocketHandler.setupConnectionHandling();
|
websocketHandler.setupConnectionHandling();
|
||||||
statistics.setNewStatisticsEntryCallback(websocketHandler.handleNewStatistic.bind(websocketHandler));
|
statistics.setNewStatisticsEntryCallback(websocketHandler.handleNewStatistic.bind(websocketHandler));
|
||||||
blocks.setNewBlockCallback(websocketHandler.handleNewBlock.bind(websocketHandler));
|
blocks.setNewBlockCallback(websocketHandler.handleNewBlock.bind(websocketHandler));
|
||||||
@@ -153,10 +163,12 @@ class Server {
|
|||||||
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 + '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 + 'fees/recommended', routes.getRecommendedFees)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/recommended', routes.getRecommendedFees)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/mempool-blocks', routes.getMempoolBlocks)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/mempool-blocks', routes.getMempoolBlocks)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'backend-info', routes.getBackendInfo)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'backend-info', routes.getBackendInfo)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'init-data', routes.getInitData)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'init-data', routes.getInitData)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'validate-address/:address', routes.validateAddress)
|
||||||
.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('https://mempool.space/api/v1/donations', { responseType: 'stream', timeout: 10000 });
|
||||||
@@ -234,9 +246,12 @@ class Server {
|
|||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mempool/txids', routes.getMempoolTxIds)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mempool/txids', routes.getMempoolTxIds)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mempool/recent', routes.getRecentMempoolTransactions)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mempool/recent', routes.getRecentMempoolTransactions)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId', routes.getTransaction)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId', routes.getTransaction)
|
||||||
|
.post(config.MEMPOOL.API_URL_PREFIX + 'tx', routes.$postTransaction)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/hex', routes.getRawTransaction)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', routes.getTransactionStatus)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', routes.getTransactionStatus)
|
||||||
.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', routes.getBlock)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', routes.getBlock)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/header', routes.getBlockHeader)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks', routes.getBlocks)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks', routes.getBlocks)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', routes.getBlocks)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', routes.getBlocks)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', routes.getBlockTipHeight)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', routes.getBlockTipHeight)
|
||||||
@@ -250,6 +265,12 @@ class Server {
|
|||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'address-prefix/:prefix', routes.getAddressPrefix)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'address-prefix/:prefix', routes.getAddressPrefix)
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.MEMPOOL.NETWORK === 'liquid') {
|
||||||
|
this.app
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs/month', routes.$getElementsPegsByMonth)
|
||||||
|
;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import transactionUtils from './api/transaction-utils';
|
|||||||
import blocks from './api/blocks';
|
import blocks from './api/blocks';
|
||||||
import loadingIndicators from './api/loading-indicators';
|
import loadingIndicators from './api/loading-indicators';
|
||||||
import { Common } from './api/common';
|
import { Common } from './api/common';
|
||||||
|
import bitcoinClient from './api/bitcoin/bitcoin-client';
|
||||||
|
import elementsParser from './api/liquid/elements-parser';
|
||||||
|
|
||||||
class Routes {
|
class Routes {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
@@ -55,7 +57,7 @@ class Routes {
|
|||||||
const result = websocketHandler.getInitData();
|
const result = websocketHandler.getInitData();
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e.message);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +76,7 @@ class Routes {
|
|||||||
const result = mempoolBlocks.getMempoolBlocks();
|
const result = mempoolBlocks.getMempoolBlocks();
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e.message);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -477,10 +479,24 @@ class Routes {
|
|||||||
res.json(transaction);
|
res.json(transaction);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let statusCode = 500;
|
let statusCode = 500;
|
||||||
if (e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
|
if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
|
||||||
statusCode = 404;
|
statusCode = 404;
|
||||||
}
|
}
|
||||||
res.status(statusCode).send(e.message || e);
|
res.status(statusCode).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getRawTransaction(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(req.params.txId, true);
|
||||||
|
res.setHeader('content-type', 'text/plain');
|
||||||
|
res.send(transaction.hex);
|
||||||
|
} catch (e) {
|
||||||
|
let statusCode = 500;
|
||||||
|
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
|
||||||
|
statusCode = 404;
|
||||||
|
}
|
||||||
|
res.status(statusCode).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,10 +506,10 @@ class Routes {
|
|||||||
res.json(transaction.status);
|
res.json(transaction.status);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let statusCode = 500;
|
let statusCode = 500;
|
||||||
if (e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
|
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
|
||||||
statusCode = 404;
|
statusCode = 404;
|
||||||
}
|
}
|
||||||
res.status(statusCode).send(e.message || e);
|
res.status(statusCode).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,7 +518,17 @@ class Routes {
|
|||||||
const result = await bitcoinApi.$getBlock(req.params.hash);
|
const result = await bitcoinApi.$getBlock(req.params.hash);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e.message || e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getBlockHeader(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const blockHeader = await bitcoinApi.$getBlockHeader(req.params.hash);
|
||||||
|
res.setHeader('content-type', 'text/plain');
|
||||||
|
res.send(blockHeader);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,7 +565,7 @@ class Routes {
|
|||||||
res.json(returnBlocks);
|
res.json(returnBlocks);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
loadingIndicators.setProgress('blocks', 100);
|
loadingIndicators.setProgress('blocks', 100);
|
||||||
res.status(500).send(e.message || e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -558,13 +584,13 @@ class Routes {
|
|||||||
transactions.push(transaction);
|
transactions.push(transaction);
|
||||||
loadingIndicators.setProgress('blocktxs-' + req.params.hash, (i + 1) / endIndex * 100);
|
loadingIndicators.setProgress('blocktxs-' + req.params.hash, (i + 1) / endIndex * 100);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('getBlockTransactions error: ' + e.message || e);
|
logger.debug('getBlockTransactions error: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
res.json(transactions);
|
res.json(transactions);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100);
|
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100);
|
||||||
res.status(500).send(e.message || e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -573,7 +599,7 @@ class Routes {
|
|||||||
const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10));
|
const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10));
|
||||||
res.send(blockHash);
|
res.send(blockHash);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e.message || e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -587,10 +613,10 @@ class Routes {
|
|||||||
const addressData = await bitcoinApi.$getAddress(req.params.address);
|
const addressData = await bitcoinApi.$getAddress(req.params.address);
|
||||||
res.json(addressData);
|
res.json(addressData);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message && e.message.indexOf('exceeds') > 0) {
|
if (e instanceof Error && e.message && e.message.indexOf('exceeds') > 0) {
|
||||||
return res.status(413).send(e.message);
|
return res.status(413).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
res.status(500).send(e.message || e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -604,10 +630,10 @@ class Routes {
|
|||||||
const transactions = await bitcoinApi.$getAddressTransactions(req.params.address, req.params.txId);
|
const transactions = await bitcoinApi.$getAddressTransactions(req.params.address, req.params.txId);
|
||||||
res.json(transactions);
|
res.json(transactions);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message && e.message.indexOf('exceeds') > 0) {
|
if (e instanceof Error && e.message && e.message.indexOf('exceeds') > 0) {
|
||||||
return res.status(413).send(e.message);
|
return res.status(413).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
res.status(500).send(e.message || e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -620,7 +646,7 @@ class Routes {
|
|||||||
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
|
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
|
||||||
res.send(blockHash);
|
res.send(blockHash);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e.message || e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -641,7 +667,7 @@ class Routes {
|
|||||||
const rawMempool = await bitcoinApi.$getRawMempool();
|
const rawMempool = await bitcoinApi.$getRawMempool();
|
||||||
res.send(rawMempool);
|
res.send(rawMempool);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e.message || e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -650,7 +676,7 @@ class Routes {
|
|||||||
const result = await bitcoinApi.$getBlockHeightTip();
|
const result = await bitcoinApi.$getBlockHeightTip();
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e.message || e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -659,13 +685,97 @@ class Routes {
|
|||||||
const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
|
const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e.message || e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async validateAddress(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const result = await bitcoinClient.validateAddress(req.params.address);
|
||||||
|
res.json(result);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTransactionOutspends(req: Request, res: Response) {
|
public getTransactionOutspends(req: Request, res: Response) {
|
||||||
res.status(501).send('Not implemented');
|
res.status(501).send('Not implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getDifficultyChange(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const DATime = blocks.getLastDifficultyAdjustmentTime();
|
||||||
|
const previousRetarget = blocks.getPreviousDifficultyRetarget();
|
||||||
|
const blockHeight = blocks.getCurrentBlockHeight();
|
||||||
|
|
||||||
|
const now = new Date().getTime() / 1000;
|
||||||
|
const diff = now - DATime;
|
||||||
|
const blocksInEpoch = blockHeight % 2016;
|
||||||
|
const progressPercent = (blocksInEpoch >= 0) ? blocksInEpoch / 2016 * 100 : 100;
|
||||||
|
const remainingBlocks = 2016 - blocksInEpoch;
|
||||||
|
const nextRetargetHeight = blockHeight + remainingBlocks;
|
||||||
|
|
||||||
|
let difficultyChange = 0;
|
||||||
|
if (blocksInEpoch > 0) {
|
||||||
|
difficultyChange = (600 / (diff / blocksInEpoch ) - 1) * 100;
|
||||||
|
}
|
||||||
|
if (difficultyChange > 300) {
|
||||||
|
difficultyChange = 300;
|
||||||
|
}
|
||||||
|
if (difficultyChange < -75) {
|
||||||
|
difficultyChange = -75;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeAvgDiff = difficultyChange * 0.1;
|
||||||
|
|
||||||
|
let timeAvgMins = 10;
|
||||||
|
if (timeAvgDiff > 0) {
|
||||||
|
timeAvgMins -= Math.abs(timeAvgDiff);
|
||||||
|
} else {
|
||||||
|
timeAvgMins += Math.abs(timeAvgDiff);
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeAvg = timeAvgMins * 60;
|
||||||
|
const remainingTime = remainingBlocks * timeAvg;
|
||||||
|
const estimatedRetargetDate = remainingTime + now;
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
progressPercent,
|
||||||
|
difficultyChange,
|
||||||
|
estimatedRetargetDate,
|
||||||
|
remainingBlocks,
|
||||||
|
remainingTime,
|
||||||
|
previousRetarget,
|
||||||
|
nextRetargetHeight,
|
||||||
|
timeAvg,
|
||||||
|
};
|
||||||
|
res.json(result);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getElementsPegsByMonth(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const pegs = await elementsParser.$getPegDataByMonth();
|
||||||
|
res.json(pegs);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $postTransaction(req: Request, res: Response) {
|
||||||
|
res.setHeader('content-type', 'text/plain');
|
||||||
|
try {
|
||||||
|
const rawtx = Object.keys(req.body)[0];
|
||||||
|
const txIdResult = await bitcoinApi.$sendRawTransaction(rawtx);
|
||||||
|
res.send(txIdResult);
|
||||||
|
} catch (e: any) {
|
||||||
|
res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
|
||||||
|
: (e.message || 'Error'));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Routes();
|
export default new Routes();
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ 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 ci --production
|
RUN npm install
|
||||||
RUN npm i typescript
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM node:12-buster-slim
|
FROM node:12-buster-slim
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ FROM node:12-buster-slim AS builder
|
|||||||
|
|
||||||
ARG commitHash
|
ARG commitHash
|
||||||
ENV DOCKER_COMMIT_HASH=${commitHash}
|
ENV DOCKER_COMMIT_HASH=${commitHash}
|
||||||
|
ENV CYPRESS_INSTALL_BINARY=0
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|||||||
18
docker/scripts/get_image_digest.sh
Executable file
18
docker/scripts/get_image_digest.sh
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
VERSION=$1
|
||||||
|
IMAGE=""
|
||||||
|
|
||||||
|
if [ -z "${VERSION}" ]; then
|
||||||
|
echo "no version provided (i.e, v2.2.0), using latest tag"
|
||||||
|
VERSION="latest"
|
||||||
|
fi
|
||||||
|
|
||||||
|
for package in frontend backend; do
|
||||||
|
PACKAGE=mempool/"$package"
|
||||||
|
IMAGE="$PACKAGE":"$VERSION"
|
||||||
|
HASH=`docker pull $IMAGE > /dev/null && docker inspect $IMAGE | sed -n '/RepoDigests/{n;p;}' | grep -o '[0-9a-f]\{64\}'`
|
||||||
|
if [ -n "${HASH}" ]; then
|
||||||
|
echo "$IMAGE"@sha256:"$HASH"
|
||||||
|
fi
|
||||||
|
done
|
||||||
3
frontend/.gitignore
vendored
3
frontend/.gitignore
vendored
@@ -59,3 +59,6 @@ generated-config.js
|
|||||||
cypress/videos
|
cypress/videos
|
||||||
cypress/screenshots
|
cypress/screenshots
|
||||||
|
|
||||||
|
# Base index
|
||||||
|
src/index.html
|
||||||
|
|
||||||
|
|||||||
@@ -32,3 +32,5 @@ https://www.transifex.com/mempool/mempool/dashboard/
|
|||||||
* Vietnamese @bitcoin_vietnam
|
* Vietnamese @bitcoin_vietnam
|
||||||
* Chinese @wdljt
|
* Chinese @wdljt
|
||||||
* Russian @TonyCrusoe @Bitconan
|
* Russian @TonyCrusoe @Bitconan
|
||||||
|
* Romanian @mirceavesa
|
||||||
|
* Macedonian @SkechBoy
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
|
"cli": {
|
||||||
|
"analytics": false
|
||||||
|
},
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"newProjectRoot": "projects",
|
"newProjectRoot": "projects",
|
||||||
"projects": {
|
"projects": {
|
||||||
@@ -23,6 +26,10 @@
|
|||||||
"translation": "src/locale/messages.ar.xlf",
|
"translation": "src/locale/messages.ar.xlf",
|
||||||
"baseHref": "/ar/"
|
"baseHref": "/ar/"
|
||||||
},
|
},
|
||||||
|
"ca": {
|
||||||
|
"translation": "src/locale/messages.ca.xlf",
|
||||||
|
"baseHref": "/ca/"
|
||||||
|
},
|
||||||
"cs": {
|
"cs": {
|
||||||
"translation": "src/locale/messages.cs.xlf",
|
"translation": "src/locale/messages.cs.xlf",
|
||||||
"baseHref": "/cs/"
|
"baseHref": "/cs/"
|
||||||
@@ -107,13 +114,25 @@
|
|||||||
"translation": "src/locale/messages.hu.xlf",
|
"translation": "src/locale/messages.hu.xlf",
|
||||||
"baseHref": "/hu/"
|
"baseHref": "/hu/"
|
||||||
},
|
},
|
||||||
|
"mk": {
|
||||||
|
"translation": "src/locale/messages.mk.xlf",
|
||||||
|
"baseHref": "/mk/"
|
||||||
|
},
|
||||||
"zh": {
|
"zh": {
|
||||||
"translation": "src/locale/messages.zh.xlf",
|
"translation": "src/locale/messages.zh.xlf",
|
||||||
"baseHref": "/zh/"
|
"baseHref": "/zh/"
|
||||||
},
|
},
|
||||||
|
"ro": {
|
||||||
|
"translation": "src/locale/messages.ro.xlf",
|
||||||
|
"baseHref": "/ro/"
|
||||||
|
},
|
||||||
"ru": {
|
"ru": {
|
||||||
"translation": "src/locale/messages.ru.xlf",
|
"translation": "src/locale/messages.ru.xlf",
|
||||||
"baseHref": "/ru/"
|
"baseHref": "/ru/"
|
||||||
|
},
|
||||||
|
"hi": {
|
||||||
|
"translation": "src/locale/messages.hi.xlf",
|
||||||
|
"baseHref": "/hi/"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -126,7 +145,6 @@
|
|||||||
"main": "src/main.ts",
|
"main": "src/main.ts",
|
||||||
"polyfills": "src/polyfills.ts",
|
"polyfills": "src/polyfills.ts",
|
||||||
"tsConfig": "tsconfig.app.json",
|
"tsConfig": "tsconfig.app.json",
|
||||||
"aot": true,
|
|
||||||
"assets": [
|
"assets": [
|
||||||
"src/favicon.ico",
|
"src/favicon.ico",
|
||||||
"src/resources",
|
"src/resources",
|
||||||
@@ -138,7 +156,13 @@
|
|||||||
],
|
],
|
||||||
"scripts": [
|
"scripts": [
|
||||||
"generated-config.js"
|
"generated-config.js"
|
||||||
]
|
],
|
||||||
|
"vendorChunk": true,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"buildOptimizer": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"optimization": false,
|
||||||
|
"namedChunks": true
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
@@ -148,7 +172,14 @@
|
|||||||
"with": "src/environments/environment.prod.ts"
|
"with": "src/environments/environment.prod.ts"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"optimization": true,
|
"optimization": {
|
||||||
|
"scripts": true,
|
||||||
|
"styles": {
|
||||||
|
"minify": true,
|
||||||
|
"inlineCritical": false
|
||||||
|
},
|
||||||
|
"fonts": true
|
||||||
|
},
|
||||||
"outputHashing": "all",
|
"outputHashing": "all",
|
||||||
"sourceMap": false,
|
"sourceMap": false,
|
||||||
"namedChunks": false,
|
"namedChunks": false,
|
||||||
@@ -167,7 +198,8 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"defaultConfiguration": ""
|
||||||
},
|
},
|
||||||
"serve": {
|
"serve": {
|
||||||
"builder": "@angular-devkit/build-angular:dev-server",
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
@@ -189,10 +221,10 @@
|
|||||||
"verbose": true
|
"verbose": true
|
||||||
},
|
},
|
||||||
"local-prod": {
|
"local-prod": {
|
||||||
"proxyConfig": "proxy.prod.conf.json",
|
"proxyConfig": "proxy.conf.js",
|
||||||
"disableHostCheck": true,
|
"disableHostCheck": true,
|
||||||
"host": "0.0.0.0",
|
"host": "0.0.0.0",
|
||||||
"verbose": true
|
"verbose": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -251,7 +283,9 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"outputPath": "dist/mempool/server",
|
"outputPath": "dist/mempool/server",
|
||||||
"main": "server.ts",
|
"main": "server.ts",
|
||||||
"tsConfig": "tsconfig.server.json"
|
"tsConfig": "tsconfig.server.json",
|
||||||
|
"sourceMap": true,
|
||||||
|
"optimization": false
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
@@ -266,7 +300,8 @@
|
|||||||
"localize": true,
|
"localize": true,
|
||||||
"optimization": true
|
"optimization": true
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"defaultConfiguration": ""
|
||||||
},
|
},
|
||||||
"serve-ssr": {
|
"serve-ssr": {
|
||||||
"builder": "@nguniversal/builders:ssr-dev-server",
|
"builder": "@nguniversal/builders:ssr-dev-server",
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
{
|
{
|
||||||
|
"projectId": "ry4br7",
|
||||||
"integrationFolder": "cypress/integration",
|
"integrationFolder": "cypress/integration",
|
||||||
"supportFile": "cypress/support/index.ts",
|
"supportFile": "cypress/support/index.ts",
|
||||||
"videosFolder": "cypress/videos",
|
"videosFolder": "cypress/videos",
|
||||||
"screenshotsFolder": "cypress/screenshots",
|
"screenshotsFolder": "cypress/screenshots",
|
||||||
"pluginsFile": "cypress/plugins/index.js",
|
"pluginsFile": "cypress/plugins/index.js",
|
||||||
"fixturesFolder": "cypress/fixtures",
|
"fixturesFolder": "cypress/fixtures",
|
||||||
"baseUrl": "http://localhost:4200"
|
"baseUrl": "http://localhost:4200",
|
||||||
|
"video": false,
|
||||||
|
"retries": {
|
||||||
|
"runMode": 3,
|
||||||
|
"openMode": 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
1
frontend/cypress/fixtures/mainnet_live2hchart.json
Normal file
1
frontend/cypress/fixtures/mainnet_live2hchart.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"live-2h-chart":{"id":1319298,"added":"2021-07-23T18:27:34.000Z","unconfirmed_transactions":546,"tx_per_second":3.93333,"vbytes_per_second":1926,"mempool_byte_weight":1106656,"total_fee":6198583,"vsizes":[255,18128,43701,58534,17144,5532,4483,1759,2394,1089,1683,7409,751,101010,1151,592,1497,703,1369,4747,800,1221,0,0,712,0,0,0,0,0,0,0,0,0,0,0,0,0]}}
|
||||||
1
frontend/cypress/fixtures/mainnet_mempoolInfo.json
Normal file
1
frontend/cypress/fixtures/mainnet_mempoolInfo.json
Normal file
File diff suppressed because one or more lines are too long
@@ -1,5 +1,7 @@
|
|||||||
describe('Bisq', () => {
|
describe('Bisq', () => {
|
||||||
|
let baseModule;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
baseModule = (Cypress.env('BASE_MODULE') && Cypress.env('BASE_MODULE') === 'bisq') ? '' : '/bisq';
|
||||||
|
|
||||||
cy.intercept('/sockjs-node/info*').as('socket');
|
cy.intercept('/sockjs-node/info*').as('socket');
|
||||||
cy.intercept('/bisq/api/markets/hloc?market=btc_usd&interval=day').as('hloc');
|
cy.intercept('/bisq/api/markets/hloc?market=btc_usd&interval=day').as('hloc');
|
||||||
@@ -10,42 +12,77 @@ describe('Bisq', () => {
|
|||||||
cy.intercept('/bisq/api/txs/*/*').as('txs');
|
cy.intercept('/bisq/api/txs/*/*').as('txs');
|
||||||
cy.intercept('/bisq/api/blocks/*/*').as('blocks');
|
cy.intercept('/bisq/api/blocks/*/*').as('blocks');
|
||||||
cy.intercept('/bisq/api/stats').as('stats');
|
cy.intercept('/bisq/api/stats').as('stats');
|
||||||
});
|
|
||||||
it('loads the dashboard', () => {
|
Cypress.Commands.add('waitForDashboard', () => {
|
||||||
cy.visit('/bisq');
|
cy.wait('@socket');
|
||||||
|
cy.wait('@hloc');
|
||||||
cy.wait('@socket');
|
cy.wait('@ticker');
|
||||||
cy.wait('@hloc');
|
cy.wait('@markets');
|
||||||
cy.wait('@ticker');
|
cy.wait('@7d');
|
||||||
cy.wait('@markets');
|
cy.wait('@trades');
|
||||||
cy.wait('@7d');
|
});
|
||||||
cy.wait('@trades');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loads the transactions screen', () => {
|
if (Cypress.env("BASE_MODULE") === '' || Cypress.env("BASE_MODULE") !== 'liquid') {
|
||||||
cy.visit('/bisq');
|
|
||||||
cy.get('li:nth-of-type(2) > a').click().then(() => {
|
it('loads the dashboard', () => {
|
||||||
cy.wait('@txs');
|
cy.visit(`${baseModule}`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
it('loads the blocks screen', () => {
|
|
||||||
cy.visit('/bisq');
|
|
||||||
cy.get('li:nth-of-type(3) > a').click().then(() => {
|
|
||||||
cy.wait('@blocks');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('loads the stats screen', () => {
|
|
||||||
cy.visit('/bisq');
|
|
||||||
cy.get('li:nth-of-type(4) > a').click().then(() => {
|
|
||||||
cy.wait('@stats');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the api screen', () => {
|
|
||||||
cy.visit('/bisq');
|
|
||||||
cy.get('li:nth-of-type(5) > a').click().then(() => {
|
|
||||||
|
|
||||||
|
it('loads the transactions screen', () => {
|
||||||
|
cy.visit(`${baseModule}`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(2) > a').click().then(() => {
|
||||||
|
cy.get('.table > tr').should('have.length', 50);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
|
it('loads the blocks screen', () => {
|
||||||
|
cy.visit(`${baseModule}`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(3) > a').click().then(() => {
|
||||||
|
cy.wait('@blocks');
|
||||||
|
cy.get('tbody tr').should('have.length', 10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the stats screen', () => {
|
||||||
|
cy.visit(`${baseModule}`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(4) > a').click().then(() => {
|
||||||
|
cy.wait('@stats');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the api screen', () => {
|
||||||
|
cy.visit(`${baseModule}`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(5) > a').click().then(() => {
|
||||||
|
cy.get('.card').should('have.length.at.least', 1);
|
||||||
|
cy.get('.card').first().click();
|
||||||
|
cy.get('.card-body');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows blocks pagination with 5 pages (desktop)', () => {
|
||||||
|
cy.viewport(760, 800);
|
||||||
|
cy.visit(`${baseModule}/blocks`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('tbody tr').should('have.length', 10);
|
||||||
|
// 5 pages + 4 buttons = 9 buttons
|
||||||
|
cy.get('.pagination-container ul.pagination').first().children().should('have.length', 9);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows blocks pagination with 3 pages (mobile)', () => {
|
||||||
|
cy.viewport(669, 800);
|
||||||
|
cy.visit(`${baseModule}/blocks`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('tbody tr').should('have.length', 10);
|
||||||
|
// 3 pages + 4 buttons = 7 buttons
|
||||||
|
cy.get('.pagination-container ul.pagination').first().children().should('have.length', 7);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
it.skip("Tests cannot be run on the selected BASE_MODULE");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,81 +1,147 @@
|
|||||||
describe('Liquid', () => {
|
describe('Liquid', () => {
|
||||||
|
let baseModule;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// TODO: Fix ng serve to deliver these files
|
baseModule = (Cypress.env('BASE_MODULE') && Cypress.env('BASE_MODULE') === 'liquid') ? '' : '/liquid';
|
||||||
cy.fixture('assets.minimal').then((json) => {
|
|
||||||
cy.intercept('/resources/assets.minimal.json', json);
|
cy.intercept('/liquid/api/block/**').as('block');
|
||||||
|
cy.intercept('/liquid/api/blocks/').as('blocks');
|
||||||
|
cy.intercept('/liquid/api/tx/**/outspends').as('outspends');
|
||||||
|
cy.intercept('/liquid/api/block/**/txs/**').as('block-txs');
|
||||||
|
cy.intercept('/resources/pools.json').as('pools');
|
||||||
|
|
||||||
|
Cypress.Commands.add('waitForBlockData', () => {
|
||||||
|
cy.wait('@socket');
|
||||||
|
cy.wait('@block');
|
||||||
|
cy.wait('@outspends');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Cypress.env("BASE_MODULE") === '' || Cypress.env("BASE_MODULE") !== 'bisq') {
|
||||||
|
|
||||||
|
it('loads the dashboard', () => {
|
||||||
|
cy.visit(`${baseModule}`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.fixture('assets').then((json) => {
|
it('loads the blocks page', () => {
|
||||||
cy.intercept('/resources/assets.json', json);
|
cy.visit(`${baseModule}/blocks`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the dashboard', () => {
|
it('loads a specific block page', () => {
|
||||||
cy.visit('/liquid');
|
cy.visit(`${baseModule}/block/7e1369a23a5ab861e7bdede2aadcccae4ea873ffd9caf11c7c5541eb5bcdff54`);
|
||||||
});
|
cy.waitForSkeletonGone();
|
||||||
|
|
||||||
it('loads the blocks page', () => {
|
|
||||||
cy.visit('/liquid/blocks');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads a specific block page', () => {
|
|
||||||
cy.visit('/liquid/blocks');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the graphs page', () => {
|
|
||||||
cy.visit('/liquid/graphs');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the tv page - desktop', () => {
|
|
||||||
cy.visit('/liquid');
|
|
||||||
cy.get('li:nth-of-type(3) > a').click().then(() => {
|
|
||||||
cy.wait(1000);
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the graphs page - mobile', () => {
|
it('loads the graphs page', () => {
|
||||||
cy.visit('/liquid');
|
cy.visit(`${baseModule}/graphs`);
|
||||||
cy.get('li:nth-of-type(3) > a').click().then(() => {
|
cy.waitForSkeletonGone();
|
||||||
cy.viewport('iphone-6');
|
|
||||||
cy.wait(1000);
|
|
||||||
// TODO: Should we really support TV Mode in Mobile for Bisq?
|
|
||||||
// cy.get('.tv-only').should('be.visible')
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe('assets', () => {
|
it('loads the tv page - desktop', () => {
|
||||||
it('shows the assets screen', () => {
|
cy.visit(`${baseModule}`);
|
||||||
cy.visit('/liquid');
|
cy.waitForSkeletonGone();
|
||||||
cy.get('li:nth-of-type(5) > a').click().then(() => {
|
cy.get('li:nth-of-type(3) > a').click().then(() => {
|
||||||
cy.get('table tr').should('have.length', 5);
|
cy.wait(1000);
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('allows searching assets', () => {
|
|
||||||
cy.visit('/liquid');
|
|
||||||
cy.get('li:nth-of-type(5) > a').click().then(() => {
|
|
||||||
cy.get('.container-xl input').click().type('Liquid Bitcoin').then(() => {
|
|
||||||
cy.get('table tr').should('have.length', 1);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('shows a specific asset ID', () => {
|
it('loads the graphs page - mobile', () => {
|
||||||
cy.visit('/liquid');
|
cy.visit(`${baseModule}`)
|
||||||
cy.get('li:nth-of-type(5) > a').click().then(() => {
|
cy.waitForSkeletonGone();
|
||||||
cy.get('.container-xl input').click().type('Liquid CAD').then(() => {
|
cy.get('li:nth-of-type(3) > a').click().then(() => {
|
||||||
cy.get('table tr td:nth-of-type(4) a').click();
|
cy.viewport('iphone-6');
|
||||||
|
cy.wait(1000);
|
||||||
|
cy.get('.tv-only').should('not.exist');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('shows a specific asset issuance TX', () => {
|
describe('assets', () => {
|
||||||
cy.visit('/liquid');
|
it('shows the assets screen', () => {
|
||||||
cy.get('li:nth-of-type(5) > a').click().then(() => {
|
cy.visit(`${baseModule}/assets`);
|
||||||
cy.get('.container-xl input').click().type('Liquid CAD').then(() => {
|
cy.waitForSkeletonGone();
|
||||||
cy.get('table tr td:nth-of-type(5) a').click();
|
cy.get('table tr').should('have.length.at.least', 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows searching assets', () => {
|
||||||
|
cy.visit(`${baseModule}/assets`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('.container-xl input').click().type('Liquid Bitcoin').then(() => {
|
||||||
|
cy.get('table tr').should('have.length', 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows a specific asset ID', () => {
|
||||||
|
cy.visit(`${baseModule}/assets`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('.container-xl input').click().type('Liquid AUD').then(() => {
|
||||||
|
cy.get('table tr td:nth-of-type(1) a').click();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
|
||||||
|
describe('unblinded TX', () => {
|
||||||
|
|
||||||
|
it('should not show an unblinding error message for regular txs', () => {
|
||||||
|
cy.visit(`${baseModule}/tx/82a479043ec3841e0d3f829afc8df4f0e2bbd675a13f013ea611b2fde0027d45`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('.error-unblinded' ).should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('show unblinded TX', () => {
|
||||||
|
cy.visit(`${baseModule}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=100000,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,0ab9f70650f16b1db8dfada05237f7d0d65191c3a13183da8a2ddddfbde9a2ad,fd98b2edc5530d76acd553f206a431f4c1fab27e10e290ad719582af878e98fc,2364760,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,90c7a43b15b905bca045ca42a01271cfe71d2efe3133f4197792c24505cb32ed,12eb5959d9293b8842e7dd8bc9aa9639fd3fd031c5de3ba911adeca94eb57a3a`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('#table-tx-vin tr').should('have.class', 'assetBox');
|
||||||
|
cy.get('#table-tx-vout tr').should('have.class', 'assetBox');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('show empty unblinded TX', () => {
|
||||||
|
cy.visit(`${baseModule}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('#table-tx-vin tr').should('have.class', '');
|
||||||
|
cy.get('#table-tx-vout tr').should('have.class', '');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('show invalid unblinded TX hex', () => {
|
||||||
|
cy.visit(`${baseModule}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=123`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('#table-tx-vin tr').should('have.class', '');
|
||||||
|
cy.get('#table-tx-vout tr').should('have.class', '');
|
||||||
|
cy.get('.error-unblinded' ).contains('Error: Invalid blinding data (invalid hex)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('show first unblinded vout', () => {
|
||||||
|
cy.visit(`${baseModule}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=100000,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,0ab9f70650f16b1db8dfada05237f7d0d65191c3a13183da8a2ddddfbde9a2ad,fd98b2edc5530d76acd553f206a431f4c1fab27e10e290ad719582af878e98fc`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('#table-tx-vout tr:first-child()').should('have.class', 'assetBox');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('show second unblinded vout', () => {
|
||||||
|
cy.visit(`${baseModule}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=2364760,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,90c7a43b15b905bca045ca42a01271cfe71d2efe3133f4197792c24505cb32ed,12eb5959d9293b8842e7dd8bc9aa9639fd3fd031c5de3ba911adeca94eb57a3a`);
|
||||||
|
cy.get('#table-tx-vout tr').should('have.class', 'assetBox');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('show invalid error unblinded TX', () => {
|
||||||
|
cy.visit(`${baseModule}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=100000,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,0ab9f70650f16b1db8dfada05237f7d0d65191c3a13183da8a2ddddfbde9a2ad,fd98b2edc5530d76acd553f206a431f4c1fab27e10e290ad719582af878e98fc,2364760,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,90c7a43b15b905bca045ca42a01271cfe71d2efe3133f4197792c24505cb32ed,12eb5959d9293b8842e7dd8bc9aa9639fd3fd031c5de3ba911adeca94eb57a3c`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('#table-tx-vout tr').should('have.class', 'assetBox');
|
||||||
|
cy.get('.error-unblinded' ).contains('Error: Invalid blinding data.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows asset peg in/out and burn transactions', () => {
|
||||||
|
cy.visit(`${baseModule}/asset/6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('#table-tx-vout tr').not('.assetBox');
|
||||||
|
cy.get('#table-tx-vin tr').not('.assetBox');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prevents regressing issue #644', () => {
|
||||||
|
cy.visit(`${baseModule}/tx/393b890966f305e7c440fcfb12a13f51a7a9011cc59ff5f14f6f93214261bd82`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
it.skip("Tests cannot be run on the selected BASE_MODULE");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,98 +1,368 @@
|
|||||||
|
import { emitMempoolInfo, dropWebSocket } from "../../support/websocket";
|
||||||
|
|
||||||
describe('Mainnet', () => {
|
describe('Mainnet', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
//cy.intercept('/sockjs-node/info*').as('socket');
|
||||||
cy.intercept('/api/block-height/*').as('block-height');
|
cy.intercept('/api/block-height/*').as('block-height');
|
||||||
cy.intercept('/api/block/*').as('block');
|
cy.intercept('/api/block/*').as('block');
|
||||||
cy.intercept('/api/block/*/txs/0').as('block-txs');
|
cy.intercept('/api/block/*/txs/0').as('block-txs');
|
||||||
cy.intercept('/api/tx/*/outspends').as('tx-outspends');
|
cy.intercept('/api/tx/*/outspends').as('tx-outspends');
|
||||||
|
cy.intercept('/resources/pools.json').as('pools');
|
||||||
|
|
||||||
// TODO: Fix ng serve to deliver this file
|
// Search Auto Complete
|
||||||
cy.fixture('pools').then((json) => {
|
cy.intercept('/api/address-prefix/1wiz').as('search-1wiz');
|
||||||
cy.intercept('/resources/pools.json', json);
|
cy.intercept('/api/address-prefix/1wizS').as('search-1wizS');
|
||||||
});
|
cy.intercept('/api/address-prefix/1wizSA').as('search-1wizSA');
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the dashboard', () => {
|
Cypress.Commands.add('waitForBlockData', () => {
|
||||||
cy.visit('/');
|
|
||||||
cy.wait(1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the blocks screen', () => {
|
|
||||||
cy.visit('/');
|
|
||||||
cy.get('li:nth-of-type(2) > a').click().then(() => {
|
|
||||||
cy.wait(1000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the graphs screen', () => {
|
|
||||||
cy.visit('/');
|
|
||||||
cy.get('li:nth-of-type(3) > a').click().then(() => {
|
|
||||||
cy.wait(1000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('tv mode', () => {
|
|
||||||
it('loads the tv screen - desktop', () => {
|
|
||||||
cy.viewport('macbook-16');
|
|
||||||
cy.visit('/');
|
|
||||||
cy.get('li:nth-of-type(4) > a').click().then(() => {
|
|
||||||
cy.viewport('macbook-16');
|
|
||||||
cy.wait(1000);
|
|
||||||
cy.get('.blockchain-wrapper').should('be.visible');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the tv screen - mobile', () => {
|
|
||||||
cy.visit('/');
|
|
||||||
cy.get('li:nth-of-type(4) > a').click().then(() => {
|
|
||||||
cy.viewport('iphone-6');
|
|
||||||
cy.wait(1000);
|
|
||||||
cy.get('.blockchain-wrapper').should('not.be.visible');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('loads the api screen', () => {
|
|
||||||
cy.visit('/');
|
|
||||||
cy.get('li:nth-of-type(5) > a').click().then(() => {
|
|
||||||
cy.wait(1000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('blocks', () => {
|
|
||||||
it('shows empty blocks properly', () => {
|
|
||||||
cy.visit('/block/0000000000000000000bd14f744ef2e006e61c32214670de7eb891a5732ee775');
|
|
||||||
cy.get('h2').invoke('text').should('equal', '1 transaction');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('expands and collapses the block details', () => {
|
|
||||||
cy.visit('/block/0');
|
|
||||||
cy.wait('@tx-outspends');
|
cy.wait('@tx-outspends');
|
||||||
cy.get('.btn.btn-outline-info').click().then(() => {
|
cy.wait('@pools');
|
||||||
cy.get('#details').should('be.visible');
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.get('.btn.btn-outline-info').click().then(() => {
|
if (Cypress.env("BASE_MODULE") === '' || Cypress.env("BASE_MODULE") === 'mempool') {
|
||||||
cy.get('#details').should('not.be.visible');
|
|
||||||
|
it('loads the status screen', () => {
|
||||||
|
cy.visit('/status');
|
||||||
|
cy.get('#mempool-block-0').should('be.visible');
|
||||||
|
cy.get('[id^="bitcoin-block-"]').should('have.length', 8);
|
||||||
|
cy.get('.footer').should('be.visible');
|
||||||
|
cy.get('.row > :nth-child(1)').invoke('text').then((text) => {
|
||||||
|
expect(text).to.match(/Tx vBytes per second:.* vB\/s/);
|
||||||
|
});
|
||||||
|
cy.get('.row > :nth-child(2)').invoke('text').then((text) => {
|
||||||
|
expect(text).to.match(/Unconfirmed:(.*)/);
|
||||||
|
});
|
||||||
|
cy.get('.row > :nth-child(3)').invoke('text').then((text) => {
|
||||||
|
expect(text).to.match(/Mempool size:(.*) (kB|MB) \((\d+) (block|blocks)\)/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows blocks with no pagination', () => {
|
it('loads dashboard, drop websocket and reconnect', () => {
|
||||||
cy.visit('/block/00000000000000000001ba40caf1ad4cec0ceb77692662315c151953bfd7c4c4');
|
cy.viewport('macbook-16');
|
||||||
cy.get('h2').invoke('text').should('equal', '19 transactions');
|
cy.mockMempoolSocket();
|
||||||
cy.get('ul.pagination').first().children().should('have.length', 5);
|
cy.visit('/');
|
||||||
|
cy.get('.badge').should('not.exist');
|
||||||
|
dropWebSocket();
|
||||||
|
cy.get('.badge').should('be.visible');
|
||||||
|
cy.get('.badge', {timeout: 25000}).should('not.exist');
|
||||||
|
emitMempoolInfo({
|
||||||
|
'params': {
|
||||||
|
loaded: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cy.get(':nth-child(1) > #bitcoin-block-0').should('not.exist');
|
||||||
|
cy.get(':nth-child(2) > #bitcoin-block-0').should('not.exist');
|
||||||
|
cy.get(':nth-child(3) > #bitcoin-block-0').should('not.exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports pagination on the block screen', () => {
|
it('loads the dashboard', () => {
|
||||||
// 41 txs
|
cy.visit('/');
|
||||||
cy.visit('/block/00000000000000000009f9b7b0f63ad50053ad12ec3b7f5ca951332f134f83d8');
|
cy.waitForSkeletonGone();
|
||||||
cy.get('.header-bg.box > a').invoke('text').then((text1) => {
|
});
|
||||||
cy.get('.active + li').first().click().then(() => {
|
|
||||||
cy.get('.header-bg.box > a').invoke('text').then((text2) => {
|
describe('search', () => {
|
||||||
expect(text1).not.to.eq(text2);
|
it('allows searching for partial Bitcoin addresses', () => {
|
||||||
|
cy.visit('/');
|
||||||
|
cy.get('.search-box-container > .form-control').type('1wiz').then(() => {
|
||||||
|
cy.wait('@search-1wiz');
|
||||||
|
cy.get('ngb-typeahead-window button.dropdown-item').should('have.length', 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('.search-box-container > .form-control').type('S').then(() => {
|
||||||
|
cy.wait('@search-1wizS');
|
||||||
|
cy.get('ngb-typeahead-window button.dropdown-item').should('have.length', 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('.search-box-container > .form-control').type('A').then(() => {
|
||||||
|
cy.wait('@search-1wizSA');
|
||||||
|
cy.get('ngb-typeahead-window button.dropdown-item').should('have.length', 1)
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('ngb-typeahead-window button.dropdown-item.active').click().then(() => {
|
||||||
|
cy.url().should('include', '/address/1wizSAYSbuyXbt9d8JV8ytm5acqq2TorC');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('.text-center').should('not.have.text', 'Invalid Bitcoin address');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
['BC1PQYQSZQ', 'bc1PqYqSzQ'].forEach((searchTerm) => {
|
||||||
|
it(`allows searching for partial case insensitive bc1 addresses: ${searchTerm}`, () => {
|
||||||
|
cy.visit('/');
|
||||||
|
cy.get('.search-box-container > .form-control').type(searchTerm).then(() => {
|
||||||
|
cy.get('ngb-typeahead-window button.dropdown-item').should('have.length', 1);
|
||||||
|
cy.get('ngb-typeahead-window button.dropdown-item.active').click().then(() => {
|
||||||
|
cy.url().should('include', '/address/bc1pqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs3wf0qm');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('.text-center').should('not.have.text', 'Invalid Bitcoin address');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
describe('blocks navigation', () => {
|
||||||
|
|
||||||
|
describe('keyboard events', () => {
|
||||||
|
it('loads first blockchain blocks visible and keypress arrow right', () => {
|
||||||
|
cy.viewport('macbook-16');
|
||||||
|
cy.visit('/');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('.blockchain-blocks-0 > a').click().then(() => {
|
||||||
|
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.waitForPageIdle();
|
||||||
|
cy.document().right();
|
||||||
|
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads first blockchain blocks visible and keypress arrow left', () => {
|
||||||
|
cy.viewport('macbook-16');
|
||||||
|
cy.visit('/');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('.blockchain-blocks-0 > a').click().then(() => {
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
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.document().left();
|
||||||
|
cy.get('.title-block h1').invoke('text').should('equal', 'Next block');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads last blockchain blocks and keypress arrow right', () => {
|
||||||
|
cy.viewport('macbook-16');
|
||||||
|
cy.visit('/');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('.blockchain-blocks-4 > a').click().then(() => {
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
|
||||||
|
// block 6
|
||||||
|
cy.document().right();
|
||||||
|
cy.wait(5000);
|
||||||
|
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
|
||||||
|
// block 7
|
||||||
|
cy.document().right();
|
||||||
|
cy.wait(5000);
|
||||||
|
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
|
||||||
|
// block 8 - last visible block
|
||||||
|
cy.document().right();
|
||||||
|
cy.wait(5000);
|
||||||
|
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
|
||||||
|
// block 9 - not visible at the blochchain blocks visible block
|
||||||
|
cy.document().right();
|
||||||
|
cy.wait(5000);
|
||||||
|
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads genesis block and keypress arrow right', () => {
|
||||||
|
cy.viewport('macbook-16');
|
||||||
|
cy.visit('/block/0');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
|
||||||
|
cy.document().right();
|
||||||
|
cy.wait(5000);
|
||||||
|
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads genesis block and keypress arrow left', () => {
|
||||||
|
cy.viewport('macbook-16');
|
||||||
|
cy.visit('/block/0');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
|
||||||
|
cy.document().left();
|
||||||
|
cy.wait(5000);
|
||||||
|
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('mouse events', () => {
|
||||||
|
it('loads first blockchain blocks visible and click on the arrow right', () => {
|
||||||
|
cy.viewport('macbook-16');
|
||||||
|
cy.visit('/');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('.blockchain-blocks-0 > a').click().then(() => {
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
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').click().then(() => {
|
||||||
|
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads genesis block and click on the arrow left', () => {
|
||||||
|
cy.viewport('macbook-16');
|
||||||
|
cy.visit('/block/0');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('not.exist');
|
||||||
|
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').click().then(() => {
|
||||||
|
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('loads skeleton when changes between networks', () => {
|
||||||
|
cy.visit('/');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
|
||||||
|
cy.changeNetwork("testnet");
|
||||||
|
cy.changeNetwork("signet");
|
||||||
|
cy.changeNetwork("liquid");
|
||||||
|
cy.changeNetwork("mainnet");
|
||||||
|
cy.changeNetwork("bisq");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the dashboard with the skeleton blocks', () => {
|
||||||
|
cy.mockMempoolSocket();
|
||||||
|
cy.visit("/");
|
||||||
|
cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible');
|
||||||
|
cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible');
|
||||||
|
cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible');
|
||||||
|
cy.get('#mempool-block-0').should('be.visible');
|
||||||
|
cy.get('#mempool-block-1').should('be.visible');
|
||||||
|
cy.get('#mempool-block-2').should('be.visible');
|
||||||
|
|
||||||
|
emitMempoolInfo({
|
||||||
|
'params': {
|
||||||
|
loaded: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get(':nth-child(1) > #bitcoin-block-0').should('not.exist');
|
||||||
|
cy.get(':nth-child(2) > #bitcoin-block-0').should('not.exist');
|
||||||
|
cy.get(':nth-child(3) > #bitcoin-block-0').should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the blocks screen', () => {
|
||||||
|
cy.visit('/');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(2) > a').click().then(() => {
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the graphs screen', () => {
|
||||||
|
cy.visit('/');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(3) > a').click().then(() => {
|
||||||
|
cy.wait(1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the tv screen - desktop', () => {
|
||||||
|
cy.viewport('macbook-16');
|
||||||
|
cy.visit('/');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(4) > a').click().then(() => {
|
||||||
|
cy.viewport('macbook-16');
|
||||||
|
cy.get('.chart-holder');
|
||||||
|
cy.get('.blockchain-wrapper').should('be.visible');
|
||||||
|
cy.get('#mempool-block-0').should('be.visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it.only('loads the tv screen - mobile', () => {
|
||||||
|
cy.viewport('iphone-6');
|
||||||
|
cy.visit('/tv');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('.chart-holder');
|
||||||
|
cy.get('.blockchain-wrapper').should('not.visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the api screen', () => {
|
||||||
|
cy.visit('/');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(5) > a').click().then(() => {
|
||||||
|
cy.wait(1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('blocks', () => {
|
||||||
|
it('shows empty blocks properly', () => {
|
||||||
|
cy.visit('/block/0000000000000000000bd14f744ef2e006e61c32214670de7eb891a5732ee775');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
cy.get('h2').invoke('text').should('equal', '1 transaction');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('expands and collapses the block details', () => {
|
||||||
|
cy.visit('/block/0');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
cy.get('.btn.btn-outline-info').click().then(() => {
|
||||||
|
cy.get('#details').should('be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('.btn.btn-outline-info').click().then(() => {
|
||||||
|
cy.get('#details').should('not.be.visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('shows blocks with no pagination', () => {
|
||||||
|
cy.visit('/block/00000000000000000001ba40caf1ad4cec0ceb77692662315c151953bfd7c4c4');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
cy.get('.block-tx-title h2').invoke('text').should('equal', '19 transactions');
|
||||||
|
cy.get('.pagination-container ul.pagination').first().children().should('have.length', 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports pagination on the block screen', () => {
|
||||||
|
// 41 txs
|
||||||
|
cy.visit('/block/00000000000000000009f9b7b0f63ad50053ad12ec3b7f5ca951332f134f83d8');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('.pagination-container a').invoke('text').then((text1) => {
|
||||||
|
cy.get('.active + li').first().click().then(() => {
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
cy.get('.header-bg.box > a').invoke('text').then((text2) => {
|
||||||
|
expect(text1).not.to.eq(text2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows blocks pagination with 5 pages (desktop)', () => {
|
||||||
|
cy.viewport(760, 800);
|
||||||
|
cy.visit('/block/000000000000000000049281946d26fcba7d99fdabc1feac524bc3a7003d69b3').then(() => {
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5 pages + 4 buttons = 9 buttons
|
||||||
|
cy.get('.pagination-container ul.pagination').first().children().should('have.length', 9);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows blocks pagination with 3 pages (mobile)', () => {
|
||||||
|
cy.viewport(669, 800);
|
||||||
|
cy.visit('/block/000000000000000000049281946d26fcba7d99fdabc1feac524bc3a7003d69b3').then(() => {
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3 pages + 4 buttons = 7 buttons
|
||||||
|
cy.get('.pagination-container ul.pagination').first().children().should('have.length', 7);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
it.skip("Tests cannot be run on the selected BASE_MODULE");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,131 @@
|
|||||||
|
import { emitMempoolInfo } from "../../support/websocket";
|
||||||
|
|
||||||
describe('Signet', () => {
|
describe('Signet', () => {
|
||||||
it('loads the dashboard', () => {
|
beforeEach(() => {
|
||||||
cy.visit('/signet');
|
cy.intercept('/api/block-height/*').as('block-height');
|
||||||
});
|
cy.intercept('/api/block/*').as('block');
|
||||||
|
cy.intercept('/api/block/*/txs/0').as('block-txs');
|
||||||
it.skip('loads all the pages properly', () => {
|
cy.intercept('/api/tx/*/outspends').as('tx-outspends');
|
||||||
|
});
|
||||||
});
|
|
||||||
|
|
||||||
|
if (Cypress.env("BASE_MODULE") === '' || Cypress.env("BASE_MODULE") === 'mempool') {
|
||||||
|
it('loads the dashboard', () => {
|
||||||
|
cy.visit('/signet');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the dashboard with the skeleton blocks', () => {
|
||||||
|
cy.mockMempoolSocket();
|
||||||
|
cy.visit("/signet");
|
||||||
|
cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible');
|
||||||
|
cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible');
|
||||||
|
cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible');
|
||||||
|
cy.get('#mempool-block-0').should('be.visible');
|
||||||
|
cy.get('#mempool-block-1').should('be.visible');
|
||||||
|
cy.get('#mempool-block-2').should('be.visible');
|
||||||
|
|
||||||
|
emitMempoolInfo({
|
||||||
|
'params': {
|
||||||
|
"network": "signet"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get(':nth-child(1) > #bitcoin-block-0').should('not.exist');
|
||||||
|
cy.get(':nth-child(2) > #bitcoin-block-0').should('not.exist');
|
||||||
|
cy.get(':nth-child(3) > #bitcoin-block-0').should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the blocks screen', () => {
|
||||||
|
cy.visit('/signet');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(2) > a').click().then(() => {
|
||||||
|
cy.wait(1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the graphs screen', () => {
|
||||||
|
cy.visit('/signet');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(3) > a').click().then(() => {
|
||||||
|
cy.wait(1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('tv mode', () => {
|
||||||
|
it('loads the tv screen - desktop', () => {
|
||||||
|
cy.viewport('macbook-16');
|
||||||
|
cy.visit('/signet');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(4) > a').click().then(() => {
|
||||||
|
cy.get('.chart-holder').should('be.visible');
|
||||||
|
cy.get('#mempool-block-0').should('be.visible');
|
||||||
|
cy.get('.tv-only').should('not.exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the tv screen - mobile', () => {
|
||||||
|
cy.visit('/signet');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(4) > a').click().then(() => {
|
||||||
|
cy.viewport('iphone-8');
|
||||||
|
cy.get('.chart-holder').should('be.visible');
|
||||||
|
//TODO: Remove comment when the bug is fixed
|
||||||
|
//cy.get('#mempool-block-0').should('be.visible');
|
||||||
|
cy.get('.tv-only').should('not.exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('loads the api screen', () => {
|
||||||
|
cy.visit('/signet');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(5) > a').click().then(() => {
|
||||||
|
cy.wait(1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('blocks', () => {
|
||||||
|
it('shows empty blocks properly', () => {
|
||||||
|
cy.visit('/signet/block/00000133d54e4589f6436703b067ec23209e0a21b8a9b12f57d0592fd85f7a42');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('h2').invoke('text').should('equal', '1 transaction');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('expands and collapses the block details', () => {
|
||||||
|
cy.visit('/signet/block/0');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('.btn.btn-outline-info').click().then(() => {
|
||||||
|
cy.get('#details').should('be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('.btn.btn-outline-info').click().then(() => {
|
||||||
|
cy.get('#details').should('not.be.visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows blocks with no pagination', () => {
|
||||||
|
cy.visit('/signet/block/00000078f920a96a69089877b934ce7fd009ab55e3170920a021262cb258e7cc');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('h2').invoke('text').should('equal', '13 transactions');
|
||||||
|
cy.get('ul.pagination').first().children().should('have.length', 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports pagination on the block screen', () => {
|
||||||
|
// 43 txs
|
||||||
|
cy.visit('/signet/block/00000094bd52f73bdbfc4bece3a94c21fec2dc968cd54210496e69e4059d66a6');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('.header-bg.box > a').invoke('text').then((text1) => {
|
||||||
|
cy.get('.active + li').first().click().then(() => {
|
||||||
|
cy.get('.header-bg.box > a').invoke('text').then((text2) => {
|
||||||
|
expect(text1).not.to.eq(text2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
it.skip("Tests cannot be run on the selected BASE_MODULE");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,128 @@
|
|||||||
|
import { confirmAddress, emitMempoolInfo, sendWsMock, showNewTx, startTrackingAddress } from "../../support/websocket";
|
||||||
|
|
||||||
describe('Testnet', () => {
|
describe('Testnet', () => {
|
||||||
it('loads the dashboard', () => {
|
beforeEach(() => {
|
||||||
cy.visit('/testnet');
|
cy.intercept('/api/block-height/*').as('block-height');
|
||||||
});
|
cy.intercept('/api/block/*').as('block');
|
||||||
|
cy.intercept('/api/block/*/txs/0').as('block-txs');
|
||||||
it.skip('loads all the pages properly', () => {
|
cy.intercept('/api/tx/*/outspends').as('tx-outspends');
|
||||||
|
});
|
||||||
});
|
|
||||||
|
if (Cypress.env("BASE_MODULE") === '' || Cypress.env("BASE_MODULE") === 'mempool') {
|
||||||
|
|
||||||
|
it('loads the dashboard', () => {
|
||||||
|
cy.visit('/testnet');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the dashboard with the skeleton blocks', () => {
|
||||||
|
cy.mockMempoolSocket();
|
||||||
|
cy.visit("/testnet");
|
||||||
|
cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible');
|
||||||
|
cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible');
|
||||||
|
cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible');
|
||||||
|
cy.get('#mempool-block-0').should('be.visible');
|
||||||
|
cy.get('#mempool-block-1').should('be.visible');
|
||||||
|
cy.get('#mempool-block-2').should('be.visible');
|
||||||
|
|
||||||
|
emitMempoolInfo({
|
||||||
|
'params': {
|
||||||
|
loaded: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get(':nth-child(1) > #bitcoin-block-0').should('not.exist');
|
||||||
|
cy.get(':nth-child(2) > #bitcoin-block-0').should('not.exist');
|
||||||
|
cy.get(':nth-child(3) > #bitcoin-block-0').should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the blocks screen', () => {
|
||||||
|
cy.visit('/testnet');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(2) > a').click().then(() => {
|
||||||
|
cy.wait(1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the graphs screen', () => {
|
||||||
|
cy.visit('/testnet');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(3) > a').click().then(() => {
|
||||||
|
cy.wait(1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('tv mode', () => {
|
||||||
|
it('loads the tv screen - desktop', () => {
|
||||||
|
cy.viewport('macbook-16');
|
||||||
|
cy.visit('/testnet');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(4) > a').click().then(() => {
|
||||||
|
cy.wait(1000);
|
||||||
|
cy.get('.tv-only').should('not.exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the tv screen - mobile', () => {
|
||||||
|
cy.visit('/testnet');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(4) > a').click().then(() => {
|
||||||
|
cy.viewport('iphone-6');
|
||||||
|
cy.wait(1000);
|
||||||
|
cy.get('.tv-only').should('not.exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('loads the api screen', () => {
|
||||||
|
cy.visit('/testnet');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(5) > a').click().then(() => {
|
||||||
|
cy.wait(1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('blocks', () => {
|
||||||
|
it('shows empty blocks properly', () => {
|
||||||
|
cy.visit('/testnet/block/0');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('h2').invoke('text').should('equal', '1 transaction');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('expands and collapses the block details', () => {
|
||||||
|
cy.visit('/testnet/block/0');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('.btn.btn-outline-info').click().then(() => {
|
||||||
|
cy.get('#details').should('be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('.btn.btn-outline-info').click().then(() => {
|
||||||
|
cy.get('#details').should('not.be.visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows blocks with no pagination', () => {
|
||||||
|
cy.visit('/testnet/block/000000000000002f8ce27716e74ecc7ad9f7b5101fed12d09e28bb721b9460ea');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('h2').invoke('text').should('equal', '11 transactions');
|
||||||
|
cy.get('ul.pagination').first().children().should('have.length', 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports pagination on the block screen', () => {
|
||||||
|
// 48 txs
|
||||||
|
cy.visit('/testnet/block/000000000000002ca3878ebd98b313a1c2d531f2e70a6575d232ca7564dea7a9');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('.header-bg.box > a').invoke('text').then((text1) => {
|
||||||
|
cy.get('.active + li').first().click().then(() => {
|
||||||
|
cy.get('.header-bg.box > a').invoke('text').then((text2) => {
|
||||||
|
expect(text1).not.to.eq(text2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
it.skip("Tests cannot be run on the selected BASE_MODULE");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
63
frontend/cypress/support/PageIdleDetector.ts
Normal file
63
frontend/cypress/support/PageIdleDetector.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
// source: chrisp_68 @ https://stackoverflow.com/questions/50525143/how-do-you-reliably-wait-for-page-idle-in-cypress-io-test
|
||||||
|
export class PageIdleDetector
|
||||||
|
{
|
||||||
|
defaultOptions: Object = { timeout: 60000 };
|
||||||
|
|
||||||
|
public WaitForPageToBeIdle(): void
|
||||||
|
{
|
||||||
|
this.WaitForPageToLoad();
|
||||||
|
this.WaitForAngularRequestsToComplete();
|
||||||
|
this.WaitForAngularDigestCycleToComplete();
|
||||||
|
this.WaitForAnimationsToStop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public WaitForPageToLoad(options: Object = this.defaultOptions): void
|
||||||
|
{
|
||||||
|
cy.document(options).should((myDocument: any) =>
|
||||||
|
{
|
||||||
|
expect(myDocument.readyState, "WaitForPageToLoad").to.be.oneOf(["interactive", "complete"]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public WaitForAngularRequestsToComplete(options: Object = this.defaultOptions): void
|
||||||
|
{
|
||||||
|
cy.window(options).should((myWindow: any) =>
|
||||||
|
{
|
||||||
|
if (!!myWindow.angular)
|
||||||
|
{
|
||||||
|
expect(this.NumberOfPendingAngularRequests(myWindow), "WaitForAngularRequestsToComplete").to.have.length(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public WaitForAngularDigestCycleToComplete(options: Object = this.defaultOptions): void
|
||||||
|
{
|
||||||
|
cy.window(options).should((myWindow: any) =>
|
||||||
|
{
|
||||||
|
if (!!myWindow.angular)
|
||||||
|
{
|
||||||
|
expect(this.AngularRootScopePhase(myWindow), "WaitForAngularDigestCycleToComplete").to.be.null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public WaitForAnimationsToStop(options: Object = this.defaultOptions): void
|
||||||
|
{
|
||||||
|
cy.get(":animated", options).should("not.exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
private getInjector(myWindow: any)
|
||||||
|
{
|
||||||
|
return myWindow.angular.element(myWindow.document.body).injector();
|
||||||
|
}
|
||||||
|
|
||||||
|
private NumberOfPendingAngularRequests(myWindow: any)
|
||||||
|
{
|
||||||
|
return this.getInjector(myWindow).get('$http').pendingRequests;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AngularRootScopePhase(myWindow: any)
|
||||||
|
{
|
||||||
|
return this.getInjector(myWindow).get("$rootScope").$$phase;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,4 +42,106 @@
|
|||||||
// -- This will overwrite an existing command --
|
// -- This will overwrite an existing command --
|
||||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
import 'cypress-wait-until';
|
import 'cypress-wait-until';
|
||||||
|
import { PageIdleDetector } from './PageIdleDetector';
|
||||||
|
import { mockWebSocket } from './websocket';
|
||||||
|
|
||||||
|
/* global Cypress */
|
||||||
|
const codes = {
|
||||||
|
ArrowLeft: 37,
|
||||||
|
ArrowUp: 38,
|
||||||
|
ArrowRight: 39,
|
||||||
|
ArrowDown: 40
|
||||||
|
}
|
||||||
|
|
||||||
|
Cypress.Commands.add('waitForSkeletonGone', () => {
|
||||||
|
cy.waitUntil(() => {
|
||||||
|
return Cypress.$('.skeleton-loader').length === 0;
|
||||||
|
}, { verbose: true, description: "waitForSkeletonGone", errorMsg: "skeleton loaders never went away", timeout: 15000, interval: 50});
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add(
|
||||||
|
"waitForPageIdle",
|
||||||
|
() => {
|
||||||
|
console.warn("Waiting for page idle state");
|
||||||
|
const pageIdleDetector = new PageIdleDetector();
|
||||||
|
pageIdleDetector.WaitForPageToBeIdle();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Cypress.Commands.add('mockMempoolSocket', () => {
|
||||||
|
mockWebSocket();
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add('changeNetwork', (network: "testnet"|"signet"|"liquid"|"bisq"|"mainnet" ) => {
|
||||||
|
cy.get('.dropdown-toggle').click().then(() => {
|
||||||
|
cy.get(`.${network}`).click().then(() => {
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
if(network !== 'bisq'){
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://github.com/bahmutov/cypress-arrows/blob/8f0303842a343550fbeaf01528d01d1ff213b70c/src/index.js
|
||||||
|
function keydownCommand ($el, key) {
|
||||||
|
const message = `sending the "${key}" keydown event`
|
||||||
|
const log = Cypress.log({
|
||||||
|
name: `keydown: ${key}`,
|
||||||
|
message: message,
|
||||||
|
consoleProps: function () {
|
||||||
|
return {
|
||||||
|
Subject: $el
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const e = $el.createEvent('KeyboardEvent')
|
||||||
|
|
||||||
|
Object.defineProperty(e, 'key', {
|
||||||
|
get: function () {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.defineProperty(e, 'keyCode', {
|
||||||
|
get: function () {
|
||||||
|
return this.keyCodeVal
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Object.defineProperty(e, 'which', {
|
||||||
|
get: function () {
|
||||||
|
return this.keyCodeVal
|
||||||
|
}
|
||||||
|
})
|
||||||
|
var metaKey = false
|
||||||
|
|
||||||
|
Object.defineProperty(e, 'metaKey', {
|
||||||
|
get: function () {
|
||||||
|
return metaKey
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.defineProperty(e, 'shiftKey', {
|
||||||
|
get: function () {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
e.keyCodeVal = codes[key]
|
||||||
|
|
||||||
|
e.initKeyboardEvent('keydown', true, true,
|
||||||
|
$el.defaultView, false, false, false, false, e.keyCodeVal, e.keyCodeVal)
|
||||||
|
|
||||||
|
$el.dispatchEvent(e)
|
||||||
|
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('right', { prevSubject: "dom" }, $el => keydownCommand($el, 'ArrowRight'))
|
||||||
|
Cypress.Commands.add('up', { prevSubject: "dom" }, $el => keydownCommand($el, 'ArrowUp'))
|
||||||
|
Cypress.Commands.add('down', { prevSubject: "dom" }, $el => keydownCommand($el, 'ArrowDown'))
|
||||||
10
frontend/cypress/support/index.d.ts
vendored
Normal file
10
frontend/cypress/support/index.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
declare namespace Cypress {
|
||||||
|
interface Chainable<Subject> {
|
||||||
|
waitForSkeletonGone(): Chainable<any>
|
||||||
|
waitForPageIdle(): Chainable<any>
|
||||||
|
mockMempoolSocket(): Chainable<any>
|
||||||
|
changeNetwork(network: "testnet"|"signet"|"liquid"|"bisq"|"mainnet"): Chainable<any>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,3 +15,6 @@
|
|||||||
|
|
||||||
// When a command from ./commands is ready to use, import with `import './commands'` syntax
|
// When a command from ./commands is ready to use, import with `import './commands'` syntax
|
||||||
import './commands';
|
import './commands';
|
||||||
|
import failOnConsoleError from 'cypress-fail-on-console-error';
|
||||||
|
|
||||||
|
failOnConsoleError();
|
||||||
92
frontend/cypress/support/websocket.ts
Normal file
92
frontend/cypress/support/websocket.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { WebSocket, Server } from 'mock-socket';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
mockServer: Server;
|
||||||
|
mockSocket: WebSocket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mocks: { [key: string]: { server: Server; websocket: WebSocket } } = {};
|
||||||
|
|
||||||
|
const cleanupMock = (url: string) => {
|
||||||
|
if (mocks[url]) {
|
||||||
|
mocks[url].websocket.close();
|
||||||
|
mocks[url].server.stop();
|
||||||
|
delete mocks[url];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createMock = (url: string) => {
|
||||||
|
cleanupMock(url);
|
||||||
|
const server = new Server(url);
|
||||||
|
const websocket = new WebSocket(url);
|
||||||
|
mocks[url] = { server, websocket };
|
||||||
|
|
||||||
|
return mocks[url];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mockWebSocket = () => {
|
||||||
|
cy.on('window:before:load', (win) => {
|
||||||
|
const winWebSocket = win.WebSocket;
|
||||||
|
cy.stub(win, 'WebSocket').callsFake((url) => {
|
||||||
|
console.log(url);
|
||||||
|
if ((new URL(url).pathname.indexOf('/sockjs-node/') !== 0)) {
|
||||||
|
const { server, websocket } = createMock(url);
|
||||||
|
|
||||||
|
win.mockServer = server;
|
||||||
|
win.mockServer.on('connection', (socket) => {
|
||||||
|
win.mockSocket = socket;
|
||||||
|
win.mockSocket.send('{"action":"init"}');
|
||||||
|
});
|
||||||
|
|
||||||
|
win.mockServer.on('message', (message) => {
|
||||||
|
console.log(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
return websocket;
|
||||||
|
} else {
|
||||||
|
return new winWebSocket(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.on('window:before:unload', () => {
|
||||||
|
for (const url in mocks) {
|
||||||
|
cleanupMock(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const emitMempoolInfo = ({
|
||||||
|
params
|
||||||
|
}: { params?: any } = {}) => {
|
||||||
|
cy.window().then((win) => {
|
||||||
|
//TODO: Refactor to take into account different parameterized mocking scenarios
|
||||||
|
switch (params.network) {
|
||||||
|
//TODO: Use network specific mocks
|
||||||
|
case "signet":
|
||||||
|
case "testnet":
|
||||||
|
default:
|
||||||
|
win.mockSocket.send('{"action":"init"}');
|
||||||
|
win.mockSocket.send('{"action":"want","data":["blocks","stats","mempool-blocks","live-2h-chart"]}');
|
||||||
|
win.mockSocket.send('{"conversions":{"USD":32365.338815782445}}');
|
||||||
|
cy.readFile('cypress/fixtures/mainnet_live2hchart.json', 'ascii').then((fixture) => {
|
||||||
|
win.mockSocket.send(JSON.stringify(fixture));
|
||||||
|
});
|
||||||
|
cy.readFile('cypress/fixtures/mainnet_mempoolInfo.json', 'ascii').then((fixture) => {
|
||||||
|
win.mockSocket.send(JSON.stringify(fixture));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
return cy.get('#mempool-block-0');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dropWebSocket = (() => {
|
||||||
|
cy.window().then((win) => {
|
||||||
|
win.mockServer.simulate("error");
|
||||||
|
});
|
||||||
|
return cy.wait(500);
|
||||||
|
});
|
||||||
@@ -2,7 +2,9 @@
|
|||||||
"extends": "../tsconfig.json",
|
"extends": "../tsconfig.json",
|
||||||
"include": ["**/*.ts"],
|
"include": ["**/*.ts"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"sourceMap": false,
|
"types": ["cypress"],
|
||||||
"types": ["cypress"]
|
"lib": ["es2015", "dom"],
|
||||||
|
"allowJs": true,
|
||||||
|
"noEmit": true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,16 @@ try {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const indexFilePath = configContent.BASE_MODULE ? 'src/index.' + configContent.BASE_MODULE + '.html' : 'src/index.mempool.html';
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.copyFileSync(indexFilePath, 'src/index.html');
|
||||||
|
console.log('Copied ' + indexFilePath + ' to src/index.html');
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Error copying the index file');
|
||||||
|
throw new Error(e);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const packageJson = fs.readFileSync('package.json');
|
const packageJson = fs.readFileSync('package.json');
|
||||||
packetJsonVersion = JSON.parse(packageJson).version;
|
packetJsonVersion = JSON.parse(packageJson).version;
|
||||||
|
|||||||
@@ -8,5 +8,8 @@
|
|||||||
"KEEP_BLOCKS_AMOUNT": 8,
|
"KEEP_BLOCKS_AMOUNT": 8,
|
||||||
"NGINX_PROTOCOL": "http",
|
"NGINX_PROTOCOL": "http",
|
||||||
"NGINX_HOSTNAME": "127.0.0.1",
|
"NGINX_HOSTNAME": "127.0.0.1",
|
||||||
"NGINX_PORT": "80"
|
"NGINX_PORT": "80",
|
||||||
|
"MEMPOOL_BLOCKS_AMOUNT": 8,
|
||||||
|
"BLOCK_WEIGHT_UNITS": 4000000,
|
||||||
|
"BASE_MODULE": "mempool"
|
||||||
}
|
}
|
||||||
|
|||||||
18464
frontend/package-lock.json
generated
18464
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.2.0",
|
"version": "2.3.0-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",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "./node_modules/@angular/cli/bin/ng",
|
"ng": "./node_modules/@angular/cli/bin/ng",
|
||||||
"tsc": "./node_modules/typescript/bin/tsc",
|
"tsc": "./node_modules/typescript/bin/tsc",
|
||||||
"i18n-extract-from-source": "./node_modules/@angular/cli/bin/ng xi18n --ivy --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 && ng serve -c local",
|
||||||
"serve:stg": "npm run generate-config && ng serve -c staging",
|
"serve:stg": "npm run generate-config && ng serve -c staging",
|
||||||
@@ -30,48 +30,57 @@
|
|||||||
"start": "npm run generate-config && npm run sync-assets-dev && ng serve -c local",
|
"start": "npm run generate-config && npm run sync-assets-dev && 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 && 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 && ng serve -c local-prod",
|
||||||
"build": "npm run generate-config && ng build --prod --localize && npm run sync-assets && npm run build-mempool.js",
|
"build": "npm run generate-config && 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",
|
||||||
"build-mempool.js": "tsc | browserify -p tinyify ./node_modules/@mempool/mempool.js/lib/index.js --standalone mempoolJS > ./dist/mempool/browser/en-US/mempool.js",
|
"build-mempool.js": "tsc | browserify -p tinyify ./node_modules/@mempool/mempool.js/lib/index.js --standalone mempoolJS > ./dist/mempool/browser/en-US/mempool.js",
|
||||||
"test": "ng test",
|
"test": "ng test",
|
||||||
"lint": "ng lint",
|
"lint": "ng lint",
|
||||||
"e2e": "ng e2e",
|
"e2e": "npm run generate-config && ng e2e",
|
||||||
|
"e2e:ci": "npm run cypress:run:ci",
|
||||||
|
"config:defaults:mempool": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=mempool && npm run generate-config",
|
||||||
|
"config:defaults:liquid": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=liquid && 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 && npm run generate-config",
|
||||||
"dev:ssr": "npm run generate-config && ng run mempool:serve-ssr",
|
"dev:ssr": "npm run generate-config && 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 && ng run mempool:server:production && ./node_modules/typescript/bin/tsc server.run.ts",
|
||||||
"prerender": "ng run mempool:prerender",
|
"prerender": "ng run mempool:prerender",
|
||||||
"cypress:open": "concurrently \"ng serve -c local-prod\" \"cypress open\" --kill-others",
|
"cypress:open": "cypress open",
|
||||||
"cypress:run": "concurrently \"ng serve -c local-prod\" \"cypress run\" --kill-others"
|
"cypress:run": "cypress run",
|
||||||
|
"cypress:run:record": "cypress run --record",
|
||||||
|
"cypress:open:ci": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:open",
|
||||||
|
"cypress:run:ci": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:run:record"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "~11.2.8",
|
"@angular/animations": "~12.2.6",
|
||||||
"@angular/common": "~11.2.8",
|
"@angular/common": "~12.2.6",
|
||||||
"@angular/compiler": "~11.2.8",
|
"@angular/compiler": "~12.2.6",
|
||||||
"@angular/core": "~11.2.8",
|
"@angular/core": "~12.2.6",
|
||||||
"@angular/forms": "~11.2.8",
|
"@angular/forms": "~12.2.6",
|
||||||
"@angular/localize": "^11.2.8",
|
"@angular/localize": "^12.2.6",
|
||||||
"@angular/platform-browser": "~11.2.8",
|
"@angular/platform-browser": "~12.2.6",
|
||||||
"@angular/platform-browser-dynamic": "~11.2.8",
|
"@angular/platform-browser-dynamic": "~12.2.6",
|
||||||
"@angular/platform-server": "~11.2.8",
|
"@angular/platform-server": "~12.2.6",
|
||||||
"@angular/router": "~11.2.8",
|
"@angular/router": "~12.2.6",
|
||||||
"@fortawesome/angular-fontawesome": "^0.8.2",
|
"@fortawesome/angular-fontawesome": "^0.8.2",
|
||||||
"@fortawesome/fontawesome-common-types": "^0.2.35",
|
"@fortawesome/fontawesome-common-types": "^0.2.35",
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.35",
|
"@fortawesome/fontawesome-svg-core": "^1.2.35",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
||||||
"@mempool/chartist": "^0.11.4",
|
"@juggle/resize-observer": "^3.3.1",
|
||||||
"@mempool/mempool.js": "^2.2.2",
|
"@mempool/mempool.js": "^2.2.4",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^7.0.0",
|
"@ng-bootstrap/ng-bootstrap": "^10.0.0",
|
||||||
"@nguniversal/express-engine": "11.2.1",
|
"@nguniversal/express-engine": "11.2.1",
|
||||||
"@types/qrcode": "^1.3.4",
|
"@types/qrcode": "^1.3.4",
|
||||||
"bootstrap": "4.5.0",
|
"bootstrap": "4.5.0",
|
||||||
"browserify": "^17.0.0",
|
"browserify": "^17.0.0",
|
||||||
"clipboard": "^2.0.4",
|
"clipboard": "^2.0.4",
|
||||||
"domino": "^2.1.6",
|
"domino": "^2.1.6",
|
||||||
|
"echarts": "^5.1.2",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"lightweight-charts": "^3.3.0",
|
"lightweight-charts": "^3.3.0",
|
||||||
"ngx-bootrap-multiselect": "^2.0.0",
|
"ngx-bootrap-multiselect": "^2.0.0",
|
||||||
|
"ngx-echarts": "^7.0.1",
|
||||||
"ngx-infinite-scroll": "^10.0.1",
|
"ngx-infinite-scroll": "^10.0.1",
|
||||||
"qrcode": "^1.4.4",
|
"qrcode": "^1.4.4",
|
||||||
"rxjs": "^6.6.7",
|
"rxjs": "^6.6.7",
|
||||||
@@ -81,32 +90,34 @@
|
|||||||
"zone.js": "~0.11.4"
|
"zone.js": "~0.11.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "^0.1102.7",
|
"@angular-devkit/build-angular": "^12.2.6",
|
||||||
"@angular/cli": "~11.2.7",
|
"@angular/cli": "~12.2.6",
|
||||||
"@angular/compiler-cli": "~11.2.8",
|
"@angular/compiler-cli": "~12.2.6",
|
||||||
"@angular/language-service": "~11.2.8",
|
"@angular/language-service": "~12.2.6",
|
||||||
"@cypress/schematic": "^1.3.0",
|
|
||||||
"@nguniversal/builders": "^11.2.1",
|
"@nguniversal/builders": "^11.2.1",
|
||||||
"@types/express": "^4.17.0",
|
"@types/express": "^4.17.0",
|
||||||
"@types/jasmine": "~3.6.0",
|
"@types/jasmine": "~3.6.0",
|
||||||
"@types/jasminewd2": "~2.0.3",
|
"@types/jasminewd2": "~2.0.3",
|
||||||
"@types/node": "^12.11.1",
|
"@types/node": "^12.11.1",
|
||||||
"codelyzer": "^6.0.1",
|
"codelyzer": "^6.0.1",
|
||||||
"concurrently": "^6.2.0",
|
|
||||||
"cypress-wait-until": "^1.7.1",
|
|
||||||
"http-proxy-middleware": "^1.0.5",
|
"http-proxy-middleware": "^1.0.5",
|
||||||
"jasmine-core": "~3.6.0",
|
"jasmine-core": "~3.6.0",
|
||||||
"jasmine-spec-reporter": "~5.0.0",
|
"jasmine-spec-reporter": "~5.0.0",
|
||||||
"karma": "~6.1.0",
|
"karma": "~6.3.4",
|
||||||
"karma-chrome-launcher": "~3.1.0",
|
"karma-chrome-launcher": "~3.1.0",
|
||||||
"karma-coverage": "~2.0.3",
|
"karma-coverage": "~2.0.3",
|
||||||
"karma-jasmine": "~4.0.0",
|
"karma-jasmine": "~4.0.0",
|
||||||
"karma-jasmine-html-reporter": "^1.5.0",
|
"karma-jasmine-html-reporter": "^1.5.0",
|
||||||
"ts-node": "~8.3.0",
|
"ts-node": "~8.3.0",
|
||||||
"tslint": "~6.1.0",
|
"tslint": "~6.1.0",
|
||||||
"typescript": "~4.1.5"
|
"typescript": "~4.3.5"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"cypress": "^7.4.0"
|
"@cypress/schematic": "^1.3.0",
|
||||||
|
"cypress": "^8.3.1",
|
||||||
|
"cypress-fail-on-console-error": "^2.1.0",
|
||||||
|
"cypress-wait-until": "^1.7.1",
|
||||||
|
"mock-socket": "^9.0.3",
|
||||||
|
"start-server-and-test": "^1.12.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
64
frontend/proxy.conf.js
Normal file
64
frontend/proxy.conf.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
let PROXY_CONFIG;
|
||||||
|
let configContent;
|
||||||
|
|
||||||
|
const CONFIG_FILE_NAME = 'mempool-frontend-config.json';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rawConfig = fs.readFileSync(CONFIG_FILE_NAME);
|
||||||
|
configContent = JSON.parse(rawConfig);
|
||||||
|
console.log(`${CONFIG_FILE_NAME} file found, using provided config`);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
if (e.code !== 'ENOENT') {
|
||||||
|
throw new Error(e);
|
||||||
|
} else {
|
||||||
|
console.log(`${CONFIG_FILE_NAME} file not found, using default config`);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PROXY_CONFIG = [
|
||||||
|
{
|
||||||
|
context: ['*',
|
||||||
|
'/api/**', '!/api/v1/ws',
|
||||||
|
'!/bisq', '!/bisq/**', '!/bisq/',
|
||||||
|
'!/liquid', '!/liquid/**', '!/liquid/',
|
||||||
|
'/testnet/api/**', '/signet/api/**'
|
||||||
|
],
|
||||||
|
target: "https://mempool.space",
|
||||||
|
ws: true,
|
||||||
|
secure: false,
|
||||||
|
changeOrigin: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: ['/api/v1/ws'],
|
||||||
|
target: "https://mempool.space",
|
||||||
|
ws: true,
|
||||||
|
secure: false,
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: ['/api/bisq**', '/bisq/api/**'],
|
||||||
|
target: "https://bisq.markets",
|
||||||
|
pathRewrite: {
|
||||||
|
"^/api/bisq/": "/bisq/api"
|
||||||
|
},
|
||||||
|
ws: true,
|
||||||
|
secure: false,
|
||||||
|
changeOrigin: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: ['/api/liquid**', '/liquid/api/**'],
|
||||||
|
target: "https://liquid.network",
|
||||||
|
pathRewrite: {
|
||||||
|
"^/api/liquid/": "/liquid/api"
|
||||||
|
},
|
||||||
|
ws: true,
|
||||||
|
secure: false,
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
module.exports = PROXY_CONFIG;
|
||||||
@@ -67,11 +67,18 @@
|
|||||||
"^/liquid/api": "/api/v1/ws"
|
"^/liquid/api": "/api/v1/ws"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/liquid/api/": {
|
"/liquid/api/v1/": {
|
||||||
"target": "http://localhost:50001/",
|
"target": "http://localhost:8999/",
|
||||||
"secure": false,
|
"secure": false,
|
||||||
"pathRewrite": {
|
"pathRewrite": {
|
||||||
"^/liquid/api/": ""
|
"^/liquid/api/": "/api/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/liquid/api/": {
|
||||||
|
"target": "http://localhost:8999/",
|
||||||
|
"secure": false,
|
||||||
|
"pathRewrite": {
|
||||||
|
"^/liquid/api/": "/api/v1/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/bisq/api/": {
|
"/bisq/api/": {
|
||||||
|
|||||||
@@ -4,14 +4,11 @@
|
|||||||
"secure": false,
|
"secure": false,
|
||||||
"ws": true
|
"ws": true
|
||||||
},
|
},
|
||||||
"/api/*": {
|
"/api": {
|
||||||
"target": "https://mempool.space",
|
"target": "https://mempool.space",
|
||||||
"secure": false,
|
"secure": false,
|
||||||
"changeOrigin": true,
|
"changeOrigin": true,
|
||||||
"logLevel": "debug",
|
"logLevel": "debug",
|
||||||
"pathRewrite": {
|
|
||||||
"^/api": "https://mempool.space/api"
|
|
||||||
},
|
|
||||||
"timeout": 3600000
|
"timeout": 3600000
|
||||||
},
|
},
|
||||||
"/testnet/api/v1/ws": {
|
"/testnet/api/v1/ws": {
|
||||||
@@ -23,7 +20,7 @@
|
|||||||
"^/testnet/api": "/api/v1/ws"
|
"^/testnet/api": "/api/v1/ws"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/testnet/api/*": {
|
"/testnet/api": {
|
||||||
"target": "https://mempool.space",
|
"target": "https://mempool.space",
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"changeOrigin": true,
|
"changeOrigin": true,
|
||||||
@@ -42,7 +39,7 @@
|
|||||||
"^/signet/api": "/api/v1/ws"
|
"^/signet/api": "/api/v1/ws"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/signet/api/*": {
|
"/signet/api": {
|
||||||
"target": "https://mempool.space",
|
"target": "https://mempool.space",
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"changeOrigin": true,
|
"changeOrigin": true,
|
||||||
@@ -61,7 +58,7 @@
|
|||||||
"^/bisq/api": "/api/v1/ws"
|
"^/bisq/api": "/api/v1/ws"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/bisq/api/*": {
|
"/bisq/api": {
|
||||||
"target": "https://mempool.space/bisq",
|
"target": "https://mempool.space/bisq",
|
||||||
"secure": false,
|
"secure": false,
|
||||||
"changeOrigin": true,
|
"changeOrigin": true,
|
||||||
@@ -75,7 +72,7 @@
|
|||||||
"secure": false,
|
"secure": false,
|
||||||
"ws": true
|
"ws": true
|
||||||
},
|
},
|
||||||
"/liquid/api/*": {
|
"/liquid/api": {
|
||||||
"target": "https://mempool.space",
|
"target": "https://mempool.space",
|
||||||
"secure": false,
|
"secure": false,
|
||||||
"changeOrigin": true,
|
"changeOrigin": true,
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
"secure": false,
|
"secure": false,
|
||||||
"changeOrigin": true,
|
"changeOrigin": true,
|
||||||
"pathRewrite": {
|
"pathRewrite": {
|
||||||
"^/liquid/api/": "/api/v1/liquid/"
|
"^/liquid/api/": "/api/liquid/"
|
||||||
},
|
},
|
||||||
"timeout": 3600000
|
"timeout": 3600000
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'zone.js/dist/zone-node';
|
import 'zone.js/node';
|
||||||
import './generated-config';
|
import './generated-config';
|
||||||
|
|
||||||
import * as domino from 'domino';
|
import * as domino from 'domino';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'zone.js/dist/zone-node';
|
import 'zone.js/node';
|
||||||
import './generated-config';
|
import './generated-config';
|
||||||
|
|
||||||
import { ngExpressEngine } from '@nguniversal/express-engine';
|
import { ngExpressEngine } from '@nguniversal/express-engine';
|
||||||
|
|||||||
@@ -16,8 +16,11 @@ import { DashboardComponent } from './dashboard/dashboard.component';
|
|||||||
import { LatestBlocksComponent } from './components/latest-blocks/latest-blocks.component';
|
import { LatestBlocksComponent } from './components/latest-blocks/latest-blocks.component';
|
||||||
import { ApiDocsComponent } from './components/api-docs/api-docs.component';
|
import { ApiDocsComponent } from './components/api-docs/api-docs.component';
|
||||||
import { TermsOfServiceComponent } from './components/terms-of-service/terms-of-service.component';
|
import { TermsOfServiceComponent } from './components/terms-of-service/terms-of-service.component';
|
||||||
|
import { PrivacyPolicyComponent } from './components/privacy-policy/privacy-policy.component';
|
||||||
|
import { TrademarkPolicyComponent } from './components/trademark-policy/trademark-policy.component';
|
||||||
import { BisqMasterPageComponent } from './components/bisq-master-page/bisq-master-page.component';
|
import { BisqMasterPageComponent } from './components/bisq-master-page/bisq-master-page.component';
|
||||||
import { SponsorComponent } from './components/sponsor/sponsor.component';
|
import { SponsorComponent } from './components/sponsor/sponsor.component';
|
||||||
|
import { LiquidMasterPageComponent } from './components/liquid-master-page/liquid-master-page.component';
|
||||||
|
|
||||||
let routes: Routes = [
|
let routes: Routes = [
|
||||||
{
|
{
|
||||||
@@ -66,6 +69,14 @@ let routes: Routes = [
|
|||||||
path: 'terms-of-service',
|
path: 'terms-of-service',
|
||||||
component: TermsOfServiceComponent
|
component: TermsOfServiceComponent
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'privacy-policy',
|
||||||
|
component: PrivacyPolicyComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'trademark-policy',
|
||||||
|
component: TrademarkPolicyComponent
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'address/:id',
|
path: 'address/:id',
|
||||||
children: [],
|
children: [],
|
||||||
@@ -293,7 +304,7 @@ const browserWindow = window || {};
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const browserWindowEnv = browserWindow.__env || {};
|
const browserWindowEnv = browserWindow.__env || {};
|
||||||
|
|
||||||
if (browserWindowEnv && browserWindowEnv.OFFICIAL_BISQ_MARKETS) {
|
if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'bisq') {
|
||||||
routes = [{
|
routes = [{
|
||||||
path: '',
|
path: '',
|
||||||
component: BisqMasterPageComponent,
|
component: BisqMasterPageComponent,
|
||||||
@@ -301,6 +312,93 @@ if (browserWindowEnv && browserWindowEnv.OFFICIAL_BISQ_MARKETS) {
|
|||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||||
|
routes = [{
|
||||||
|
path: '',
|
||||||
|
component: LiquidMasterPageComponent,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: StartComponent,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: DashboardComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'tx/:id',
|
||||||
|
component: TransactionComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'block/:id',
|
||||||
|
component: BlockComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'mempool-block/:id',
|
||||||
|
component: MempoolBlockComponent
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'blocks',
|
||||||
|
component: LatestBlocksComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'graphs',
|
||||||
|
component: StatisticsComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'address/:id',
|
||||||
|
component: AddressComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'asset/:id',
|
||||||
|
component: AssetComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'assets',
|
||||||
|
component: AssetsComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'api',
|
||||||
|
component: ApiDocsComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'about',
|
||||||
|
component: AboutComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'terms-of-service',
|
||||||
|
component: TermsOfServiceComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'privacy-policy',
|
||||||
|
component: PrivacyPolicyComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'trademark-policy',
|
||||||
|
component: TrademarkPolicyComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'sponsor',
|
||||||
|
component: SponsorComponent,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'tv',
|
||||||
|
component: TelevisionComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'status',
|
||||||
|
component: StatusViewComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '**',
|
||||||
|
redirectTo: ''
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [RouterModule.forRoot(routes, {
|
imports: [RouterModule.forRoot(routes, {
|
||||||
initialNavigation: 'enabled',
|
initialNavigation: 'enabled',
|
||||||
|
|||||||
@@ -31,6 +31,46 @@ export const mempoolFeeColors = [
|
|||||||
'b9254b',
|
'b9254b',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const chartColors = [
|
||||||
|
"#D81B60",
|
||||||
|
"#8E24AA",
|
||||||
|
"#5E35B1",
|
||||||
|
"#3949AB",
|
||||||
|
"#1E88E5",
|
||||||
|
"#039BE5",
|
||||||
|
"#00ACC1",
|
||||||
|
"#00897B",
|
||||||
|
"#43A047",
|
||||||
|
"#7CB342",
|
||||||
|
"#C0CA33",
|
||||||
|
"#FDD835",
|
||||||
|
"#FFB300",
|
||||||
|
"#FB8C00",
|
||||||
|
"#F4511E",
|
||||||
|
"#6D4C41",
|
||||||
|
"#757575",
|
||||||
|
"#546E7A",
|
||||||
|
"#b71c1c",
|
||||||
|
"#880E4F",
|
||||||
|
"#4A148C",
|
||||||
|
"#311B92",
|
||||||
|
"#1A237E",
|
||||||
|
"#0D47A1",
|
||||||
|
"#01579B",
|
||||||
|
"#006064",
|
||||||
|
"#004D40",
|
||||||
|
"#1B5E20",
|
||||||
|
"#33691E",
|
||||||
|
"#827717",
|
||||||
|
"#F57F17",
|
||||||
|
"#FF6F00",
|
||||||
|
"#E65100",
|
||||||
|
"#BF360C",
|
||||||
|
"#3E2723",
|
||||||
|
"#212121",
|
||||||
|
"#263238",
|
||||||
|
];
|
||||||
|
|
||||||
export const feeLevels = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
|
export const feeLevels = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
|
||||||
250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000];
|
250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000];
|
||||||
|
|
||||||
@@ -43,7 +83,7 @@ export const languages: Language[] = [
|
|||||||
{ code: 'ar', name: 'العربية' }, // Arabic
|
{ code: 'ar', name: 'العربية' }, // Arabic
|
||||||
// { code: 'bg', name: 'Български' }, // Bulgarian
|
// { code: 'bg', name: 'Български' }, // Bulgarian
|
||||||
// { code: 'bs', name: 'Bosanski' }, // Bosnian
|
// { code: 'bs', name: 'Bosanski' }, // Bosnian
|
||||||
// { code: 'ca', name: 'Català' }, // Catalan
|
{ code: 'ca', name: 'Català' }, // Catalan
|
||||||
{ code: 'cs', name: 'Čeština' }, // Czech
|
{ code: 'cs', name: 'Čeština' }, // Czech
|
||||||
// { code: 'da', name: 'Dansk' }, // Danish
|
// { code: 'da', name: 'Dansk' }, // Danish
|
||||||
{ code: 'de', name: 'Deutsch' }, // German
|
{ code: 'de', name: 'Deutsch' }, // German
|
||||||
@@ -59,13 +99,14 @@ export const languages: Language[] = [
|
|||||||
{ code: 'ko', name: '한국어' }, // Korean
|
{ code: 'ko', name: '한국어' }, // Korean
|
||||||
// { code: 'hr', name: 'Hrvatski' }, // Croatian
|
// { code: 'hr', name: 'Hrvatski' }, // Croatian
|
||||||
// { code: 'id', name: 'Bahasa Indonesia' },// Indonesian
|
// { code: 'id', name: 'Bahasa Indonesia' },// Indonesian
|
||||||
|
{ code: 'hi', name: 'हिन्दी' }, // Hindi
|
||||||
{ code: 'it', name: 'Italiano' }, // Italian
|
{ code: 'it', name: 'Italiano' }, // Italian
|
||||||
{ code: 'he', name: 'עברית' }, // Hebrew
|
{ code: 'he', name: 'עברית' }, // Hebrew
|
||||||
{ code: 'ka', name: 'ქართული' }, // Georgian
|
{ code: 'ka', name: 'ქართული' }, // Georgian
|
||||||
// { code: 'lv', name: 'Latviešu' }, // Latvian
|
// { code: 'lv', name: 'Latviešu' }, // Latvian
|
||||||
// { code: 'lt', name: 'Lietuvių' }, // Lithuanian
|
// { code: 'lt', name: 'Lietuvių' }, // Lithuanian
|
||||||
{ code: 'hu', name: 'Magyar' }, // Hungarian
|
{ code: 'hu', name: 'Magyar' }, // Hungarian
|
||||||
// { code: 'mk', name: 'Македонски' }, // Macedonian
|
{ code: 'mk', name: 'Македонски' }, // Macedonian
|
||||||
// { code: 'ms', name: 'Bahasa Melayu' }, // Malay
|
// { code: 'ms', name: 'Bahasa Melayu' }, // Malay
|
||||||
{ code: 'nl', name: 'Nederlands' }, // Dutch
|
{ code: 'nl', name: 'Nederlands' }, // Dutch
|
||||||
{ code: 'ja', name: '日本語' }, // Japanese
|
{ code: 'ja', name: '日本語' }, // Japanese
|
||||||
@@ -74,7 +115,7 @@ export const languages: Language[] = [
|
|||||||
{ code: 'pl', name: 'Polski' }, // Polish
|
{ code: 'pl', name: 'Polski' }, // Polish
|
||||||
{ code: 'pt', name: 'Português' }, // Portuguese
|
{ code: 'pt', name: 'Português' }, // Portuguese
|
||||||
// { code: 'pt-BR', name: 'Português (Brazil)' }, // Portuguese (Brazil)
|
// { code: 'pt-BR', name: 'Português (Brazil)' }, // Portuguese (Brazil)
|
||||||
// { code: 'ro', name: 'Română' }, // Romanian
|
{ code: 'ro', name: 'Română' }, // Romanian
|
||||||
{ code: 'ru', name: 'Русский' }, // Russian
|
{ code: 'ru', name: 'Русский' }, // Russian
|
||||||
// { code: 'sk', name: 'Slovenčina' }, // Slovak
|
// { code: 'sk', name: 'Slovenčina' }, // Slovak
|
||||||
{ code: 'sl', name: 'Slovenščina' }, // Slovenian
|
{ code: 'sl', name: 'Slovenščina' }, // Slovenian
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { NgModule } from '@angular/core';
|
|||||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
||||||
|
import { NgxEchartsModule } from 'ngx-echarts';
|
||||||
|
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
import { AppComponent } from './components/app/app.component';
|
import { AppComponent } from './components/app/app.component';
|
||||||
@@ -22,19 +23,21 @@ import { AddressLabelsComponent } from './components/address-labels/address-labe
|
|||||||
import { MempoolBlocksComponent } from './components/mempool-blocks/mempool-blocks.component';
|
import { MempoolBlocksComponent } from './components/mempool-blocks/mempool-blocks.component';
|
||||||
import { MasterPageComponent } from './components/master-page/master-page.component';
|
import { MasterPageComponent } from './components/master-page/master-page.component';
|
||||||
import { BisqMasterPageComponent } from './components/bisq-master-page/bisq-master-page.component';
|
import { BisqMasterPageComponent } from './components/bisq-master-page/bisq-master-page.component';
|
||||||
|
import { LiquidMasterPageComponent } from './components/liquid-master-page/liquid-master-page.component';
|
||||||
import { AboutComponent } from './components/about/about.component';
|
import { AboutComponent } from './components/about/about.component';
|
||||||
import { TelevisionComponent } from './components/television/television.component';
|
import { TelevisionComponent } from './components/television/television.component';
|
||||||
import { StatisticsComponent } from './components/statistics/statistics.component';
|
import { StatisticsComponent } from './components/statistics/statistics.component';
|
||||||
import { ChartistComponent } from './components/statistics/chartist.component';
|
|
||||||
import { BlockchainBlocksComponent } from './components/blockchain-blocks/blockchain-blocks.component';
|
import { BlockchainBlocksComponent } from './components/blockchain-blocks/blockchain-blocks.component';
|
||||||
import { BlockchainComponent } from './components/blockchain/blockchain.component';
|
import { BlockchainComponent } from './components/blockchain/blockchain.component';
|
||||||
import { FooterComponent } from './components/footer/footer.component';
|
import { FooterComponent } from './components/footer/footer.component';
|
||||||
import { AudioService } from './services/audio.service';
|
import { AudioService } from './services/audio.service';
|
||||||
import { MempoolBlockComponent } from './components/mempool-block/mempool-block.component';
|
import { MempoolBlockComponent } from './components/mempool-block/mempool-block.component';
|
||||||
import { FeeDistributionGraphComponent } from './components/fee-distribution-graph/fee-distribution-graph.component';
|
import { FeeDistributionGraphComponent } from './components/fee-distribution-graph/fee-distribution-graph.component';
|
||||||
import { TimespanComponent } from './components/timespan/timespan.component';
|
import { IncomingTransactionsGraphComponent } from './components/incoming-transactions-graph/incoming-transactions-graph.component';
|
||||||
|
import { TimeSpanComponent } from './components/time-span/time-span.component';
|
||||||
import { SeoService } from './services/seo.service';
|
import { SeoService } from './services/seo.service';
|
||||||
import { MempoolGraphComponent } from './components/mempool-graph/mempool-graph.component';
|
import { MempoolGraphComponent } from './components/mempool-graph/mempool-graph.component';
|
||||||
|
import { LbtcPegsGraphComponent } from './components/lbtc-pegs-graph/lbtc-pegs-graph.component';
|
||||||
import { AssetComponent } from './components/asset/asset.component';
|
import { AssetComponent } from './components/asset/asset.component';
|
||||||
import { AssetsComponent } from './assets/assets.component';
|
import { AssetsComponent } from './assets/assets.component';
|
||||||
import { StatusViewComponent } from './components/status-view/status-view.component';
|
import { StatusViewComponent } from './components/status-view/status-view.component';
|
||||||
@@ -44,11 +47,13 @@ import { NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
|
|||||||
import { FeesBoxComponent } from './components/fees-box/fees-box.component';
|
import { FeesBoxComponent } from './components/fees-box/fees-box.component';
|
||||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||||
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
|
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
|
||||||
import { faAngleDown, faAngleUp, faBolt, faChartArea, faCogs, faCubes, faDatabase, faExchangeAlt, faInfoCircle,
|
import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faDatabase, faExchangeAlt, faInfoCircle,
|
||||||
faLink, faList, faSearch, faTachometerAlt, faThList, faTint, faTv, faAngleDoubleDown, faAngleDoubleUp, faChevronDown, faFileAlt, faRedoAlt } from '@fortawesome/free-solid-svg-icons';
|
faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown, faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { ApiDocsComponent } from './components/api-docs/api-docs.component';
|
import { ApiDocsComponent } from './components/api-docs/api-docs.component';
|
||||||
import { CodeTemplateComponent } from './components/api-docs/code-template.component';
|
import { CodeTemplateComponent } from './components/api-docs/code-template.component';
|
||||||
import { TermsOfServiceComponent } from './components/terms-of-service/terms-of-service.component';
|
import { TermsOfServiceComponent } from './components/terms-of-service/terms-of-service.component';
|
||||||
|
import { PrivacyPolicyComponent } from './components/privacy-policy/privacy-policy.component';
|
||||||
|
import { TrademarkPolicyComponent } from './components/trademark-policy/trademark-policy.component';
|
||||||
import { StorageService } from './services/storage.service';
|
import { StorageService } from './services/storage.service';
|
||||||
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
|
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
|
||||||
import { SponsorComponent } from './components/sponsor/sponsor.component';
|
import { SponsorComponent } from './components/sponsor/sponsor.component';
|
||||||
@@ -59,6 +64,7 @@ import { SponsorComponent } from './components/sponsor/sponsor.component';
|
|||||||
AboutComponent,
|
AboutComponent,
|
||||||
MasterPageComponent,
|
MasterPageComponent,
|
||||||
BisqMasterPageComponent,
|
BisqMasterPageComponent,
|
||||||
|
LiquidMasterPageComponent,
|
||||||
TelevisionComponent,
|
TelevisionComponent,
|
||||||
BlockchainComponent,
|
BlockchainComponent,
|
||||||
StartComponent,
|
StartComponent,
|
||||||
@@ -71,14 +77,15 @@ import { SponsorComponent } from './components/sponsor/sponsor.component';
|
|||||||
AmountComponent,
|
AmountComponent,
|
||||||
LatestBlocksComponent,
|
LatestBlocksComponent,
|
||||||
SearchFormComponent,
|
SearchFormComponent,
|
||||||
TimespanComponent,
|
TimeSpanComponent,
|
||||||
AddressLabelsComponent,
|
AddressLabelsComponent,
|
||||||
MempoolBlocksComponent,
|
MempoolBlocksComponent,
|
||||||
ChartistComponent,
|
|
||||||
FooterComponent,
|
FooterComponent,
|
||||||
MempoolBlockComponent,
|
MempoolBlockComponent,
|
||||||
FeeDistributionGraphComponent,
|
FeeDistributionGraphComponent,
|
||||||
|
IncomingTransactionsGraphComponent,
|
||||||
MempoolGraphComponent,
|
MempoolGraphComponent,
|
||||||
|
LbtcPegsGraphComponent,
|
||||||
AssetComponent,
|
AssetComponent,
|
||||||
AssetsComponent,
|
AssetsComponent,
|
||||||
MinerComponent,
|
MinerComponent,
|
||||||
@@ -88,6 +95,8 @@ import { SponsorComponent } from './components/sponsor/sponsor.component';
|
|||||||
ApiDocsComponent,
|
ApiDocsComponent,
|
||||||
CodeTemplateComponent,
|
CodeTemplateComponent,
|
||||||
TermsOfServiceComponent,
|
TermsOfServiceComponent,
|
||||||
|
PrivacyPolicyComponent,
|
||||||
|
TrademarkPolicyComponent,
|
||||||
SponsorComponent,
|
SponsorComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
@@ -100,6 +109,9 @@ import { SponsorComponent } from './components/sponsor/sponsor.component';
|
|||||||
NgbTypeaheadModule,
|
NgbTypeaheadModule,
|
||||||
FontAwesomeModule,
|
FontAwesomeModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
|
NgxEchartsModule.forRoot({
|
||||||
|
echarts: () => import('echarts')
|
||||||
|
})
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
ElectrsApiService,
|
ElectrsApiService,
|
||||||
@@ -128,6 +140,7 @@ export class AppModule {
|
|||||||
library.addIcons(faLink);
|
library.addIcons(faLink);
|
||||||
library.addIcons(faBolt);
|
library.addIcons(faBolt);
|
||||||
library.addIcons(faTint);
|
library.addIcons(faTint);
|
||||||
|
library.addIcons(faFilter);
|
||||||
library.addIcons(faAngleDown);
|
library.addIcons(faAngleDown);
|
||||||
library.addIcons(faAngleUp);
|
library.addIcons(faAngleUp);
|
||||||
library.addIcons(faExchangeAlt);
|
library.addIcons(faExchangeAlt);
|
||||||
@@ -136,5 +149,12 @@ export class AppModule {
|
|||||||
library.addIcons(faChevronDown);
|
library.addIcons(faChevronDown);
|
||||||
library.addIcons(faFileAlt);
|
library.addIcons(faFileAlt);
|
||||||
library.addIcons(faRedoAlt);
|
library.addIcons(faRedoAlt);
|
||||||
|
library.addIcons(faArrowAltCircleRight);
|
||||||
|
library.addIcons(faExternalLinkAlt);
|
||||||
|
library.addIcons(faSortUp);
|
||||||
|
library.addIcons(faCaretUp);
|
||||||
|
library.addIcons(faCaretDown);
|
||||||
|
library.addIcons(faAngleRight);
|
||||||
|
library.addIcons(faAngleLeft);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,15 +20,13 @@
|
|||||||
<th i18n="Asset ticker header">Ticker</th>
|
<th i18n="Asset ticker header">Ticker</th>
|
||||||
<th class="d-none d-md-block" i18n="Asset Issuer Domain header">Issuer domain</th>
|
<th class="d-none d-md-block" i18n="Asset Issuer Domain header">Issuer domain</th>
|
||||||
<th i18n="Asset ID header">Asset ID</th>
|
<th i18n="Asset ID header">Asset ID</th>
|
||||||
<th class="d-none d-lg-block" i18n="Asset issuance transaction header">Issuance TX</th>
|
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let asset of filteredAssets; trackBy: trackByAsset">
|
<tr *ngFor="let asset of filteredAssets; trackBy: trackByAsset">
|
||||||
<td class="td-name">{{ asset.name }}</td>
|
<td class="td-name"><a [routerLink]="['/asset/' | relativeUrl, asset.asset_id]">{{ asset.name }}</a></td>
|
||||||
<td>{{ asset.ticker }}</td>
|
<td>{{ asset.ticker }}</td>
|
||||||
<td class="d-none d-md-block"><a *ngIf="asset.entity" target="_blank" href="{{ 'http://' + asset.entity.domain }}">{{ asset.entity.domain }}</a></td>
|
<td class="d-none d-md-block">{{ asset.entity && asset.entity.domain }}</td>
|
||||||
<td><a [routerLink]="['/asset/' | relativeUrl, asset.asset_id]">{{ asset.asset_id | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="asset.asset_id"></app-clipboard></td>
|
<td><a [routerLink]="['/asset/' | relativeUrl, asset.asset_id]">{{ asset.asset_id | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="asset.asset_id"></app-clipboard></td>
|
||||||
<td class="d-none d-lg-block"><ng-template [ngIf]="asset.issuance_txin"><a [routerLink]="['/tx/' | relativeUrl, asset.issuance_txin.txid]">{{ asset.issuance_txin.txid | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="asset.issuance_txin.txid"></app-clipboard></ng-template></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -47,15 +45,13 @@
|
|||||||
<th i18n="Asset ticker header">Ticker</th>
|
<th i18n="Asset ticker header">Ticker</th>
|
||||||
<th i18n="Asset Issuer Domain header">Issuer domain</th>
|
<th i18n="Asset Issuer Domain header">Issuer domain</th>
|
||||||
<th i18n="Asset ID header">Asset ID</th>
|
<th i18n="Asset ID header">Asset ID</th>
|
||||||
<th i18n="Asset issuance transaction header">Issuance TX</th>
|
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let dummy of [0,0,0]">
|
<tr *ngFor="let dummy of [0,0,0,0,0,0,0,0,0,0]">
|
||||||
<td><span class="skeleton-loader"></span></td>
|
<td><span class="skeleton-loader"></span></td>
|
||||||
<td><span class="skeleton-loader"></span></td>
|
<td><span class="skeleton-loader"></span></td>
|
||||||
<td class="d-none d-md-block"><span class="skeleton-loader"></span></td>
|
<td class="d-none d-md-block"><span class="skeleton-loader"></span></td>
|
||||||
<td><span class="skeleton-loader"></span></td>
|
<td><span class="skeleton-loader"></span></td>
|
||||||
<td class="d-none d-lg-block"><span class="skeleton-loader"></span></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export class AssetsComponent implements OnInit {
|
|||||||
const start = (this.page - 1) * this.itemsPerPage;
|
const start = (this.page - 1) * this.itemsPerPage;
|
||||||
if (searchText.length ) {
|
if (searchText.length ) {
|
||||||
const filteredAssets = this.assetsCache.filter((asset) => asset.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1
|
const filteredAssets = this.assetsCache.filter((asset) => asset.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1
|
||||||
|| asset.ticker.toLowerCase().indexOf(searchText.toLowerCase()) > -1);
|
|| (asset.ticker || '').toLowerCase().indexOf(searchText.toLowerCase()) > -1);
|
||||||
this.assets = filteredAssets;
|
this.assets = filteredAssets;
|
||||||
return filteredAssets.slice(start, this.itemsPerPage + start);
|
return filteredAssets.slice(start, this.itemsPerPage + start);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
<span style="float: left;" class="d-none d-md-block">{{ tx.id }}</span>
|
<span style="float: left;" class="d-none d-md-block">{{ tx.id }}</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="float-right">
|
<div class="float-right">
|
||||||
{{ tx.time | date:'yyyy-MM-dd HH:mm' }}
|
‎{{ tx.time | date:'yyyy-MM-dd HH:mm' }}
|
||||||
</div>
|
</div>
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td i18n="transaction.timestamp|Transaction Timestamp">Timestamp</td>
|
<td i18n="transaction.timestamp|Transaction Timestamp">Timestamp</td>
|
||||||
<td>
|
<td>
|
||||||
{{ block.time | date:'yyyy-MM-dd HH:mm' }}
|
‎{{ block.time | date:'yyyy-MM-dd HH:mm' }}
|
||||||
<div class="lg-inline">
|
<div class="lg-inline">
|
||||||
<i class="symbol">(<app-time-since [time]="block.time / 1000" [fastRender]="true"></app-time-since>)</i>
|
<i class="symbol">(<app-time-since [time]="block.time / 1000" [fastRender]="true"></app-time-since>)</i>
|
||||||
</div>
|
</div>
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
<span style="float: left;" class="d-none d-md-block">{{ tx.id }}</span>
|
<span style="float: left;" class="d-none d-md-block">{{ tx.id }}</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="float-right">
|
<div class="float-right">
|
||||||
{{ tx.time | date:'yyyy-MM-dd HH:mm' }}
|
‎{{ tx.time | date:'yyyy-MM-dd HH:mm' }}
|
||||||
</div>
|
</div>
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<div class="container-xl">
|
<div class="container-xl" (window:resize)="onResize($event)">
|
||||||
<h1 style="float: left;" i18n="Bisq blocks header">BSQ Blocks</h1>
|
<h1 style="float: left;" i18n="Bisq blocks header">BSQ Blocks</h1>
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
@@ -26,9 +26,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
<div class="pagination">
|
<ngb-pagination *ngIf="blocks.value" class="pagination-container" [size]="paginationSize" [collectionSize]="blocks.value[1]" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="false"></ngb-pagination>
|
||||||
<ngb-pagination *ngIf="blocks.value" [size]="paginationSize" [collectionSize]="blocks.value[1]" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true"></ngb-pagination>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
.pagination {
|
.pagination-container {
|
||||||
overflow: hidden;
|
float: none;
|
||||||
|
margin-bottom: 200px;
|
||||||
|
@media(min-width: 400px){
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-xl {
|
||||||
|
padding-bottom: 110px;
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,7 @@ export class BisqBlocksComponent implements OnInit {
|
|||||||
isLoading = true;
|
isLoading = true;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
paginationSize: 'sm' | 'lg' = 'md';
|
paginationSize: 'sm' | 'lg' = 'md';
|
||||||
paginationMaxSize = 4;
|
paginationMaxSize = 5;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private websocketService: WebsocketService,
|
private websocketService: WebsocketService,
|
||||||
@@ -38,7 +38,7 @@ export class BisqBlocksComponent implements OnInit {
|
|||||||
this.seoService.setTitle($localize`:@@8a7b4bd44c0ac71b2e72de0398b303257f7d2f54:Blocks`);
|
this.seoService.setTitle($localize`:@@8a7b4bd44c0ac71b2e72de0398b303257f7d2f54:Blocks`);
|
||||||
this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10);
|
this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10);
|
||||||
this.loadingItems = Array(this.itemsPerPage);
|
this.loadingItems = Array(this.itemsPerPage);
|
||||||
if (document.body.clientWidth < 768) {
|
if (document.body.clientWidth < 670) {
|
||||||
this.paginationSize = 'sm';
|
this.paginationSize = 'sm';
|
||||||
this.paginationMaxSize = 3;
|
this.paginationMaxSize = 3;
|
||||||
}
|
}
|
||||||
@@ -83,4 +83,8 @@ export class BisqBlocksComponent implements OnInit {
|
|||||||
queryParamsHandling: 'merge',
|
queryParamsHandling: 'merge',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onResize(event: any) {
|
||||||
|
this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
<div class="container-info">
|
<div class="container-info">
|
||||||
<h1>
|
<h1>
|
||||||
<ng-template [ngIf]="stateService.env.OFFICIAL_BISQ_MARKETS" [ngIfElse]="nonOfficialMarkets" i18n="Bisq All Markets">Markets</ng-template>
|
<ng-template [ngIf]="stateService.env.BASE_MODULE === 'bisq'" [ngIfElse]="nonOfficialMarkets" i18n="Bisq All Markets">Markets</ng-template>
|
||||||
<ng-template #nonOfficialMarkets i18n="Bisq Bitcoin Markets">Bitcoin Markets</ng-template>
|
<ng-template #nonOfficialMarkets i18n="Bisq Bitcoin Markets">Bitcoin Markets</ng-template>
|
||||||
</h1>
|
</h1>
|
||||||
<ng-container *ngIf="{ value: (tickers$ | async) } as tickers">
|
<ng-container *ngIf="{ value: (tickers$ | async) } as tickers">
|
||||||
|
|||||||
@@ -17,19 +17,6 @@
|
|||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
|
||||||
overflow: scroll;
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
scrollbar-width: none;
|
|
||||||
font-size: 13px;
|
|
||||||
@media(min-width: 576px){
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-info{
|
.container-info{
|
||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ export class BisqDashboardComponent implements OnInit {
|
|||||||
const newTickers = [];
|
const newTickers = [];
|
||||||
for (const t in tickers) {
|
for (const t in tickers) {
|
||||||
|
|
||||||
if (!this.stateService.env.OFFICIAL_BISQ_MARKETS) {
|
if (this.stateService.env.BASE_MODULE !== 'bisq') {
|
||||||
const pair = t.split('_');
|
const pair = t.split('_');
|
||||||
if (pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1) {
|
if (pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1) {
|
||||||
continue;
|
continue;
|
||||||
@@ -106,7 +106,7 @@ export class BisqDashboardComponent implements OnInit {
|
|||||||
])
|
])
|
||||||
.pipe(
|
.pipe(
|
||||||
map(([trades, markets]) => {
|
map(([trades, markets]) => {
|
||||||
if (!this.stateService.env.OFFICIAL_BISQ_MARKETS) {
|
if (this.stateService.env.BASE_MODULE !== 'bisq') {
|
||||||
trades = trades.filter((trade) => {
|
trades = trades.filter((trade) => {
|
||||||
const pair = trade.market.split('_');
|
const pair = trade.market.split('_');
|
||||||
return !(pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1);
|
return !(pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1);
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title text-center">
|
<h5 class="card-title text-center">
|
||||||
<ng-template [ngIf]="stateService.env.OFFICIAL_BISQ_MARKETS" [ngIfElse]="nonOfficialMarkets" i18n="Bisq All Markets">Markets</ng-template>
|
<ng-template [ngIf]="stateService.env.BASE_MODULE === 'bisq'" [ngIfElse]="nonOfficialMarkets" i18n="Bisq All Markets">Markets</ng-template>
|
||||||
<ng-template #nonOfficialMarkets i18n="Bisq Bitcoin Markets">Bitcoin Markets</ng-template>
|
<ng-template #nonOfficialMarkets i18n="Bisq Bitcoin Markets">Bitcoin Markets</ng-template>
|
||||||
</h5>
|
</h5>
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<app-language-selector *ngIf="!stateService.env.OFFICIAL_BISQ_MARKETS"></app-language-selector>
|
<app-language-selector *ngIf="stateService.env.BASE_MODULE !== 'bisq'"></app-language-selector>
|
||||||
|
|
||||||
<div class="text-small text-center mt-3">
|
<div class="text-small text-center mt-3">
|
||||||
<a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a>
|
<a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a>
|
||||||
|
|||||||
@@ -18,15 +18,44 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.table-container {
|
||||||
font-size: 13px;
|
overflow: scroll;
|
||||||
@media(min-width: 576px){
|
scrollbar-width: none;
|
||||||
font-size: 16px;
|
font-size: 13px;
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
@media(min-width: 576px){
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
thead th{
|
||||||
|
text-align: right;
|
||||||
|
&:first-child {
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
&::-webkit-scrollbar {
|
&:nth-child(3) {
|
||||||
|
display: none;
|
||||||
|
@media(min-width: 1100px){
|
||||||
|
display: table-cell;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
td {
|
||||||
|
text-align: right;
|
||||||
|
&:first-child {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
&:nth-child(3) {
|
||||||
display: none;
|
display: none;
|
||||||
|
@media(min-width: 1100px){
|
||||||
|
display: table-cell;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.chart-container {
|
.chart-container {
|
||||||
height: 300px;
|
height: 300px;
|
||||||
}
|
}
|
||||||
@@ -41,44 +70,43 @@
|
|||||||
background-color: #1d1f31;
|
background-color: #1d1f31;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-title {
|
.card-title {
|
||||||
color: #4a68b9;
|
color: #4a68b9;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-block {
|
.info-block {
|
||||||
float: left;
|
float: left;
|
||||||
width: 350px;
|
width: 350px;
|
||||||
line-height: 25px;
|
line-height: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress {
|
.progress {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #2d3348;
|
background-color: #2d3348;
|
||||||
height: 1.1rem;
|
height: 1.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-warning {
|
.bg-warning {
|
||||||
background-color: #b58800 !important;
|
background-color: #b58800 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skeleton-loader {
|
.skeleton-loader {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
&.shorter {
|
&.shorter {
|
||||||
max-width: 150px;
|
max-width: 150px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.more-padding {
|
.more-padding {
|
||||||
padding: 1.25rem 2rem 1.25rem 2rem;
|
padding: 1.25rem 2rem 1.25rem 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.graph-card {
|
.graph-card {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@media (min-width: 992px) {
|
@media (min-width: 992px) {
|
||||||
height: 385px;
|
height: 385px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ export class BisqMainDashboardComponent implements OnInit {
|
|||||||
const newTickers = [];
|
const newTickers = [];
|
||||||
for (const t in tickers) {
|
for (const t in tickers) {
|
||||||
|
|
||||||
if (!this.stateService.env.OFFICIAL_BISQ_MARKETS) {
|
if (this.stateService.env.BASE_MODULE !== 'bisq') {
|
||||||
const pair = t.split('_');
|
const pair = t.split('_');
|
||||||
if (pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1) {
|
if (pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1) {
|
||||||
continue;
|
continue;
|
||||||
@@ -114,7 +114,7 @@ export class BisqMainDashboardComponent implements OnInit {
|
|||||||
])
|
])
|
||||||
.pipe(
|
.pipe(
|
||||||
map(([trades, markets]) => {
|
map(([trades, markets]) => {
|
||||||
if (!this.stateService.env.OFFICIAL_BISQ_MARKETS) {
|
if (this.stateService.env.BASE_MODULE !== 'bisq') {
|
||||||
trades = trades.filter((trade) => {
|
trades = trades.filter((trade) => {
|
||||||
const pair = trade.market.split('_');
|
const pair = trade.market.split('_');
|
||||||
return !(pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1);
|
return !(pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<tbody *ngIf="(trades$ | async) as trades; else loadingTmpl">
|
<tbody *ngIf="(trades$ | async) as trades; else loadingTmpl">
|
||||||
<tr *ngFor="let trade of trades;">
|
<tr *ngFor="let trade of trades;">
|
||||||
<td>
|
<td>
|
||||||
{{ trade.trade_date | date:'yyyy-MM-dd HH:mm' }}
|
‎{{ trade.trade_date | date:'yyyy-MM-dd HH:mm' }}
|
||||||
</td>
|
</td>
|
||||||
<td *ngIf="view === 'all'">
|
<td *ngIf="view === 'all'">
|
||||||
<ng-container *ngIf="(trade._market || market).rtype === 'fiat'; else priceCrypto"><span class="green-color">{{ trade.price | currency: (trade._market || market).rsymbol }}</span></ng-container>
|
<ng-container *ngIf="(trade._market || market).rtype === 'fiat'; else priceCrypto"><span class="green-color">{{ trade.price | currency: (trade._market || market).rsymbol }}</span></ng-container>
|
||||||
|
|||||||
@@ -1,12 +1,38 @@
|
|||||||
|
|
||||||
.table-container {
|
.table-container {
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
-ms-overflow-style: none;
|
scrollbar-width: none;
|
||||||
scrollbar-width: none;
|
font-size: 13px;
|
||||||
font-size: 13px;
|
&::-webkit-scrollbar {
|
||||||
@media(min-width: 576px){
|
display: none;
|
||||||
font-size: 16px;
|
}
|
||||||
|
@media(min-width: 576px){
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
thead th{
|
||||||
|
text-align: right;
|
||||||
|
&:first-child{
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
&::-webkit-scrollbar {
|
&:nth-child(2) {
|
||||||
|
display: none;
|
||||||
|
@media(min-width: 1100px){
|
||||||
|
display: table-cell;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
td {
|
||||||
|
text-align: right;
|
||||||
|
&:first-child{
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
&:nth-child(2) {
|
||||||
display: none;
|
display: none;
|
||||||
|
@media(min-width: 1100px){
|
||||||
|
display: table-cell;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,98 +1,102 @@
|
|||||||
<div class="container-xl">
|
<div class="container-xl">
|
||||||
|
|
||||||
|
<div class="title-block">
|
||||||
<ng-template [ngIf]="!isLoading && !error">
|
<ng-template [ngIf]="!isLoading && !error">
|
||||||
|
|
||||||
<button *ngIf="(latestBlock$ | async) as latestBlock" type="button" class="btn btn-sm btn-success float-right">
|
<div>
|
||||||
<ng-container *ngTemplateOutlet="latestBlock.height - bisqTx.blockHeight + 1 == 1 ? confirmationSingular : confirmationPlural; context: {$implicit: latestBlock.height - bisqTx.blockHeight + 1}"></ng-container>
|
<div class="title">
|
||||||
<ng-template #confirmationSingular let-i i18n="shared.confirmation-count.singular|Transaction singular confirmation count">{{ i }} confirmation</ng-template>
|
<h1 i18n="shared.transaction">Transaction</h1>
|
||||||
<ng-template #confirmationPlural let-i i18n="shared.confirmation-count.plural|Transaction plural confirmation count">{{ i }} confirmations</ng-template>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
|
|
||||||
<h1 i18n="shared.transaction">Transaction</h1>
|
|
||||||
|
|
||||||
<div class="tx-link">
|
|
||||||
<a [routerLink]="['/bisq-tx' | relativeUrl, bisqTx.id]">
|
|
||||||
<span class="d-inline d-lg-none">{{ bisqTx.id | shortenString : 24 }}</span>
|
|
||||||
<span class="d-none d-lg-inline">{{ bisqTx.id }}</span>
|
|
||||||
</a>
|
|
||||||
<app-clipboard [text]="bisqTx.id"></app-clipboard>
|
|
||||||
</div>
|
|
||||||
<div class="clearfix"></div>
|
|
||||||
|
|
||||||
<div class="box transaction-container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm">
|
|
||||||
<table class="table table-borderless table-striped">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td i18n="transaction.timestamp|Transaction Timestamp">Timestamp</td>
|
|
||||||
<td>
|
|
||||||
{{ bisqTx.time | date:'yyyy-MM-dd HH:mm' }}
|
|
||||||
<div class="lg-inline">
|
|
||||||
<i class="symbol">(<app-time-since [time]="bisqTx.time / 1000" [fastRender]="true"></app-time-since>)</i>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="td-width" i18n="transaction.included-in-block|Transaction included in block">Included in block</td>
|
|
||||||
<td>
|
|
||||||
<a [routerLink]="['/block/' | relativeUrl, bisqTx.blockHash]" [state]="{ data: { blockHeight: bisqTx.blockHeight } }">{{ bisqTx.blockHeight }}</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="td-width" i18n="transaction.features|Transaction features">Features</td>
|
|
||||||
<td>
|
|
||||||
<app-tx-features *ngIf="tx; else loadingTx" [tx]="tx"></app-tx-features>
|
|
||||||
<ng-template #loadingTx>
|
|
||||||
<span class="skeleton-loader"></span>
|
|
||||||
</ng-template>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm">
|
|
||||||
<table class="table table-borderless table-striped">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td class="td-width" i18n="BSQ burnt amount">Burnt amount</td>
|
|
||||||
<td>
|
|
||||||
{{ bisqTx.burntFee / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span> <span class="fiat"><app-bsq-amount [bsq]="bisqTx.burntFee" [forceFiat]="true" [green]="true"></app-bsq-amount></span>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td i18n="transaction.fee-per-vbyte|Transaction fee">Fee per vByte</td>
|
|
||||||
<td *ngIf="!isLoadingTx; else loadingTxFee">
|
|
||||||
{{ tx.fee / (tx.weight / 4) | number : '1.1-1' }} <span class="symbol">sat/vB</span>
|
|
||||||
|
|
||||||
<app-tx-fee-rating [tx]="tx"></app-tx-fee-rating>
|
|
||||||
</td>
|
|
||||||
<ng-template #loadingTxFee>
|
|
||||||
<td><span class="skeleton-loader"></span></td>
|
|
||||||
</ng-template>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="tx-link">
|
||||||
|
<a [routerLink]="['/bisq-tx' | relativeUrl, bisqTx.id]">
|
||||||
|
<span class="d-inline d-lg-none">{{ bisqTx.id | shortenString : 24 }}</span>
|
||||||
|
<span class="d-none d-lg-inline">{{ bisqTx.id }}</span>
|
||||||
|
</a>
|
||||||
|
<app-clipboard [text]="bisqTx.id"></app-clipboard>
|
||||||
|
</div>
|
||||||
|
<div class="container-buttons">
|
||||||
|
<button *ngIf="(latestBlock$ | async) as latestBlock" type="button" class="btn btn-sm btn-success float-right">
|
||||||
|
<ng-container *ngTemplateOutlet="latestBlock.height - bisqTx.blockHeight + 1 == 1 ? confirmationSingular : confirmationPlural; context: {$implicit: latestBlock.height - bisqTx.blockHeight + 1}"></ng-container>
|
||||||
|
<ng-template #confirmationSingular let-i i18n="shared.confirmation-count.singular|Transaction singular confirmation count">{{ i }} confirmation</ng-template>
|
||||||
|
<ng-template #confirmationPlural let-i i18n="shared.confirmation-count.plural|Transaction plural confirmation count">{{ i }} confirmations</ng-template>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
|
||||||
|
<div class="box transaction-container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm">
|
||||||
|
<table class="table table-borderless table-striped">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td i18n="transaction.timestamp|Transaction Timestamp">Timestamp</td>
|
||||||
|
<td>
|
||||||
|
‎{{ bisqTx.time | date:'yyyy-MM-dd HH:mm' }}
|
||||||
|
<div class="lg-inline">
|
||||||
|
<i class="symbol">(<app-time-since [time]="bisqTx.time / 1000" [fastRender]="true"></app-time-since>)</i>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="td-width" i18n="transaction.included-in-block|Transaction included in block">Included in block</td>
|
||||||
|
<td>
|
||||||
|
<a [routerLink]="['/block/' | relativeUrl, bisqTx.blockHash]" [state]="{ data: { blockHeight: bisqTx.blockHeight } }">{{ bisqTx.blockHeight }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="td-width" i18n="transaction.features|Transaction features">Features</td>
|
||||||
|
<td>
|
||||||
|
<app-tx-features *ngIf="tx; else loadingTx" [tx]="tx"></app-tx-features>
|
||||||
|
<ng-template #loadingTx>
|
||||||
|
<span class="skeleton-loader"></span>
|
||||||
|
</ng-template>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm">
|
||||||
|
<table class="table table-borderless table-striped">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="td-width" i18n="BSQ burnt amount">Burnt amount</td>
|
||||||
|
<td>
|
||||||
|
{{ bisqTx.burntFee / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span> <span class="fiat"><app-bsq-amount [bsq]="bisqTx.burntFee" [forceFiat]="true" [green]="true"></app-bsq-amount></span>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td i18n="transaction.fee-per-vbyte|Transaction fee">Fee per vByte</td>
|
||||||
|
<td *ngIf="!isLoadingTx; else loadingTxFee">
|
||||||
|
{{ tx.fee / (tx.weight / 4) | feeRounding }} <span class="symbol">sat/vB</span>
|
||||||
|
|
||||||
|
<app-tx-fee-rating [tx]="tx"></app-tx-fee-rating>
|
||||||
|
</td>
|
||||||
|
<ng-template #loadingTxFee>
|
||||||
|
<td><span class="skeleton-loader"></span></td>
|
||||||
|
</ng-template>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<h2 i18n="transaction.details">Details</h2>
|
||||||
|
|
||||||
|
|
||||||
|
<app-bisq-transaction-details [tx]="bisqTx"></app-bisq-transaction-details>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<h2 i18n="transaction.inputs-and-outputs|Transaction inputs and outputs">Inputs & Outputs</h2>
|
||||||
|
|
||||||
|
<app-bisq-transfers [tx]="bisqTx"></app-bisq-transfers>
|
||||||
|
|
||||||
|
<br>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<h2 i18n="transaction.details">Details</h2>
|
|
||||||
|
|
||||||
|
|
||||||
<app-bisq-transaction-details [tx]="bisqTx"></app-bisq-transaction-details>
|
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<h2 i18n="transaction.inputs-and-outputs|Transaction inputs and outputs">Inputs & Outputs</h2>
|
|
||||||
|
|
||||||
<app-bisq-transfers [tx]="bisqTx"></app-bisq-transfers>
|
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template [ngIf="isLoading && !error">
|
<ng-template [ngIf="isLoading && !error">
|
||||||
|
|||||||
@@ -1,126 +1 @@
|
|||||||
.adjust-btn-padding {
|
@import "./../../components/transaction/transaction.component.scss";
|
||||||
padding: 0.55rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-block {
|
|
||||||
padding-top: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
|
||||||
.mobile-bottomcol {
|
|
||||||
margin-top: 15px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.td-width {
|
|
||||||
width: 150px;
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
width: 175px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
position: relative;
|
|
||||||
top: -1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-small-height {
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow-green {
|
|
||||||
color: #1a9436;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow-red {
|
|
||||||
color: #dc3545;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-success {
|
|
||||||
float: right;
|
|
||||||
margin-bottom: 0px;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tx-link {
|
|
||||||
display: block;
|
|
||||||
width: auto;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
margin-left: 2px;
|
|
||||||
margin-top: 40px;
|
|
||||||
position: absolute;
|
|
||||||
@media (min-width: 700px) {
|
|
||||||
margin-top: 14px;
|
|
||||||
margin-left: 10px;
|
|
||||||
position: relative;
|
|
||||||
text-align: left;
|
|
||||||
width: 100%;
|
|
||||||
top: 14px;
|
|
||||||
left: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-xl {
|
|
||||||
margin-bottom: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row{
|
|
||||||
flex-direction: column;
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
|
||||||
.mobile-bottomcol {
|
|
||||||
margin-top: 15px;
|
|
||||||
}
|
|
||||||
.details-table td:first-child {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.fiat {
|
|
||||||
display: block;
|
|
||||||
font-size: 13px;
|
|
||||||
@media (min-width: 576px){
|
|
||||||
display: inline-block;
|
|
||||||
margin-left: 15px;
|
|
||||||
font-size: 14px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h1{
|
|
||||||
font-size: 1.75rem;
|
|
||||||
margin-top: 2px;
|
|
||||||
margin-bottom: 0;
|
|
||||||
float: left;
|
|
||||||
padding-bottom: 40px;
|
|
||||||
@media (min-width: 375px){
|
|
||||||
margin-top: 0px;
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
@media (min-width: 768px){
|
|
||||||
padding-bottom: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.transaction-container {
|
|
||||||
font-size: 14px;
|
|
||||||
@media (min-width: 576px){
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
tr td {
|
|
||||||
&:last-child{
|
|
||||||
text-align: right;
|
|
||||||
@media (min-width: 768px){
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -85,7 +85,11 @@ export class BisqTransactionComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tx.version) {
|
if (tx.version) {
|
||||||
this.router.navigate(['/tx/', this.txId], { state: { data: tx, bsqTx: true }});
|
if (this.stateService.env.BASE_MODULE === 'bisq') {
|
||||||
|
window.location.replace('https://mempool.space/tx/' + this.txId);
|
||||||
|
} else {
|
||||||
|
this.router.navigate(['/tx/', this.txId], { state: { data: tx, bsqTx: true }});
|
||||||
|
}
|
||||||
return of(null);
|
return of(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<div class="container-xl">
|
<div class="container-xl" (window:resize)="onResize($event)">
|
||||||
<h1 style="float: left;" i18n>BSQ Transactions</h1>
|
<h1 style="float: left;" i18n>BSQ Transactions</h1>
|
||||||
|
|
||||||
<div class="d-block float-right">
|
<div class="d-block float-right">
|
||||||
@@ -44,9 +44,7 @@
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
<div class="pagination">
|
<ngb-pagination class="pagination-container" *ngIf="transactions.value" [collectionSize]="transactions.value[1]" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="false"></ngb-pagination>
|
||||||
<ngb-pagination *ngIf="transactions.value" [size]="paginationSize" [collectionSize]="transactions.value[1]" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true"></ngb-pagination>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,13 +8,16 @@ label {
|
|||||||
left: inherit;
|
left: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination {
|
.pagination-container {
|
||||||
overflow: hidden;
|
float: none;
|
||||||
|
@media(min-width: 400px){
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h1{
|
.container-xl {
|
||||||
font-size: 1.5rem;
|
padding-bottom: 60px;
|
||||||
@media(min-width: 375px){
|
@media(min-width: 400px){
|
||||||
font-size: 2rem;
|
padding-bottom: 100px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef, OnDestroy } from '@angular/core';
|
||||||
import { BisqTransaction, BisqOutput } from '../bisq.interfaces';
|
import { BisqTransaction, BisqOutput } from '../bisq.interfaces';
|
||||||
|
|
||||||
import { merge, Observable } from 'rxjs';
|
import { Observable, Subscription } from 'rxjs';
|
||||||
import { switchMap, map, tap, filter } from 'rxjs/operators';
|
import { switchMap, map, tap } from 'rxjs/operators';
|
||||||
import { BisqApiService } from '../bisq-api.service';
|
import { BisqApiService } from '../bisq-api.service';
|
||||||
import { SeoService } from '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';
|
||||||
@@ -16,7 +16,7 @@ import { WebsocketService } from 'src/app/services/websocket.service';
|
|||||||
styleUrls: ['./bisq-transactions.component.scss'],
|
styleUrls: ['./bisq-transactions.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class BisqTransactionsComponent implements OnInit {
|
export class BisqTransactionsComponent implements OnInit, OnDestroy {
|
||||||
transactions$: Observable<[BisqTransaction[], number]>;
|
transactions$: Observable<[BisqTransaction[], number]>;
|
||||||
page = 1;
|
page = 1;
|
||||||
itemsPerPage = 50;
|
itemsPerPage = 50;
|
||||||
@@ -25,6 +25,7 @@ export class BisqTransactionsComponent implements OnInit {
|
|||||||
loadingItems: number[];
|
loadingItems: number[];
|
||||||
radioGroupForm: FormGroup;
|
radioGroupForm: FormGroup;
|
||||||
types: string[] = [];
|
types: string[] = [];
|
||||||
|
radioGroupSubscription: Subscription;
|
||||||
|
|
||||||
txTypeOptions: IMultiSelectOption[] = [
|
txTypeOptions: IMultiSelectOption[] = [
|
||||||
{ id: 1, name: $localize`Asset listing fee` },
|
{ id: 1, name: $localize`Asset listing fee` },
|
||||||
@@ -60,7 +61,7 @@ export class BisqTransactionsComponent implements OnInit {
|
|||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
paginationSize: 'sm' | 'lg' = 'md';
|
paginationSize: 'sm' | 'lg' = 'md';
|
||||||
paginationMaxSize = 4;
|
paginationMaxSize = 5;
|
||||||
|
|
||||||
txTypes = ['ASSET_LISTING_FEE', 'BLIND_VOTE', 'COMPENSATION_REQUEST', 'GENESIS', 'LOCKUP', 'PAY_TRADE_FEE',
|
txTypes = ['ASSET_LISTING_FEE', 'BLIND_VOTE', 'COMPENSATION_REQUEST', 'GENESIS', 'LOCKUP', 'PAY_TRADE_FEE',
|
||||||
'PROOF_OF_BURN', 'PROPOSAL', 'REIMBURSEMENT_REQUEST', 'TRANSFER_BSQ', 'UNLOCK', 'VOTE_REVEAL', 'IRREGULAR'];
|
'PROOF_OF_BURN', 'PROPOSAL', 'REIMBURSEMENT_REQUEST', 'TRANSFER_BSQ', 'UNLOCK', 'VOTE_REVEAL', 'IRREGULAR'];
|
||||||
@@ -85,57 +86,44 @@ export class BisqTransactionsComponent implements OnInit {
|
|||||||
|
|
||||||
this.loadingItems = Array(this.itemsPerPage);
|
this.loadingItems = Array(this.itemsPerPage);
|
||||||
|
|
||||||
if (document.body.clientWidth < 768) {
|
if (document.body.clientWidth < 670) {
|
||||||
this.paginationSize = 'sm';
|
this.paginationSize = 'sm';
|
||||||
this.paginationMaxSize = 3;
|
this.paginationMaxSize = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.transactions$ = merge(
|
this.transactions$ = this.route.queryParams
|
||||||
this.route.queryParams
|
|
||||||
.pipe(
|
|
||||||
filter((queryParams) => {
|
|
||||||
const newPage = parseInt(queryParams.page, 10);
|
|
||||||
const types = queryParams.types;
|
|
||||||
if (newPage !== this.page || types !== this.types.map((type) => this.txTypes.indexOf(type) + 1).join(',')) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}),
|
|
||||||
tap((queryParams) => {
|
|
||||||
if (queryParams.page) {
|
|
||||||
const newPage = parseInt(queryParams.page, 10);
|
|
||||||
this.page = newPage;
|
|
||||||
} else {
|
|
||||||
this.page = 1;
|
|
||||||
}
|
|
||||||
if (queryParams.types) {
|
|
||||||
const types = queryParams.types.split(',').map((str: string) => parseInt(str, 10));
|
|
||||||
this.types = types.map((id: number) => this.txTypes[id - 1]);
|
|
||||||
this.radioGroupForm.get('txTypes').setValue(types, { emitEvent: false });
|
|
||||||
} else {
|
|
||||||
this.types = [];
|
|
||||||
this.radioGroupForm.get('txTypes').setValue(this.txTypesDefaultChecked, { emitEvent: false });
|
|
||||||
}
|
|
||||||
this.cd.markForCheck();
|
|
||||||
})
|
|
||||||
),
|
|
||||||
this.radioGroupForm.valueChanges
|
|
||||||
.pipe(
|
|
||||||
tap((data) => {
|
|
||||||
this.types = data.txTypes.map((id: number) => this.txTypes[id - 1]);
|
|
||||||
if (this.types.length === this.txTypes.length) {
|
|
||||||
this.types = [];
|
|
||||||
}
|
|
||||||
this.page = 1;
|
|
||||||
this.typesChanged(data.txTypes);
|
|
||||||
this.cd.markForCheck();
|
|
||||||
})
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.pipe(
|
.pipe(
|
||||||
|
tap((queryParams) => {
|
||||||
|
if (queryParams.page) {
|
||||||
|
const newPage = parseInt(queryParams.page, 10);
|
||||||
|
this.page = newPage;
|
||||||
|
} else {
|
||||||
|
this.page = 1;
|
||||||
|
}
|
||||||
|
if (queryParams.types) {
|
||||||
|
const types = queryParams.types.split(',').map((str: string) => parseInt(str, 10));
|
||||||
|
this.types = types.map((id: number) => this.txTypes[id - 1]);
|
||||||
|
this.radioGroupForm.get('txTypes').setValue(types, { emitEvent: false });
|
||||||
|
} else {
|
||||||
|
this.types = [];
|
||||||
|
this.radioGroupForm.get('txTypes').setValue([], { emitEvent: false });
|
||||||
|
}
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}),
|
||||||
switchMap(() => this.bisqApiService.listTransactions$((this.page - 1) * this.itemsPerPage, this.itemsPerPage, this.types)),
|
switchMap(() => this.bisqApiService.listTransactions$((this.page - 1) * this.itemsPerPage, this.itemsPerPage, this.types)),
|
||||||
map((response) => [response.body, parseInt(response.headers.get('x-total-count'), 10)])
|
map((response) => [response.body, parseInt(response.headers.get('x-total-count'), 10)])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.radioGroupSubscription = this.radioGroupForm.valueChanges
|
||||||
|
.subscribe((data) => {
|
||||||
|
this.types = data.txTypes.map((id: number) => this.txTypes[id - 1]);
|
||||||
|
if (this.types.length === this.txTypes.length) {
|
||||||
|
this.types = [];
|
||||||
|
}
|
||||||
|
this.page = 1;
|
||||||
|
this.typesChanged(data.txTypes);
|
||||||
|
this.cd.markForCheck();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pageChange(page: number) {
|
pageChange(page: number) {
|
||||||
@@ -144,8 +132,6 @@ export class BisqTransactionsComponent implements OnInit {
|
|||||||
queryParams: { page: page },
|
queryParams: { page: page },
|
||||||
queryParamsHandling: 'merge',
|
queryParamsHandling: 'merge',
|
||||||
});
|
});
|
||||||
// trigger queryParams change
|
|
||||||
this.page = -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
typesChanged(types: number[]) {
|
typesChanged(types: number[]) {
|
||||||
@@ -168,4 +154,12 @@ export class BisqTransactionsComponent implements OnInit {
|
|||||||
trackByFn(index: number) {
|
trackByFn(index: number) {
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onResize(event: any) {
|
||||||
|
this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.radioGroupSubscription.unsubscribe();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,13 @@
|
|||||||
<tr *ngIf="input.isVerified">
|
<tr *ngIf="input.isVerified">
|
||||||
<td class="arrow-td">
|
<td class="arrow-td">
|
||||||
<ng-template [ngIf]="input.spendingTxId === null" [ngIfElse]="hasPreoutput">
|
<ng-template [ngIf]="input.spendingTxId === null" [ngIfElse]="hasPreoutput">
|
||||||
<i class="arrow grey"></i>
|
<span class="grey">
|
||||||
|
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
|
||||||
|
</span>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #hasPreoutput>
|
<ng-template #hasPreoutput>
|
||||||
<a [routerLink]="['/tx/' | relativeUrl, input.spendingTxId]">
|
<a [routerLink]="['/tx/' | relativeUrl, input.spendingTxId]" class="red">
|
||||||
<i class="arrow red"></i>
|
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
|
||||||
</a>
|
</a>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</td>
|
</td>
|
||||||
@@ -44,10 +46,14 @@
|
|||||||
<td class="text-right nowrap">
|
<td class="text-right nowrap">
|
||||||
<app-bsq-amount [bsq]="output.bsqAmount"></app-bsq-amount>
|
<app-bsq-amount [bsq]="output.bsqAmount"></app-bsq-amount>
|
||||||
</td>
|
</td>
|
||||||
<td class="pl-1 arrow-td">
|
<td class="arrow-td">
|
||||||
<i *ngIf="!output.spentInfo; else spent" class="arrow green"></i>
|
<span *ngIf="!output.spentInfo; else spent" class="green">
|
||||||
|
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
|
||||||
|
</span>
|
||||||
<ng-template #spent>
|
<ng-template #spent>
|
||||||
<a [routerLink]="['/tx/' | relativeUrl, output.spentInfo.txId]"><i class="arrow red"></i></a>
|
<a [routerLink]="['/tx/' | relativeUrl, output.spentInfo.txId]" class="red">
|
||||||
|
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
|
||||||
|
</a>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -1,62 +1,26 @@
|
|||||||
|
|
||||||
.arrow-td {
|
.arrow-td {
|
||||||
width: 22px;
|
width: 20px;
|
||||||
}
|
}
|
||||||
|
.green, .grey, .red {
|
||||||
.arrow {
|
font-size: 16px;
|
||||||
display: inline-block!important;
|
top: -2px;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 14px;
|
@media( min-width: 576px){
|
||||||
height: 22px;
|
font-size: 19px;
|
||||||
box-sizing: content-box
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow:before {
|
.green {
|
||||||
position: absolute;
|
color:#28a745;
|
||||||
content: '';
|
|
||||||
margin: auto;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: calc(-1*30px/3);
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-top: 6.66px solid transparent;
|
|
||||||
border-bottom: 6.66px solid transparent
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow:after {
|
.red {
|
||||||
position: absolute;
|
color:#dc3545;
|
||||||
content: '';
|
|
||||||
margin: auto;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: calc(30px/6);
|
|
||||||
width: calc(30px/3);
|
|
||||||
height: calc(20px/3);
|
|
||||||
background: rgba(0, 0, 0, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow.green:before {
|
.grey {
|
||||||
border-left: 10px solid #28a745;
|
color:#6c757d;
|
||||||
}
|
|
||||||
.arrow.green:after {
|
|
||||||
background-color:#28a745;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow.red:before {
|
|
||||||
border-left: 10px solid #dc3545;
|
|
||||||
}
|
|
||||||
.arrow.red:after {
|
|
||||||
background-color:#dc3545;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow.grey:before {
|
|
||||||
border-left: 10px solid #6c757d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow.grey:after {
|
|
||||||
background-color:#6c757d;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
@media (max-width: 767.98px) {
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ interface BisqScriptPubKey {
|
|||||||
addresses: string[];
|
addresses: string[];
|
||||||
asm: string;
|
asm: string;
|
||||||
hex: string;
|
hex: string;
|
||||||
reqSigs: number;
|
reqSigs?: number;
|
||||||
type: string;
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="about-text">
|
<div class="about-text" *ngIf="stateService.env.BASE_MODULE === 'mempool'; else marginBox">
|
||||||
<h5><ng-container i18n="about.about-the-project">The Mempool Open Source Project</ng-container> ™</h5>
|
<h5><ng-container i18n="about.about-the-project">The Mempool Open Source Project</ng-container><ng-template [ngIf]="locale.substr(0, 2) === 'en'"> ™</ng-template></h5>
|
||||||
<p i18n>Building a mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, without any advertising, altcoins, or third-party trackers.</p>
|
<p i18n>Building a mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, without any advertising, altcoins, or third-party trackers.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<ng-template #marginBox>
|
||||||
|
<div class="no-about-margin"></div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
<div class="social-icons">
|
<div class="social-icons">
|
||||||
<a target="_blank" href="https://github.com/mempool/mempool">
|
<a target="_blank" href="https://github.com/mempool/mempool">
|
||||||
@@ -43,6 +46,14 @@
|
|||||||
<img class="image" src="/resources/profile/exodus.svg" />
|
<img class="image" src="/resources/profile/exodus.svg" />
|
||||||
<span>Exodus</span>
|
<span>Exodus</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://foundrydigital.com/" target="_blank" title="Foundry">
|
||||||
|
<img class="image" src="/resources/profile/foundry.svg" />
|
||||||
|
<span>Foundry</span>
|
||||||
|
</a>
|
||||||
|
<a href="https://unchained.com/" target="_blank" title="Unchained">
|
||||||
|
<img class="image" src="/resources/profile/unchained.svg" />
|
||||||
|
<span>Unchained</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -115,6 +126,14 @@
|
|||||||
<img class="image" src="/resources/profile/blixt.png" />
|
<img class="image" src="/resources/profile/blixt.png" />
|
||||||
<span>Blixt</span>
|
<span>Blixt</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://github.com/ZeusLN/zeus" target="_blank" title="Zeus">
|
||||||
|
<img class="image" src="/resources/profile/zeus.png" />
|
||||||
|
<span>Zeus</span>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/vulpemventures/marina" target="_blank" title="Marina Wallet">
|
||||||
|
<img class="image" src="/resources/profile/marina.svg" />
|
||||||
|
<span>Marina</span>
|
||||||
|
</a>
|
||||||
<a href="https://github.com/Satpile/satpile" target="_blank" title="Satpile Watch-Only Wallet">
|
<a href="https://github.com/Satpile/satpile" target="_blank" title="Satpile Watch-Only Wallet">
|
||||||
<img class="image" src="/resources/profile/satpile.jpg" />
|
<img class="image" src="/resources/profile/satpile.jpg" />
|
||||||
<span>Satpile</span>
|
<span>Satpile</span>
|
||||||
@@ -123,10 +142,6 @@
|
|||||||
<img class="image" src="/resources/profile/blw.png" />
|
<img class="image" src="/resources/profile/blw.png" />
|
||||||
<span>BLW</span>
|
<span>BLW</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/pxsocs/warden" target="_blank" title="WARden Portfolio">
|
|
||||||
<img class="image" src="/resources/profile/warden.jpg" />
|
|
||||||
<span>WARden</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -175,25 +190,31 @@
|
|||||||
|
|
||||||
<div class="copyright">
|
<div class="copyright">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
Copyright (c) 2019-2021<br />
|
Copyright © 2019-2021<br>
|
||||||
The Mempool Open Source Project
|
The Mempool Open Source Project
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
This program is free software; you can redistribute it and/or modify it under the terms of (at your option) either:<br>
|
<a href="https://github.com/mempool/mempool">The Mempool Open Source Project</a> is free software; you can redistribute it and/or modify it under the terms of (at your option) either:<br>
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
1) the <a href="https://www.gnu.org/licenses/agpl-3.0-standalone.html">GNU Affero General Public License</a> as published by the Free Software Foundation, either version 3 of the License or any later version approved by a proxy statement published on <https://mempool.space/about>; or<br>
|
1) the <a href="https://www.gnu.org/licenses/agpl-3.0-standalone.html">GNU Affero General Public License</a> as published by the Free Software Foundation, either version 3 of the License or any later version approved by a proxy statement published on <https://mempool.space/about>; or<br>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
2) the <a href="https://www.gnu.org/licenses/gpl-3.0-standalone.html">GNU General Public License</a> as published by the Free Software Foundation, either version 3 of the License or any later version approved by a proxy statement published on <https://mempool.space/about>.<br>
|
2) the <a href="https://www.gnu.org/licenses/gpl-3.0-standalone.html">GNU General Public License</a> as published by the Free Software Foundation, either version 3 of the License or any later version approved by a proxy statement published on <https://mempool.space/about>.<br>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
<p>
|
||||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the full license terms for more details.<br>
|
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the full license terms for more details.<br>
|
||||||
</p>
|
</p>
|
||||||
|
<div class="title">
|
||||||
|
Trademark Notice<br>
|
||||||
|
</div>
|
||||||
<p>
|
<p>
|
||||||
mempool.space™, The Mempool Open Source Project™, and the Mempool block logo are trademarks of Mempool Space K.K. in Japan.
|
The Mempool Open Source Project™, mempool.space™, the mempool logo™, the mempool.space logos™, the mempool square logo™, and the mempool blocks logo™ are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on <https://mempool.space/trademark-policy>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -176,4 +176,8 @@
|
|||||||
.footer-version {
|
.footer-version {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-about-margin {
|
||||||
|
height: 10px;
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnInit } from '@angular/core';
|
||||||
import { WebsocketService } from '../../services/websocket.service';
|
import { WebsocketService } from '../../services/websocket.service';
|
||||||
import { SeoService } from 'src/app/services/seo.service';
|
import { SeoService } from 'src/app/services/seo.service';
|
||||||
import { StateService } from 'src/app/services/state.service';
|
import { StateService } from 'src/app/services/state.service';
|
||||||
@@ -25,9 +25,10 @@ export class AboutComponent implements OnInit {
|
|||||||
constructor(
|
constructor(
|
||||||
private websocketService: WebsocketService,
|
private websocketService: WebsocketService,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
private stateService: StateService,
|
public stateService: StateService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
|
@Inject(LOCALE_ID) public locale: string,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@@ -40,7 +41,7 @@ export class AboutComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sponsor() {
|
sponsor() {
|
||||||
if (this.officialMempoolSpace) {
|
if (this.officialMempoolSpace && this.stateService.env.BASE_MODULE === 'mempool') {
|
||||||
this.router.navigateByUrl('/sponsor');
|
this.router.navigateByUrl('/sponsor');
|
||||||
} else {
|
} else {
|
||||||
this.showNavigateToSponsor = true;
|
this.showNavigateToSponsor = true;
|
||||||
|
|||||||
@@ -18,6 +18,10 @@
|
|||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<table class="table table-borderless table-striped">
|
<table class="table table-borderless table-striped">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<tr *ngIf="addressInfo && addressInfo.unconfidential">
|
||||||
|
<td i18n="address.unconfidential">Unconfidential</td>
|
||||||
|
<td><a [routerLink]="['/address/' | relativeUrl, addressInfo.unconfidential]">{{ addressInfo.unconfidential }}</a> <app-clipboard [text]="addressInfo.unconfidential"></app-clipboard></td>
|
||||||
|
</tr>
|
||||||
<ng-template [ngIf]="!address.electrum">
|
<ng-template [ngIf]="!address.electrum">
|
||||||
<tr>
|
<tr>
|
||||||
<td i18n="address.total-received">Total received</td>
|
<td i18n="address.total-received">Total received</td>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { AudioService } from 'src/app/services/audio.service';
|
|||||||
import { ApiService } from 'src/app/services/api.service';
|
import { ApiService } from 'src/app/services/api.service';
|
||||||
import { of, merge, Subscription, Observable } from 'rxjs';
|
import { of, merge, Subscription, Observable } from 'rxjs';
|
||||||
import { SeoService } from 'src/app/services/seo.service';
|
import { SeoService } from 'src/app/services/seo.service';
|
||||||
|
import { AddressInformation } from 'src/app/interfaces/node-api.interface';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-address',
|
selector: 'app-address',
|
||||||
@@ -27,6 +28,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
error: any;
|
error: any;
|
||||||
mainSubscription: Subscription;
|
mainSubscription: Subscription;
|
||||||
addressLoadingStatus$: Observable<number>;
|
addressLoadingStatus$: Observable<number>;
|
||||||
|
addressInfo: null | AddressInformation = null;
|
||||||
|
|
||||||
totalConfirmedTxCount = 0;
|
totalConfirmedTxCount = 0;
|
||||||
loadedConfirmedTxCount = 0;
|
loadedConfirmedTxCount = 0;
|
||||||
@@ -67,8 +69,12 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
this.address = null;
|
this.address = null;
|
||||||
this.isLoadingTransactions = true;
|
this.isLoadingTransactions = true;
|
||||||
this.transactions = null;
|
this.transactions = null;
|
||||||
|
this.addressInfo = null;
|
||||||
document.body.scrollTo(0, 0);
|
document.body.scrollTo(0, 0);
|
||||||
this.addressString = params.get('id') || '';
|
this.addressString = params.get('id') || '';
|
||||||
|
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(this.addressString)) {
|
||||||
|
this.addressString = this.addressString.toLowerCase();
|
||||||
|
}
|
||||||
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
|
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
|
||||||
|
|
||||||
return merge(
|
return merge(
|
||||||
@@ -92,10 +98,20 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
)
|
)
|
||||||
.pipe(
|
.pipe(
|
||||||
filter((address) => !!address),
|
filter((address) => !!address),
|
||||||
|
tap((address: Address) => {
|
||||||
|
if (this.stateService.network === 'liquid' && /^([m-zA-HJ-NP-Z1-9]{26,35}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[a-km-zA-HJ-NP-Z1-9]{80})$/.test(address.address)) {
|
||||||
|
this.apiService.validateAddress$(address.address)
|
||||||
|
.subscribe((addressInfo) => {
|
||||||
|
this.addressInfo = addressInfo;
|
||||||
|
this.websocketService.startTrackAddress(addressInfo.unconfidential);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.websocketService.startTrackAddress(address.address);
|
||||||
|
}
|
||||||
|
}),
|
||||||
switchMap((address) => {
|
switchMap((address) => {
|
||||||
this.address = address;
|
this.address = address;
|
||||||
this.updateChainStats();
|
this.updateChainStats();
|
||||||
this.websocketService.startTrackAddress(address.address);
|
|
||||||
this.isLoadingAddress = false;
|
this.isLoadingAddress = false;
|
||||||
this.isLoadingTransactions = true;
|
this.isLoadingTransactions = true;
|
||||||
return this.electrsApiService.getAddressTransactions$(address.address);
|
return this.electrsApiService.getAddressTransactions$(address.address);
|
||||||
@@ -126,7 +142,13 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
this.tempTransactions[this.timeTxIndexes[index]].firstSeen = time;
|
this.tempTransactions[this.timeTxIndexes[index]].firstSeen = time;
|
||||||
});
|
});
|
||||||
this.tempTransactions.sort((a, b) => {
|
this.tempTransactions.sort((a, b) => {
|
||||||
return b.status.block_time - a.status.block_time || b.firstSeen - a.firstSeen;
|
if (b.status.confirmed) {
|
||||||
|
if (b.status.block_height === a.status.block_height) {
|
||||||
|
return b.status.block_time - a.status.block_time;
|
||||||
|
}
|
||||||
|
return b.status.block_height - a.status.block_height;
|
||||||
|
}
|
||||||
|
return b.firstSeen - a.firstSeen;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.transactions = this.tempTransactions;
|
this.transactions = this.tempTransactions;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Component, OnInit, Input, ChangeDetectionStrategy } from '@angular/core';
|
import { Component, OnInit, OnDestroy, Input, ChangeDetectionStrategy } from '@angular/core';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, Subscription } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-amount',
|
selector: 'app-amount',
|
||||||
@@ -8,11 +8,13 @@ import { Observable } from 'rxjs';
|
|||||||
styleUrls: ['./amount.component.scss'],
|
styleUrls: ['./amount.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class AmountComponent implements OnInit {
|
export class AmountComponent implements OnInit, OnDestroy {
|
||||||
conversions$: Observable<any>;
|
conversions$: Observable<any>;
|
||||||
viewFiat$: Observable<boolean>;
|
viewFiat$: Observable<boolean>;
|
||||||
network = '';
|
network = '';
|
||||||
|
|
||||||
|
stateSubscription: Subscription;
|
||||||
|
|
||||||
@Input() satoshis: number;
|
@Input() satoshis: number;
|
||||||
@Input() digitsInfo = '1.8-8';
|
@Input() digitsInfo = '1.8-8';
|
||||||
@Input() noFiat = false;
|
@Input() noFiat = false;
|
||||||
@@ -24,7 +26,13 @@ export class AmountComponent implements OnInit {
|
|||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.viewFiat$ = this.stateService.viewFiat$.asObservable();
|
this.viewFiat$ = this.stateService.viewFiat$.asObservable();
|
||||||
this.conversions$ = this.stateService.conversions$.asObservable();
|
this.conversions$ = this.stateService.conversions$.asObservable();
|
||||||
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
this.stateSubscription = this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
if (this.stateSubscription) {
|
||||||
|
this.stateSubscription.unsubscribe();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user