Compare commits
296 Commits
v2.2.0-dev
...
v2.2.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af5e0d7cd6 | ||
|
|
a2f1003916 | ||
|
|
f3b470b63e | ||
|
|
398a72c1a6 | ||
|
|
45af88774f | ||
|
|
71c6b0e11d | ||
|
|
994eb378af | ||
|
|
34d46e8ca5 | ||
|
|
a940f7e3b4 | ||
|
|
8c29395533 | ||
|
|
8208bbf0b7 | ||
|
|
1223c58a98 | ||
|
|
7d3757676f | ||
|
|
073bd60ed8 | ||
|
|
18c38fc1c1 | ||
|
|
0eb95447bb | ||
|
|
72a35200b3 | ||
|
|
11817c04f7 | ||
|
|
7a8b2db3fb | ||
|
|
6d910a5e24 | ||
|
|
99adccf43c | ||
|
|
b0c708659b | ||
|
|
e31b906084 | ||
|
|
7249620471 | ||
|
|
dc9d5d0be3 | ||
|
|
a9009d4de2 | ||
|
|
a265787cd4 | ||
|
|
4680519d2e | ||
|
|
5b17f88de2 | ||
|
|
a6d34ba4f1 | ||
|
|
508c8b0be3 | ||
|
|
f03249761b | ||
|
|
cb5877ba0a | ||
|
|
96f14d2781 | ||
|
|
8eb70416da | ||
|
|
b9246a72f2 | ||
|
|
43e222b9df | ||
|
|
5548d08a9e | ||
|
|
10fa39634e | ||
|
|
d92827a411 | ||
|
|
1c01094e07 | ||
|
|
f28a85f91b | ||
|
|
4895343d4e | ||
|
|
a0559cbb24 | ||
|
|
0293ba4a52 | ||
|
|
8b0d1db776 | ||
|
|
837e714b1f | ||
|
|
91a37d8fe8 | ||
|
|
226e72451c | ||
|
|
857a5ff6fc | ||
|
|
e6f8cf6cc8 | ||
|
|
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 | ||
|
|
eb6db6caf3 | ||
|
|
78c44eedbc | ||
|
|
b48a48a6be | ||
|
|
8e1aae1bbf | ||
|
|
807d4b0327 | ||
|
|
df588695ec | ||
|
|
da13349b14 | ||
|
|
f6e4907128 | ||
|
|
6be733490f | ||
|
|
fdf15c39a6 | ||
|
|
3b020046b7 | ||
|
|
8574ee6edd | ||
|
|
f937ea5745 | ||
|
|
741a020579 | ||
|
|
33d37a9b5b | ||
|
|
446bdfebea | ||
|
|
ca91afe45b | ||
|
|
33a5be5a7d | ||
|
|
6a4eee3711 | ||
|
|
13931ceec6 | ||
|
|
0c418a9e33 | ||
|
|
6f8b95a17f | ||
|
|
389c1d794c | ||
|
|
fca66f1b9f | ||
|
|
4c7d0cd2e5 | ||
|
|
1016586992 | ||
|
|
38c8f3acb4 | ||
|
|
962023fbc4 | ||
|
|
b4f8bb2f48 | ||
|
|
c26461fada | ||
|
|
1a996e1640 | ||
|
|
c80532b420 | ||
|
|
74c49b9ae7 | ||
|
|
3f03c9c2b6 | ||
|
|
f00e727e68 | ||
|
|
4338dd6c3f | ||
|
|
8385c50605 | ||
|
|
93c4b1caf1 | ||
|
|
49810b6a47 | ||
|
|
28d685a661 | ||
|
|
95d3d0feaf | ||
|
|
cbc5d67f62 | ||
|
|
87575bc0a2 | ||
|
|
8f74ef58f8 | ||
|
|
2475c67d5b | ||
|
|
bf45bf7b39 | ||
|
|
a1f0417997 | ||
|
|
237f265aab | ||
|
|
0087700aa5 | ||
|
|
861344ed6d | ||
|
|
9e343b346a | ||
|
|
e857dbc874 | ||
|
|
a10cd09ba8 | ||
|
|
f30777934f | ||
|
|
4f6bf297bf | ||
|
|
0121052f0b | ||
|
|
1bd0c40c15 | ||
|
|
2ee96cae44 | ||
|
|
28c8d7dba0 | ||
|
|
9b05ecedc6 | ||
|
|
8fbd273733 | ||
|
|
dec8ae2930 | ||
|
|
353b0e8729 | ||
|
|
71bfcea8a6 | ||
|
|
c54c30209e | ||
|
|
abc6b1519e | ||
|
|
4dcda2cf47 | ||
|
|
d055fabfeb | ||
|
|
dbb365f5e3 | ||
|
|
efb5deda43 | ||
|
|
a4cd6450e3 | ||
|
|
edad15da0d | ||
|
|
e70fd0045d | ||
|
|
794bc99cb6 | ||
|
|
cd1ec53af0 | ||
|
|
3e435d1394 | ||
|
|
50b94f8b72 | ||
|
|
f6f5b69487 | ||
|
|
66b27b9dd0 | ||
|
|
71fa2d67cb | ||
|
|
5cd2cfa097 | ||
|
|
cfd13b3655 | ||
|
|
3ffa60db1f | ||
|
|
4442964124 | ||
|
|
cb034020ef | ||
|
|
5aa57d6df9 | ||
|
|
c1a79e3a33 | ||
|
|
bbd21c9401 | ||
|
|
ad22f9cb46 | ||
|
|
939955fb84 | ||
|
|
63e67dba38 | ||
|
|
8a1230623e | ||
|
|
f20c73af7b | ||
|
|
12c99b86b7 | ||
|
|
934dd67384 | ||
|
|
870bd54b38 | ||
|
|
89300dae98 | ||
|
|
482a891cec | ||
|
|
098ab7d3a7 | ||
|
|
147d44d14b | ||
|
|
8ccdf3973c | ||
|
|
c09eb651ef | ||
|
|
ac91d814d6 | ||
|
|
be2f024da1 | ||
|
|
f137f45cef | ||
|
|
90784deacc | ||
|
|
8ed664e3a9 | ||
|
|
17b6916f31 | ||
|
|
b778d96910 | ||
|
|
5b2eb16d1c | ||
|
|
af61357ced | ||
|
|
f281e84396 | ||
|
|
0dc255edf9 | ||
|
|
2f8f3ca2e9 | ||
|
|
39bb93970b | ||
|
|
72d01a0b67 | ||
|
|
0b4da88802 | ||
|
|
d2fe000ad0 | ||
|
|
dcedc8a5ff | ||
|
|
0d03a9e6cc | ||
|
|
24b7acdc60 | ||
|
|
1000f4dd4d | ||
|
|
d5dba9128e | ||
|
|
84b0375c0c | ||
|
|
bf23a6649c | ||
|
|
aea35d4c86 | ||
|
|
52b7efdd53 | ||
|
|
492abad7a6 | ||
|
|
f566eae471 | ||
|
|
2f2be5c64b | ||
|
|
5d1af0a86e | ||
|
|
5cd5280b21 | ||
|
|
3a957ece05 | ||
|
|
3ead05fa51 | ||
|
|
8a838cd4dc | ||
|
|
b05f731332 | ||
|
|
06fd821bf8 | ||
|
|
6dbfcc9d1a | ||
|
|
001bddd529 | ||
|
|
56518b9655 | ||
|
|
da050ee3dc | ||
|
|
5878a2e631 | ||
|
|
c1fc08196b | ||
|
|
95a80157a7 | ||
|
|
165aa6eee2 | ||
|
|
b8fe7b621c | ||
|
|
04ec5e9564 | ||
|
|
2d4dff6de8 | ||
|
|
5cb98b9813 | ||
|
|
d4508bd876 | ||
|
|
6ccac1df79 | ||
|
|
b38fc824e6 | ||
|
|
cdbe90c182 | ||
|
|
6b5b80f866 | ||
|
|
d74677628b | ||
|
|
f0d46d6ed8 | ||
|
|
220d9afd97 | ||
|
|
dfd88a7ff9 | ||
|
|
eec36ae4e6 | ||
|
|
0a07a16650 | ||
|
|
e62ee72149 | ||
|
|
117f5410d7 | ||
|
|
f6ea45b61f | ||
|
|
344d1247bd | ||
|
|
fcf7955d63 | ||
|
|
1ae002385d | ||
|
|
dc36bfcfe4 | ||
|
|
da77dbece1 | ||
|
|
8e29a4cefd | ||
|
|
146fcfc16d | ||
|
|
308dd2c7ad | ||
|
|
1d4ed85d50 | ||
|
|
d99fd5d59a | ||
|
|
2fca34faaa | ||
|
|
38e866995f | ||
|
|
eeb7447988 |
34
.github/workflows/cypress.yml
vendored
Normal file
34
.github/workflows/cypress.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
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
|
||||
uses: cypress-io/github-action@v2
|
||||
with:
|
||||
working-directory: frontend
|
||||
build: npm run config:defaults
|
||||
start: npm run start:local-prod
|
||||
wait-on: 'http://localhost:4200'
|
||||
wait-on-timeout: 120
|
||||
record: true
|
||||
parallel: true
|
||||
group: Tests on ${{ matrix.browser }}
|
||||
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 }}
|
||||
15
.github/workflows/on-tag.yml
vendored
15
.github/workflows/on-tag.yml
vendored
@@ -27,6 +27,9 @@ jobs:
|
||||
- name: Show set environment variables
|
||||
run: |
|
||||
printf " TAG: %s\n" "$TAG"
|
||||
|
||||
- name: Add SHORT_SHA env property with commit short sha
|
||||
run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV
|
||||
|
||||
- name: Login to Docker for building
|
||||
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
||||
@@ -64,14 +67,6 @@ jobs:
|
||||
--cache-to "type=local,dest=/tmp/.buildx-cache" \
|
||||
--platform linux/amd64,linux/arm64,linux/arm/v7 \
|
||||
--tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:$TAG \
|
||||
--output "type=registry" ./${{ matrix.service }}/
|
||||
|
||||
- name: Run Docker buildx for ${{ matrix.service }} against latest
|
||||
run: |
|
||||
docker buildx build \
|
||||
--cache-from "type=local,src=/tmp/.buildx-cache" \
|
||||
--cache-to "type=local,dest=/tmp/.buildx-cache" \
|
||||
--platform linux/amd64,linux/arm64,linux/arm/v7 \
|
||||
--tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:latest \
|
||||
--output "type=registry" ./${{ matrix.service }}/
|
||||
|
||||
--output "type=registry" ./${{ matrix.service }}/ \
|
||||
--build-arg commitHash=$SHORT_SHA
|
||||
|
||||
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
|
||||
proxy statement published on <https://mempool.space/about>.
|
||||
|
||||
However, these licenses do not grant you any rights to use the "mempool.space"
|
||||
trademarks or logos, or any other trademarks of Mempool Space K.K.
|
||||
However, this copyright license does not include an implied right or license to
|
||||
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
|
||||
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.
|
||||
|
||||

|
||||

|
||||
|
||||
## 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:
|
||||
|
||||
```bash
|
||||
sudo rsync -av --delete dist/mempool/ /var/www/html/
|
||||
sudo rsync -av --delete dist/mempool /var/www/
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
# 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
|
||||
certbot --nginx -d example.com
|
||||
|
||||
@@ -50,11 +50,7 @@
|
||||
"ENABLED": true,
|
||||
"TX_PER_SECOND_SAMPLE_PERIOD": 150
|
||||
},
|
||||
"BISQ_BLOCKS": {
|
||||
"ENABLED": false,
|
||||
"DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db/json"
|
||||
},
|
||||
"BISQ_MARKETS": {
|
||||
"BISQ": {
|
||||
"ENABLED": false,
|
||||
"DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db"
|
||||
}
|
||||
|
||||
114
backend/package-lock.json
generated
114
backend/package-lock.json
generated
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "mempool-backend",
|
||||
"version": "2.0.0",
|
||||
"version": "2.2.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mempool-backend",
|
||||
"version": "2.0.0",
|
||||
"version": "2.2.1",
|
||||
"license": "GNU Affero General Public License v3.0",
|
||||
"dependencies": {
|
||||
"@mempool/bitcoin": "^3.0.2",
|
||||
"@mempool/bitcoin": "^3.0.3",
|
||||
"@mempool/electrum-client": "^1.1.7",
|
||||
"axios": "^0.21.1",
|
||||
"bitcoinjs-lib": "^5.2.0",
|
||||
@@ -18,13 +18,13 @@
|
||||
"locutus": "^2.0.12",
|
||||
"mysql2": "2.2.5",
|
||||
"node-worker-threads-pool": "^1.4.3",
|
||||
"ws": "^7.4.4"
|
||||
"ws": "^7.4.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/compression": "^1.0.1",
|
||||
"@types/express": "^4.17.2",
|
||||
"@types/locutus": "^0.0.6",
|
||||
"@types/ws": "^6.0.4",
|
||||
"@types/ws": "^7.4.4",
|
||||
"tslint": "^6.1.0",
|
||||
"typescript": "^4.1.5"
|
||||
}
|
||||
@@ -56,9 +56,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mempool/bitcoin": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@mempool/bitcoin/-/bitcoin-3.0.2.tgz",
|
||||
"integrity": "sha512-WNHFTDJEEBmakSPAbrJ933mGgm1uYxmOElyQYZVW7D7CRUd8mKek+QlViin63e71vyfMVOGXtWwSb87dxghggQ==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@mempool/bitcoin/-/bitcoin-3.0.3.tgz",
|
||||
"integrity": "sha512-10UdbwchnevlebDTN+Xhv75AEhDmTMy9UgWHlqx5MG2mheFG6+eqmtHsdxeYnv3IAtTtlRfA6fY0RbV/x4TNFQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
}
|
||||
@@ -163,10 +163,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.4.tgz",
|
||||
"integrity": "sha512-PpPrX7SZW9re6+Ha8ojZG4Se8AZXgf0GK6zmfqEuCsY49LFDNXO3SByp44X3dFEqtB73lkCDAdUazhAjVPiNwg==",
|
||||
"version": "7.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.4.tgz",
|
||||
"integrity": "sha512-d/7W23JAXPodQNbOZNXvl2K+bqAQrCMwlh/nuQsPSQk6Fq0opHoPrUw43aHsvSbIiQPr8Of2hkFbnz1XBFVyZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
@@ -548,17 +549,17 @@
|
||||
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
|
||||
},
|
||||
"node_modules/elliptic": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
|
||||
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
|
||||
"version": "6.5.4",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
|
||||
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
|
||||
"dependencies": {
|
||||
"bn.js": "^4.4.0",
|
||||
"brorand": "^1.0.1",
|
||||
"bn.js": "^4.11.9",
|
||||
"brorand": "^1.1.0",
|
||||
"hash.js": "^1.0.0",
|
||||
"hmac-drbg": "^1.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
"minimalistic-assert": "^1.0.0",
|
||||
"minimalistic-crypto-utils": "^1.0.0"
|
||||
"hmac-drbg": "^1.0.1",
|
||||
"inherits": "^2.0.4",
|
||||
"minimalistic-assert": "^1.0.1",
|
||||
"minimalistic-crypto-utils": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
@@ -569,11 +570,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/es6-promise": {
|
||||
"version": "4.2.8",
|
||||
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
|
||||
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
@@ -895,14 +891,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/locutus": {
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmjs.org/locutus/-/locutus-2.0.14.tgz",
|
||||
"integrity": "sha512-0H1o1iHNEp3kJ5rW57bT/CAP5g6Qm0Zd817Wcx2+rOMTYyIJoc482Ja1v9dB6IUjwvWKcBNdYi7x2lRXtlJ3bA==",
|
||||
"dependencies": {
|
||||
"es6-promise": "^4.2.5"
|
||||
},
|
||||
"version": "2.0.15",
|
||||
"resolved": "https://registry.npmjs.org/locutus/-/locutus-2.0.15.tgz",
|
||||
"integrity": "sha512-2xWC4RkoAoCVXEb/stzEgG1TNgd+mrkLBj6TuEDNyUoKeQ2XzDTyJUC23sMiqbL6zJmJSP3w59OZo+zc4IBOmA==",
|
||||
"engines": {
|
||||
"node": ">= 0.12.0"
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/long": {
|
||||
@@ -1544,9 +1537,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "7.4.4",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
|
||||
"integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==",
|
||||
"version": "7.4.6",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
|
||||
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
},
|
||||
@@ -1597,9 +1590,9 @@
|
||||
}
|
||||
},
|
||||
"@mempool/bitcoin": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@mempool/bitcoin/-/bitcoin-3.0.2.tgz",
|
||||
"integrity": "sha512-WNHFTDJEEBmakSPAbrJ933mGgm1uYxmOElyQYZVW7D7CRUd8mKek+QlViin63e71vyfMVOGXtWwSb87dxghggQ=="
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@mempool/bitcoin/-/bitcoin-3.0.3.tgz",
|
||||
"integrity": "sha512-10UdbwchnevlebDTN+Xhv75AEhDmTMy9UgWHlqx5MG2mheFG6+eqmtHsdxeYnv3IAtTtlRfA6fY0RbV/x4TNFQ=="
|
||||
},
|
||||
"@mempool/electrum-client": {
|
||||
"version": "1.1.8",
|
||||
@@ -1698,9 +1691,9 @@
|
||||
}
|
||||
},
|
||||
"@types/ws": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.4.tgz",
|
||||
"integrity": "sha512-PpPrX7SZW9re6+Ha8ojZG4Se8AZXgf0GK6zmfqEuCsY49LFDNXO3SByp44X3dFEqtB73lkCDAdUazhAjVPiNwg==",
|
||||
"version": "7.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.4.tgz",
|
||||
"integrity": "sha512-d/7W23JAXPodQNbOZNXvl2K+bqAQrCMwlh/nuQsPSQk6Fq0opHoPrUw43aHsvSbIiQPr8Of2hkFbnz1XBFVyZQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
@@ -2042,17 +2035,17 @@
|
||||
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
|
||||
},
|
||||
"elliptic": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
|
||||
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
|
||||
"version": "6.5.4",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
|
||||
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
|
||||
"requires": {
|
||||
"bn.js": "^4.4.0",
|
||||
"brorand": "^1.0.1",
|
||||
"bn.js": "^4.11.9",
|
||||
"brorand": "^1.1.0",
|
||||
"hash.js": "^1.0.0",
|
||||
"hmac-drbg": "^1.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
"minimalistic-assert": "^1.0.0",
|
||||
"minimalistic-crypto-utils": "^1.0.0"
|
||||
"hmac-drbg": "^1.0.1",
|
||||
"inherits": "^2.0.4",
|
||||
"minimalistic-assert": "^1.0.1",
|
||||
"minimalistic-crypto-utils": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"encodeurl": {
|
||||
@@ -2060,11 +2053,6 @@
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
|
||||
},
|
||||
"es6-promise": {
|
||||
"version": "4.2.8",
|
||||
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
|
||||
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
|
||||
},
|
||||
"escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
@@ -2321,12 +2309,9 @@
|
||||
}
|
||||
},
|
||||
"locutus": {
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmjs.org/locutus/-/locutus-2.0.14.tgz",
|
||||
"integrity": "sha512-0H1o1iHNEp3kJ5rW57bT/CAP5g6Qm0Zd817Wcx2+rOMTYyIJoc482Ja1v9dB6IUjwvWKcBNdYi7x2lRXtlJ3bA==",
|
||||
"requires": {
|
||||
"es6-promise": "^4.2.5"
|
||||
}
|
||||
"version": "2.0.15",
|
||||
"resolved": "https://registry.npmjs.org/locutus/-/locutus-2.0.15.tgz",
|
||||
"integrity": "sha512-2xWC4RkoAoCVXEb/stzEgG1TNgd+mrkLBj6TuEDNyUoKeQ2XzDTyJUC23sMiqbL6zJmJSP3w59OZo+zc4IBOmA=="
|
||||
},
|
||||
"long": {
|
||||
"version": "4.0.0",
|
||||
@@ -2833,10 +2818,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"ws": {
|
||||
"version": "7.4.4",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
|
||||
"integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==",
|
||||
"requires": {}
|
||||
"version": "7.4.6",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
|
||||
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A=="
|
||||
},
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mempool-backend",
|
||||
"version": "2.2.0-dev",
|
||||
"version": "2.2.1",
|
||||
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
||||
"license": "GNU Affero General Public License v3.0",
|
||||
"homepage": "https://mempool.space",
|
||||
@@ -28,7 +28,7 @@
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mempool/bitcoin": "^3.0.2",
|
||||
"@mempool/bitcoin": "^3.0.3",
|
||||
"@mempool/electrum-client": "^1.1.7",
|
||||
"axios": "^0.21.1",
|
||||
"bitcoinjs-lib": "^5.2.0",
|
||||
@@ -37,13 +37,13 @@
|
||||
"locutus": "^2.0.12",
|
||||
"mysql2": "2.2.5",
|
||||
"node-worker-threads-pool": "^1.4.3",
|
||||
"ws": "^7.4.4"
|
||||
"ws": "^7.4.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/compression": "^1.0.1",
|
||||
"@types/express": "^4.17.2",
|
||||
"@types/locutus": "^0.0.6",
|
||||
"@types/ws": "^6.0.4",
|
||||
"@types/ws": "^7.4.4",
|
||||
"tslint": "^6.1.0",
|
||||
"typescript": "^4.1.5"
|
||||
}
|
||||
|
||||
@@ -8,9 +8,10 @@ import { StaticPool } from 'node-worker-threads-pool';
|
||||
import logger from '../../logger';
|
||||
|
||||
class Bisq {
|
||||
private static BLOCKS_JSON_FILE_PATH = config.BISQ_BLOCKS.DATA_PATH + '/all/blocks.json';
|
||||
private static BLOCKS_JSON_FILE_PATH = config.BISQ.DATA_PATH + '/json/all/blocks.json';
|
||||
private latestBlockHeight = 0;
|
||||
private blocks: BisqBlock[] = [];
|
||||
private allBlocks: BisqBlock[] = [];
|
||||
private transactions: BisqTransaction[] = [];
|
||||
private transactionIndex: { [txId: string]: BisqTransaction } = {};
|
||||
private blockIndex: { [hash: string]: BisqBlock } = {};
|
||||
@@ -98,7 +99,7 @@ class Bisq {
|
||||
this.topDirectoryWatcher.close();
|
||||
}
|
||||
let fsWait: NodeJS.Timeout | null = null;
|
||||
this.topDirectoryWatcher = fs.watch(config.BISQ_BLOCKS.DATA_PATH, () => {
|
||||
this.topDirectoryWatcher = fs.watch(config.BISQ.DATA_PATH + '/json', () => {
|
||||
if (fsWait) {
|
||||
clearTimeout(fsWait);
|
||||
}
|
||||
@@ -126,7 +127,7 @@ class Bisq {
|
||||
return;
|
||||
}
|
||||
let fsWait: NodeJS.Timeout | null = null;
|
||||
this.subdirectoryWatcher = fs.watch(config.BISQ_BLOCKS.DATA_PATH + '/all', () => {
|
||||
this.subdirectoryWatcher = fs.watch(config.BISQ.DATA_PATH + '/json/all', () => {
|
||||
if (fsWait) {
|
||||
clearTimeout(fsWait);
|
||||
}
|
||||
@@ -161,7 +162,7 @@ class Bisq {
|
||||
this.buildIndex();
|
||||
this.calculateStats();
|
||||
} catch (e) {
|
||||
logger.err('loadBisqDumpFile() error.' + e.message || e);
|
||||
logger.info('loadBisqDumpFile() error.' + e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +172,7 @@ class Bisq {
|
||||
this.transactionIndex = {};
|
||||
this.addressIndex = {};
|
||||
|
||||
this.blocks.forEach((block) => {
|
||||
this.allBlocks.forEach((block) => {
|
||||
/* Build block index */
|
||||
if (!this.blockIndex[block.hash]) {
|
||||
this.blockIndex[block.hash] = block;
|
||||
@@ -245,9 +246,10 @@ class Bisq {
|
||||
if (cacheData && cacheData.length !== 0) {
|
||||
logger.debug('Processing Bisq data dump...');
|
||||
const data: BisqBlocks = await this.jsonParsePool.exec(cacheData);
|
||||
if (data.blocks && data.blocks.length !== this.blocks.length) {
|
||||
this.blocks = data.blocks.filter((block) => block.txs.length > 0);
|
||||
this.blocks.reverse();
|
||||
if (data.blocks && data.blocks.length !== this.allBlocks.length) {
|
||||
this.allBlocks = data.blocks;
|
||||
this.allBlocks.reverse();
|
||||
this.blocks = this.allBlocks.filter((block) => block.txs.length > 0);
|
||||
this.latestBlockHeight = data.chainHeight;
|
||||
const time = new Date().getTime() - start;
|
||||
logger.debug('Bisq dump processed in ' + time + ' ms (worker thread)');
|
||||
|
||||
@@ -457,6 +457,30 @@ class BisqMarketsApi {
|
||||
}
|
||||
}
|
||||
|
||||
getVolumesByTime(time: number): MarketVolume[] {
|
||||
const timestamp_from = new Date().getTime() / 1000 - time;
|
||||
const timestamp_to = new Date().getTime() / 1000;
|
||||
|
||||
const trades = this.getTradesByCriteria(undefined, timestamp_to, timestamp_from,
|
||||
undefined, undefined, undefined, 'asc', Number.MAX_SAFE_INTEGER);
|
||||
|
||||
const markets: any = {};
|
||||
|
||||
for (const trade of trades) {
|
||||
if (!markets[trade._market]) {
|
||||
markets[trade._market] = {
|
||||
'volume': 0,
|
||||
'num_trades': 0,
|
||||
};
|
||||
}
|
||||
|
||||
markets[trade._market]['volume'] += this.fiatCurrenciesIndexed[trade.currency] ? trade._tradeAmount : trade._tradeVolume;
|
||||
markets[trade._market]['num_trades']++;
|
||||
}
|
||||
|
||||
return markets;
|
||||
}
|
||||
|
||||
private getTradesSummarized(trades: TradesData[], timestamp_from: number, interval?: string): SummarizedIntervals {
|
||||
const intervals: any = {};
|
||||
const intervals_prices: any = {};
|
||||
|
||||
@@ -6,7 +6,7 @@ import logger from '../../logger';
|
||||
|
||||
class Bisq {
|
||||
private static FOLDER_WATCH_CHANGE_DETECTION_DEBOUNCE = 4000;
|
||||
private static MARKET_JSON_PATH = config.BISQ_MARKETS.DATA_PATH;
|
||||
private static MARKET_JSON_PATH = config.BISQ.DATA_PATH;
|
||||
private static MARKET_JSON_FILE_PATHS = {
|
||||
activeCryptoCurrency: '/active_crypto_currency_list.json',
|
||||
activeFiatCurrency: '/active_fiat_currency_list.json',
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface AbstractBitcoinApi {
|
||||
$getBlockHeightTip(): Promise<number>;
|
||||
$getTxIdsForBlock(hash: string): Promise<string[]>;
|
||||
$getBlockHash(height: number): Promise<string>;
|
||||
$getBlockHeader(hash: string): Promise<string>;
|
||||
$getBlock(hash: string): Promise<IEsploraApi.Block>;
|
||||
$getAddress(address: string): Promise<IEsploraApi.Address>;
|
||||
$getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
|
||||
|
||||
@@ -113,4 +113,46 @@ export namespace IBitcoinApi {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -56,10 +56,19 @@ class BitcoinApi implements AbstractBitcoinApi {
|
||||
.then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx);
|
||||
}
|
||||
|
||||
$getRawBlock(hash: string): Promise<string> {
|
||||
return this.bitcoindClient.getBlock(hash, 0);
|
||||
}
|
||||
|
||||
|
||||
$getBlockHash(height: number): Promise<string> {
|
||||
return this.bitcoindClient.getBlockHash(height);
|
||||
}
|
||||
|
||||
$getBlockHeader(hash: string): Promise<string> {
|
||||
return this.bitcoindClient.getBlockHeader(hash,false);
|
||||
}
|
||||
|
||||
async $getBlock(hash: string): Promise<IEsploraApi.Block> {
|
||||
const foundBlock = blocks.getBlocks().find((block) => block.id === hash);
|
||||
if (foundBlock) {
|
||||
@@ -147,6 +156,9 @@ class BitcoinApi implements AbstractBitcoinApi {
|
||||
esploraTransaction = await this.$calculateFeeFromInputs(esploraTransaction, addPrevout);
|
||||
} else {
|
||||
esploraTransaction = await this.$appendMempoolFeeData(esploraTransaction);
|
||||
if (addPrevout) {
|
||||
esploraTransaction = await this.$calculateFeeFromInputs(esploraTransaction, addPrevout);
|
||||
}
|
||||
}
|
||||
|
||||
return esploraTransaction;
|
||||
|
||||
@@ -40,6 +40,10 @@ class BitcoinBaseApi {
|
||||
}
|
||||
return this.bitcoindClient.getMempoolInfo();
|
||||
}
|
||||
|
||||
$getBlockchainInfo(): Promise<IBitcoinApi.BlockchainInfo> {
|
||||
return this.bitcoindClient.getBlockchainInfo();
|
||||
}
|
||||
}
|
||||
|
||||
export default new BitcoinBaseApi();
|
||||
|
||||
@@ -9,6 +9,7 @@ export namespace IEsploraApi {
|
||||
vin: Vin[];
|
||||
vout: Vout[];
|
||||
status: Status;
|
||||
hex?: string;
|
||||
}
|
||||
|
||||
export interface Recent {
|
||||
|
||||
@@ -35,6 +35,11 @@ class ElectrsApi implements AbstractBitcoinApi {
|
||||
.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> {
|
||||
return axios.get<IEsploraApi.Block>(config.ESPLORA.REST_API_URL + '/block/' + hash, this.axiosConfig)
|
||||
.then((response) => response.data);
|
||||
|
||||
@@ -6,12 +6,15 @@ import { BlockExtended, TransactionExtended } from '../mempool.interfaces';
|
||||
import { Common } from './common';
|
||||
import diskCache from './disk-cache';
|
||||
import transactionUtils from './transaction-utils';
|
||||
import bitcoinBaseApi from './bitcoin/bitcoin-base.api';
|
||||
|
||||
class Blocks {
|
||||
private static INITIAL_BLOCK_AMOUNT = 8;
|
||||
private blocks: BlockExtended[] = [];
|
||||
private currentBlockHeight = 0;
|
||||
private currentDifficulty = 0;
|
||||
private lastDifficultyAdjustmentTime = 0;
|
||||
private previousDifficultyRetarget = 0;
|
||||
private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
|
||||
|
||||
constructor() { }
|
||||
@@ -43,10 +46,21 @@ class Blocks {
|
||||
}
|
||||
|
||||
if (!this.lastDifficultyAdjustmentTime) {
|
||||
const heightDiff = blockHeightTip % 2016;
|
||||
const blockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff);
|
||||
const block = await bitcoinApi.$getBlock(blockHash);
|
||||
this.lastDifficultyAdjustmentTime = block.timestamp;
|
||||
const blockchainInfo = await bitcoinBaseApi.$getBlockchainInfo();
|
||||
if (blockchainInfo.blocks === blockchainInfo.headers) {
|
||||
const heightDiff = blockHeightTip % 2016;
|
||||
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) {
|
||||
@@ -101,7 +115,9 @@ class Blocks {
|
||||
blockExtended.feeRange = transactions.length > 1 ? Common.getFeesInRange(transactions, 8) : [0, 0];
|
||||
|
||||
if (block.height % 2016 === 0) {
|
||||
this.previousDifficultyRetarget = (block.difficulty - this.currentDifficulty) / this.currentDifficulty * 100;
|
||||
this.lastDifficultyAdjustmentTime = block.timestamp;
|
||||
this.currentDifficulty = block.difficulty;
|
||||
}
|
||||
|
||||
this.blocks.push(blockExtended);
|
||||
@@ -122,6 +138,10 @@ class Blocks {
|
||||
return this.lastDifficultyAdjustmentTime;
|
||||
}
|
||||
|
||||
public getPreviousDifficultyRetarget(): number {
|
||||
return this.previousDifficultyRetarget;
|
||||
}
|
||||
|
||||
public getCurrentBlockHeight(): number {
|
||||
return this.currentBlockHeight;
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ export class Common {
|
||||
totalFees += tx.bestDescendant.fee;
|
||||
}
|
||||
|
||||
tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
|
||||
tx.effectiveFeePerVsize = Math.max(1, totalFees / (totalWeight / 4));
|
||||
tx.cpfpChecked = true;
|
||||
|
||||
return {
|
||||
|
||||
@@ -43,7 +43,7 @@ class FeeApi {
|
||||
const multiplier = (pBlock.blockVSize - 500000) / 500000;
|
||||
return Math.max(Math.round(useFee * multiplier), this.defaultFee);
|
||||
}
|
||||
return Math.round(useFee);
|
||||
return Math.ceil(useFee);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ class FiatConversion {
|
||||
|
||||
public startService() {
|
||||
logger.info('Starting currency rates service');
|
||||
setInterval(this.updateCurrency.bind(this), 1000 * 60 * 60);
|
||||
setInterval(this.updateCurrency.bind(this), 1000 * 60);
|
||||
this.updateCurrency();
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class WebsocketHandler {
|
||||
|
||||
this.wss.on('connection', (client: WebSocket) => {
|
||||
client.on('error', logger.info);
|
||||
client.on('message', (message: string) => {
|
||||
client.on('message', async (message: string) => {
|
||||
try {
|
||||
const parsedMessage: WebsocketResponse = JSON.parse(message);
|
||||
const response = {};
|
||||
@@ -53,9 +53,25 @@ class WebsocketHandler {
|
||||
if (parsedMessage['watch-mempool']) {
|
||||
const tx = memPool.getMempool()[client['track-tx']];
|
||||
if (tx) {
|
||||
response['tx'] = tx;
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
response['tx'] = tx;
|
||||
} else {
|
||||
// tx.prevouts is missing from transactions when in bitcoind mode
|
||||
try {
|
||||
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
|
||||
response['tx'] = fullTx;
|
||||
} catch (e) {
|
||||
logger.debug('Error finding transaction: ' + e.message || e);
|
||||
}
|
||||
}
|
||||
} 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.message || e);
|
||||
client['track-mempool-tx'] = parsedMessage['track-tx'];
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -96,6 +112,14 @@ class WebsocketHandler {
|
||||
client['track-donation'] = parsedMessage['track-donation'];
|
||||
}
|
||||
|
||||
if (parsedMessage['track-bisq-market']) {
|
||||
if (/^[a-z]{3}_[a-z]{3}$/.test(parsedMessage['track-bisq-market'])) {
|
||||
client['track-bisq-market'] = parsedMessage['track-bisq-market'];
|
||||
} else {
|
||||
client['track-bisq-market'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(response).length) {
|
||||
client.send(JSON.stringify(response));
|
||||
}
|
||||
@@ -155,6 +179,7 @@ class WebsocketHandler {
|
||||
'mempoolInfo': memPool.getMempoolInfo(),
|
||||
'vBytesPerSecond': memPool.getVBytesPerSecond(),
|
||||
'lastDifficultyAdjustment': blocks.getLastDifficultyAdjustmentTime(),
|
||||
'previousRetarget': blocks.getPreviousDifficultyRetarget(),
|
||||
'blocks': _blocks,
|
||||
'conversions': fiatConversion.getConversionRates(),
|
||||
'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
|
||||
@@ -367,6 +392,7 @@ class WebsocketHandler {
|
||||
'block': block,
|
||||
'mempoolInfo': memPool.getMempoolInfo(),
|
||||
'lastDifficultyAdjustment': blocks.getLastDifficultyAdjustmentTime(),
|
||||
'previousRetarget': blocks.getPreviousDifficultyRetarget(),
|
||||
};
|
||||
|
||||
if (mBlocks && client['want-mempool-blocks']) {
|
||||
|
||||
@@ -52,11 +52,7 @@ interface IConfig {
|
||||
ENABLED: boolean;
|
||||
TX_PER_SECOND_SAMPLE_PERIOD: number;
|
||||
};
|
||||
BISQ_BLOCKS: {
|
||||
ENABLED: boolean;
|
||||
DATA_PATH: string;
|
||||
};
|
||||
BISQ_MARKETS: {
|
||||
BISQ: {
|
||||
ENABLED: boolean;
|
||||
DATA_PATH: string;
|
||||
};
|
||||
@@ -114,11 +110,7 @@ const defaults: IConfig = {
|
||||
'ENABLED': true,
|
||||
'TX_PER_SECOND_SAMPLE_PERIOD': 150
|
||||
},
|
||||
'BISQ_BLOCKS': {
|
||||
'ENABLED': false,
|
||||
'DATA_PATH': '/bisq/statsnode-data/btc_mainnet/db/json'
|
||||
},
|
||||
'BISQ_MARKETS': {
|
||||
'BISQ': {
|
||||
'ENABLED': false,
|
||||
'DATA_PATH': '/bisq/statsnode-data/btc_mainnet/db'
|
||||
},
|
||||
@@ -133,8 +125,7 @@ class Config implements IConfig {
|
||||
DATABASE: IConfig['DATABASE'];
|
||||
SYSLOG: IConfig['SYSLOG'];
|
||||
STATISTICS: IConfig['STATISTICS'];
|
||||
BISQ_BLOCKS: IConfig['BISQ_BLOCKS'];
|
||||
BISQ_MARKETS: IConfig['BISQ_MARKETS'];
|
||||
BISQ: IConfig['BISQ'];
|
||||
|
||||
constructor() {
|
||||
const configs = this.merge(configFile, defaults);
|
||||
@@ -146,8 +137,7 @@ class Config implements IConfig {
|
||||
this.DATABASE = configs.DATABASE;
|
||||
this.SYSLOG = configs.SYSLOG;
|
||||
this.STATISTICS = configs.STATISTICS;
|
||||
this.BISQ_BLOCKS = configs.BISQ_BLOCKS;
|
||||
this.BISQ_MARKETS = configs.BISQ_MARKETS;
|
||||
this.BISQ = configs.BISQ;
|
||||
}
|
||||
|
||||
merge = (...objects: object[]): IConfig => {
|
||||
|
||||
@@ -81,7 +81,7 @@ class Server {
|
||||
await checkDbConnection();
|
||||
}
|
||||
|
||||
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED) {
|
||||
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && cluster.isMaster) {
|
||||
statistics.startStatistics();
|
||||
}
|
||||
|
||||
@@ -90,13 +90,10 @@ class Server {
|
||||
this.setUpHttpApiRoutes();
|
||||
this.runMainUpdateLoop();
|
||||
|
||||
if (config.BISQ_BLOCKS.ENABLED) {
|
||||
if (config.BISQ.ENABLED) {
|
||||
bisq.startBisqService();
|
||||
bisq.setPriceCallbackFunction((price) => websocketHandler.setExtraInitProperties('bsq-price', price));
|
||||
blocks.setNewBlockCallback(bisq.handleNewBitcoinBlock.bind(bisq));
|
||||
}
|
||||
|
||||
if (config.BISQ_MARKETS.ENABLED) {
|
||||
bisqMarkets.startBisqService();
|
||||
}
|
||||
|
||||
@@ -156,6 +153,7 @@ class Server {
|
||||
this.app
|
||||
.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 + 'difficulty-adjustment', routes.getDifficultyChange)
|
||||
.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 + 'backend-info', routes.getBackendInfo)
|
||||
@@ -210,7 +208,7 @@ class Server {
|
||||
;
|
||||
}
|
||||
|
||||
if (config.BISQ_BLOCKS.ENABLED) {
|
||||
if (config.BISQ.ENABLED) {
|
||||
this.app
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/stats', routes.getBisqStats)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/tx/:txId', routes.getBisqTransaction)
|
||||
@@ -219,11 +217,6 @@ class Server {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/blocks/:index/:length', routes.getBisqBlocks)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/address/:address', routes.getBisqAddress)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/txs/:index/:length', routes.getBisqTransactions)
|
||||
;
|
||||
}
|
||||
|
||||
if (config.BISQ_MARKETS.ENABLED) {
|
||||
this.app
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/currencies', routes.getBisqMarketCurrencies.bind(routes))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/depth', routes.getBisqMarketDepth.bind(routes))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/hloc', routes.getBisqMarketHloc.bind(routes))
|
||||
@@ -232,6 +225,7 @@ class Server {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/ticker', routes.getBisqMarketTicker.bind(routes))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/trades', routes.getBisqMarketTrades.bind(routes))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/volumes', routes.getBisqMarketVolumes.bind(routes))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/volumes/7d', routes.getBisqMarketVolumes7d.bind(routes))
|
||||
;
|
||||
}
|
||||
|
||||
@@ -241,9 +235,11 @@ class Server {
|
||||
.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 + 'tx/:txId', routes.getTransaction)
|
||||
.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/outspends', routes.getTransactionOutspends)
|
||||
.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/:height', routes.getBlocks)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', routes.getBlockTipHeight)
|
||||
|
||||
@@ -73,7 +73,7 @@ class Logger {
|
||||
}
|
||||
|
||||
private getNetwork(): string {
|
||||
if (config.BISQ_BLOCKS.ENABLED) {
|
||||
if (config.BISQ.ENABLED) {
|
||||
return 'bisq';
|
||||
}
|
||||
if (config.MEMPOOL.NETWORK && config.MEMPOOL.NETWORK !== 'mainnet') {
|
||||
|
||||
@@ -144,6 +144,7 @@ export interface WebsocketResponse {
|
||||
'track-tx': string;
|
||||
'track-address': string;
|
||||
'watch-mempool': boolean;
|
||||
'track-bisq-market': string;
|
||||
}
|
||||
|
||||
export interface VbytesPerSecond {
|
||||
|
||||
@@ -426,6 +426,15 @@ class Routes {
|
||||
}
|
||||
}
|
||||
|
||||
public getBisqMarketVolumes7d(req: Request, res: Response) {
|
||||
const result = bisqMarket.getVolumesByTime(604800);
|
||||
if (result) {
|
||||
res.json(result);
|
||||
} else {
|
||||
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketVolumes7d error'));
|
||||
}
|
||||
}
|
||||
|
||||
private parseRequestParameters(requestParams: object, params: RequiredSpec): { [name: string]: any; } {
|
||||
const final = {};
|
||||
for (const i in params) {
|
||||
@@ -475,6 +484,20 @@ class Routes {
|
||||
}
|
||||
}
|
||||
|
||||
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.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
|
||||
statusCode = 404;
|
||||
}
|
||||
res.status(statusCode).send(e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
public async getTransactionStatus(req: Request, res: Response) {
|
||||
try {
|
||||
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true);
|
||||
@@ -497,6 +520,16 @@ class Routes {
|
||||
}
|
||||
}
|
||||
|
||||
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.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
public async getBlocks(req: Request, res: Response) {
|
||||
try {
|
||||
loadingIndicators.setProgress('blocks', 0);
|
||||
@@ -657,6 +690,47 @@ class Routes {
|
||||
public getTransactionOutspends(req: Request, res: Response) {
|
||||
res.status(501).send('Not implemented');
|
||||
}
|
||||
|
||||
public getDifficultyChange(req: Request, res: Response) {
|
||||
try {
|
||||
const now = new Date().getTime() / 1000;
|
||||
const DATime = blocks.getLastDifficultyAdjustmentTime();
|
||||
const previousRetarget = blocks.getPreviousDifficultyRetarget();
|
||||
const diff = now - DATime;
|
||||
const blockHeight = blocks.getCurrentBlockHeight();
|
||||
const blocksInEpoch = blockHeight % 2016;
|
||||
const difficultyChange = (600 / (diff / blocksInEpoch) - 1) * 100;
|
||||
|
||||
const timeAvgDiff = difficultyChange * 0.1;
|
||||
|
||||
let timeAvgMins = 10;
|
||||
if (timeAvgDiff > 0 ){
|
||||
timeAvgMins -= Math.abs(timeAvgDiff);
|
||||
} else {
|
||||
timeAvgMins += Math.abs(timeAvgDiff);
|
||||
}
|
||||
|
||||
const remainingBlocks = 2016 - blocksInEpoch;
|
||||
const timeAvgSeconds = timeAvgMins * 60;
|
||||
const remainingTime = remainingBlocks * timeAvgSeconds;
|
||||
const estimatedRetargetDate = (remainingTime + now);
|
||||
const totalTime = estimatedRetargetDate-DATime;
|
||||
const progressPercent = 100 - ((remainingTime * 100) / totalTime);
|
||||
|
||||
const result={
|
||||
progressPercent,
|
||||
difficultyChange,
|
||||
estimatedRetargetDate,
|
||||
remainingBlocks,
|
||||
remainingTime,
|
||||
previousRetarget,
|
||||
}
|
||||
res.json(result);
|
||||
|
||||
} catch (e) {
|
||||
res.status(500).send(e.message || e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new Routes();
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
FROM node:12-buster-slim AS builder
|
||||
|
||||
ARG commitHash
|
||||
ENV DOCKER_COMMIT_HASH=${commitHash}
|
||||
ENV CYPRESS_INSTALL_BINARY=0
|
||||
|
||||
WORKDIR /build
|
||||
COPY . .
|
||||
RUN apt-get update
|
||||
|
||||
5
frontend/.gitignore
vendored
5
frontend/.gitignore
vendored
@@ -54,3 +54,8 @@ src/resources/pools.json
|
||||
# environment config
|
||||
mempool-frontend-config.json
|
||||
generated-config.js
|
||||
|
||||
# e2e results
|
||||
cypress/videos
|
||||
cypress/screenshots
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ https://www.transifex.com/mempool/mempool/dashboard/
|
||||
* Dutch @m__btc
|
||||
* Japanese @wiz @japananon
|
||||
* Norwegian @T82771355
|
||||
* Polish @maciejsoltysiak
|
||||
* Portugese @jgcastro1985
|
||||
* Slovenian @thepkbadger
|
||||
* Finnish @bio_bitcoin
|
||||
@@ -30,3 +31,4 @@ https://www.transifex.com/mempool/mempool/dashboard/
|
||||
* Ukrainian @volbil
|
||||
* Vietnamese @bitcoin_vietnam
|
||||
* Chinese @wdljt
|
||||
* Russian @TonyCrusoe @Bitconan
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"cli": {
|
||||
"analytics": false
|
||||
},
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
@@ -15,14 +18,18 @@
|
||||
"prefix": "app",
|
||||
"i18n": {
|
||||
"sourceLocale": {
|
||||
"code":"en-US",
|
||||
"baseHref":"/"
|
||||
"code": "en-US",
|
||||
"baseHref": "/"
|
||||
},
|
||||
"locales": {
|
||||
"ar": {
|
||||
"translation": "src/locale/messages.ar.xlf",
|
||||
"baseHref": "/ar/"
|
||||
},
|
||||
"ca": {
|
||||
"translation": "src/locale/messages.ca.xlf",
|
||||
"baseHref": "/ca/"
|
||||
},
|
||||
"cs": {
|
||||
"translation": "src/locale/messages.cs.xlf",
|
||||
"baseHref": "/cs/"
|
||||
@@ -71,6 +78,10 @@
|
||||
"translation": "src/locale/messages.nb.xlf",
|
||||
"baseHref": "/nb/"
|
||||
},
|
||||
"pl": {
|
||||
"translation": "src/locale/messages.pl.xlf",
|
||||
"baseHref": "/pl/"
|
||||
},
|
||||
"pt": {
|
||||
"translation": "src/locale/messages.pt.xlf",
|
||||
"baseHref": "/pt/"
|
||||
@@ -106,6 +117,14 @@
|
||||
"zh": {
|
||||
"translation": "src/locale/messages.zh.xlf",
|
||||
"baseHref": "/zh/"
|
||||
},
|
||||
"ru": {
|
||||
"translation": "src/locale/messages.ru.xlf",
|
||||
"baseHref": "/ru/"
|
||||
},
|
||||
"hi": {
|
||||
"translation": "src/locale/messages.hi.xlf",
|
||||
"baseHref": "/hi/"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -169,6 +188,22 @@
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "mempool:build:production"
|
||||
},
|
||||
"local": {
|
||||
"proxyConfig": "proxy.conf.json",
|
||||
"verbose": true
|
||||
},
|
||||
"staging": {
|
||||
"proxyConfig": "proxy.stg.conf.json",
|
||||
"disableHostCheck": true,
|
||||
"host": "0.0.0.0",
|
||||
"verbose": true
|
||||
},
|
||||
"local-prod": {
|
||||
"proxyConfig": "proxy.prod.conf.json",
|
||||
"disableHostCheck": true,
|
||||
"host": "0.0.0.0",
|
||||
"verbose": false
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -201,8 +236,8 @@
|
||||
"tsConfig": [
|
||||
"tsconfig.app.json",
|
||||
"tsconfig.spec.json",
|
||||
"e2e/tsconfig.json",
|
||||
"tsconfig.server.json"
|
||||
"tsconfig.server.json",
|
||||
"cypress/tsconfig.json"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
@@ -210,10 +245,11 @@
|
||||
}
|
||||
},
|
||||
"e2e": {
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"builder": "@cypress/schematic:cypress",
|
||||
"options": {
|
||||
"protractorConfig": "e2e/protractor.conf.js",
|
||||
"devServerTarget": "mempool:serve"
|
||||
"devServerTarget": "mempool:serve:local-prod",
|
||||
"watch": true,
|
||||
"headless": false
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
@@ -268,8 +304,27 @@
|
||||
"configurations": {
|
||||
"production": {}
|
||||
}
|
||||
},
|
||||
"cypress-run": {
|
||||
"builder": "@cypress/schematic:cypress",
|
||||
"options": {
|
||||
"devServerTarget": "mempool:serve"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"devServerTarget": "mempool:serve:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"cypress-open": {
|
||||
"builder": "@cypress/schematic:cypress",
|
||||
"options": {
|
||||
"watch": true,
|
||||
"headless": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}},
|
||||
}
|
||||
},
|
||||
"defaultProject": "mempool"
|
||||
}
|
||||
|
||||
15
frontend/cypress.json
Normal file
15
frontend/cypress.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"projectId": "ry4br7",
|
||||
"integrationFolder": "cypress/integration",
|
||||
"supportFile": "cypress/support/index.ts",
|
||||
"videosFolder": "cypress/videos",
|
||||
"screenshotsFolder": "cypress/screenshots",
|
||||
"pluginsFile": "cypress/plugins/index.js",
|
||||
"fixturesFolder": "cypress/fixtures",
|
||||
"baseUrl": "http://localhost:4200",
|
||||
"video": false,
|
||||
"retries": {
|
||||
"runMode": 3,
|
||||
"openMode": 0
|
||||
}
|
||||
}
|
||||
119
frontend/cypress/fixtures/assets.json
Normal file
119
frontend/cypress/fixtures/assets.json
Normal file
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"f59c5f3e8141f322276daa63ed5f307085808aea6d4ef9ba61e28154533fdec7": {
|
||||
"asset_id": "f59c5f3e8141f322276daa63ed5f307085808aea6d4ef9ba61e28154533fdec7",
|
||||
"contract": {
|
||||
"entity": {
|
||||
"domain": "listedreserve.com"
|
||||
},
|
||||
"issuer_pubkey": "031cc579d142a03b33cdd745922112821c16e5e8b74e3bd57f16f7fda872b6f1d0",
|
||||
"name": "Liquid AUD",
|
||||
"precision": 2,
|
||||
"ticker": "AUDL",
|
||||
"version": 0
|
||||
},
|
||||
"issuance_txin": {
|
||||
"txid": "e5c5144ba3dc48259ae29023fe9f7775dec1fc049f456dd3d1f7178e31901fb5",
|
||||
"vin": 0
|
||||
},
|
||||
"issuance_prevout": {
|
||||
"txid": "ed48be2e035ffa425d2c6faaa82b6a7b648aed1246b6ac76c72e0408db8cf057",
|
||||
"vout": 1
|
||||
},
|
||||
"name": "Liquid AUD",
|
||||
"ticker": "AUDL",
|
||||
"precision": 2,
|
||||
"entity": {
|
||||
"domain": "listedreserve.com"
|
||||
},
|
||||
"version": 0,
|
||||
"issuer_pubkey": "031cc579d142a03b33cdd745922112821c16e5e8b74e3bd57f16f7fda872b6f1d0"
|
||||
},
|
||||
"0e99c1a6da379d1f4151fb9df90449d40d0608f6cb33a5bcbfc8c265f42bab0a": {
|
||||
"asset_id": "0e99c1a6da379d1f4151fb9df90449d40d0608f6cb33a5bcbfc8c265f42bab0a",
|
||||
"contract": {
|
||||
"entity": {
|
||||
"domain": "lcad.bullbitcoin.com"
|
||||
},
|
||||
"issuer_pubkey": "027fa34026195b05f3aa217335416811dca4f5b579d00271a1bb6304c0152458a8",
|
||||
"name": "Liquid CAD",
|
||||
"precision": 8,
|
||||
"ticker": "LCAD",
|
||||
"version": 0
|
||||
},
|
||||
"issuance_txin": {
|
||||
"txid": "238badf029cadcf546d90ce23c7eafc2fa2082585c9bd62dc26f1aa11c7bd850",
|
||||
"vin": 0
|
||||
},
|
||||
"issuance_prevout": {
|
||||
"txid": "a87f13917c08c7ccd8eddb1830c5c9a2bcd59c7d167e9d528659ba40808a6b76",
|
||||
"vout": 0
|
||||
},
|
||||
"name": "Liquid CAD",
|
||||
"ticker": "LCAD",
|
||||
"precision": 8,
|
||||
"entity": {
|
||||
"domain": "lcad.bullbitcoin.com"
|
||||
},
|
||||
"version": 0,
|
||||
"issuer_pubkey": "027fa34026195b05f3aa217335416811dca4f5b579d00271a1bb6304c0152458a8"
|
||||
},
|
||||
"3438ecb49fc45c08e687de4749ed628c511e326460ea4336794e1cf02741329e": {
|
||||
"asset_id": "3438ecb49fc45c08e687de4749ed628c511e326460ea4336794e1cf02741329e",
|
||||
"contract": {
|
||||
"entity": {
|
||||
"domain": "settlenet.io"
|
||||
},
|
||||
"issuer_pubkey": "037b09d542bf7cea6a19fa624b4441790c1a6e44823597bf190e981a846a196541",
|
||||
"name": "SETTLENET JPY Stablecoin by Crypto Garage",
|
||||
"precision": 0,
|
||||
"ticker": "JPYS",
|
||||
"version": 0
|
||||
},
|
||||
"issuance_txin": {
|
||||
"txid": "e33ad5ce8879297d8bfa7daa193920b94abd3fb12f4e8dade9543dbb292387cb",
|
||||
"vin": 0
|
||||
},
|
||||
"issuance_prevout": {
|
||||
"txid": "328c4fadd817ea75e634e3648eb4be0bf7e669539b8da921c0f77af3bc148894",
|
||||
"vout": 1
|
||||
},
|
||||
"name": "SETTLENET JPY Stablecoin by Crypto Garage",
|
||||
"ticker": "JPYS",
|
||||
"precision": 0,
|
||||
"entity": {
|
||||
"domain": "settlenet.io"
|
||||
},
|
||||
"version": 0,
|
||||
"issuer_pubkey": "037b09d542bf7cea6a19fa624b4441790c1a6e44823597bf190e981a846a196541"
|
||||
},
|
||||
"ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2": {
|
||||
"asset_id": "ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2",
|
||||
"contract": {
|
||||
"entity": {
|
||||
"domain": "tether.to"
|
||||
},
|
||||
"issuer_pubkey": "0337cceec0beea0232ebe14cba0197a9fbd45fcf2ec946749de920e71434c2b904",
|
||||
"name": "Tether USD",
|
||||
"precision": 8,
|
||||
"ticker": "USDt",
|
||||
"version": 0
|
||||
},
|
||||
"issuance_txin": {
|
||||
"txid": "abb4080d91849e933ee2ed65da6b436f7c385cf363fb4aa08399f1e27c58ff3d",
|
||||
"vin": 0
|
||||
},
|
||||
"issuance_prevout": {
|
||||
"txid": "9596d259270ef5bac0020435e6d859aea633409483ba64e232b8ba04ce288668",
|
||||
"vout": 0
|
||||
},
|
||||
"name": "Tether USD",
|
||||
"ticker": "USDt",
|
||||
"precision": 8,
|
||||
"entity": {
|
||||
"domain": "tether.to"
|
||||
},
|
||||
"version": 0,
|
||||
"issuer_pubkey": "0337cceec0beea0232ebe14cba0197a9fbd45fcf2ec946749de920e71434c2b904"
|
||||
}
|
||||
}
|
||||
|
||||
33
frontend/cypress/fixtures/assets.minimal.json
Normal file
33
frontend/cypress/fixtures/assets.minimal.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"f59c5f3e8141f322276daa63ed5f307085808aea6d4ef9ba61e28154533fdec7": [
|
||||
"listedreserve.com",
|
||||
"AUDL",
|
||||
"Liquid AUD",
|
||||
2
|
||||
],
|
||||
"0e99c1a6da379d1f4151fb9df90449d40d0608f6cb33a5bcbfc8c265f42bab0a": [
|
||||
"lcad.bullbitcoin.com",
|
||||
"LCAD",
|
||||
"Liquid CAD",
|
||||
8
|
||||
],
|
||||
"6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d": [
|
||||
null,
|
||||
"L-BTC",
|
||||
"Liquid Bitcoin",
|
||||
8
|
||||
],
|
||||
"ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2": [
|
||||
"tether.to",
|
||||
"USDt",
|
||||
"Tether USD",
|
||||
8
|
||||
],
|
||||
"3438ecb49fc45c08e687de4749ed628c511e326460ea4336794e1cf02741329e": [
|
||||
"settlenet.io",
|
||||
"JPYS",
|
||||
"SETTLENET JPY Stablecoin by Crypto Garage",
|
||||
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
1178
frontend/cypress/fixtures/pools.json
Normal file
1178
frontend/cypress/fixtures/pools.json
Normal file
File diff suppressed because it is too large
Load Diff
81
frontend/cypress/integration/bisq/bisq.spec.ts
Normal file
81
frontend/cypress/integration/bisq/bisq.spec.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
describe('Bisq', () => {
|
||||
beforeEach(() => {
|
||||
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/ticker').as('ticker');
|
||||
cy.intercept('/bisq/api/markets/markets').as('markets');
|
||||
cy.intercept('/bisq/api/markets/volumes/7d').as('7d');
|
||||
cy.intercept('/bisq/api/markets/trades?market=all').as('trades');
|
||||
cy.intercept('/bisq/api/txs/*/*').as('txs');
|
||||
cy.intercept('/bisq/api/blocks/*/*').as('blocks');
|
||||
cy.intercept('/bisq/api/stats').as('stats');
|
||||
|
||||
Cypress.Commands.add('waitForDashboard', () => {
|
||||
cy.wait('@socket');
|
||||
cy.wait('@hloc');
|
||||
cy.wait('@ticker');
|
||||
cy.wait('@markets');
|
||||
cy.wait('@7d');
|
||||
cy.wait('@trades');
|
||||
});
|
||||
});
|
||||
|
||||
it('loads the dashboard', () => {
|
||||
cy.visit('/bisq');
|
||||
cy.waitForSkeletonGone();
|
||||
});
|
||||
|
||||
it('loads the transactions screen', () => {
|
||||
cy.visit('/bisq');
|
||||
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('/bisq');
|
||||
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('/bisq');
|
||||
cy.waitForSkeletonGone();
|
||||
cy.get('li:nth-of-type(4) > a').click().then(() => {
|
||||
cy.wait('@stats');
|
||||
});
|
||||
});
|
||||
|
||||
it('loads the api screen', () => {
|
||||
cy.visit('/bisq');
|
||||
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('/bisq/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('/bisq/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);
|
||||
});
|
||||
|
||||
});
|
||||
149
frontend/cypress/integration/liquid/liquid.spec.ts
Normal file
149
frontend/cypress/integration/liquid/liquid.spec.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
describe('Liquid', () => {
|
||||
beforeEach(() => {
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
it('loads the dashboard', () => {
|
||||
cy.visit('/liquid');
|
||||
cy.waitForSkeletonGone();
|
||||
});
|
||||
|
||||
it('loads the blocks page', () => {
|
||||
cy.visit('/liquid/blocks');
|
||||
cy.waitForSkeletonGone();
|
||||
});
|
||||
|
||||
it('loads a specific block page', () => {
|
||||
cy.visit('/liquid/block/7e1369a23a5ab861e7bdede2aadcccae4ea873ffd9caf11c7c5541eb5bcdff54');
|
||||
cy.waitForSkeletonGone();
|
||||
});
|
||||
|
||||
it('loads the graphs page', () => {
|
||||
cy.visit('/liquid/graphs');
|
||||
cy.waitForSkeletonGone();
|
||||
});
|
||||
|
||||
it('loads the tv page - desktop', () => {
|
||||
cy.visit('/liquid');
|
||||
cy.waitForSkeletonGone();
|
||||
cy.get('li:nth-of-type(3) > a').click().then(() => {
|
||||
cy.wait(1000);
|
||||
});
|
||||
});
|
||||
|
||||
it('loads the graphs page - mobile', () => {
|
||||
cy.visit('/liquid');
|
||||
cy.waitForSkeletonGone();
|
||||
cy.get('li:nth-of-type(3) > a').click().then(() => {
|
||||
cy.viewport('iphone-6');
|
||||
cy.wait(1000);
|
||||
cy.get('.tv-only').should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('assets', () => {
|
||||
it('shows the assets screen', () => {
|
||||
cy.visit('/liquid');
|
||||
cy.waitForSkeletonGone();
|
||||
cy.get('li:nth-of-type(5) > a').click().then(() => {
|
||||
cy.get('table tr').should('have.length', 5);
|
||||
});
|
||||
});
|
||||
|
||||
it('allows searching assets', () => {
|
||||
cy.visit('/liquid');
|
||||
cy.waitForSkeletonGone();
|
||||
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', () => {
|
||||
cy.visit('/liquid');
|
||||
cy.waitForSkeletonGone();
|
||||
cy.get('li:nth-of-type(5) > a').click().then(() => {
|
||||
cy.get('.container-xl input').click().type('Liquid CAD').then(() => {
|
||||
cy.get('table tr td:nth-of-type(4) a').click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('shows a specific asset issuance TX', () => {
|
||||
cy.visit('/liquid');
|
||||
cy.waitForSkeletonGone();
|
||||
cy.get('li:nth-of-type(5) > a').click().then(() => {
|
||||
cy.get('.container-xl input').click().type('Liquid CAD').then(() => {
|
||||
cy.get('table tr td:nth-of-type(5) a').click();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('unblinded TX', () => {
|
||||
it('show unblinded TX', () => {
|
||||
cy.visit('/liquid/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('/liquid/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('/liquid/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('/liquid/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('/liquid/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('/liquid/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('/liquid/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('/liquid/tx/393b890966f305e7c440fcfb12a13f51a7a9011cc59ff5f14f6f93214261bd82');
|
||||
cy.waitForSkeletonGone();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
198
frontend/cypress/integration/mainnet/mainnet.spec.ts
Normal file
198
frontend/cypress/integration/mainnet/mainnet.spec.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import { emitMempoolInfo, dropWebSocket } from "../../support/websocket";
|
||||
|
||||
describe('Mainnet', () => {
|
||||
beforeEach(() => {
|
||||
//cy.intercept('/sockjs-node/info*').as('socket');
|
||||
cy.intercept('/api/block-height/*').as('block-height');
|
||||
cy.intercept('/api/block/*').as('block');
|
||||
cy.intercept('/api/block/*/txs/0').as('block-txs');
|
||||
cy.intercept('/api/tx/*/outspends').as('tx-outspends');
|
||||
cy.intercept('/resources/pools.json').as('pools');
|
||||
|
||||
Cypress.Commands.add('waitForBlockData', () => {
|
||||
cy.wait('@tx-outspends');
|
||||
cy.wait('@pools');
|
||||
});
|
||||
});
|
||||
|
||||
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('loads dashboard, drop websocket and reconnect', () => {
|
||||
cy.viewport('macbook-16');
|
||||
cy.mockMempoolSocket();
|
||||
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('loads the dashboard', () => {
|
||||
cy.visit('/');
|
||||
cy.waitForSkeletonGone();
|
||||
});
|
||||
|
||||
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('loads the tv screen - mobile', () => {
|
||||
cy.viewport('iphone-6');
|
||||
cy.visit('/tv');
|
||||
cy.waitForSkeletonGone();
|
||||
cy.get('.chart-holder');
|
||||
cy.get('.blockchain-wrapper').should('be.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);
|
||||
});
|
||||
});
|
||||
});
|
||||
126
frontend/cypress/integration/signet/signet.spec.ts
Normal file
126
frontend/cypress/integration/signet/signet.spec.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { emitMempoolInfo } from "../../support/websocket";
|
||||
|
||||
describe('Signet', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('/api/block-height/*').as('block-height');
|
||||
cy.intercept('/api/block/*').as('block');
|
||||
cy.intercept('/api/block/*/txs/0').as('block-txs');
|
||||
cy.intercept('/api/tx/*/outspends').as('tx-outspends');
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
122
frontend/cypress/integration/testnet/testnet.spec.ts
Normal file
122
frontend/cypress/integration/testnet/testnet.spec.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { emitMempoolInfo } from "../../support/websocket";
|
||||
|
||||
describe('Testnet', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('/api/block-height/*').as('block-height');
|
||||
cy.intercept('/api/block/*').as('block');
|
||||
cy.intercept('/api/block/*/txs/0').as('block-txs');
|
||||
cy.intercept('/api/tx/*/outspends').as('tx-outspends');
|
||||
});
|
||||
|
||||
it('loads the dashboard', () => {
|
||||
cy.visit('/testnet');
|
||||
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': {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
1
frontend/cypress/plugins/index.js
Normal file
1
frontend/cypress/plugins/index.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = (on, config) => {}
|
||||
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;
|
||||
}
|
||||
}
|
||||
77
frontend/cypress/support/commands.ts
Normal file
77
frontend/cypress/support/commands.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
// ***********************************************
|
||||
// This example namespace declaration will help
|
||||
// with Intellisense and code completion in your
|
||||
// IDE or Text Editor.
|
||||
// ***********************************************
|
||||
// declare namespace Cypress {
|
||||
// interface Chainable<Subject = any> {
|
||||
// customCommand(param: any): typeof customCommand;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// function customCommand(param: any): void {
|
||||
// console.warn(param);
|
||||
// }
|
||||
//
|
||||
// NOTE: You can use it like so:
|
||||
// Cypress.Commands.add('customCommand', customCommand);
|
||||
//
|
||||
// ***********************************************
|
||||
// This example commands.js shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add("login", (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||
|
||||
import 'cypress-wait-until';
|
||||
import { PageIdleDetector } from './PageIdleDetector';
|
||||
import { mockWebSocket } from './websocket';
|
||||
|
||||
Cypress.Commands.add('waitForSkeletonGone', () => {
|
||||
cy.waitUntil(() => {
|
||||
return Cypress.$('.skeleton-loader').length === 0;
|
||||
}, { verbose: true, description: "waitForSkeletonGone", errorMsg: "skeleton loaders never went away", timeout: 7000, 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
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>
|
||||
}
|
||||
}
|
||||
20
frontend/cypress/support/index.ts
Normal file
20
frontend/cypress/support/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// When a command from ./commands is ready to use, import with `import './commands'` syntax
|
||||
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);
|
||||
});
|
||||
10
frontend/cypress/tsconfig.json
Normal file
10
frontend/cypress/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"include": ["**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"types": ["cypress"],
|
||||
"lib": ["es2015", "dom"],
|
||||
"allowJs": true,
|
||||
"noEmit": true,
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// @ts-check
|
||||
// Protractor configuration file, see link for more information
|
||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||
|
||||
const { SpecReporter } = require('jasmine-spec-reporter');
|
||||
|
||||
/**
|
||||
* @type { import("protractor").Config }
|
||||
*/
|
||||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
specs: [
|
||||
'./src/**/*.e2e-spec.ts'
|
||||
],
|
||||
capabilities: {
|
||||
'browserName': 'chrome'
|
||||
},
|
||||
directConnect: true,
|
||||
baseUrl: 'http://localhost:4200/',
|
||||
framework: 'jasmine',
|
||||
jasmineNodeOpts: {
|
||||
showColors: true,
|
||||
defaultTimeoutInterval: 30000,
|
||||
print: function() {}
|
||||
},
|
||||
onPrepare() {
|
||||
require('ts-node').register({
|
||||
project: require('path').join(__dirname, './tsconfig.json')
|
||||
});
|
||||
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
|
||||
}
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
import { AppPage } from './app.po';
|
||||
import { browser, logging } from 'protractor';
|
||||
|
||||
describe('workspace-project App', () => {
|
||||
let page: AppPage;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new AppPage();
|
||||
});
|
||||
|
||||
it('should display welcome message', () => {
|
||||
page.navigateTo();
|
||||
expect(page.getTitleText()).toEqual('Welcome to mempool!');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Assert that there are no errors emitted from the browser
|
||||
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
|
||||
expect(logs).not.toContain(jasmine.objectContaining({
|
||||
level: logging.Level.SEVERE,
|
||||
} as logging.Entry));
|
||||
});
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import { browser, by, element } from 'protractor';
|
||||
|
||||
export class AppPage {
|
||||
navigateTo() {
|
||||
return browser.get(browser.baseUrl) as Promise<any>;
|
||||
}
|
||||
|
||||
getTitleText() {
|
||||
return element(by.css('app-root h1')).getText() as Promise<string>;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/e2e",
|
||||
"module": "commonjs",
|
||||
"target": "es2018",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"jasminewd2",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
var fs = require('fs');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
const CONFIG_FILE_NAME = 'mempool-frontend-config.json';
|
||||
const GENERATED_CONFIG_FILE_NAME = 'generated-config.js';
|
||||
@@ -11,15 +12,19 @@ let packetJsonVersion = '';
|
||||
try {
|
||||
const rawConfig = fs.readFileSync(CONFIG_FILE_NAME);
|
||||
configContent = JSON.parse(rawConfig);
|
||||
console.log(`${CONFIG_FILE_NAME} file found, using provided config`);
|
||||
} catch (e) {
|
||||
if (e.code !== 'ENOENT') {
|
||||
throw new Error(e);
|
||||
} else {
|
||||
console.log(`${CONFIG_FILE_NAME} file not found, using default config`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const packageJson = fs.readFileSync('package.json');
|
||||
packetJsonVersion = JSON.parse(packageJson).version;
|
||||
console.log(`mempool version ${packetJsonVersion}`);
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
}
|
||||
@@ -31,23 +36,64 @@ for (setting in configContent) {
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
gitCommitHash = fs.readFileSync('../.git/refs/heads/master').toString().trim();
|
||||
} catch (e) {
|
||||
console.log('Could not load git commit info: ' + e.message || e);
|
||||
if (process.env.DOCKER_COMMIT_HASH) {
|
||||
gitCommitHash = process.env.DOCKER_COMMIT_HASH;
|
||||
} else {
|
||||
try {
|
||||
const gitRevParse = spawnSync('git', ['rev-parse', '--short', 'HEAD']);
|
||||
|
||||
if (!gitRevParse.error) {
|
||||
gitCommitHash = gitRevParse.stdout.toString('utf-8').replace(/[\n\r\s]+$/, '');
|
||||
console.log(`mempool revision ${gitCommitHash}`);
|
||||
} else if (gitRevParse.error.code === 'ENOENT') {
|
||||
console.log('git not found, cannot parse git hash');
|
||||
gitCommitHash = '?';
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Could not load git commit info: ' + e.message);
|
||||
gitCommitHash = '?';
|
||||
}
|
||||
}
|
||||
|
||||
const code = `(function (window) {
|
||||
const newConfig = `(function (window) {
|
||||
window.__env = window.__env || {};${settings.reduce((str, obj) => `${str}
|
||||
window.__env.${obj.key} = ${ typeof obj.value === 'string' ? `'${obj.value}'` : obj.value };`, '')}
|
||||
window.__env.GIT_COMMIT_HASH = '${gitCommitHash}';
|
||||
window.__env.PACKAGE_JSON_VERSION = '${packetJsonVersion}';
|
||||
}(global || this));`;
|
||||
|
||||
try {
|
||||
fs.writeFileSync(GENERATED_CONFIG_FILE_NAME, code, 'utf8');
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
function readConfig(path) {
|
||||
try {
|
||||
const currentConfig = fs.readFileSync(path).toString().trim();
|
||||
return currentConfig;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Config file generated');
|
||||
function writeConfig(path, config) {
|
||||
try {
|
||||
fs.writeFileSync(path, config, 'utf8');
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
const currentConfig = readConfig(GENERATED_CONFIG_FILE_NAME);
|
||||
|
||||
if (currentConfig && currentConfig === newConfig) {
|
||||
console.log(`No configuration updates, skipping ${GENERATED_CONFIG_FILE_NAME} file update`);
|
||||
return;
|
||||
} else if (!currentConfig) {
|
||||
console.log(`${GENERATED_CONFIG_FILE_NAME} file not found, creating new config file`);
|
||||
console.log('CONFIG: ', newConfig);
|
||||
writeConfig(GENERATED_CONFIG_FILE_NAME, newConfig);
|
||||
console.log(`${GENERATED_CONFIG_FILE_NAME} file saved`);
|
||||
return;
|
||||
} else {
|
||||
console.log(`Configuration changes detected, updating ${GENERATED_CONFIG_FILE_NAME} file`);
|
||||
console.log('OLD CONFIG: ', currentConfig);
|
||||
console.log('NEW CONFIG: ', newConfig);
|
||||
writeConfig(GENERATED_CONFIG_FILE_NAME, newConfig);
|
||||
console.log(`${GENERATED_CONFIG_FILE_NAME} file updated`);
|
||||
};
|
||||
|
||||
9472
frontend/package-lock.json
generated
9472
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mempool-frontend",
|
||||
"version": "2.2.0-dev",
|
||||
"version": "2.2.1",
|
||||
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
||||
"license": "GNU Affero General Public License v3.0",
|
||||
"homepage": "https://mempool.space",
|
||||
@@ -22,10 +22,14 @@
|
||||
"scripts": {
|
||||
"ng": "./node_modules/@angular/cli/bin/ng",
|
||||
"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 --ivy --out-file ./src/locale/messages.xlf",
|
||||
"i18n-pull-from-transifex": "tx pull -a --parallel --minimum-perc 1 --force",
|
||||
"serve": "ng serve --proxy-config proxy.conf.json",
|
||||
"start": "npm run generate-config && npm run sync-assets-dev && ng serve --proxy-config proxy.conf.json",
|
||||
"serve": "npm run generate-config && ng serve -c local",
|
||||
"serve:stg": "npm run generate-config && ng serve -c staging",
|
||||
"serve:local-prod": "npm run generate-config && ng serve -c local-prod",
|
||||
"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: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",
|
||||
"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",
|
||||
@@ -34,10 +38,17 @@
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e",
|
||||
"e2e:ci": "npm run cypress:run:ci",
|
||||
"config:defaults": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config",
|
||||
"dev:ssr": "npm run generate-config && ng run mempool:serve-ssr",
|
||||
"serve:ssr": "node server.run.js",
|
||||
"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": "cypress open",
|
||||
"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": {
|
||||
"@angular/animations": "~11.2.8",
|
||||
@@ -55,7 +66,7 @@
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.35",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
||||
"@mempool/chartist": "^0.11.4",
|
||||
"@mempool/mempool.js": "^2.2.0",
|
||||
"@mempool/mempool.js": "^2.2.4",
|
||||
"@ng-bootstrap/ng-bootstrap": "^7.0.0",
|
||||
"@nguniversal/express-engine": "11.2.1",
|
||||
"@types/qrcode": "^1.3.4",
|
||||
@@ -64,6 +75,7 @@
|
||||
"clipboard": "^2.0.4",
|
||||
"domino": "^2.1.6",
|
||||
"express": "^4.17.1",
|
||||
"lightweight-charts": "^3.3.0",
|
||||
"ngx-bootrap-multiselect": "^2.0.0",
|
||||
"ngx-infinite-scroll": "^10.0.1",
|
||||
"qrcode": "^1.4.4",
|
||||
@@ -92,9 +104,16 @@
|
||||
"karma-coverage": "~2.0.3",
|
||||
"karma-jasmine": "~4.0.0",
|
||||
"karma-jasmine-html-reporter": "^1.5.0",
|
||||
"protractor": "~7.0.0",
|
||||
"ts-node": "~8.3.0",
|
||||
"tslint": "~6.1.0",
|
||||
"typescript": "~4.1.5"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@cypress/schematic": "^1.3.0",
|
||||
"cypress": "^7.7.0",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,5 +88,20 @@
|
||||
"pathRewrite": {
|
||||
"^/bisq/api": "/api/v1/ws"
|
||||
}
|
||||
},
|
||||
"/resources/assets.minimal.json": {
|
||||
"target": "https://mempool.space",
|
||||
"secure": false,
|
||||
"changeOrigin": true
|
||||
},
|
||||
"/resources/assets.json": {
|
||||
"target": "https://mempool.space",
|
||||
"secure": false,
|
||||
"changeOrigin": true
|
||||
},
|
||||
"/resources/pools.json": {
|
||||
"target": "https://mempool.space",
|
||||
"secure": false,
|
||||
"changeOrigin": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
99
frontend/proxy.prod.conf.json
Normal file
99
frontend/proxy.prod.conf.json
Normal file
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"/api/v1/ws": {
|
||||
"target": "https://mempool.space",
|
||||
"secure": false,
|
||||
"ws": true
|
||||
},
|
||||
"/api": {
|
||||
"target": "https://mempool.space",
|
||||
"secure": false,
|
||||
"changeOrigin": true,
|
||||
"logLevel": "debug",
|
||||
"timeout": 3600000
|
||||
},
|
||||
"/testnet/api/v1/ws": {
|
||||
"target": "https://mempool.space/testnet",
|
||||
"secure": false,
|
||||
"ws": true,
|
||||
"loglevel": "debug",
|
||||
"pathRewrite": {
|
||||
"^/testnet/api": "/api/v1/ws"
|
||||
}
|
||||
},
|
||||
"/testnet/api": {
|
||||
"target": "https://mempool.space",
|
||||
"secure": true,
|
||||
"changeOrigin": true,
|
||||
"loglevel": "debug",
|
||||
"pathRewrite": {
|
||||
"/testnet/api": "/testnet/api"
|
||||
},
|
||||
"timeout": 3600000
|
||||
},
|
||||
"/signet/api/v1/ws": {
|
||||
"target": "https://mempool.space/signet",
|
||||
"secure": false,
|
||||
"ws": true,
|
||||
"loglevel": "debug",
|
||||
"pathRewrite": {
|
||||
"^/signet/api": "/api/v1/ws"
|
||||
}
|
||||
},
|
||||
"/signet/api": {
|
||||
"target": "https://mempool.space",
|
||||
"secure": true,
|
||||
"changeOrigin": true,
|
||||
"loglevel": "debug",
|
||||
"pathRewrite": {
|
||||
"/signet/api": "/signet/api"
|
||||
},
|
||||
"timeout": 3600000
|
||||
},
|
||||
|
||||
"/bisq/api/v1/ws": {
|
||||
"target": "https://mempool.space/bisq",
|
||||
"secure": false,
|
||||
"ws": true,
|
||||
"pathRewrite": {
|
||||
"^/bisq/api": "/api/v1/ws"
|
||||
}
|
||||
},
|
||||
"/bisq/api": {
|
||||
"target": "https://mempool.space/bisq",
|
||||
"secure": false,
|
||||
"changeOrigin": true,
|
||||
"pathRewrite": {
|
||||
"^/bisq/api/": "/api/v1/bisq/"
|
||||
},
|
||||
"timeout": 3600000
|
||||
},
|
||||
"/liquid/api/v1/ws": {
|
||||
"target": "https://mempool.space",
|
||||
"secure": false,
|
||||
"ws": true
|
||||
},
|
||||
"/liquid/api": {
|
||||
"target": "https://mempool.space",
|
||||
"secure": false,
|
||||
"changeOrigin": true,
|
||||
"pathRewrite": {
|
||||
"^/liquid/api/": "/liquid/api/"
|
||||
},
|
||||
"timeout": 3600000
|
||||
},
|
||||
"/resources/assets.minimal.json": {
|
||||
"target": "https://mempool.space",
|
||||
"secure": false,
|
||||
"changeOrigin": true
|
||||
},
|
||||
"/resources/assets.json": {
|
||||
"target": "https://mempool.space",
|
||||
"secure": false,
|
||||
"changeOrigin": true
|
||||
},
|
||||
"/resources/pools.json": {
|
||||
"target": "https://mempool.space",
|
||||
"secure": false,
|
||||
"changeOrigin": true
|
||||
}
|
||||
}
|
||||
100
frontend/proxy.stg.conf.json
Normal file
100
frontend/proxy.stg.conf.json
Normal file
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"/api/v1/ws": {
|
||||
"target": "https://mempool.ninja",
|
||||
"secure": false,
|
||||
"ws": true
|
||||
},
|
||||
"/api/*": {
|
||||
"target": "https://mempool.ninja",
|
||||
"secure": false,
|
||||
"changeOrigin": true,
|
||||
"logLevel": "debug",
|
||||
"pathRewrite": {
|
||||
"^/api": "https://mempool.ninja/api"
|
||||
},
|
||||
"timeout": 3600000
|
||||
},
|
||||
"/testnet/api/v1/ws": {
|
||||
"target": "https://mempool.ninja/testnet",
|
||||
"secure": false,
|
||||
"ws": true,
|
||||
"pathRewrite": {
|
||||
"^/testnet/api": "/api/v1/ws"
|
||||
}
|
||||
},
|
||||
"/testnet/api/v1/*": {
|
||||
"target": "https://mempool.ninja/testnet",
|
||||
"secure": false,
|
||||
"changeOrigin": true,
|
||||
"pathRewrite": {
|
||||
"^/testnet/api/v1": "/api/v1"
|
||||
},
|
||||
"timeout": 3600000
|
||||
},
|
||||
"/signet/api/v1/ws": {
|
||||
"target": "https://mempool.ninja/signet",
|
||||
"secure": false,
|
||||
"ws": true,
|
||||
"pathRewrite": {
|
||||
"^/signet/api": "/api/v1/ws"
|
||||
}
|
||||
},
|
||||
"/signet/api/v1/*": {
|
||||
"target": "https://mempool.ninja/signet",
|
||||
"secure": false,
|
||||
"changeOrigin": true,
|
||||
"pathRewrite": {
|
||||
"^/signet/api/v1": "/api/v1"
|
||||
},
|
||||
"timeout": 3600000
|
||||
},
|
||||
"/bisq/api/v1/ws": {
|
||||
"target": "https://mempool.ninja/bisq",
|
||||
"secure": false,
|
||||
"ws": true,
|
||||
"pathRewrite": {
|
||||
"^/bisq/api": "/api/v1/ws"
|
||||
}
|
||||
},
|
||||
"/bisq/api/*": {
|
||||
"target": "https://mempool.ninja/bisq",
|
||||
"secure": false,
|
||||
"changeOrigin": true,
|
||||
"pathRewrite": {
|
||||
"^/bisq/api/": "/api/v1/bisq/"
|
||||
},
|
||||
"timeout": 3600000
|
||||
},
|
||||
"/liquid/api/v1/ws": {
|
||||
"target": "https://mempool.ninja/liquid",
|
||||
"secure": false,
|
||||
"ws": true,
|
||||
"pathRewrite": {
|
||||
"^/liquid/api": "/api/v1/ws"
|
||||
}
|
||||
},
|
||||
"/liquid/api/*": {
|
||||
"target": "https://mempool.ninja/liquid",
|
||||
"secure": false,
|
||||
"changeOrigin": true,
|
||||
"pathRewrite": {
|
||||
"^/liquid/api/": "/api/liquid/"
|
||||
},
|
||||
"timeout": 3600000
|
||||
},
|
||||
"/resources/assets.minimal.json": {
|
||||
"target": "https://mempool.space",
|
||||
"secure": false,
|
||||
"changeOrigin": true
|
||||
},
|
||||
"/resources/assets.json": {
|
||||
"target": "https://mempool.space",
|
||||
"secure": false,
|
||||
"changeOrigin": true
|
||||
},
|
||||
"/resources/pools.json": {
|
||||
"target": "https://mempool.space",
|
||||
"secure": false,
|
||||
"changeOrigin": true
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,12 @@ import { DashboardComponent } from './dashboard/dashboard.component';
|
||||
import { LatestBlocksComponent } from './components/latest-blocks/latest-blocks.component';
|
||||
import { ApiDocsComponent } from './components/api-docs/api-docs.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 { SponsorComponent } from './components/sponsor/sponsor.component';
|
||||
|
||||
const routes: Routes = [
|
||||
let routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: MasterPageComponent,
|
||||
@@ -64,11 +68,23 @@ const routes: Routes = [
|
||||
path: 'terms-of-service',
|
||||
component: TermsOfServiceComponent
|
||||
},
|
||||
{
|
||||
path: 'privacy-policy',
|
||||
component: PrivacyPolicyComponent
|
||||
},
|
||||
{
|
||||
path: 'trademark-policy',
|
||||
component: TrademarkPolicyComponent
|
||||
},
|
||||
{
|
||||
path: 'address/:id',
|
||||
children: [],
|
||||
component: AddressComponent
|
||||
},
|
||||
{
|
||||
path: 'sponsor',
|
||||
component: SponsorComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -283,9 +299,23 @@ const routes: Routes = [
|
||||
},
|
||||
];
|
||||
|
||||
const browserWindow = window || {};
|
||||
// @ts-ignore
|
||||
const browserWindowEnv = browserWindow.__env || {};
|
||||
|
||||
if (browserWindowEnv && browserWindowEnv.OFFICIAL_BISQ_MARKETS) {
|
||||
routes = [{
|
||||
path: '',
|
||||
component: BisqMasterPageComponent,
|
||||
loadChildren: () => import('./bisq/bisq.module').then(m => m.BisqModule)
|
||||
}];
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes, {
|
||||
initialNavigation: 'enabled'
|
||||
initialNavigation: 'enabled',
|
||||
scrollPositionRestoration: 'enabled',
|
||||
anchorScrolling: 'enabled'
|
||||
})],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
|
||||
@@ -43,7 +43,7 @@ export const languages: Language[] = [
|
||||
{ code: 'ar', name: 'العربية' }, // Arabic
|
||||
// { code: 'bg', name: 'Български' }, // Bulgarian
|
||||
// { code: 'bs', name: 'Bosanski' }, // Bosnian
|
||||
// { code: 'ca', name: 'Català' }, // Catalan
|
||||
{ code: 'ca', name: 'Català' }, // Catalan
|
||||
{ code: 'cs', name: 'Čeština' }, // Czech
|
||||
// { code: 'da', name: 'Dansk' }, // Danish
|
||||
{ code: 'de', name: 'Deutsch' }, // German
|
||||
@@ -59,6 +59,7 @@ export const languages: Language[] = [
|
||||
{ code: 'ko', name: '한국어' }, // Korean
|
||||
// { code: 'hr', name: 'Hrvatski' }, // Croatian
|
||||
// { code: 'id', name: 'Bahasa Indonesia' },// Indonesian
|
||||
{ code: 'hi', name: 'हिन्दी' }, // Hindi
|
||||
{ code: 'it', name: 'Italiano' }, // Italian
|
||||
{ code: 'he', name: 'עברית' }, // Hebrew
|
||||
{ code: 'ka', name: 'ქართული' }, // Georgian
|
||||
@@ -71,11 +72,11 @@ export const languages: Language[] = [
|
||||
{ code: 'ja', name: '日本語' }, // Japanese
|
||||
{ code: 'nb', name: 'Norsk' }, // Norwegian Bokmål
|
||||
// { code: 'nn', name: 'Norsk Nynorsk' }, // Norwegian Nynorsk
|
||||
// { code: 'pl', name: 'Polski' }, // Polish
|
||||
{ code: 'pl', name: 'Polski' }, // Polish
|
||||
{ code: 'pt', name: 'Português' }, // Portuguese
|
||||
// { code: 'pt-BR', name: 'Português (Brazil)' }, // Portuguese (Brazil)
|
||||
// { code: 'ro', name: 'Română' }, // Romanian
|
||||
// { code: 'ru', name: 'Русский' }, // Russian
|
||||
{ code: 'ru', name: 'Русский' }, // Russian
|
||||
// { code: 'sk', name: 'Slovenčina' }, // Slovak
|
||||
{ code: 'sl', name: 'Slovenščina' }, // Slovenian
|
||||
// { code: 'sr', name: 'Српски / srpski' }, // Serbian
|
||||
|
||||
@@ -21,6 +21,7 @@ import { WebsocketService } from './services/websocket.service';
|
||||
import { AddressLabelsComponent } from './components/address-labels/address-labels.component';
|
||||
import { MempoolBlocksComponent } from './components/mempool-blocks/mempool-blocks.component';
|
||||
import { MasterPageComponent } from './components/master-page/master-page.component';
|
||||
import { BisqMasterPageComponent } from './components/bisq-master-page/bisq-master-page.component';
|
||||
import { AboutComponent } from './components/about/about.component';
|
||||
import { TelevisionComponent } from './components/television/television.component';
|
||||
import { StatisticsComponent } from './components/statistics/statistics.component';
|
||||
@@ -31,7 +32,7 @@ import { FooterComponent } from './components/footer/footer.component';
|
||||
import { AudioService } from './services/audio.service';
|
||||
import { MempoolBlockComponent } from './components/mempool-block/mempool-block.component';
|
||||
import { FeeDistributionGraphComponent } from './components/fee-distribution-graph/fee-distribution-graph.component';
|
||||
import { TimespanComponent } from './components/timespan/timespan.component';
|
||||
import { TimeSpanComponent } from './components/time-span/time-span.component';
|
||||
import { SeoService } from './services/seo.service';
|
||||
import { MempoolGraphComponent } from './components/mempool-graph/mempool-graph.component';
|
||||
import { AssetComponent } from './components/asset/asset.component';
|
||||
@@ -44,17 +45,22 @@ import { FeesBoxComponent } from './components/fees-box/fees-box.component';
|
||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
|
||||
import { faAngleDown, faAngleUp, faBolt, faChartArea, faCogs, faCubes, faDatabase, faExchangeAlt, faInfoCircle,
|
||||
faLink, faList, faSearch, faTachometerAlt, faThList, faTint, faTv, faAngleDoubleDown, faAngleDoubleUp } 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 { CodeTemplateComponent } from './components/api-docs/code-template.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 { HttpCacheInterceptor } from './services/http-cache.interceptor';
|
||||
import { SponsorComponent } from './components/sponsor/sponsor.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
AboutComponent,
|
||||
MasterPageComponent,
|
||||
BisqMasterPageComponent,
|
||||
TelevisionComponent,
|
||||
BlockchainComponent,
|
||||
StartComponent,
|
||||
@@ -67,7 +73,7 @@ import { HttpCacheInterceptor } from './services/http-cache.interceptor';
|
||||
AmountComponent,
|
||||
LatestBlocksComponent,
|
||||
SearchFormComponent,
|
||||
TimespanComponent,
|
||||
TimeSpanComponent,
|
||||
AddressLabelsComponent,
|
||||
MempoolBlocksComponent,
|
||||
ChartistComponent,
|
||||
@@ -82,7 +88,11 @@ import { HttpCacheInterceptor } from './services/http-cache.interceptor';
|
||||
FeesBoxComponent,
|
||||
DashboardComponent,
|
||||
ApiDocsComponent,
|
||||
CodeTemplateComponent,
|
||||
TermsOfServiceComponent,
|
||||
PrivacyPolicyComponent,
|
||||
TrademarkPolicyComponent,
|
||||
SponsorComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule.withServerTransition({ appId: 'serverApp' }),
|
||||
@@ -127,5 +137,13 @@ export class AppModule {
|
||||
library.addIcons(faExchangeAlt);
|
||||
library.addIcons(faAngleDoubleUp);
|
||||
library.addIcons(faAngleDoubleDown);
|
||||
library.addIcons(faChevronDown);
|
||||
library.addIcons(faFileAlt);
|
||||
library.addIcons(faRedoAlt);
|
||||
library.addIcons(faArrowAltCircleRight);
|
||||
library.addIcons(faExternalLinkAlt);
|
||||
library.addIcons(faSortUp);
|
||||
library.addIcons(faCaretUp);
|
||||
library.addIcons(faCaretDown);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<div class="container-xl">
|
||||
<h1 style="float: left;" i18n="shared.address">Address</h1>
|
||||
<a [routerLink]="['/address/' | relativeUrl, addressString]" style="line-height: 56px; margin-left: 10px;">
|
||||
<span class="d-inline d-lg-none">{{ addressString | shortenString : 24 }}</span>
|
||||
<span class="d-none d-lg-inline">{{ addressString }}</span>
|
||||
</a>
|
||||
<app-clipboard [text]="addressString"></app-clipboard>
|
||||
<h1 i18n="shared.address">Address</h1>
|
||||
<span class="address-link">
|
||||
<a [routerLink]="['/address/' | relativeUrl, addressString]">
|
||||
<span class="d-inline d-lg-none">{{ addressString | shortenString : 24 }}</span>
|
||||
<span class="d-none d-lg-inline">{{ addressString }}</span>
|
||||
</a>
|
||||
<app-clipboard [text]="addressString"></app-clipboard>
|
||||
</span>
|
||||
<br>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
@@ -13,26 +15,26 @@
|
||||
<div class="box">
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="col-md">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td i18n="address.total-received">Total received</td>
|
||||
<td>{{ totalReceived / 100 | number: '1.2-2' }} BSQ</td>
|
||||
<td>{{ totalReceived / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="address.total-sent">Total sent</td>
|
||||
<td>{{ totalSent / 100 | number: '1.2-2' }} BSQ</td>
|
||||
<td>{{ totalSent / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="address.balance">Balance</td>
|
||||
<td>{{ (totalReceived - totalSent) / 100 | number: '1.2-2' }} BSQ (<app-bsq-amount [bsq]="totalReceived - totalSent" [forceFiat]="true" [green]="true"></app-bsq-amount>)</td>
|
||||
<td>{{ (totalReceived - totalSent) / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span> <span class="fiat"><app-bsq-amount [bsq]="totalReceived - totalSent" [forceFiat]="true" [green]="true"></app-bsq-amount></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="w-100 d-block d-md-none"></div>
|
||||
<div class="col qrcode-col">
|
||||
<div class="col-md qrcode-col">
|
||||
<div class="qr-wrapper">
|
||||
<app-qrcode [data]="addressString"></app-qrcode>
|
||||
</div>
|
||||
|
||||
@@ -3,21 +3,73 @@
|
||||
padding: 10px;
|
||||
padding-bottom: 5px;
|
||||
display: inline-block;
|
||||
margin-right: 25px;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.qrcode-col {
|
||||
text-align: right;
|
||||
}
|
||||
.qrcode-col {
|
||||
text-align: center;
|
||||
}
|
||||
@media (max-width: 575.98px) {
|
||||
.qrcode-col {
|
||||
|
||||
.qrcode-col > div {
|
||||
margin: 20px auto 5px;
|
||||
@media (min-width: 768px) {
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.qrcode-col > div {
|
||||
margin-top: 20px;
|
||||
margin-right: 0px;
|
||||
.fiat {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
@media (min-width: 768px) {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
.table {
|
||||
tr td {
|
||||
&:last-child {
|
||||
text-align: right;
|
||||
@media (min-width: 768px) {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
@media (min-width: 576px) {
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.address-link {
|
||||
line-height: 26px;
|
||||
margin-left: 0px;
|
||||
top: 14px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@media (min-width: 768px) {
|
||||
line-height: 38px;
|
||||
}
|
||||
}
|
||||
|
||||
.row{
|
||||
flex-direction: column;
|
||||
@media (min-width: 576px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.mobile-bottomcol {
|
||||
margin-top: 15px;
|
||||
}
|
||||
.details-table td:first-child {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { ParamMap, ActivatedRoute } from '@angular/router';
|
||||
import { Subscription, of } from 'rxjs';
|
||||
import { BisqTransaction } from '../bisq.interfaces';
|
||||
import { BisqApiService } from '../bisq-api.service';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-address',
|
||||
@@ -22,12 +23,15 @@ export class BisqAddressComponent implements OnInit, OnDestroy {
|
||||
totalSent = 0;
|
||||
|
||||
constructor(
|
||||
private websocketService: WebsocketService,
|
||||
private route: ActivatedRoute,
|
||||
private seoService: SeoService,
|
||||
private bisqApiService: BisqApiService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
this.mainSubscription = this.route.paramMap
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpResponse, HttpParams } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { BisqTransaction, BisqBlock, BisqStats } from './bisq.interfaces';
|
||||
import { BisqTransaction, BisqBlock, BisqStats, MarketVolume, Trade, Markets, Tickers, Offers, Currencies, HighLowOpenClose, SummarizedInterval } from './bisq.interfaces';
|
||||
|
||||
const API_BASE_URL = '/bisq/api';
|
||||
|
||||
@@ -42,4 +42,37 @@ export class BisqApiService {
|
||||
getAddress$(address: string): Observable<BisqTransaction[]> {
|
||||
return this.httpClient.get<BisqTransaction[]>(API_BASE_URL + '/address/' + address);
|
||||
}
|
||||
|
||||
getMarkets$(): Observable<Markets> {
|
||||
return this.httpClient.get<Markets>(API_BASE_URL + '/markets/markets');
|
||||
}
|
||||
|
||||
getMarketsTicker$(): Observable<Tickers> {
|
||||
return this.httpClient.get<Tickers>(API_BASE_URL + '/markets/ticker');
|
||||
}
|
||||
|
||||
getMarketsCurrencies$(): Observable<Currencies> {
|
||||
return this.httpClient.get<Currencies>(API_BASE_URL + '/markets/currencies');
|
||||
}
|
||||
|
||||
getMarketsHloc$(market: string, interval: 'minute' | 'half_hour' | 'hour' | 'half_day' | 'day'
|
||||
| 'week' | 'month' | 'year' | 'auto'): Observable<SummarizedInterval[]> {
|
||||
return this.httpClient.get<SummarizedInterval[]>(API_BASE_URL + '/markets/hloc?market=' + market + '&interval=' + interval);
|
||||
}
|
||||
|
||||
getMarketOffers$(market: string): Observable<Offers> {
|
||||
return this.httpClient.get<Offers>(API_BASE_URL + '/markets/offers?market=' + market);
|
||||
}
|
||||
|
||||
getMarketTrades$(market: string): Observable<Trade[]> {
|
||||
return this.httpClient.get<Trade[]>(API_BASE_URL + '/markets/trades?market=' + market);
|
||||
}
|
||||
|
||||
getMarketVolumesByTime$(period: string): Observable<HighLowOpenClose[]> {
|
||||
return this.httpClient.get<HighLowOpenClose[]>(API_BASE_URL + '/markets/volumes/' + period);
|
||||
}
|
||||
|
||||
getAllVolumesDay$(): Observable<MarketVolume[]> {
|
||||
return this.httpClient.get<MarketVolume[]>(API_BASE_URL + '/markets/volumes?interval=week');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
|
||||
<ng-template [ngIf]="!isLoading && !error">
|
||||
|
||||
<div class="box">
|
||||
<div class="box block-container">
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
@@ -22,13 +22,13 @@
|
||||
<td>
|
||||
{{ block.time | date:'yyyy-MM-dd HH:mm' }}
|
||||
<div class="lg-inline">
|
||||
<i>(<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>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
@@ -73,7 +73,7 @@
|
||||
<ng-template [ngIf]="isLoading && !error">
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
@@ -86,7 +86,7 @@
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
|
||||
@@ -1,10 +1,44 @@
|
||||
|
||||
.td-width {
|
||||
width: 175px;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.td-width {
|
||||
width: 140px;
|
||||
width: 140px;
|
||||
@media (min-width: 768px) {
|
||||
width: 175px;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
@media (min-width: 576px) {
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.row{
|
||||
flex-direction: column;
|
||||
@media (min-width: 768px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
.block-container {
|
||||
.table {
|
||||
tr td {
|
||||
&:last-child {
|
||||
text-align: right;
|
||||
@media (min-width: 992px) {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.fiat {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
@media (min-width: 992px) {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { switchMap, catchError } from 'rxjs/operators';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { ElectrsApiService } from 'src/app/services/electrs-api.service';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-block',
|
||||
@@ -23,6 +24,7 @@ export class BisqBlockComponent implements OnInit, OnDestroy {
|
||||
error: HttpErrorResponse | null;
|
||||
|
||||
constructor(
|
||||
private websocketService: WebsocketService,
|
||||
private bisqApiService: BisqApiService,
|
||||
private route: ActivatedRoute,
|
||||
private seoService: SeoService,
|
||||
@@ -32,6 +34,8 @@ export class BisqBlockComponent implements OnInit, OnDestroy {
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
this.subscription = this.route.paramMap
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="container-xl">
|
||||
<h1 style="float: left;" i18n="Bisq blocks header">Blocks</h1>
|
||||
<div class="container-xl" (window:resize)="onResize($event)">
|
||||
<h1 style="float: left;" i18n="Bisq blocks header">BSQ Blocks</h1>
|
||||
<br>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
@@ -18,7 +18,7 @@
|
||||
<tr *ngFor="let block of blocks.value[0]; trackBy: trackByFn">
|
||||
<td><a [routerLink]="['/block/' | relativeUrl, block.hash]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
|
||||
<td><app-time-since [time]="block.time / 1000" [fastRender]="true"></app-time-since></td>
|
||||
<td>{{ calculateTotalOutput(block) / 100 | number: '1.2-2' }}<span class="d-none d-md-inline"> BSQ</span></td>
|
||||
<td>{{ calculateTotalOutput(block) / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
|
||||
<td class="d-none d-md-block">{{ block.txs.length }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -26,9 +26,7 @@
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
.pagination-container {
|
||||
float: none;
|
||||
margin-bottom: 200px;
|
||||
@media(min-width: 400px){
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.container-xl {
|
||||
padding-bottom: 110px;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { Observable } from 'rxjs';
|
||||
import { BisqBlock, BisqOutput, BisqTransaction } from '../bisq.interfaces';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-blocks',
|
||||
@@ -22,9 +23,10 @@ export class BisqBlocksComponent implements OnInit {
|
||||
isLoading = true;
|
||||
// @ts-ignore
|
||||
paginationSize: 'sm' | 'lg' = 'md';
|
||||
paginationMaxSize = 10;
|
||||
paginationMaxSize = 5;
|
||||
|
||||
constructor(
|
||||
private websocketService: WebsocketService,
|
||||
private bisqApiService: BisqApiService,
|
||||
private seoService: SeoService,
|
||||
private route: ActivatedRoute,
|
||||
@@ -32,10 +34,11 @@ export class BisqBlocksComponent implements OnInit {
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.websocketService.want(['blocks']);
|
||||
this.seoService.setTitle($localize`:@@8a7b4bd44c0ac71b2e72de0398b303257f7d2f54:Blocks`);
|
||||
this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10);
|
||||
this.loadingItems = Array(this.itemsPerPage);
|
||||
if (document.body.clientWidth < 768) {
|
||||
if (document.body.clientWidth < 670) {
|
||||
this.paginationSize = 'sm';
|
||||
this.paginationMaxSize = 3;
|
||||
}
|
||||
@@ -80,4 +83,8 @@ export class BisqBlocksComponent implements OnInit {
|
||||
queryParamsHandling: 'merge',
|
||||
});
|
||||
}
|
||||
|
||||
onResize(event: any) {
|
||||
this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<div class="container-xl">
|
||||
|
||||
<h1 i18n="Bisq markets title">Bisq Trading Volume</h1>
|
||||
|
||||
<div id="volumeHolder">
|
||||
<ng-template #loadingVolumes>
|
||||
<div class="text-center loadingVolumes">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-container *ngIf="volumes$ | async as volumes; else loadingVolumes">
|
||||
<app-lightweight-charts-area [data]="volumes.data" [lineData]="volumes.linesData"></app-lightweight-charts-area>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<br><br>
|
||||
|
||||
<div class="container-info">
|
||||
<h1>
|
||||
<ng-template [ngIf]="stateService.env.OFFICIAL_BISQ_MARKETS" [ngIfElse]="nonOfficialMarkets" i18n="Bisq All Markets">Markets</ng-template>
|
||||
<ng-template #nonOfficialMarkets i18n="Bisq Bitcoin Markets">Bitcoin Markets</ng-template>
|
||||
</h1>
|
||||
<ng-container *ngIf="{ value: (tickers$ | async) } as tickers">
|
||||
<div class="table-container">
|
||||
<table class="table table-borderless table-striped">
|
||||
<thead>
|
||||
<th><ng-container i18n>Currency</ng-container> <button [disabled]="(sort$ | async) === 'name'" class="btn btn-link btn-sm" (click)="sort('name')"><fa-icon [icon]="['fas', 'chevron-down']" [fixedWidth]="true"></fa-icon></button></th>
|
||||
<th i18n>Price</th>
|
||||
<th><ng-container i18n="Trading volume 7D">Volume (7d)</ng-container> <button [disabled]="(sort$ | async) === 'volumes'" class="btn btn-link btn-sm" (click)="sort('volumes')"><fa-icon [icon]="['fas', 'chevron-down']" [fixedWidth]="true"></fa-icon></button></th>
|
||||
<th><ng-container i18n="Trades amount 7D">Trades (7d)</ng-container> <button [disabled]="(sort$ | async) === 'trades'" class="btn btn-link btn-sm" (click)="sort('trades')"><fa-icon [icon]="['fas', 'chevron-down']" [fixedWidth]="true"></fa-icon></button></th>
|
||||
</thead>
|
||||
<tbody *ngIf="tickers.value; else loadingTmpl">
|
||||
<tr *ngFor="let ticker of tickers.value; trackBy: trackByFn;">
|
||||
<td><a [routerLink]="['/market' | relativeUrl, ticker.pair_url]">{{ ticker.name }})</a></td>
|
||||
<td>
|
||||
<app-fiat *ngIf="ticker.market.rtype === 'crypto'; else fiat" [value]="ticker.last * 100000000"></app-fiat>
|
||||
<ng-template #fiat>
|
||||
<span class="green-color">{{ ticker.last | currency: ticker.market.rsymbol }}</span>
|
||||
</ng-template>
|
||||
</td>
|
||||
<td>
|
||||
<app-fiat [value]="ticker.volume?.volume"></app-fiat>
|
||||
</td>
|
||||
<td>{{ ticker.volume?.num_trades ? ticker.volume?.num_trades : 0 }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<br><br>
|
||||
|
||||
<h2 i18n="Latest Trades header">Latest Trades</h2>
|
||||
<app-bisq-trades [trades$]="trades$"></app-bisq-trades>
|
||||
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #loadingTmpl>
|
||||
<tr *ngFor="let i of [1,2,3,4,5,6,7,8,9,10]">
|
||||
<td *ngFor="let j of [1, 2, 3, 4]"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,35 @@
|
||||
#volumeHolder {
|
||||
height: 500px;
|
||||
background-color: #000;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.table {
|
||||
max-width: 100%;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.loadingVolumes {
|
||||
position: relative;
|
||||
top: 45%;
|
||||
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{
|
||||
overflow-x: scroll;
|
||||
}
|
||||
131
frontend/src/app/bisq/bisq-dashboard/bisq-dashboard.component.ts
Normal file
131
frontend/src/app/bisq/bisq-dashboard/bisq-dashboard.component.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { Observable, combineLatest, BehaviorSubject, of } from 'rxjs';
|
||||
import { map, share, switchMap } from 'rxjs/operators';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
import { BisqApiService } from '../bisq-api.service';
|
||||
import { Trade } from '../bisq.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-dashboard',
|
||||
templateUrl: './bisq-dashboard.component.html',
|
||||
styleUrls: ['./bisq-dashboard.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class BisqDashboardComponent implements OnInit {
|
||||
tickers$: Observable<any>;
|
||||
volumes$: Observable<any>;
|
||||
trades$: Observable<Trade[]>;
|
||||
sort$ = new BehaviorSubject<string>('trades');
|
||||
|
||||
allowCryptoCoins = ['usdc', 'l-btc', 'bsq'];
|
||||
|
||||
constructor(
|
||||
private websocketService: WebsocketService,
|
||||
private bisqApiService: BisqApiService,
|
||||
public stateService: StateService,
|
||||
private seoService: SeoService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle(`Markets`);
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
this.volumes$ = this.bisqApiService.getAllVolumesDay$()
|
||||
.pipe(
|
||||
map((volumes) => {
|
||||
const data = volumes.map((volume) => {
|
||||
return {
|
||||
time: volume.period_start,
|
||||
value: volume.volume,
|
||||
};
|
||||
});
|
||||
|
||||
const linesData = volumes.map((volume) => {
|
||||
return {
|
||||
time: volume.period_start,
|
||||
value: volume.num_trades,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
data: data,
|
||||
linesData: linesData,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const getMarkets = this.bisqApiService.getMarkets$().pipe(share());
|
||||
|
||||
this.tickers$ = combineLatest([
|
||||
this.bisqApiService.getMarketsTicker$(),
|
||||
getMarkets,
|
||||
this.bisqApiService.getMarketVolumesByTime$('7d'),
|
||||
])
|
||||
.pipe(
|
||||
map(([tickers, markets, volumes]) => {
|
||||
|
||||
const newTickers = [];
|
||||
for (const t in tickers) {
|
||||
|
||||
if (!this.stateService.env.OFFICIAL_BISQ_MARKETS) {
|
||||
const pair = t.split('_');
|
||||
if (pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const mappedTicker: any = tickers[t];
|
||||
|
||||
mappedTicker.pair_url = t;
|
||||
mappedTicker.pair = t.replace('_', '/').toUpperCase();
|
||||
mappedTicker.market = markets[t];
|
||||
mappedTicker.volume = volumes[t];
|
||||
mappedTicker.name = `${mappedTicker.market.rtype === 'crypto' ? mappedTicker.market.lname : mappedTicker.market.rname} (${mappedTicker.market.rtype === 'crypto' ? mappedTicker.market.lsymbol : mappedTicker.market.rsymbol}`;
|
||||
newTickers.push(mappedTicker);
|
||||
}
|
||||
return newTickers;
|
||||
}),
|
||||
switchMap((tickers) => combineLatest([this.sort$, of(tickers)])),
|
||||
map(([sort, tickers]) => {
|
||||
if (sort === 'trades') {
|
||||
tickers.sort((a, b) => (b.volume && b.volume.num_trades || 0) - (a.volume && a.volume.num_trades || 0));
|
||||
} else if (sort === 'volumes') {
|
||||
tickers.sort((a, b) => (b.volume && b.volume.volume || 0) - (a.volume && a.volume.volume || 0));
|
||||
} else if (sort === 'name') {
|
||||
tickers.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
return tickers;
|
||||
})
|
||||
);
|
||||
|
||||
this.trades$ = combineLatest([
|
||||
this.bisqApiService.getMarketTrades$('all'),
|
||||
getMarkets,
|
||||
])
|
||||
.pipe(
|
||||
map(([trades, markets]) => {
|
||||
if (!this.stateService.env.OFFICIAL_BISQ_MARKETS) {
|
||||
trades = trades.filter((trade) => {
|
||||
const pair = trade.market.split('_');
|
||||
return !(pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1);
|
||||
});
|
||||
}
|
||||
return trades.map((trade => {
|
||||
trade._market = markets[trade.market];
|
||||
return trade;
|
||||
}));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
trackByFn(index: number) {
|
||||
return index;
|
||||
}
|
||||
|
||||
sort(by: string) {
|
||||
this.sort$.next(by);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<router-outlet></router-outlet>
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-explorer',
|
||||
templateUrl: './bisq-explorer.component.html',
|
||||
styleUrls: ['./bisq-explorer.component.scss']
|
||||
})
|
||||
export class BisqExplorerComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
private websocketService: WebsocketService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.websocketService.want(['blocks']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<div class="container-xl">
|
||||
|
||||
<br>
|
||||
|
||||
<div class="row row-cols-1 row-cols-md-2">
|
||||
<div class="col mb-4">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title" i18n="bisq-dashboard.price-index-title">Bisq Price Index</h5>
|
||||
<div class="big-fiat">
|
||||
<span *ngIf="usdPrice$ | async as usdPrice; else loading">
|
||||
<span [appColoredPrice]="usdPrice">{{ usdPrice | currency:'USD':'symbol':'1.2-2' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col mb-4">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title" i18n="bisq-dashboard.market-price-title">Bisq Market Price</h5>
|
||||
<div class="big-fiat">
|
||||
<span class="green-color" *ngIf="bisqMarketPrice; else loading">
|
||||
<span [appColoredPrice]="bisqMarketPrice">{{ bisqMarketPrice | currency:'USD':'symbol':'1.2-2' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-cols-1 row-cols-md-2">
|
||||
<div class="col mb-4">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">US Dollar - BTC/USD</h5>
|
||||
<div class="chart-container">
|
||||
<ng-container *ngIf="hlocData$ | async as hlocData; else loadingSpinner">
|
||||
<app-lightweight-charts [height]="300" [data]="hlocData.hloc" [volumeData]="hlocData.volume" [precision]="2"></app-lightweight-charts>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col mb-4">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title" i18n="Bisq markets title">Bisq Trading Volume</h5>
|
||||
<div class="chart-container">
|
||||
<ng-container *ngIf="volumes$ | async as volumes; else loadingSpinner">
|
||||
<app-lightweight-charts-area [height]="300" [data]="volumes.data" [lineData]="volumes.linesData"></app-lightweight-charts-area>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-cols-1 row-cols-md-2">
|
||||
<ng-container *ngIf="{ value: (tickers$ | async) } as tickers">
|
||||
|
||||
<div class="col mb-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<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 #nonOfficialMarkets i18n="Bisq Bitcoin Markets">Bitcoin Markets</ng-template>
|
||||
</h5>
|
||||
|
||||
<div class="table-container">
|
||||
<table class="table table-borderless table-striped">
|
||||
<thead>
|
||||
<th><ng-container i18n>Currency</ng-container> <button [disabled]="(sort$ | async) === 'name'" class="btn btn-link btn-sm" (click)="sort('name')"><fa-icon [icon]="['fas', 'chevron-down']" [fixedWidth]="true"></fa-icon></button></th>
|
||||
<th i18n>Price</th>
|
||||
<th><ng-container i18n="Trades amount 7D">Trades (7d)</ng-container> <button [disabled]="(sort$ | async) === 'trades'" class="btn btn-link btn-sm" (click)="sort('trades')"><fa-icon [icon]="['fas', 'chevron-down']" [fixedWidth]="true"></fa-icon></button></th>
|
||||
</thead>
|
||||
<tbody *ngIf="tickers.value; else loadingTmpl">
|
||||
<tr *ngFor="let ticker of tickers.value; trackBy: trackByFn;">
|
||||
<td><a [routerLink]="['/market' | relativeUrl, ticker.pair_url]">{{ ticker.name }})</a></td>
|
||||
<td>
|
||||
<app-fiat *ngIf="ticker.market.rtype === 'crypto'; else fiat" [value]="ticker.last * 100000000"></app-fiat>
|
||||
<ng-template #fiat>
|
||||
<span class="green-color">{{ ticker.last | currency: ticker.market.rsymbol }}</span>
|
||||
</ng-template>
|
||||
</td>
|
||||
<td>{{ ticker.volume?.num_trades ? ticker.volume?.num_trades : 0 }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="text-center"><a href="" [routerLink]="['/markets' | relativeUrl]" i18n="dashboard.view-all">View all »</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col mb-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-center" i18n="Latest Trades header">Latest Trades</h5>
|
||||
<app-bisq-trades [trades$]="trades$" view="small"></app-bisq-trades>
|
||||
<div class="text-center"><a href="" [routerLink]="['/markets' | relativeUrl]" i18n="dashboard.view-all">View all »</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<app-language-selector *ngIf="!stateService.env.OFFICIAL_BISQ_MARKETS"></app-language-selector>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<ng-template #loadingTmpl>
|
||||
<tr *ngFor="let i of [1,2,3,4,5,6,7,8,9,10]">
|
||||
<td *ngFor="let j of [1, 2, 3]"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #loadingSpinner>
|
||||
<div class="text-center loadingGraphs">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #loading>
|
||||
<div class="skeleton-loader shorter"></div>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,84 @@
|
||||
#volumeHolder {
|
||||
height: 500px;
|
||||
background-color: #000;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.table {
|
||||
max-width: 100%;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.loadingGraphs {
|
||||
position: relative;
|
||||
top: 45%;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
font-size: 13px;
|
||||
@media(min-width: 576px){
|
||||
font-size: 16px;
|
||||
}
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.big-fiat {
|
||||
color: #3bcc49;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
|
||||
.card {
|
||||
background-color: #1d1f31;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
color: #4a68b9;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.info-block {
|
||||
float: left;
|
||||
width: 350px;
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
.progress {
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
background-color: #2d3348;
|
||||
height: 1.1rem;
|
||||
}
|
||||
|
||||
.bg-warning {
|
||||
background-color: #b58800 !important;
|
||||
}
|
||||
|
||||
.skeleton-loader {
|
||||
max-width: 100%;
|
||||
&.shorter {
|
||||
max-width: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
.more-padding {
|
||||
padding: 1.25rem 2rem 1.25rem 2rem;
|
||||
}
|
||||
|
||||
.graph-card {
|
||||
height: 100%;
|
||||
@media (min-width: 992px) {
|
||||
height: 385px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { Observable, combineLatest, BehaviorSubject, of } from 'rxjs';
|
||||
import { map, share, switchMap } from 'rxjs/operators';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
import { BisqApiService } from '../bisq-api.service';
|
||||
import { Trade } from '../bisq.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'app-main-bisq-dashboard',
|
||||
templateUrl: './bisq-main-dashboard.component.html',
|
||||
styleUrls: ['./bisq-main-dashboard.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class BisqMainDashboardComponent implements OnInit {
|
||||
tickers$: Observable<any>;
|
||||
volumes$: Observable<any>;
|
||||
trades$: Observable<Trade[]>;
|
||||
sort$ = new BehaviorSubject<string>('trades');
|
||||
hlocData$: Observable<any>;
|
||||
usdPrice$: Observable<number>;
|
||||
isLoadingGraph = true;
|
||||
bisqMarketPrice = 0;
|
||||
|
||||
allowCryptoCoins = ['usdc', 'l-btc', 'bsq'];
|
||||
|
||||
constructor(
|
||||
private websocketService: WebsocketService,
|
||||
private bisqApiService: BisqApiService,
|
||||
public stateService: StateService,
|
||||
private seoService: SeoService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle(`Markets`);
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
this.usdPrice$ = this.stateService.conversions$.asObservable().pipe(
|
||||
map((conversions) => conversions.USD)
|
||||
);
|
||||
|
||||
this.volumes$ = this.bisqApiService.getAllVolumesDay$()
|
||||
.pipe(
|
||||
map((volumes) => {
|
||||
const data = volumes.map((volume) => {
|
||||
return {
|
||||
time: volume.period_start,
|
||||
value: volume.volume,
|
||||
};
|
||||
});
|
||||
|
||||
const linesData = volumes.map((volume) => {
|
||||
return {
|
||||
time: volume.period_start,
|
||||
value: volume.num_trades,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
data: data,
|
||||
linesData: linesData,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const getMarkets = this.bisqApiService.getMarkets$().pipe(share());
|
||||
|
||||
this.tickers$ = combineLatest([
|
||||
this.bisqApiService.getMarketsTicker$(),
|
||||
getMarkets,
|
||||
this.bisqApiService.getMarketVolumesByTime$('7d'),
|
||||
])
|
||||
.pipe(
|
||||
map(([tickers, markets, volumes]) => {
|
||||
|
||||
const newTickers = [];
|
||||
for (const t in tickers) {
|
||||
|
||||
if (!this.stateService.env.OFFICIAL_BISQ_MARKETS) {
|
||||
const pair = t.split('_');
|
||||
if (pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const mappedTicker: any = tickers[t];
|
||||
|
||||
mappedTicker.pair_url = t;
|
||||
mappedTicker.pair = t.replace('_', '/').toUpperCase();
|
||||
mappedTicker.market = markets[t];
|
||||
mappedTicker.volume = volumes[t];
|
||||
mappedTicker.name = `${mappedTicker.market.rtype === 'crypto' ? mappedTicker.market.lname : mappedTicker.market.rname} (${mappedTicker.market.rtype === 'crypto' ? mappedTicker.market.lsymbol : mappedTicker.market.rsymbol}`;
|
||||
newTickers.push(mappedTicker);
|
||||
}
|
||||
return newTickers;
|
||||
}),
|
||||
switchMap((tickers) => combineLatest([this.sort$, of(tickers)])),
|
||||
map(([sort, tickers]) => {
|
||||
if (sort === 'trades') {
|
||||
tickers.sort((a, b) => (b.volume && b.volume.num_trades || 0) - (a.volume && a.volume.num_trades || 0));
|
||||
} else if (sort === 'volumes') {
|
||||
tickers.sort((a, b) => (b.volume && b.volume.volume || 0) - (a.volume && a.volume.volume || 0));
|
||||
} else if (sort === 'name') {
|
||||
tickers.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
return tickers.slice(0, 10);
|
||||
})
|
||||
);
|
||||
|
||||
this.trades$ = combineLatest([
|
||||
this.bisqApiService.getMarketTrades$('all'),
|
||||
getMarkets,
|
||||
])
|
||||
.pipe(
|
||||
map(([trades, markets]) => {
|
||||
if (!this.stateService.env.OFFICIAL_BISQ_MARKETS) {
|
||||
trades = trades.filter((trade) => {
|
||||
const pair = trade.market.split('_');
|
||||
return !(pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1);
|
||||
});
|
||||
}
|
||||
return trades.map((trade => {
|
||||
trade._market = markets[trade.market];
|
||||
return trade;
|
||||
})).slice(0, 10);
|
||||
})
|
||||
);
|
||||
|
||||
this.hlocData$ = this.bisqApiService.getMarketsHloc$('btc_usd', 'day')
|
||||
.pipe(
|
||||
map((hlocData) => {
|
||||
this.isLoadingGraph = false;
|
||||
|
||||
hlocData = hlocData.map((h) => {
|
||||
h.time = h.period_start;
|
||||
return h;
|
||||
});
|
||||
|
||||
const hlocVolume = hlocData.map((h) => {
|
||||
return {
|
||||
time: h.time,
|
||||
value: h.volume_right,
|
||||
color: h.close > h.avg ? 'rgba(0, 41, 74, 0.7)' : 'rgba(0, 41, 74, 1)',
|
||||
};
|
||||
});
|
||||
|
||||
// Add whitespace
|
||||
if (hlocData.length > 1) {
|
||||
const newHloc = [];
|
||||
newHloc.push(hlocData[0]);
|
||||
|
||||
const period = 86400;
|
||||
let periods = 0;
|
||||
const startingDate = hlocData[0].period_start;
|
||||
let index = 1;
|
||||
while (true) {
|
||||
periods++;
|
||||
if (hlocData[index].period_start > startingDate + period * periods) {
|
||||
newHloc.push({
|
||||
time: startingDate + period * periods,
|
||||
});
|
||||
} else {
|
||||
newHloc.push(hlocData[index]);
|
||||
index++;
|
||||
if (!hlocData[index]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
hlocData = newHloc;
|
||||
}
|
||||
|
||||
this.bisqMarketPrice = hlocData[hlocData.length - 1].close;
|
||||
|
||||
return {
|
||||
hloc: hlocData,
|
||||
volume: hlocVolume,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
trackByFn(index: number) {
|
||||
return index;
|
||||
}
|
||||
|
||||
sort(by: string) {
|
||||
this.sort$.next(by);
|
||||
}
|
||||
|
||||
}
|
||||
112
frontend/src/app/bisq/bisq-market/bisq-market.component.html
Normal file
112
frontend/src/app/bisq/bisq-market/bisq-market.component.html
Normal file
@@ -0,0 +1,112 @@
|
||||
<div class="container-xl">
|
||||
|
||||
<ng-container *ngIf="hlocData$ | async as hlocData; else loadingSpinner">
|
||||
|
||||
<ng-container *ngIf="currency$ | async as currency; else loadingSpinner">
|
||||
<h1>{{ currency.market.rtype === 'crypto' ? currency.market.lname : currency.market.rname }} - {{ currency.pair }}</h1>
|
||||
<div class="priceheader">
|
||||
<ng-container *ngIf="currency.market.rtype === 'fiat'; else headerPriceCrypto"><span class="green-color">{{ hlocData.hloc[hlocData.hloc.length - 1].close | currency: currency.market.rsymbol }}</span></ng-container>
|
||||
<ng-template #headerPriceCrypto>{{ hlocData.hloc[hlocData.hloc.length - 1].close | number: '1.' + currency.market.rprecision + '-' + currency.market.rprecision }} {{ currency.market.rsymbol }}</ng-template>
|
||||
</div>
|
||||
|
||||
<form [formGroup]="radioGroupForm" class="mb-3 radio-form">
|
||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="interval">
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'half_hour'" (click)="setFragment('half_hour')"> 30M
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'hour'" (click)="setFragment('hour')"> 1H
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'half_day'" (click)="setFragment('half_day')"> 12H
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'day'" (click)="setFragment('day')"> 1D
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'week'" (click)="setFragment('week')"> 1W
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'month'" (click)="setFragment('month')"> 1M
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'year'" (click)="setFragment('year')"> 1Y
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div id="graphHolder">
|
||||
<div class="text-center loadingChart" [hidden]="!isLoadingGraph">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
<app-lightweight-charts [data]="hlocData.hloc" [volumeData]="hlocData.volume" [precision]="currency.market.rtype === 'crypto' ? currency.market.lprecision : currency.market.rprecision"></app-lightweight-charts>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<ng-container *ngIf="offers$ | async as offers; else loadingSpinner">
|
||||
<div class="row row-cols-1 row-cols-md-2">
|
||||
<ng-container *ngTemplateOutlet="offersList; context: { offers: offers.buys, direction: 'BUY', market: currency.market }"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="offersList; context: { offers: offers.sells, direction: 'SELL', market: currency.market }"></ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<br><br>
|
||||
|
||||
<ng-container *ngIf="trades$ | async as trades; else loadingSpinner">
|
||||
<h2 i18n="Latest Trades header">Latest Trades</h2>
|
||||
|
||||
<app-bisq-trades [trades$]="trades$" [market]="currency.market"></app-bisq-trades>
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<ng-template #offersList let-offers="offers" let-direction="direction", let-market="market">
|
||||
<div class="col">
|
||||
<h2>
|
||||
<ng-template [ngIf]="direction === 'BUY'" [ngIfElse]="sellOffers" i18n="Bisq Buy Offers">Buy Offers</ng-template>
|
||||
<ng-template #sellOffers i18n="Bisq Sell Offers">Sell Offers</ng-template>
|
||||
</h2>
|
||||
<div class="table-container">
|
||||
<table class="table table-borderless table-striped">
|
||||
<thead>
|
||||
<th i18n>Price</th>
|
||||
<th><ng-container *ngTemplateOutlet="amount; context: {$implicit: market.lsymbol }"></ng-container></th>
|
||||
<th><ng-container *ngTemplateOutlet="amount; context: {$implicit: market.rsymbol }"></ng-container></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let offer of offers">
|
||||
<td>
|
||||
<ng-container *ngIf="market.rtype === 'fiat'; else priceCrypto"><span class="green-color">{{ offer.price | currency: market.rsymbol }}</span></ng-container>
|
||||
<ng-template #priceCrypto>{{ offer.price | number: '1.2-' + market.rprecision }} <span class="symbol">{{ market.rsymbol }}</span></ng-template>
|
||||
</td>
|
||||
<td>
|
||||
<ng-container *ngIf="market.ltype === 'fiat'; else amountCrypto"><span class="green-color">{{ offer.amount | currency: market.rsymbol }}</span></ng-container>
|
||||
<ng-template #amountCrypto>{{ offer.amount | number: '1.2-' + market.lprecision }} <span class="symbol">{{ market.lsymbol }}</span></ng-template>
|
||||
</td>
|
||||
<td>
|
||||
<ng-container *ngIf="market.rtype === 'fiat'; else volumeCrypto"><span class="green-color">{{ offer.volume | currency: market.rsymbol }}</span></ng-container>
|
||||
<ng-template #volumeCrypto>{{ offer.volume | number: '1.2-' + market.rprecision }} <span class="symbol">{{ market.rsymbol }}</span></ng-template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #loadingSpinner>
|
||||
<br>
|
||||
<br>
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #amount let-i i18n="Trade amount (Symbol)">Amount ({{ i }})</ng-template>
|
||||
46
frontend/src/app/bisq/bisq-market/bisq-market.component.scss
Normal file
46
frontend/src/app/bisq/bisq-market/bisq-market.component.scss
Normal file
@@ -0,0 +1,46 @@
|
||||
.priceheader {
|
||||
font-size: 24px;
|
||||
@media(min-width: 576px){
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
.radio-form {
|
||||
@media(min-width: 576px){
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.loadingChart {
|
||||
z-index: 100;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
#graphHolder {
|
||||
height: 550px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.col {
|
||||
&:last-child{
|
||||
margin-top: 50px;
|
||||
@media(min-width: 576px){
|
||||
margin-top: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
158
frontend/src/app/bisq/bisq-market/bisq-market.component.ts
Normal file
158
frontend/src/app/bisq/bisq-market/bisq-market.component.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { combineLatest, merge, Observable, of } from 'rxjs';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
import { BisqApiService } from '../bisq-api.service';
|
||||
import { OffersMarket, Trade } from '../bisq.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-market',
|
||||
templateUrl: './bisq-market.component.html',
|
||||
styleUrls: ['./bisq-market.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class BisqMarketComponent implements OnInit, OnDestroy {
|
||||
hlocData$: Observable<any>;
|
||||
currency$: Observable<any>;
|
||||
offers$: Observable<OffersMarket>;
|
||||
trades$: Observable<Trade[]>;
|
||||
radioGroupForm: FormGroup;
|
||||
defaultInterval = 'day';
|
||||
|
||||
isLoadingGraph = false;
|
||||
|
||||
constructor(
|
||||
private websocketService: WebsocketService,
|
||||
private route: ActivatedRoute,
|
||||
private bisqApiService: BisqApiService,
|
||||
private formBuilder: FormBuilder,
|
||||
private seoService: SeoService,
|
||||
private router: Router,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.radioGroupForm = this.formBuilder.group({
|
||||
interval: [this.defaultInterval],
|
||||
});
|
||||
|
||||
if (['half_hour', 'hour', 'half_day', 'day', 'week', 'month', 'year', 'auto'].indexOf(this.route.snapshot.fragment) > -1) {
|
||||
this.radioGroupForm.controls.interval.setValue(this.route.snapshot.fragment, { emitEvent: false });
|
||||
}
|
||||
|
||||
this.currency$ = this.bisqApiService.getMarkets$()
|
||||
.pipe(
|
||||
switchMap((markets) => combineLatest([of(markets), this.route.paramMap])),
|
||||
map(([markets, routeParams]) => {
|
||||
const pair = routeParams.get('pair');
|
||||
const pairUpperCase = pair.replace('_', '/').toUpperCase();
|
||||
this.seoService.setTitle(`Bisq market: ${pairUpperCase}`);
|
||||
|
||||
return {
|
||||
pair: pairUpperCase,
|
||||
market: markets[pair],
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
this.trades$ = this.route.paramMap
|
||||
.pipe(
|
||||
map(routeParams => routeParams.get('pair')),
|
||||
switchMap((marketPair) => this.bisqApiService.getMarketTrades$(marketPair)),
|
||||
);
|
||||
|
||||
this.offers$ = this.route.paramMap
|
||||
.pipe(
|
||||
map(routeParams => routeParams.get('pair')),
|
||||
switchMap((marketPair) => this.bisqApiService.getMarketOffers$(marketPair)),
|
||||
map((offers) => offers[Object.keys(offers)[0]])
|
||||
);
|
||||
|
||||
this.hlocData$ = combineLatest([
|
||||
this.route.paramMap,
|
||||
merge(this.radioGroupForm.get('interval').valueChanges, of(this.radioGroupForm.get('interval').value)),
|
||||
])
|
||||
.pipe(
|
||||
switchMap(([routeParams, interval]) => {
|
||||
this.isLoadingGraph = true;
|
||||
const pair = routeParams.get('pair');
|
||||
return this.bisqApiService.getMarketsHloc$(pair, interval);
|
||||
}),
|
||||
map((hlocData) => {
|
||||
this.isLoadingGraph = false;
|
||||
|
||||
hlocData = hlocData.map((h) => {
|
||||
h.time = h.period_start;
|
||||
return h;
|
||||
});
|
||||
|
||||
const hlocVolume = hlocData.map((h) => {
|
||||
return {
|
||||
time: h.time,
|
||||
value: h.volume_right,
|
||||
color: h.close > h.avg ? 'rgba(0, 41, 74, 0.7)' : 'rgba(0, 41, 74, 1)',
|
||||
};
|
||||
});
|
||||
|
||||
// Add whitespace
|
||||
if (hlocData.length > 1) {
|
||||
const newHloc = [];
|
||||
newHloc.push(hlocData[0]);
|
||||
|
||||
const period = this.getUnixTimestampFromInterval(this.radioGroupForm.get('interval').value); // temp
|
||||
let periods = 0;
|
||||
const startingDate = hlocData[0].period_start;
|
||||
let index = 1;
|
||||
while (true) {
|
||||
periods++;
|
||||
if (hlocData[index].period_start > startingDate + period * periods) {
|
||||
newHloc.push({
|
||||
time: startingDate + period * periods,
|
||||
});
|
||||
} else {
|
||||
newHloc.push(hlocData[index]);
|
||||
index++;
|
||||
if (!hlocData[index]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
hlocData = newHloc;
|
||||
}
|
||||
|
||||
return {
|
||||
hloc: hlocData,
|
||||
volume: hlocVolume,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
setFragment(fragment: string) {
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParamsHandling: 'merge',
|
||||
fragment: fragment
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.websocketService.stopTrackingBisqMarket();
|
||||
}
|
||||
|
||||
getUnixTimestampFromInterval(interval: string): number {
|
||||
switch (interval) {
|
||||
case 'minute': return 60;
|
||||
case 'half_hour': return 1800;
|
||||
case 'hour': return 3600;
|
||||
case 'half_day': return 43200;
|
||||
case 'day': return 86400;
|
||||
case 'week': return 604800;
|
||||
case 'month': return 2592000;
|
||||
case 'year': return 31579200;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,20 +5,20 @@
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="col-md">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody *ngIf="!isLoading; else loadingTemplate">
|
||||
<tr>
|
||||
<td class="td-width" i18n="BSQ existing amount">Existing amount</td>
|
||||
<td>{{ (stats.minted - stats.burnt) | number: '1.2-2' }} BSQ</td>
|
||||
<td>{{ (stats.minted - stats.burnt) | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="BSQ minted amount">Minted amount</td>
|
||||
<td>{{ stats.minted | number: '1.2-2' }} BSQ</td>
|
||||
<td>{{ stats.minted | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="BSQ burnt amount">Burnt amount</td>
|
||||
<td>{{ stats.burnt | number: '1.2-2' }} BSQ</td>
|
||||
<td>{{ stats.burnt | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="BSQ addresses">Addresses</td>
|
||||
@@ -33,7 +33,7 @@
|
||||
<td>{{ stats.spent_txos | number }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="BSQ token price">Price</td>
|
||||
<td i18n>Price</td>
|
||||
<td><app-fiat [value]="price"></app-fiat></td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -44,7 +44,7 @@
|
||||
</table>
|
||||
|
||||
</div>
|
||||
<div class="col-sm"></div>
|
||||
<div class="col-md"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -7,3 +7,12 @@
|
||||
width: 175px;
|
||||
}
|
||||
}
|
||||
|
||||
.fiat {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
@media (min-width: 768px) {
|
||||
font-size: 14px;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { BisqApiService } from '../bisq-api.service';
|
||||
import { BisqStats } from '../bisq.interfaces';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-stats',
|
||||
@@ -15,12 +16,15 @@ export class BisqStatsComponent implements OnInit {
|
||||
price: number;
|
||||
|
||||
constructor(
|
||||
private websocketService: WebsocketService,
|
||||
private bisqApiService: BisqApiService,
|
||||
private seoService: SeoService,
|
||||
private stateService: StateService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
this.seoService.setTitle($localize`:@@2a30a4cdb123a03facc5ab8c5b3e6d8b8dbbc3d4:BSQ statistics`);
|
||||
this.stateService.bsqPrice$
|
||||
.subscribe((bsqPrice) => {
|
||||
|
||||
46
frontend/src/app/bisq/bisq-trades/bisq-trades.component.html
Normal file
46
frontend/src/app/bisq/bisq-trades/bisq-trades.component.html
Normal file
@@ -0,0 +1,46 @@
|
||||
<div class="table-container">
|
||||
<table class="table table-borderless table-striped">
|
||||
<thead>
|
||||
<th i18n>Date</th>
|
||||
<th *ngIf="view === 'all'" i18n>Price</th>
|
||||
<th><ng-container *ngTemplateOutlet="amount; context: {$implicit: 'BTC' }"></ng-container></th>
|
||||
<th>
|
||||
<ng-template [ngIf]="market" [ngIfElse]="noMarket"><ng-container *ngTemplateOutlet="amount; context: {$implicit: market.lsymbol === 'BTC' ? market.rsymbol : market.lsymbol }"></ng-container></ng-template>
|
||||
<ng-template #noMarket i18n>Amount</ng-template>
|
||||
</th>
|
||||
</thead>
|
||||
<tbody *ngIf="(trades$ | async) as trades; else loadingTmpl">
|
||||
<tr *ngFor="let trade of trades;">
|
||||
<td>
|
||||
{{ trade.trade_date | date:'yyyy-MM-dd HH:mm' }}
|
||||
</td>
|
||||
<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-template #priceCrypto>{{ trade.price | number: '1.2-' + (trade._market || market).rprecision }} <span class="symbol">{{ (trade._market || market).rsymbol }}</span></ng-template>
|
||||
</td>
|
||||
<ng-container *ngTemplateOutlet="(trade._market || market).rsymbol === 'BTC' ? tradeVolume : tradeAmount"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="(trade._market || market).rsymbol === 'BTC' ? tradeAmount : tradeVolume"></ng-container>
|
||||
<ng-template #tradeAmount>
|
||||
<td>
|
||||
<ng-container *ngIf="(trade._market || market).ltype === 'fiat'; else amountCrypto"><span class="green-color">{{ trade.amount | currency: (trade._market || market).rsymbol }}</span></ng-container>
|
||||
<ng-template #amountCrypto>{{ trade.amount | number: '1.2-' + (trade._market || market).lprecision }} <span class="symbol">{{ (trade._market || market).lsymbol }}</span></ng-template>
|
||||
</td>
|
||||
</ng-template>
|
||||
<ng-template #tradeVolume>
|
||||
<td>
|
||||
<ng-container *ngIf="(trade._market || market).rtype === 'fiat'; else volumeCrypto"><span class="green-color">{{ trade.volume | currency: (trade._market || market).rsymbol }}</span></ng-container>
|
||||
<ng-template #volumeCrypto>{{ trade.volume | number: '1.2-' + (trade._market || market).rprecision }} <span class="symbol">{{ (trade._market || market).rsymbol }}</span></ng-template>
|
||||
</td>
|
||||
</ng-template>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<ng-template #loadingTmpl>
|
||||
<tr *ngFor="let i of [1,2,3,4,5,6,7,8,9,10]">
|
||||
<td *ngFor="let j of loadingColumns"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #amount let-i i18n="Trade amount (Symbol)">Amount ({{ i }})</ng-template>
|
||||
12
frontend/src/app/bisq/bisq-trades/bisq-trades.component.scss
Normal file
12
frontend/src/app/bisq/bisq-trades/bisq-trades.component.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
22
frontend/src/app/bisq/bisq-trades/bisq-trades.component.ts
Normal file
22
frontend/src/app/bisq/bisq-trades/bisq-trades.component.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-trades',
|
||||
templateUrl: './bisq-trades.component.html',
|
||||
styleUrls: ['./bisq-trades.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BisqTradesComponent implements OnChanges {
|
||||
@Input() trades$: Observable<any>;
|
||||
@Input() market: any;
|
||||
@Input() view: 'all' | 'small' = 'all';
|
||||
|
||||
loadingColumns = [1, 2, 3, 4];
|
||||
|
||||
ngOnChanges() {
|
||||
if (this.view === 'small') {
|
||||
this.loadingColumns = [1, 2, 3];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,24 @@
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="col-md">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td-width" i18n="transaction.inputs">Inputs</td>
|
||||
<td>{{ totalInput / 100 | number: '1.2-2' }} BSQ</td>
|
||||
<td>{{ totalInput / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="transaction.outputs">Outputs</td>
|
||||
<td>{{ totalOutput / 100 | number: '1.2-2' }} BSQ</td>
|
||||
<td>{{ totalOutput / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="asset.issued-amount|Liquid Asset issued amount">Issued amount</td>
|
||||
<td>{{ totalIssued / 100 | number: '1.2-2' }} BSQ</td>
|
||||
<td>{{ totalIssued / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="col-md">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody class="mobile-even">
|
||||
<tr>
|
||||
|
||||
@@ -8,4 +8,15 @@
|
||||
.mobile-even tr:nth-of-type(odd) {
|
||||
background-color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.table {
|
||||
tr td {
|
||||
&:last-child{
|
||||
text-align: right;
|
||||
@media(min-width: 768px){
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,96 +1,102 @@
|
||||
<div class="container-xl">
|
||||
|
||||
<h1 class="float-left mr-3 mb-md-3" i18n="shared.transaction">Transaction</h1>
|
||||
|
||||
<div class="title-block">
|
||||
<ng-template [ngIf]="!isLoading && !error">
|
||||
|
||||
<button *ngIf="(latestBlock$ | async) as latestBlock" type="button" class="btn btn-sm btn-success float-right mr-2 mt-1 mt-md-3">
|
||||
<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>
|
||||
<a [routerLink]="['/bisq-tx' | relativeUrl, bisqTx.id]" style="line-height: 56px;">
|
||||
<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">
|
||||
<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>(<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' }} BSQ (<app-bsq-amount [bsq]="bisqTx.burntFee" [forceFiat]="true" [green]="true"></app-bsq-amount>)
|
||||
</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' }} sat/vB
|
||||
|
||||
<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 class="title">
|
||||
<h1 i18n="shared.transaction">Transaction</h1>
|
||||
</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) | 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>
|
||||
|
||||
<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>
|
||||
|
||||
<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 [ngIf="isLoading && !error">
|
||||
|
||||
@@ -1,9 +1 @@
|
||||
.td-width {
|
||||
width: 175px;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.td-width {
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
@import "./../../components/transaction/transaction.component.scss";
|
||||
@@ -9,6 +9,7 @@ import { BisqApiService } from '../bisq-api.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { ElectrsApiService } from 'src/app/services/electrs-api.service';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-transaction',
|
||||
@@ -27,6 +28,7 @@ export class BisqTransactionComponent implements OnInit, OnDestroy {
|
||||
subscription: Subscription;
|
||||
|
||||
constructor(
|
||||
private websocketService: WebsocketService,
|
||||
private route: ActivatedRoute,
|
||||
private bisqApiService: BisqApiService,
|
||||
private electrsApiService: ElectrsApiService,
|
||||
@@ -36,6 +38,8 @@ export class BisqTransactionComponent implements OnInit, OnDestroy {
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
this.subscription = this.route.paramMap.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
this.isLoading = true;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="container-xl">
|
||||
<h1 style="float: left;" i18n>Transactions</h1>
|
||||
<div class="container-xl" (window:resize)="onResize($event)">
|
||||
<h1 style="float: left;" i18n>BSQ Transactions</h1>
|
||||
|
||||
<div class="d-block float-right">
|
||||
<form [formGroup]="radioGroupForm">
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
<table class="table table-borderless table-striped">
|
||||
<thead>
|
||||
<th style="width: 20%;" i18n>Transaction</th>
|
||||
<th style="width: 20%;" i18n>TXID</th>
|
||||
<th class="d-none d-md-block" style="width: 100%;" i18n>Type</th>
|
||||
<th style="width: 20%;" i18n>Amount</th>
|
||||
<th style="width: 20%;" i18n>Confirmed</th>
|
||||
@@ -26,15 +26,15 @@
|
||||
<td><a [routerLink]="['/tx/' | relativeUrl, tx.id]" [state]="{ data: tx }">{{ tx.id | slice : 0 : 8 }}</a></td>
|
||||
<td class="d-none d-md-block">
|
||||
<app-bisq-icon class="mr-1" [txType]="tx.txType"></app-bisq-icon>
|
||||
<span class="d-none d-md-inline"> {{ tx.txTypeDisplayString }}</span>
|
||||
<span class="d-none d-md-inline"> {{ getStringByTxType(tx.txType) }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<app-bisq-icon class="d-inline d-md-none mr-1" [txType]="tx.txType"></app-bisq-icon>
|
||||
<ng-template [ngIf]="tx.txType === 'PAY_TRADE_FEE' || tx.txType === 'ASSET_LISTING_FEE'" [ngIfElse]="defaultTxType">
|
||||
{{ tx.burntFee / 100 | number: '1.2-2' }}<span class="d-none d-md-inline"> BSQ</span>
|
||||
{{ tx.burntFee / 100 | number: '1.2-2' }} <span class="d-none d-md-inline symbol">BSQ</span>
|
||||
</ng-template>
|
||||
<ng-template #defaultTxType>
|
||||
{{ calculateTotalOutput(tx.outputs) / 100 | number: '1.2-2' }}<span class="d-none d-md-inline"> BSQ</span>
|
||||
{{ calculateTotalOutput(tx.outputs) / 100 | number: '1.2-2' }} <span class="d-none d-md-inline symbol">BSQ</span>
|
||||
</ng-template>
|
||||
</td>
|
||||
<td><app-time-since [time]="tx.time / 1000" [fastRender]="true"></app-time-since></td>
|
||||
@@ -44,8 +44,7 @@
|
||||
</table>
|
||||
|
||||
<br>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
</ng-container>
|
||||
</div>
|
||||
@@ -54,4 +53,4 @@
|
||||
<tr *ngFor="let i of loadingItems">
|
||||
<td *ngFor="let j of [1, 2, 3, 4, 5]"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
|
||||
@@ -7,3 +7,17 @@ label {
|
||||
right: 0px;
|
||||
left: inherit;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
float: none;
|
||||
@media(min-width: 400px){
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.container-xl {
|
||||
padding-bottom: 60px;
|
||||
@media(min-width: 400px){
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { SeoService } from 'src/app/services/seo.service';
|
||||
import { FormGroup, FormBuilder } from '@angular/forms';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts } from 'ngx-bootrap-multiselect';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-transactions',
|
||||
@@ -26,19 +27,19 @@ export class BisqTransactionsComponent implements OnInit {
|
||||
types: string[] = [];
|
||||
|
||||
txTypeOptions: IMultiSelectOption[] = [
|
||||
{ id: 1, name: 'Asset listing fee' },
|
||||
{ id: 2, name: 'Blind vote' },
|
||||
{ id: 3, name: 'Compensation request' },
|
||||
{ id: 4, name: 'Genesis' },
|
||||
{ id: 13, name: 'Irregular' },
|
||||
{ id: 5, name: 'Lockup' },
|
||||
{ id: 6, name: 'Pay trade fee' },
|
||||
{ id: 7, name: 'Proof of burn' },
|
||||
{ id: 8, name: 'Proposal' },
|
||||
{ id: 9, name: 'Reimbursement request' },
|
||||
{ id: 10, name: 'Transfer BSQ' },
|
||||
{ id: 11, name: 'Unlock' },
|
||||
{ id: 12, name: 'Vote reveal' },
|
||||
{ id: 1, name: $localize`Asset listing fee` },
|
||||
{ id: 2, name: $localize`Blind vote` },
|
||||
{ id: 3, name: $localize`Compensation request` },
|
||||
{ id: 4, name: $localize`Genesis` },
|
||||
{ id: 13, name: $localize`Irregular` },
|
||||
{ id: 5, name: $localize`Lockup` },
|
||||
{ id: 6, name: $localize`Pay trade fee` },
|
||||
{ id: 7, name: $localize`Proof of burn` },
|
||||
{ id: 8, name: $localize`Proposal` },
|
||||
{ id: 9, name: $localize`Reimbursement request` },
|
||||
{ id: 10, name: $localize`Transfer BSQ` },
|
||||
{ id: 11, name: $localize`Unlock` },
|
||||
{ id: 12, name: $localize`Vote reveal` },
|
||||
];
|
||||
txTypesDefaultChecked = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
|
||||
|
||||
@@ -59,12 +60,13 @@ export class BisqTransactionsComponent implements OnInit {
|
||||
|
||||
// @ts-ignore
|
||||
paginationSize: 'sm' | 'lg' = 'md';
|
||||
paginationMaxSize = 10;
|
||||
paginationMaxSize = 5;
|
||||
|
||||
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'];
|
||||
|
||||
constructor(
|
||||
private websocketService: WebsocketService,
|
||||
private bisqApiService: BisqApiService,
|
||||
private seoService: SeoService,
|
||||
private formBuilder: FormBuilder,
|
||||
@@ -74,6 +76,7 @@ export class BisqTransactionsComponent implements OnInit {
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.websocketService.want(['blocks']);
|
||||
this.seoService.setTitle($localize`:@@add4cd82e3e38a3110fe67b3c7df56e9602644ee:Transactions`);
|
||||
|
||||
this.radioGroupForm = this.formBuilder.group({
|
||||
@@ -82,7 +85,7 @@ export class BisqTransactionsComponent implements OnInit {
|
||||
|
||||
this.loadingItems = Array(this.itemsPerPage);
|
||||
|
||||
if (document.body.clientWidth < 768) {
|
||||
if (document.body.clientWidth < 670) {
|
||||
this.paginationSize = 'sm';
|
||||
this.paginationMaxSize = 3;
|
||||
}
|
||||
@@ -157,7 +160,16 @@ export class BisqTransactionsComponent implements OnInit {
|
||||
return outputs.reduce((acc: number, output: BisqOutput) => acc + output.bsqAmount, 0);
|
||||
}
|
||||
|
||||
getStringByTxType(type: string) {
|
||||
const id = this.txTypes.indexOf(type) + 1;
|
||||
return this.txTypeOptions.find((type) => id === type.id).name;
|
||||
}
|
||||
|
||||
trackByFn(index: number) {
|
||||
return index;
|
||||
}
|
||||
|
||||
onResize(event: any) {
|
||||
this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
<tr *ngIf="input.isVerified">
|
||||
<td class="arrow-td">
|
||||
<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 #hasPreoutput>
|
||||
<a [routerLink]="['/tx/' | relativeUrl, input.spendingTxId]">
|
||||
<i class="arrow red"></i>
|
||||
<a [routerLink]="['/tx/' | relativeUrl, input.spendingTxId]" class="red">
|
||||
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
|
||||
</a>
|
||||
</ng-template>
|
||||
</td>
|
||||
@@ -44,10 +46,14 @@
|
||||
<td class="text-right nowrap">
|
||||
<app-bsq-amount [bsq]="output.bsqAmount"></app-bsq-amount>
|
||||
</td>
|
||||
<td class="pl-1 arrow-td">
|
||||
<i *ngIf="!output.spentInfo; else spent" class="arrow green"></i>
|
||||
<td class="arrow-td">
|
||||
<span *ngIf="!output.spentInfo; else spent" class="green">
|
||||
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
|
||||
</span>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -58,11 +64,11 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="float-left mt-2-5" *ngIf="showConfirmations && tx.burntFee">
|
||||
<ng-container i18n="BSQ burnt amount">Burnt amount</ng-container>: {{ tx.burntFee / 100 | number: '1.2-2' }} BSQ (<app-bsq-amount [bsq]="tx.burntFee" [forceFiat]="true" [green]="true"></app-bsq-amount>)
|
||||
<div class="transaction-fee" *ngIf="showConfirmations && tx.burntFee">
|
||||
<ng-container i18n="BSQ burnt amount">Burnt amount</ng-container>: {{ tx.burntFee / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span> <span class="extra-info"><span class="fiat"><app-bsq-amount [bsq]="tx.burntFee" [forceFiat]="true" [green]="true"></app-bsq-amount></span></span>
|
||||
</div>
|
||||
|
||||
<div class="float-right">
|
||||
<div class="btn-container">
|
||||
<span *ngIf="showConfirmations && latestBlock$ | async as latestBlock">
|
||||
<button type="button" class="btn btn-sm btn-success mt-2">
|
||||
<ng-container *ngTemplateOutlet="latestBlock.height - tx.blockHeight + 1 == 1 ? confirmationSingular : confirmationPlural; context: {$implicit: latestBlock.height - tx.blockHeight + 1}"></ng-container>
|
||||
|
||||
@@ -1,84 +1,103 @@
|
||||
|
||||
.arrow-td {
|
||||
width: 22px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
display: inline-block!important;
|
||||
.green, .grey, .red {
|
||||
font-size: 16px;
|
||||
top: -2px;
|
||||
position: relative;
|
||||
width: 14px;
|
||||
height: 22px;
|
||||
box-sizing: content-box
|
||||
@media( min-width: 576px){
|
||||
font-size: 19px;
|
||||
}
|
||||
}
|
||||
|
||||
.arrow:before {
|
||||
position: absolute;
|
||||
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
|
||||
.green {
|
||||
color:#28a745;
|
||||
}
|
||||
|
||||
.arrow:after {
|
||||
position: absolute;
|
||||
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);
|
||||
.red {
|
||||
color:#dc3545;
|
||||
}
|
||||
|
||||
.arrow.green:before {
|
||||
border-left: 10px solid #28a745;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
|
||||
.scriptmessage {
|
||||
max-width: 280px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.scriptmessage.longer {
|
||||
max-width: 500px;
|
||||
.grey {
|
||||
color:#6c757d;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.mobile-bottomcol {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.scriptmessage {
|
||||
max-width: 90px !important;
|
||||
.details-table td:first-child {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.scriptmessage.longer {
|
||||
max-width: 280px !important;
|
||||
}
|
||||
|
||||
|
||||
.details-table {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.details-table td {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.details-table td:nth-child(2) {
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.smaller-text {
|
||||
font-size: 12px;
|
||||
@media (min-width: 576px) {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.longer {
|
||||
max-width: 100% !important;
|
||||
width: 200px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.row{
|
||||
flex-direction: column;
|
||||
@media (min-width: 992px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
.extra-info {
|
||||
display: inline-table;
|
||||
.fiat {
|
||||
font-size: 14px;
|
||||
display: block;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.transaction-fee {
|
||||
display: block;
|
||||
margin: 0px auto 5px;
|
||||
@media (min-width: 576px) {
|
||||
display: inline-table;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.fiat {
|
||||
margin-left: 10px;
|
||||
font-size: 13px;
|
||||
@media (min-width: 576px) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-container {
|
||||
text-align: right;
|
||||
@media (min-width: 576px) {
|
||||
display: inline-table;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
@@ -80,3 +80,182 @@ interface SpentInfo {
|
||||
inputIndex: number;
|
||||
txId: string;
|
||||
}
|
||||
|
||||
|
||||
export interface BisqTrade {
|
||||
direction: string;
|
||||
price: string;
|
||||
amount: string;
|
||||
volume: string;
|
||||
payment_method: string;
|
||||
trade_id: string;
|
||||
trade_date: number;
|
||||
market?: string;
|
||||
}
|
||||
|
||||
export interface Currencies { [txid: string]: Currency; }
|
||||
|
||||
export interface Currency {
|
||||
code: string;
|
||||
name: string;
|
||||
precision: number;
|
||||
|
||||
_type: string;
|
||||
}
|
||||
|
||||
export interface Depth { [market: string]: Market; }
|
||||
|
||||
interface Market {
|
||||
'buys': string[];
|
||||
'sells': string[];
|
||||
}
|
||||
|
||||
export interface HighLowOpenClose {
|
||||
period_start: number | string;
|
||||
open: string;
|
||||
high: string;
|
||||
low: string;
|
||||
close: string;
|
||||
volume_left: string;
|
||||
volume_right: string;
|
||||
avg: string;
|
||||
}
|
||||
|
||||
export interface Markets { [txid: string]: Pair; }
|
||||
|
||||
interface Pair {
|
||||
pair: string;
|
||||
lname: string;
|
||||
rname: string;
|
||||
lsymbol: string;
|
||||
rsymbol: string;
|
||||
lprecision: number;
|
||||
rprecision: number;
|
||||
ltype: string;
|
||||
rtype: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Offers { [market: string]: OffersMarket; }
|
||||
|
||||
export interface OffersMarket {
|
||||
buys: Offer[] | null;
|
||||
sells: Offer[] | null;
|
||||
}
|
||||
|
||||
export interface OffersData {
|
||||
direction: string;
|
||||
currencyCode: string;
|
||||
minAmount: number;
|
||||
amount: number;
|
||||
price: number;
|
||||
date: number;
|
||||
useMarketBasedPrice: boolean;
|
||||
marketPriceMargin: number;
|
||||
paymentMethod: string;
|
||||
id: string;
|
||||
currencyPair: string;
|
||||
primaryMarketDirection: string;
|
||||
priceDisplayString: string;
|
||||
primaryMarketAmountDisplayString: string;
|
||||
primaryMarketMinAmountDisplayString: string;
|
||||
primaryMarketVolumeDisplayString: string;
|
||||
primaryMarketMinVolumeDisplayString: string;
|
||||
primaryMarketPrice: number;
|
||||
primaryMarketAmount: number;
|
||||
primaryMarketMinAmount: number;
|
||||
primaryMarketVolume: number;
|
||||
primaryMarketMinVolume: number;
|
||||
}
|
||||
|
||||
export interface Offer {
|
||||
offer_id: string;
|
||||
offer_date: number;
|
||||
direction: string;
|
||||
min_amount: string;
|
||||
amount: string;
|
||||
price: string;
|
||||
volume: string;
|
||||
payment_method: string;
|
||||
offer_fee_txid: any;
|
||||
}
|
||||
|
||||
export interface Tickers { [market: string]: Ticker | null; }
|
||||
|
||||
export interface Ticker {
|
||||
last: string;
|
||||
high: string;
|
||||
low: string;
|
||||
volume_left: string;
|
||||
volume_right: string;
|
||||
buy: string | null;
|
||||
sell: string | null;
|
||||
}
|
||||
|
||||
export interface Trade {
|
||||
market?: string;
|
||||
price: string;
|
||||
amount: string;
|
||||
volume: string;
|
||||
payment_method: string;
|
||||
trade_id: string;
|
||||
trade_date: number;
|
||||
_market: Pair;
|
||||
}
|
||||
|
||||
export interface TradesData {
|
||||
currency: string;
|
||||
direction: string;
|
||||
tradePrice: number;
|
||||
tradeAmount: number;
|
||||
tradeDate: number;
|
||||
paymentMethod: string;
|
||||
offerDate: number;
|
||||
useMarketBasedPrice: boolean;
|
||||
marketPriceMargin: number;
|
||||
offerAmount: number;
|
||||
offerMinAmount: number;
|
||||
offerId: string;
|
||||
depositTxId?: string;
|
||||
currencyPair: string;
|
||||
primaryMarketDirection: string;
|
||||
primaryMarketTradePrice: number;
|
||||
primaryMarketTradeAmount: number;
|
||||
primaryMarketTradeVolume: number;
|
||||
|
||||
_market: string;
|
||||
_tradePriceStr: string;
|
||||
_tradeAmountStr: string;
|
||||
_tradeVolumeStr: string;
|
||||
_offerAmountStr: string;
|
||||
_tradePrice: number;
|
||||
_tradeAmount: number;
|
||||
_tradeVolume: number;
|
||||
_offerAmount: number;
|
||||
}
|
||||
|
||||
export interface MarketVolume {
|
||||
period_start: number;
|
||||
num_trades: number;
|
||||
volume: string;
|
||||
}
|
||||
|
||||
export interface MarketsApiError {
|
||||
success: number;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export type Interval = 'minute' | 'half_hour' | 'hour' | 'half_day' | 'day' | 'week' | 'month' | 'year' | 'auto';
|
||||
|
||||
export interface SummarizedIntervals { [market: string]: SummarizedInterval; }
|
||||
export interface SummarizedInterval {
|
||||
period_start: number;
|
||||
open: number;
|
||||
close: number;
|
||||
high: number;
|
||||
low: number;
|
||||
avg: number;
|
||||
volume_right: number;
|
||||
volume_left: number;
|
||||
time?: number;
|
||||
}
|
||||
|
||||
@@ -3,10 +3,15 @@ import { BisqRoutingModule } from './bisq.routing.module';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { NgxBootstrapMultiselectModule } from 'ngx-bootrap-multiselect';
|
||||
|
||||
import { LightweightChartsComponent } from './lightweight-charts/lightweight-charts.component';
|
||||
import { LightweightChartsAreaComponent } from './lightweight-charts-area/lightweight-charts-area.component';
|
||||
import { BisqMarketComponent } from './bisq-market/bisq-market.component';
|
||||
import { BisqTransactionsComponent } from './bisq-transactions/bisq-transactions.component';
|
||||
import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { BisqTransactionComponent } from './bisq-transaction/bisq-transaction.component';
|
||||
import { BisqBlockComponent } from './bisq-block/bisq-block.component';
|
||||
import { BisqDashboardComponent } from './bisq-dashboard/bisq-dashboard.component';
|
||||
import { BisqMainDashboardComponent } from './bisq-main-dashboard/bisq-main-dashboard.component';
|
||||
import { BisqIconComponent } from './bisq-icon/bisq-icon.component';
|
||||
import { BisqTransactionDetailsComponent } from './bisq-transaction-details/bisq-transaction-details.component';
|
||||
import { BisqTransfersComponent } from './bisq-transfers/bisq-transfers.component';
|
||||
@@ -14,11 +19,11 @@ import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontaweso
|
||||
import { faLeaf, faQuestion, faExclamationTriangle, faRocket, faRetweet, faFileAlt, faMoneyBill,
|
||||
faEye, faEyeSlash, faLock, faLockOpen, faExclamationCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { BisqBlocksComponent } from './bisq-blocks/bisq-blocks.component';
|
||||
import { BisqExplorerComponent } from './bisq-explorer/bisq-explorer.component';
|
||||
import { BisqApiService } from './bisq-api.service';
|
||||
import { BisqAddressComponent } from './bisq-address/bisq-address.component';
|
||||
import { BisqStatsComponent } from './bisq-stats/bisq-stats.component';
|
||||
import { BsqAmountComponent } from './bsq-amount/bsq-amount.component';
|
||||
import { BisqTradesComponent } from './bisq-trades/bisq-trades.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -30,10 +35,15 @@ import { BsqAmountComponent } from './bsq-amount/bsq-amount.component';
|
||||
BisqTransactionDetailsComponent,
|
||||
BisqTransfersComponent,
|
||||
BisqBlocksComponent,
|
||||
BisqExplorerComponent,
|
||||
BisqAddressComponent,
|
||||
BisqStatsComponent,
|
||||
BsqAmountComponent,
|
||||
LightweightChartsComponent,
|
||||
LightweightChartsAreaComponent,
|
||||
BisqDashboardComponent,
|
||||
BisqMarketComponent,
|
||||
BisqTradesComponent,
|
||||
BisqMainDashboardComponent,
|
||||
],
|
||||
imports: [
|
||||
BisqRoutingModule,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user