Compare commits
504 Commits
mononaut/b
...
mononaut/u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b1f8ef4a2 | ||
|
|
86ef1e5fce | ||
|
|
b8725ab13a | ||
|
|
6bf47f69eb | ||
|
|
0192c2bffa | ||
|
|
91aa815d88 | ||
|
|
dcdb9361e8 | ||
|
|
89531d1703 | ||
|
|
8311e842bb | ||
|
|
a492223369 | ||
|
|
cd9e336165 | ||
|
|
5b2ecac1f6 | ||
|
|
d2f7864266 | ||
|
|
c9432aa0d1 | ||
|
|
eeb2bae9f3 | ||
|
|
659ef0b6b9 | ||
|
|
9b83ac29b7 | ||
|
|
722b81623b | ||
|
|
648dd5adb2 | ||
|
|
75e7bb3255 | ||
|
|
10fc4d27ad | ||
|
|
ace422c0d5 | ||
|
|
b6682eed2a | ||
|
|
8080b8b56a | ||
|
|
0e8d04e464 | ||
|
|
fc76146085 | ||
|
|
4142413002 | ||
|
|
49e4855982 | ||
|
|
65cad2025a | ||
|
|
64327b3d25 | ||
|
|
54289368b1 | ||
|
|
297c226fab | ||
|
|
7f5b5f487e | ||
|
|
f2a6828015 | ||
|
|
45936805e9 | ||
|
|
0fae0b21ad | ||
|
|
9bd741b1d9 | ||
|
|
c15393981d | ||
|
|
9495d7d2ea | ||
|
|
788a95b260 | ||
|
|
4c697d0008 | ||
|
|
463e66081c | ||
|
|
a7e501570c | ||
|
|
4a76cb083a | ||
|
|
6a767ce5c4 | ||
|
|
0fcd132df4 | ||
|
|
0990cfe072 | ||
|
|
d92bf14b50 | ||
|
|
de8462e81e | ||
|
|
15bd58c746 | ||
|
|
26da18810d | ||
|
|
f80c99576b | ||
|
|
1e5a55917c | ||
|
|
375e43c191 | ||
|
|
feab4d2a51 | ||
|
|
73f0cdde7d | ||
|
|
810c2cbde4 | ||
|
|
a9ff9c3bc7 | ||
|
|
69faa1d493 | ||
|
|
a29b29300e | ||
|
|
56a0d89b88 | ||
|
|
2e49ef38c9 | ||
|
|
8ec5dd70e0 | ||
|
|
89363bfbff | ||
|
|
e45a03729b | ||
|
|
395ef82ad4 | ||
|
|
02f1b11ad6 | ||
|
|
c53fb9266c | ||
|
|
3d1edffd18 | ||
|
|
24ea3199dd | ||
|
|
44116424b0 | ||
|
|
a8868b5f0f | ||
|
|
5172f032e7 | ||
|
|
f8d30bf528 | ||
|
|
ba27ff9c42 | ||
|
|
02878d6a58 | ||
|
|
a676d23a54 | ||
|
|
36f1f778c3 | ||
|
|
f83404421a | ||
|
|
cd8abe5c06 | ||
|
|
471a2397c0 | ||
|
|
a582a3b0ed | ||
|
|
20df70c449 | ||
|
|
af13b6c9f8 | ||
|
|
06ee82af0b | ||
|
|
57b466d80f | ||
|
|
dbd4d152ce | ||
|
|
95f47409d8 | ||
|
|
ed64b72676 | ||
|
|
f9948055e4 | ||
|
|
6ad709f93a | ||
|
|
f9498cf5a2 | ||
|
|
f5333c529c | ||
|
|
6b2cc23cd3 | ||
|
|
34329d04f0 | ||
|
|
701c283e97 | ||
|
|
f9fd589af2 | ||
|
|
0d9602693b | ||
|
|
27be568eb5 | ||
|
|
8f06aea736 | ||
|
|
65191538e2 | ||
|
|
d49485c363 | ||
|
|
1de028840e | ||
|
|
3f9fb68c5f | ||
|
|
33ef15511f | ||
|
|
139c384e97 | ||
|
|
ccb27dbdb9 | ||
|
|
6eb21ffd24 | ||
|
|
b9b3c8f78a | ||
|
|
4f4215577a | ||
|
|
ac0f56325b | ||
|
|
fd8df2a035 | ||
|
|
b4decb8f91 | ||
|
|
836c1e002e | ||
|
|
701701e13c | ||
|
|
ea5c3212d6 | ||
|
|
65705a8a1d | ||
|
|
8310ad9f24 | ||
|
|
1a5c2c4d3a | ||
|
|
cd689afaaa | ||
|
|
00e0eea60e | ||
|
|
d89ba767f1 | ||
|
|
c4acc95b06 | ||
|
|
ed9f792d21 | ||
|
|
31c008afa5 | ||
|
|
a66920c51e | ||
|
|
1c9e2b546b | ||
|
|
2297ff72f5 | ||
|
|
fbaa39a300 | ||
|
|
bd2c15c6bb | ||
|
|
a4d8f2db58 | ||
|
|
2a43255802 | ||
|
|
f3232b2d5c | ||
|
|
c7cb7d1ac4 | ||
|
|
bd5a23ff0d | ||
|
|
dd0c1eb74d | ||
|
|
9950ef16ce | ||
|
|
fed2c034ee | ||
|
|
4944e3c257 | ||
|
|
b865fa33f6 | ||
|
|
bb44ed15da | ||
|
|
93919421ee | ||
|
|
8d1831129b | ||
|
|
5e6e2c037e | ||
|
|
ddfceddc57 | ||
|
|
5b19ffd524 | ||
|
|
7422001ed5 | ||
|
|
3b0ef48cdf | ||
|
|
d13875e3e7 | ||
|
|
6ebc7fab47 | ||
|
|
9d931c1d13 | ||
|
|
f86b6ac3de | ||
|
|
ce1124284e | ||
|
|
233f19d23d | ||
|
|
89ef5fe33d | ||
|
|
7cd4345264 | ||
|
|
bbaa5dafb9 | ||
|
|
cee1b39640 | ||
|
|
e388410e34 | ||
|
|
c2d649d3f4 | ||
|
|
d9dec44c8f | ||
|
|
c94a3a98eb | ||
|
|
ec9b8359df | ||
|
|
3db89e3dee | ||
|
|
7f8dc74146 | ||
|
|
83cea33727 | ||
|
|
ada18a0413 | ||
|
|
96d85dcacd | ||
|
|
64ba033602 | ||
|
|
26807e80db | ||
|
|
e4f00285b3 | ||
|
|
d443e51d30 | ||
|
|
b4352f5f25 | ||
|
|
2c1b4a2547 | ||
|
|
b54685bed7 | ||
|
|
9032d5ac11 | ||
|
|
942747a0be | ||
|
|
fdd6463ac0 | ||
|
|
e9afc4ec0f | ||
|
|
dff811f615 | ||
|
|
d1e382ddf7 | ||
|
|
ccd460cf70 | ||
|
|
7b1ba912be | ||
|
|
f3864c9100 | ||
|
|
1082889b64 | ||
|
|
36839fdcb9 | ||
|
|
ebe54d47d8 | ||
|
|
7219823847 | ||
|
|
336fc7a64a | ||
|
|
84f62f8025 | ||
|
|
6bd6dfec49 | ||
|
|
a6e27f1312 | ||
|
|
c913df837a | ||
|
|
42bd9811e3 | ||
|
|
7fbf13fd9d | ||
|
|
fb086b5ad5 | ||
|
|
d4f51979d4 | ||
|
|
aeb54f59f1 | ||
|
|
5ca3b24527 | ||
|
|
88df2878cb | ||
|
|
f601bbc499 | ||
|
|
24e9ae6440 | ||
|
|
8c3f11622c | ||
|
|
da25ed431f | ||
|
|
ecc234a96a | ||
|
|
29851537eb | ||
|
|
5f2d7b32ae | ||
|
|
de00d49d7b | ||
|
|
bafa0f50fc | ||
|
|
80c75f9aeb | ||
|
|
407eba194d | ||
|
|
2eac3e6555 | ||
|
|
61a308cbc6 | ||
|
|
49b9a6f53d | ||
|
|
db1cc0d0e1 | ||
|
|
f89bd5b972 | ||
|
|
d94a8cb58c | ||
|
|
faf79e85fd | ||
|
|
0d72e88c6a | ||
|
|
786ec7fff1 | ||
|
|
8df497e53d | ||
|
|
1dd7a6ebac | ||
|
|
2c12e9f64b | ||
|
|
9a77135d30 | ||
|
|
f3b3b9b3f0 | ||
|
|
55c4d4d03d | ||
|
|
061d341d8b | ||
|
|
1c29b8b260 | ||
|
|
79bfe9c866 | ||
|
|
3e6d38656d | ||
|
|
5697486cea | ||
|
|
df0da605e4 | ||
|
|
fa9aaf0423 | ||
|
|
89de288fec | ||
|
|
940003552b | ||
|
|
0660296b51 | ||
|
|
affeb0a169 | ||
|
|
2504653426 | ||
|
|
b6a9ad67d3 | ||
|
|
3d4741eac2 | ||
|
|
5611f57e9e | ||
|
|
44027f5bc0 | ||
|
|
41b25a78e9 | ||
|
|
e263f94765 | ||
|
|
80c7754e48 | ||
|
|
165340324c | ||
|
|
735dddd604 | ||
|
|
3e3bd32705 | ||
|
|
f83025a9ff | ||
|
|
ba6a7b58aa | ||
|
|
fb8808ea59 | ||
|
|
73f241e9c3 | ||
|
|
317b1b6ac5 | ||
|
|
cabe629f17 | ||
|
|
9f6521b987 | ||
|
|
6daffe5b13 | ||
|
|
4c7f93d1ef | ||
|
|
f81bbb93a5 | ||
|
|
b0058e94ce | ||
|
|
de069f704a | ||
|
|
bcb8493cd0 | ||
|
|
e55b1740db | ||
|
|
df673b2a4e | ||
|
|
a4753769d2 | ||
|
|
be75a87e88 | ||
|
|
0ac3ae1cb1 | ||
|
|
a5e1e95534 | ||
|
|
7f6ab0b854 | ||
|
|
8420ecd380 | ||
|
|
192fd09a00 | ||
|
|
d9a59c6d1e | ||
|
|
69c3c3162c | ||
|
|
61ba832dfd | ||
|
|
f332bba468 | ||
|
|
ba8cca6ba5 | ||
|
|
7a098952c8 | ||
|
|
347b74a55d | ||
|
|
d5b0adeeed | ||
|
|
d8c4d36d4b | ||
|
|
aac32c5bff | ||
|
|
ff86e55622 | ||
|
|
894c4cb139 | ||
|
|
2792016383 | ||
|
|
46215871aa | ||
|
|
cfc06be975 | ||
|
|
527589ac04 | ||
|
|
43845cda5c | ||
|
|
b74b8a8a5a | ||
|
|
226c6d8432 | ||
|
|
9f79258dec | ||
|
|
13bcc99095 | ||
|
|
fef9b93a05 | ||
|
|
ccac3437cf | ||
|
|
e849a31668 | ||
|
|
88de5412f8 | ||
|
|
d13c48d31d | ||
|
|
4c807866a3 | ||
|
|
d7a4a95c05 | ||
|
|
b35422ff9f | ||
|
|
b6be5d71bb | ||
|
|
4c5eddcf6d | ||
|
|
51f0b75a64 | ||
|
|
fe4648cd9e | ||
|
|
03867ada49 | ||
|
|
7959188c06 | ||
|
|
11eaa0ca50 | ||
|
|
dc6dba416a | ||
|
|
c8e7cc773a | ||
|
|
efe43329a1 | ||
|
|
3f97c17af2 | ||
|
|
91493e8769 | ||
|
|
5f36cb88ab | ||
|
|
a0ef635c92 | ||
|
|
d68904fec0 | ||
|
|
b679581cf2 | ||
|
|
f7e6fa026d | ||
|
|
96f16f1f2e | ||
|
|
ad8fa8722f | ||
|
|
e477f09cd5 | ||
|
|
be5eb9ef70 | ||
|
|
b952642570 | ||
|
|
47cc74a351 | ||
|
|
cff572a104 | ||
|
|
48e16e64c2 | ||
|
|
46172836f1 | ||
|
|
eacb72a05b | ||
|
|
c1fefaab92 | ||
|
|
f2f86457ee | ||
|
|
c251b5831b | ||
|
|
8c589d3000 | ||
|
|
ddc599f6b7 | ||
|
|
6822c3a04b | ||
|
|
aa0c70bd44 | ||
|
|
94f649b345 | ||
|
|
5e07e9dceb | ||
|
|
91a8a8be34 | ||
|
|
99e1890795 | ||
|
|
5583befbba | ||
|
|
c637055859 | ||
|
|
c3acfb8781 | ||
|
|
fd073a7043 | ||
|
|
bed00fbd41 | ||
|
|
501b79fdce | ||
|
|
ed6af8f560 | ||
|
|
32495736d4 | ||
|
|
bff48b0a64 | ||
|
|
f6228240ba | ||
|
|
215c8a7ff4 | ||
|
|
0f217bd753 | ||
|
|
7e920f4bae | ||
|
|
cde3d878b1 | ||
|
|
621a6ea42d | ||
|
|
cfc3615e64 | ||
|
|
2f8d0d90cd | ||
|
|
c827953ca5 | ||
|
|
79dd263fb1 | ||
|
|
1ca05a029a | ||
|
|
4c205eb09d | ||
|
|
ee92f6639a | ||
|
|
7721f16f9f | ||
|
|
854222b8cc | ||
|
|
0bc86541c6 | ||
|
|
c45111333d | ||
|
|
ba6fedc430 | ||
|
|
caa34fa8bb | ||
|
|
fa7c0ab58e | ||
|
|
2543eb6861 | ||
|
|
c4cf3363ee | ||
|
|
98d4efd509 | ||
|
|
a99e65ee48 | ||
|
|
3fd4d24c93 | ||
|
|
55a564a5a8 | ||
|
|
04f6490eeb | ||
|
|
b5026789d6 | ||
|
|
705e570cf5 | ||
|
|
10a41fb0d1 | ||
|
|
2a591646c3 | ||
|
|
e55898b414 | ||
|
|
2df476406d | ||
|
|
4a05b35f2b | ||
|
|
3ad8e56c25 | ||
|
|
c1704758fd | ||
|
|
fe580f2b2b | ||
|
|
d82e482acc | ||
|
|
9d6142dc79 | ||
|
|
2dec83735b | ||
|
|
9f4204b815 | ||
|
|
40f2c972d7 | ||
|
|
eee2385d0d | ||
|
|
492c652157 | ||
|
|
af8e060dc6 | ||
|
|
6a19b9058f | ||
|
|
bcbd21b922 | ||
|
|
aac76d68b0 | ||
|
|
25bb942e04 | ||
|
|
8ac71f21d1 | ||
|
|
287559f028 | ||
|
|
7fbb93cf41 | ||
|
|
7b76e93631 | ||
|
|
896c451c3e | ||
|
|
0874386001 | ||
|
|
ce6808b3c4 | ||
|
|
40fa1c5acb | ||
|
|
7307990f6f | ||
|
|
5104da500e | ||
|
|
3a8c46bbed | ||
|
|
fc5312549d | ||
|
|
c14e8797e2 | ||
|
|
0f26940018 | ||
|
|
26227e2f3b | ||
|
|
dcf78fab06 | ||
|
|
0813592a6d | ||
|
|
abdb27af3f | ||
|
|
b421be3315 | ||
|
|
b74fbee069 | ||
|
|
dab9357b40 | ||
|
|
ccd89604c0 | ||
|
|
cd135b7171 | ||
|
|
7e16d550b0 | ||
|
|
404079ef4e | ||
|
|
d13f78f046 | ||
|
|
60040c3914 | ||
|
|
e408fbd8d6 | ||
|
|
6931ecd468 | ||
|
|
ed5d30ea5b | ||
|
|
ead7613579 | ||
|
|
ca0613ec17 | ||
|
|
727208ff84 | ||
|
|
53c3de2af5 | ||
|
|
3f50d57ed1 | ||
|
|
55f9d0f875 | ||
|
|
37cb9b0fe8 | ||
|
|
a3b5f79094 | ||
|
|
1b1ffa7109 | ||
|
|
2b3021c8fe | ||
|
|
0ccc47786d | ||
|
|
24366b929e | ||
|
|
2cab2a7885 | ||
|
|
4284038c4b | ||
|
|
425d158777 | ||
|
|
1a11b1813f | ||
|
|
85a86d4b06 | ||
|
|
073578243c | ||
|
|
69578f8086 | ||
|
|
ec92f83a58 | ||
|
|
37c0adbbfa | ||
|
|
e9c40692a6 | ||
|
|
32383b0bc2 | ||
|
|
d2f912d15c | ||
|
|
c35c08b17a | ||
|
|
4efd0dda83 | ||
|
|
e7f8bcf83b | ||
|
|
08fa6d55b5 | ||
|
|
995a26b944 | ||
|
|
3a2aad645b | ||
|
|
2c655a08be | ||
|
|
bd359fdd31 | ||
|
|
13216687dc | ||
|
|
1bf4f9902e | ||
|
|
9908ec01aa | ||
|
|
051533bb14 | ||
|
|
3d900bdfe5 | ||
|
|
b32cce1440 | ||
|
|
37aee77eb4 | ||
|
|
72b8ae2a55 | ||
|
|
3e3bed9aa4 | ||
|
|
844a86f997 | ||
|
|
468f17c76c | ||
|
|
68a7a89fd6 | ||
|
|
931551a17f | ||
|
|
7b79384f27 | ||
|
|
69568421f3 | ||
|
|
7fab57193d | ||
|
|
49146e1402 | ||
|
|
f28abd68f4 | ||
|
|
4bb1320563 | ||
|
|
0a848f25a4 | ||
|
|
83dbae3a70 | ||
|
|
ce057bad20 | ||
|
|
8bec0cacdb | ||
|
|
faa990a2c6 | ||
|
|
872e4b4a17 | ||
|
|
3e0c36e714 | ||
|
|
974eaeb02f | ||
|
|
f833904d7b | ||
|
|
96ba7d0656 | ||
|
|
6852319e4d | ||
|
|
8bc1eaebc0 | ||
|
|
eeefaa6374 | ||
|
|
1bba1cfeb1 | ||
|
|
fb6aec0afe | ||
|
|
7063d9e113 | ||
|
|
0445c150a2 | ||
|
|
cfb1e1d908 | ||
|
|
3f80dbc5ce | ||
|
|
b8a48314c1 | ||
|
|
0090d36514 | ||
|
|
f4b9301f55 | ||
|
|
60996a99f0 | ||
|
|
1eb52d8a35 | ||
|
|
8fee195577 | ||
|
|
89d805b500 | ||
|
|
572f6bdbac | ||
|
|
42a222338b |
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
- name: Install ${{ steps.gettoolchain.outputs.toolchain }} Rust toolchain
|
||||
# Latest version available on this commit is 1.71.1
|
||||
# Commit date is Aug 3, 2023
|
||||
uses: dtolnay/rust-toolchain@dc6353516c68da0f06325f42ad880f76a5e77ec9
|
||||
uses: dtolnay/rust-toolchain@d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a
|
||||
with:
|
||||
toolchain: ${{ steps.gettoolchain.outputs.toolchain }}
|
||||
|
||||
@@ -251,7 +251,6 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# module: ["mempool", "liquid", "bisq"] Disabling bisq support for now
|
||||
module: ["mempool", "liquid"]
|
||||
include:
|
||||
- module: "mempool"
|
||||
@@ -263,9 +262,6 @@ jobs:
|
||||
spec: |
|
||||
cypress/e2e/liquid/liquid.spec.ts
|
||||
cypress/e2e/liquidtestnet/liquidtestnet.spec.ts
|
||||
# - module: "bisq"
|
||||
# spec: |
|
||||
# cypress/e2e/bisq/bisq.spec.ts
|
||||
|
||||
name: E2E tests for ${{ matrix.module }}
|
||||
steps:
|
||||
|
||||
2
.github/workflows/on-tag.yml
vendored
2
.github/workflows/on-tag.yml
vendored
@@ -101,5 +101,7 @@ jobs:
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:$TAG \
|
||||
--tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:latest \
|
||||
--build-context rustgbt=./rust \
|
||||
--build-context backend=./backend \
|
||||
--output "type=registry" ./${{ matrix.service }}/ \
|
||||
--build-arg commitHash=$SHORT_SHA
|
||||
|
||||
@@ -103,7 +103,7 @@ In particular, make sure:
|
||||
- the correct Bitcoin Core RPC credentials are specified in `CORE_RPC`
|
||||
- the correct `BACKEND` is specified in `MEMPOOL`:
|
||||
- "electrum" if you're using [romanz/electrs](https://github.com/romanz/electrs) or [cculianu/Fulcrum](https://github.com/cculianu/Fulcrum)
|
||||
- "esplora" if you're using [Blockstream/electrs](https://github.com/Blockstream/electrs)
|
||||
- "esplora" if you're using [mempool/electrs](https://github.com/mempool/electrs)
|
||||
- "none" if you're not using any Electrum Server
|
||||
|
||||
### 6. Run Mempool Backend
|
||||
|
||||
@@ -35,7 +35,8 @@
|
||||
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
|
||||
"ALLOW_UNREACHABLE": true,
|
||||
"PRICE_UPDATES_PER_HOUR": 1,
|
||||
"MAX_TRACKED_ADDRESSES": 100
|
||||
"MAX_TRACKED_ADDRESSES": 100,
|
||||
"UNIX_SOCKET_PATH": ""
|
||||
},
|
||||
"CORE_RPC": {
|
||||
"HOST": "127.0.0.1",
|
||||
@@ -97,10 +98,6 @@
|
||||
"GEOLITE2_ASN": "/usr/local/share/GeoIP/GeoLite2-ASN.mmdb",
|
||||
"GEOIP2_ISP": "/usr/local/share/GeoIP/GeoIP2-ISP.mmdb"
|
||||
},
|
||||
"BISQ": {
|
||||
"ENABLED": false,
|
||||
"DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db"
|
||||
},
|
||||
"LIGHTNING": {
|
||||
"ENABLED": false,
|
||||
"BACKEND": "lnd",
|
||||
@@ -131,9 +128,7 @@
|
||||
"MEMPOOL_API": "https://mempool.space/api/v1",
|
||||
"MEMPOOL_ONION": "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1",
|
||||
"LIQUID_API": "https://liquid.network/api/v1",
|
||||
"LIQUID_ONION": "http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1",
|
||||
"BISQ_URL": "https://bisq.markets/api",
|
||||
"BISQ_ONION": "http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api"
|
||||
"LIQUID_ONION": "http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1"
|
||||
},
|
||||
"REDIS": {
|
||||
"ENABLED": false,
|
||||
@@ -157,6 +152,7 @@
|
||||
},
|
||||
"FIAT_PRICE": {
|
||||
"ENABLED": true,
|
||||
"PAID": false,
|
||||
"API_KEY": "your-api-key-from-freecurrencyapi.com"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ set -e
|
||||
|
||||
# Cleaning up inside the node_modules folder
|
||||
cd package/node_modules
|
||||
rm -r \
|
||||
rm -rf \
|
||||
typescript \
|
||||
@typescript-eslint \
|
||||
@napi-rs
|
||||
|
||||
42
backend/package-lock.json
generated
42
backend/package-lock.json
generated
@@ -18,12 +18,12 @@
|
||||
"crypto-js": "~4.2.0",
|
||||
"express": "~4.19.2",
|
||||
"maxmind": "~4.3.11",
|
||||
"mysql2": "~3.9.1",
|
||||
"mysql2": "~3.9.7",
|
||||
"redis": "^4.6.6",
|
||||
"rust-gbt": "file:./rust-gbt",
|
||||
"socks-proxy-agent": "~7.0.0",
|
||||
"typescript": "~4.9.3",
|
||||
"ws": "~8.13.0"
|
||||
"ws": "~8.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
@@ -32,7 +32,7 @@
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/ws": "~8.5.5",
|
||||
"@types/ws": "~8.5.10",
|
||||
"@typescript-eslint/eslint-plugin": "^5.55.0",
|
||||
"@typescript-eslint/parser": "^5.55.0",
|
||||
"eslint": "^8.36.0",
|
||||
@@ -1858,9 +1858,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz",
|
||||
"integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==",
|
||||
"version": "8.5.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz",
|
||||
"integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
@@ -6197,9 +6197,9 @@
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/mysql2": {
|
||||
"version": "3.9.1",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.1.tgz",
|
||||
"integrity": "sha512-3njoWAAhGBYy0tWBabqUQcLtczZUxrmmtc2vszQUekg3kTJyZ5/IeLC3Fo04u6y6Iy5Sba7pIIa2P/gs8D3ZeQ==",
|
||||
"version": "3.9.7",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.7.tgz",
|
||||
"integrity": "sha512-KnJT8vYRcNAZv73uf9zpXqNbvBG7DJrs+1nACsjZP1HMJ1TgXEy8wnNilXAn/5i57JizXKtrUtwDB7HxT9DDpw==",
|
||||
"dependencies": {
|
||||
"denque": "^2.1.0",
|
||||
"generate-function": "^2.3.1",
|
||||
@@ -7690,9 +7690,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.13.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
|
||||
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
|
||||
"version": "8.17.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
|
||||
"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
@@ -9198,9 +9198,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@types/ws": {
|
||||
"version": "8.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz",
|
||||
"integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==",
|
||||
"version": "8.5.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz",
|
||||
"integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
@@ -12382,9 +12382,9 @@
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"mysql2": {
|
||||
"version": "3.9.1",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.1.tgz",
|
||||
"integrity": "sha512-3njoWAAhGBYy0tWBabqUQcLtczZUxrmmtc2vszQUekg3kTJyZ5/IeLC3Fo04u6y6Iy5Sba7pIIa2P/gs8D3ZeQ==",
|
||||
"version": "3.9.7",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.7.tgz",
|
||||
"integrity": "sha512-KnJT8vYRcNAZv73uf9zpXqNbvBG7DJrs+1nACsjZP1HMJ1TgXEy8wnNilXAn/5i57JizXKtrUtwDB7HxT9DDpw==",
|
||||
"requires": {
|
||||
"denque": "^2.1.0",
|
||||
"generate-function": "^2.3.1",
|
||||
@@ -13424,9 +13424,9 @@
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.13.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
|
||||
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
|
||||
"version": "8.17.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
|
||||
"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
|
||||
"requires": {}
|
||||
},
|
||||
"y18n": {
|
||||
|
||||
@@ -47,12 +47,12 @@
|
||||
"crypto-js": "~4.2.0",
|
||||
"express": "~4.19.2",
|
||||
"maxmind": "~4.3.11",
|
||||
"mysql2": "~3.9.1",
|
||||
"mysql2": "~3.9.7",
|
||||
"rust-gbt": "file:./rust-gbt",
|
||||
"redis": "^4.6.6",
|
||||
"socks-proxy-agent": "~7.0.0",
|
||||
"typescript": "~4.9.3",
|
||||
"ws": "~8.13.0"
|
||||
"ws": "~8.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
@@ -61,7 +61,7 @@
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/ws": "~8.5.5",
|
||||
"@types/ws": "~8.5.10",
|
||||
"@typescript-eslint/eslint-plugin": "^5.55.0",
|
||||
"@typescript-eslint/parser": "^5.55.0",
|
||||
"eslint": "^8.36.0",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"BLOCKS_SUMMARIES_INDEXING": true,
|
||||
"GOGGLES_INDEXING": false,
|
||||
"HTTP_PORT": 1,
|
||||
"UNIX_SOCKET_PATH": "/mempool/socket/mempool-bitcoin-mainnet",
|
||||
"SPAWN_CLUSTER_PROCS": 2,
|
||||
"API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
|
||||
"AUTOMATIC_BLOCK_REINDEXING": false,
|
||||
@@ -93,10 +94,6 @@
|
||||
"ENABLED": false,
|
||||
"TX_PER_SECOND_SAMPLE_PERIOD": 20
|
||||
},
|
||||
"BISQ": {
|
||||
"ENABLED": true,
|
||||
"DATA_PATH": "__BISQ_DATA_PATH__"
|
||||
},
|
||||
"SOCKS5PROXY": {
|
||||
"ENABLED": true,
|
||||
"USE_ONION": true,
|
||||
@@ -109,9 +106,7 @@
|
||||
"MEMPOOL_API": "__EXTERNAL_DATA_SERVER_MEMPOOL_API__",
|
||||
"MEMPOOL_ONION": "__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__",
|
||||
"LIQUID_API": "__EXTERNAL_DATA_SERVER_LIQUID_API__",
|
||||
"LIQUID_ONION": "__EXTERNAL_DATA_SERVER_LIQUID_ONION__",
|
||||
"BISQ_URL": "__EXTERNAL_DATA_SERVER_BISQ_URL__",
|
||||
"BISQ_ONION": "__EXTERNAL_DATA_SERVER_BISQ_ONION__"
|
||||
"LIQUID_ONION": "__EXTERNAL_DATA_SERVER_LIQUID_ONION__"
|
||||
},
|
||||
"LIGHTNING": {
|
||||
"ENABLED": true,
|
||||
@@ -149,6 +144,7 @@
|
||||
},
|
||||
"FIAT_PRICE": {
|
||||
"ENABLED": true,
|
||||
"PAID": false,
|
||||
"API_KEY": "__MEMPOOL_CURRENCY_API_KEY__"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ describe('Mempool Backend Config', () => {
|
||||
BLOCKS_SUMMARIES_INDEXING: false,
|
||||
GOGGLES_INDEXING: false,
|
||||
HTTP_PORT: 8999,
|
||||
UNIX_SOCKET_PATH: '',
|
||||
SPAWN_CLUSTER_PROCS: 0,
|
||||
API_URL_PREFIX: '/api/v1/',
|
||||
AUTOMATIC_BLOCK_REINDEXING: false,
|
||||
@@ -107,8 +108,6 @@ describe('Mempool Backend Config', () => {
|
||||
|
||||
expect(config.STATISTICS).toStrictEqual({ ENABLED: true, TX_PER_SECOND_SAMPLE_PERIOD: 150 });
|
||||
|
||||
expect(config.BISQ).toStrictEqual({ ENABLED: false, DATA_PATH: '/bisq/statsnode-data/btc_mainnet/db' });
|
||||
|
||||
expect(config.SOCKS5PROXY).toStrictEqual({
|
||||
ENABLED: false,
|
||||
USE_ONION: true,
|
||||
@@ -122,9 +121,7 @@ describe('Mempool Backend Config', () => {
|
||||
MEMPOOL_API: 'https://mempool.space/api/v1',
|
||||
MEMPOOL_ONION: 'http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1',
|
||||
LIQUID_API: 'https://liquid.network/api/v1',
|
||||
LIQUID_ONION: 'http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1',
|
||||
BISQ_URL: 'https://bisq.markets/api',
|
||||
BISQ_ONION: 'http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api'
|
||||
LIQUID_ONION: 'http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1'
|
||||
});
|
||||
|
||||
expect(config.MAXMIND).toStrictEqual({
|
||||
@@ -154,6 +151,7 @@ describe('Mempool Backend Config', () => {
|
||||
|
||||
expect(config.FIAT_PRICE).toStrictEqual({
|
||||
ENABLED: true,
|
||||
PAID: false,
|
||||
API_KEY: '',
|
||||
});
|
||||
});
|
||||
@@ -182,8 +180,6 @@ describe('Mempool Backend Config', () => {
|
||||
|
||||
expect(config.STATISTICS).toStrictEqual(fixture.STATISTICS);
|
||||
|
||||
expect(config.BISQ).toStrictEqual(fixture.BISQ);
|
||||
|
||||
expect(config.SOCKS5PROXY).toStrictEqual(fixture.SOCKS5PROXY);
|
||||
|
||||
expect(config.EXTERNAL_DATA_SERVER).toStrictEqual(fixture.EXTERNAL_DATA_SERVER);
|
||||
|
||||
87
backend/src/api/about.routes.ts
Normal file
87
backend/src/api/about.routes.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { Application } from "express";
|
||||
import config from "../config";
|
||||
import axios from "axios";
|
||||
import logger from "../logger";
|
||||
|
||||
class AboutRoutes {
|
||||
public initRoutes(app: Application) {
|
||||
app
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations`, { responseType: 'stream', timeout: 10000 });
|
||||
response.data.pipe(res);
|
||||
} catch (e) {
|
||||
res.status(500).end();
|
||||
}
|
||||
})
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'donations/images/:id', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations/images/${req.params.id}`, {
|
||||
responseType: 'stream', timeout: 10000
|
||||
});
|
||||
response.data.pipe(res);
|
||||
} catch (e) {
|
||||
res.status(500).end();
|
||||
}
|
||||
})
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'contributors', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors`, { responseType: 'stream', timeout: 10000 });
|
||||
response.data.pipe(res);
|
||||
} catch (e) {
|
||||
res.status(500).end();
|
||||
}
|
||||
})
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'contributors/images/:id', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors/images/${req.params.id}`, {
|
||||
responseType: 'stream', timeout: 10000
|
||||
});
|
||||
response.data.pipe(res);
|
||||
} catch (e) {
|
||||
res.status(500).end();
|
||||
}
|
||||
})
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'translators', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators`, { responseType: 'stream', timeout: 10000 });
|
||||
response.data.pipe(res);
|
||||
} catch (e) {
|
||||
res.status(500).end();
|
||||
}
|
||||
})
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'translators/images/:id', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators/images/${req.params.id}`, {
|
||||
responseType: 'stream', timeout: 10000
|
||||
});
|
||||
response.data.pipe(res);
|
||||
} catch (e) {
|
||||
res.status(500).end();
|
||||
}
|
||||
})
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'services/sponsors', async (req, res) => {
|
||||
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
|
||||
try {
|
||||
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });
|
||||
response.data.pipe(res);
|
||||
} catch (e) {
|
||||
logger.err(`Unable to fetch sponsors from ${url}. ${e}`, 'About Page');
|
||||
res.status(500).end();
|
||||
}
|
||||
})
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'services/account/images/:username', async (req, res) => {
|
||||
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
|
||||
try {
|
||||
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });
|
||||
response.data.pipe(res);
|
||||
} catch (e) {
|
||||
logger.err(`Unable to fetch sponsor profile image from ${url}. ${e}`, 'About Page');
|
||||
res.status(500).end();
|
||||
}
|
||||
})
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
export default new AboutRoutes();
|
||||
69
backend/src/api/acceleration/acceleration.routes.ts
Normal file
69
backend/src/api/acceleration/acceleration.routes.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { Application, Request, Response } from 'express';
|
||||
import config from '../../config';
|
||||
import axios from 'axios';
|
||||
import logger from '../../logger';
|
||||
import mempool from '../mempool';
|
||||
import AccelerationRepository from '../../repositories/AccelerationRepository';
|
||||
|
||||
class AccelerationRoutes {
|
||||
private tag = 'Accelerator';
|
||||
|
||||
public initRoutes(app: Application): void {
|
||||
app
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations', this.$getAcceleratorAccelerations.bind(this))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/history', this.$getAcceleratorAccelerationsHistory.bind(this))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/history/aggregated', this.$getAcceleratorAccelerationsHistoryAggregated.bind(this))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/stats', this.$getAcceleratorAccelerationsStats.bind(this))
|
||||
;
|
||||
}
|
||||
|
||||
private async $getAcceleratorAccelerations(req: Request, res: Response): Promise<void> {
|
||||
const accelerations = mempool.getAccelerations();
|
||||
res.status(200).send(Object.values(accelerations));
|
||||
}
|
||||
|
||||
private async $getAcceleratorAccelerationsHistory(req: Request, res: Response): Promise<void> {
|
||||
const history = await AccelerationRepository.$getAccelerationInfo(null, req.query.blockHeight ? parseInt(req.query.blockHeight as string, 10) : null);
|
||||
res.status(200).send(history.map(accel => ({
|
||||
txid: accel.txid,
|
||||
added: accel.added,
|
||||
status: 'completed',
|
||||
effectiveFee: accel.effective_fee,
|
||||
effectiveVsize: accel.effective_vsize,
|
||||
boostRate: accel.boost_rate,
|
||||
boostCost: accel.boost_cost,
|
||||
blockHeight: accel.height,
|
||||
pools: [accel.pool],
|
||||
})));
|
||||
}
|
||||
|
||||
private async $getAcceleratorAccelerationsHistoryAggregated(req: Request, res: Response): Promise<void> {
|
||||
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
|
||||
try {
|
||||
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });
|
||||
for (const key in response.headers) {
|
||||
res.setHeader(key, response.headers[key]);
|
||||
}
|
||||
response.data.pipe(res);
|
||||
} catch (e) {
|
||||
logger.err(`Unable to get aggregated acceleration history from ${url} in $getAcceleratorAccelerationsHistoryAggregated(), ${e}`, this.tag);
|
||||
res.status(500).end();
|
||||
}
|
||||
}
|
||||
|
||||
private async $getAcceleratorAccelerationsStats(req: Request, res: Response): Promise<void> {
|
||||
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
|
||||
try {
|
||||
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });
|
||||
for (const key in response.headers) {
|
||||
res.setHeader(key, response.headers[key]);
|
||||
}
|
||||
response.data.pipe(res);
|
||||
} catch (e) {
|
||||
logger.err(`Unable to get acceleration stats from ${url} in $getAcceleratorAccelerationsStats(), ${e}`, this.tag);
|
||||
res.status(500).end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new AccelerationRoutes();
|
||||
@@ -1,6 +1,6 @@
|
||||
import logger from '../logger';
|
||||
import { MempoolTransactionExtended } from '../mempool.interfaces';
|
||||
import { IEsploraApi } from './bitcoin/esplora-api.interface';
|
||||
import logger from '../../logger';
|
||||
import { MempoolTransactionExtended } from '../../mempool.interfaces';
|
||||
import { IEsploraApi } from '../bitcoin/esplora-api.interface';
|
||||
|
||||
const BLOCK_WEIGHT_UNITS = 4_000_000;
|
||||
const BLOCK_SIGOPS = 80_000;
|
||||
@@ -75,10 +75,6 @@ class Audit {
|
||||
let failures = 0;
|
||||
let blockIndex = 1;
|
||||
while (projectedBlocks[blockIndex] && failures < 500) {
|
||||
if (index >= projectedBlocks[blockIndex].transactionIds.length) {
|
||||
index = 0;
|
||||
blockIndex++;
|
||||
}
|
||||
const txid = projectedBlocks[blockIndex].transactionIds[index];
|
||||
const tx = mempool[txid];
|
||||
if (tx) {
|
||||
@@ -102,6 +98,10 @@ class Audit {
|
||||
logger.warn('projected transaction missing from mempool cache');
|
||||
}
|
||||
index++;
|
||||
if (index >= projectedBlocks[blockIndex].transactionIds.length) {
|
||||
index = 0;
|
||||
blockIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
// mark unexpected transactions in the mined block as 'added'
|
||||
|
||||
@@ -1,381 +0,0 @@
|
||||
import { Application, Request, Response } from 'express';
|
||||
import config from '../../config';
|
||||
import { RequiredSpec } from '../../mempool.interfaces';
|
||||
import bisq from './bisq';
|
||||
import { MarketsApiError } from './interfaces';
|
||||
import marketsApi from './markets-api';
|
||||
|
||||
class BisqRoutes {
|
||||
public initRoutes(app: Application) {
|
||||
app
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/stats', this.getBisqStats)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/tx/:txId', this.getBisqTransaction)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/block/:hash', this.getBisqBlock)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/blocks/tip/height', this.getBisqTip)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/blocks/:index/:length', this.getBisqBlocks)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/address/:address', this.getBisqAddress)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/txs/:index/:length', this.getBisqTransactions)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/currencies', this.getBisqMarketCurrencies.bind(this))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/depth', this.getBisqMarketDepth.bind(this))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/hloc', this.getBisqMarketHloc.bind(this))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/markets', this.getBisqMarketMarkets.bind(this))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/offers', this.getBisqMarketOffers.bind(this))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/ticker', this.getBisqMarketTicker.bind(this))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/trades', this.getBisqMarketTrades.bind(this))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/volumes', this.getBisqMarketVolumes.bind(this))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/volumes/7d', this.getBisqMarketVolumes7d.bind(this))
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
private getBisqStats(req: Request, res: Response) {
|
||||
const result = bisq.getStats();
|
||||
res.json(result);
|
||||
}
|
||||
|
||||
private getBisqTip(req: Request, res: Response) {
|
||||
const result = bisq.getLatestBlockHeight();
|
||||
res.type('text/plain');
|
||||
res.send(result.toString());
|
||||
}
|
||||
|
||||
private getBisqTransaction(req: Request, res: Response) {
|
||||
const result = bisq.getTransaction(req.params.txId);
|
||||
if (result) {
|
||||
res.json(result);
|
||||
} else {
|
||||
res.status(404).send('Bisq transaction not found');
|
||||
}
|
||||
}
|
||||
|
||||
private getBisqTransactions(req: Request, res: Response) {
|
||||
const types: string[] = [];
|
||||
req.query.types = req.query.types || [];
|
||||
if (!Array.isArray(req.query.types)) {
|
||||
res.status(500).send('Types is not an array');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const _type in req.query.types) {
|
||||
if (typeof req.query.types[_type] === 'string') {
|
||||
types.push(req.query.types[_type].toString());
|
||||
}
|
||||
}
|
||||
|
||||
const index = parseInt(req.params.index, 10) || 0;
|
||||
const length = parseInt(req.params.length, 10) > 100 ? 100 : parseInt(req.params.length, 10) || 25;
|
||||
const [transactions, count] = bisq.getTransactions(index, length, types);
|
||||
res.header('X-Total-Count', count.toString());
|
||||
res.json(transactions);
|
||||
}
|
||||
|
||||
private getBisqBlock(req: Request, res: Response) {
|
||||
const result = bisq.getBlock(req.params.hash);
|
||||
if (result) {
|
||||
res.json(result);
|
||||
} else {
|
||||
res.status(404).send('Bisq block not found');
|
||||
}
|
||||
}
|
||||
|
||||
private getBisqBlocks(req: Request, res: Response) {
|
||||
const index = parseInt(req.params.index, 10) || 0;
|
||||
const length = parseInt(req.params.length, 10) > 100 ? 100 : parseInt(req.params.length, 10) || 25;
|
||||
const [transactions, count] = bisq.getBlocks(index, length);
|
||||
res.header('X-Total-Count', count.toString());
|
||||
res.json(transactions);
|
||||
}
|
||||
|
||||
private getBisqAddress(req: Request, res: Response) {
|
||||
const result = bisq.getAddress(req.params.address.substr(1));
|
||||
if (result) {
|
||||
res.json(result);
|
||||
} else {
|
||||
res.status(404).send('Bisq address not found');
|
||||
}
|
||||
}
|
||||
|
||||
private getBisqMarketCurrencies(req: Request, res: Response) {
|
||||
const constraints: RequiredSpec = {
|
||||
'type': {
|
||||
required: false,
|
||||
types: ['crypto', 'fiat', 'all']
|
||||
},
|
||||
};
|
||||
|
||||
const p = this.parseRequestParameters(req.query, constraints);
|
||||
if (p.error) {
|
||||
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
|
||||
return;
|
||||
}
|
||||
|
||||
const result = marketsApi.getCurrencies(p.type);
|
||||
if (result) {
|
||||
res.json(result);
|
||||
} else {
|
||||
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketCurrencies error'));
|
||||
}
|
||||
}
|
||||
|
||||
private getBisqMarketDepth(req: Request, res: Response) {
|
||||
const constraints: RequiredSpec = {
|
||||
'market': {
|
||||
required: true,
|
||||
types: ['@string']
|
||||
},
|
||||
};
|
||||
|
||||
const p = this.parseRequestParameters(req.query, constraints);
|
||||
if (p.error) {
|
||||
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
|
||||
return;
|
||||
}
|
||||
|
||||
const result = marketsApi.getDepth(p.market);
|
||||
if (result) {
|
||||
res.json(result);
|
||||
} else {
|
||||
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketDepth error'));
|
||||
}
|
||||
}
|
||||
|
||||
private getBisqMarketMarkets(req: Request, res: Response) {
|
||||
const result = marketsApi.getMarkets();
|
||||
if (result) {
|
||||
res.json(result);
|
||||
} else {
|
||||
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketMarkets error'));
|
||||
}
|
||||
}
|
||||
|
||||
private getBisqMarketTrades(req: Request, res: Response) {
|
||||
const constraints: RequiredSpec = {
|
||||
'market': {
|
||||
required: true,
|
||||
types: ['@string']
|
||||
},
|
||||
'timestamp_from': {
|
||||
required: false,
|
||||
types: ['@number']
|
||||
},
|
||||
'timestamp_to': {
|
||||
required: false,
|
||||
types: ['@number']
|
||||
},
|
||||
'trade_id_to': {
|
||||
required: false,
|
||||
types: ['@string']
|
||||
},
|
||||
'trade_id_from': {
|
||||
required: false,
|
||||
types: ['@string']
|
||||
},
|
||||
'direction': {
|
||||
required: false,
|
||||
types: ['buy', 'sell']
|
||||
},
|
||||
'limit': {
|
||||
required: false,
|
||||
types: ['@number']
|
||||
},
|
||||
'sort': {
|
||||
required: false,
|
||||
types: ['asc', 'desc']
|
||||
}
|
||||
};
|
||||
|
||||
const p = this.parseRequestParameters(req.query, constraints);
|
||||
if (p.error) {
|
||||
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
|
||||
return;
|
||||
}
|
||||
|
||||
const result = marketsApi.getTrades(p.market, p.timestamp_from,
|
||||
p.timestamp_to, p.trade_id_from, p.trade_id_to, p.direction, p.limit, p.sort);
|
||||
if (result) {
|
||||
res.json(result);
|
||||
} else {
|
||||
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketTrades error'));
|
||||
}
|
||||
}
|
||||
|
||||
private getBisqMarketOffers(req: Request, res: Response) {
|
||||
const constraints: RequiredSpec = {
|
||||
'market': {
|
||||
required: true,
|
||||
types: ['@string']
|
||||
},
|
||||
'direction': {
|
||||
required: false,
|
||||
types: ['buy', 'sell']
|
||||
},
|
||||
};
|
||||
|
||||
const p = this.parseRequestParameters(req.query, constraints);
|
||||
if (p.error) {
|
||||
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
|
||||
return;
|
||||
}
|
||||
|
||||
const result = marketsApi.getOffers(p.market, p.direction);
|
||||
if (result) {
|
||||
res.json(result);
|
||||
} else {
|
||||
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketOffers error'));
|
||||
}
|
||||
}
|
||||
|
||||
private getBisqMarketVolumes(req: Request, res: Response) {
|
||||
const constraints: RequiredSpec = {
|
||||
'market': {
|
||||
required: false,
|
||||
types: ['@string']
|
||||
},
|
||||
'interval': {
|
||||
required: false,
|
||||
types: ['minute', 'half_hour', 'hour', 'half_day', 'day', 'week', 'month', 'year', 'auto']
|
||||
},
|
||||
'timestamp_from': {
|
||||
required: false,
|
||||
types: ['@number']
|
||||
},
|
||||
'timestamp_to': {
|
||||
required: false,
|
||||
types: ['@number']
|
||||
},
|
||||
'milliseconds': {
|
||||
required: false,
|
||||
types: ['@boolean']
|
||||
},
|
||||
'timestamp': {
|
||||
required: false,
|
||||
types: ['no', 'yes']
|
||||
},
|
||||
};
|
||||
|
||||
const p = this.parseRequestParameters(req.query, constraints);
|
||||
if (p.error) {
|
||||
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
|
||||
return;
|
||||
}
|
||||
|
||||
const result = marketsApi.getVolumes(p.market, p.timestamp_from, p.timestamp_to, p.interval, p.milliseconds, p.timestamp);
|
||||
if (result) {
|
||||
res.json(result);
|
||||
} else {
|
||||
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketVolumes error'));
|
||||
}
|
||||
}
|
||||
|
||||
private getBisqMarketHloc(req: Request, res: Response) {
|
||||
const constraints: RequiredSpec = {
|
||||
'market': {
|
||||
required: true,
|
||||
types: ['@string']
|
||||
},
|
||||
'interval': {
|
||||
required: false,
|
||||
types: ['minute', 'half_hour', 'hour', 'half_day', 'day', 'week', 'month', 'year', 'auto']
|
||||
},
|
||||
'timestamp_from': {
|
||||
required: false,
|
||||
types: ['@number']
|
||||
},
|
||||
'timestamp_to': {
|
||||
required: false,
|
||||
types: ['@number']
|
||||
},
|
||||
'milliseconds': {
|
||||
required: false,
|
||||
types: ['@boolean']
|
||||
},
|
||||
'timestamp': {
|
||||
required: false,
|
||||
types: ['no', 'yes']
|
||||
},
|
||||
};
|
||||
|
||||
const p = this.parseRequestParameters(req.query, constraints);
|
||||
if (p.error) {
|
||||
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
|
||||
return;
|
||||
}
|
||||
|
||||
const result = marketsApi.getHloc(p.market, p.interval, p.timestamp_from, p.timestamp_to, p.milliseconds, p.timestamp);
|
||||
if (result) {
|
||||
res.json(result);
|
||||
} else {
|
||||
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketHloc error'));
|
||||
}
|
||||
}
|
||||
|
||||
private getBisqMarketTicker(req: Request, res: Response) {
|
||||
const constraints: RequiredSpec = {
|
||||
'market': {
|
||||
required: false,
|
||||
types: ['@string']
|
||||
},
|
||||
};
|
||||
|
||||
const p = this.parseRequestParameters(req.query, constraints);
|
||||
if (p.error) {
|
||||
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
|
||||
return;
|
||||
}
|
||||
|
||||
const result = marketsApi.getTicker(p.market);
|
||||
if (result) {
|
||||
res.json(result);
|
||||
} else {
|
||||
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketTicker error'));
|
||||
}
|
||||
}
|
||||
|
||||
private getBisqMarketVolumes7d(req: Request, res: Response) {
|
||||
const result = marketsApi.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) {
|
||||
if (params.hasOwnProperty(i)) {
|
||||
if (params[i].required && requestParams[i] === undefined) {
|
||||
return { error: i + ' parameter missing'};
|
||||
}
|
||||
if (typeof requestParams[i] === 'string') {
|
||||
const str = (requestParams[i] || '').toString().toLowerCase();
|
||||
if (params[i].types.indexOf('@number') > -1) {
|
||||
const number = parseInt((str).toString(), 10);
|
||||
final[i] = number;
|
||||
} else if (params[i].types.indexOf('@string') > -1) {
|
||||
final[i] = str;
|
||||
} else if (params[i].types.indexOf('@boolean') > -1) {
|
||||
final[i] = str === 'true' || str === 'yes';
|
||||
} else if (params[i].types.indexOf(str) > -1) {
|
||||
final[i] = str;
|
||||
} else {
|
||||
return { error: i + ' parameter invalid'};
|
||||
}
|
||||
} else if (typeof requestParams[i] === 'number') {
|
||||
final[i] = requestParams[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
return final;
|
||||
}
|
||||
|
||||
private getBisqMarketErrorResponse(message: string): MarketsApiError {
|
||||
return {
|
||||
'success': 0,
|
||||
'error': message
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default new BisqRoutes;
|
||||
@@ -1,359 +0,0 @@
|
||||
import config from '../../config';
|
||||
import * as fs from 'fs';
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import { SocksProxyAgent } from 'socks-proxy-agent';
|
||||
import { BisqBlocks, BisqBlock, BisqTransaction, BisqStats, BisqTrade } from './interfaces';
|
||||
import { Common } from '../common';
|
||||
import { BlockExtended } from '../../mempool.interfaces';
|
||||
import backendInfo from '../backend-info';
|
||||
import logger from '../../logger';
|
||||
|
||||
class Bisq {
|
||||
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 } = {};
|
||||
private addressIndex: { [address: string]: BisqTransaction[] } = {};
|
||||
private stats: BisqStats = {
|
||||
minted: 0,
|
||||
burnt: 0,
|
||||
addresses: 0,
|
||||
unspent_txos: 0,
|
||||
spent_txos: 0,
|
||||
};
|
||||
private price: number = 0;
|
||||
private priceUpdateCallbackFunction: ((price: number) => void) | undefined;
|
||||
private topDirectoryWatcher: fs.FSWatcher | undefined;
|
||||
private subdirectoryWatcher: fs.FSWatcher | undefined;
|
||||
|
||||
constructor() {}
|
||||
|
||||
startBisqService(): void {
|
||||
try {
|
||||
this.checkForBisqDataFolder();
|
||||
} catch (e) {
|
||||
logger.info('Retrying to start bisq service in 3 minutes');
|
||||
setTimeout(this.startBisqService.bind(this), 180000);
|
||||
return;
|
||||
}
|
||||
this.loadBisqDumpFile();
|
||||
setInterval(this.updatePrice.bind(this), 1000 * 60 * 60);
|
||||
this.updatePrice();
|
||||
this.startTopDirectoryWatcher();
|
||||
this.startSubDirectoryWatcher();
|
||||
}
|
||||
|
||||
handleNewBitcoinBlock(block: BlockExtended): void {
|
||||
if (block.height - 10 > this.latestBlockHeight && this.latestBlockHeight !== 0) {
|
||||
logger.warn(`Bitcoin block height (#${block.height}) has diverged from the latest Bisq block height (#${this.latestBlockHeight}). Restarting watchers...`);
|
||||
this.startTopDirectoryWatcher();
|
||||
this.startSubDirectoryWatcher();
|
||||
}
|
||||
}
|
||||
|
||||
getTransaction(txId: string): BisqTransaction | undefined {
|
||||
return this.transactionIndex[txId];
|
||||
}
|
||||
|
||||
getTransactions(start: number, length: number, types: string[]): [BisqTransaction[], number] {
|
||||
let transactions = this.transactions;
|
||||
if (types.length) {
|
||||
transactions = transactions.filter((tx) => types.indexOf(tx.txType) > -1);
|
||||
}
|
||||
return [transactions.slice(start, length + start), transactions.length];
|
||||
}
|
||||
|
||||
getBlock(hash: string): BisqBlock | undefined {
|
||||
return this.blockIndex[hash];
|
||||
}
|
||||
|
||||
getAddress(hash: string): BisqTransaction[] {
|
||||
return this.addressIndex[hash];
|
||||
}
|
||||
|
||||
getBlocks(start: number, length: number): [BisqBlock[], number] {
|
||||
return [this.blocks.slice(start, length + start), this.blocks.length];
|
||||
}
|
||||
|
||||
getStats(): BisqStats {
|
||||
return this.stats;
|
||||
}
|
||||
|
||||
setPriceCallbackFunction(fn: (price: number) => void) {
|
||||
this.priceUpdateCallbackFunction = fn;
|
||||
}
|
||||
|
||||
getLatestBlockHeight(): number {
|
||||
return this.latestBlockHeight;
|
||||
}
|
||||
|
||||
private checkForBisqDataFolder() {
|
||||
if (!fs.existsSync(Bisq.BLOCKS_JSON_FILE_PATH)) {
|
||||
logger.warn(Bisq.BLOCKS_JSON_FILE_PATH + ` doesn't exist. Make sure Bisq is running and the config is correct before starting the server.`);
|
||||
throw new Error(`Cannot load BISQ ${Bisq.BLOCKS_JSON_FILE_PATH} file`);
|
||||
}
|
||||
}
|
||||
|
||||
private startTopDirectoryWatcher() {
|
||||
if (this.topDirectoryWatcher) {
|
||||
this.topDirectoryWatcher.close();
|
||||
}
|
||||
let fsWait: NodeJS.Timeout | null = null;
|
||||
this.topDirectoryWatcher = fs.watch(config.BISQ.DATA_PATH + '/json', () => {
|
||||
if (fsWait) {
|
||||
clearTimeout(fsWait);
|
||||
}
|
||||
if (this.subdirectoryWatcher) {
|
||||
this.subdirectoryWatcher.close();
|
||||
}
|
||||
fsWait = setTimeout(() => {
|
||||
logger.debug(`Bisq restart detected. Resetting both watchers in 3 minutes.`);
|
||||
setTimeout(() => {
|
||||
this.startTopDirectoryWatcher();
|
||||
this.startSubDirectoryWatcher();
|
||||
this.loadBisqDumpFile();
|
||||
}, 180000);
|
||||
}, 15000);
|
||||
});
|
||||
}
|
||||
|
||||
private startSubDirectoryWatcher() {
|
||||
if (this.subdirectoryWatcher) {
|
||||
this.subdirectoryWatcher.close();
|
||||
}
|
||||
if (!fs.existsSync(Bisq.BLOCKS_JSON_FILE_PATH)) {
|
||||
logger.warn(Bisq.BLOCKS_JSON_FILE_PATH + ` doesn't exist. Trying to restart sub directory watcher again in 3 minutes.`);
|
||||
setTimeout(() => this.startSubDirectoryWatcher(), 180000);
|
||||
return;
|
||||
}
|
||||
let fsWait: NodeJS.Timeout | null = null;
|
||||
this.subdirectoryWatcher = fs.watch(config.BISQ.DATA_PATH + '/json/all', () => {
|
||||
if (fsWait) {
|
||||
clearTimeout(fsWait);
|
||||
}
|
||||
fsWait = setTimeout(() => {
|
||||
logger.debug(`Change detected in the Bisq data folder.`);
|
||||
this.loadBisqDumpFile();
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
private async updatePrice() {
|
||||
type axiosOptions = {
|
||||
headers: {
|
||||
'User-Agent': string
|
||||
};
|
||||
timeout: number;
|
||||
httpAgent?: http.Agent;
|
||||
httpsAgent?: https.Agent;
|
||||
}
|
||||
const setDelay = (secs: number = 1): Promise<void> => new Promise(resolve => setTimeout(() => resolve(), secs * 1000));
|
||||
const BISQ_URL = (config.SOCKS5PROXY.ENABLED === true) && (config.SOCKS5PROXY.USE_ONION === true) ? config.EXTERNAL_DATA_SERVER.BISQ_ONION : config.EXTERNAL_DATA_SERVER.BISQ_URL;
|
||||
const isHTTP = (new URL(BISQ_URL).protocol.split(':')[0] === 'http') ? true : false;
|
||||
const axiosOptions: axiosOptions = {
|
||||
headers: {
|
||||
'User-Agent': (config.MEMPOOL.USER_AGENT === 'mempool') ? `mempool/v${backendInfo.getBackendInfo().version}` : `${config.MEMPOOL.USER_AGENT}`
|
||||
},
|
||||
timeout: config.SOCKS5PROXY.ENABLED ? 30000 : 10000
|
||||
};
|
||||
let retry = 0;
|
||||
|
||||
while(retry < config.MEMPOOL.EXTERNAL_MAX_RETRY) {
|
||||
try {
|
||||
if (config.SOCKS5PROXY.ENABLED) {
|
||||
const socksOptions: any = {
|
||||
agentOptions: {
|
||||
keepAlive: true,
|
||||
},
|
||||
hostname: config.SOCKS5PROXY.HOST,
|
||||
port: config.SOCKS5PROXY.PORT
|
||||
};
|
||||
|
||||
if (config.SOCKS5PROXY.USERNAME && config.SOCKS5PROXY.PASSWORD) {
|
||||
socksOptions.username = config.SOCKS5PROXY.USERNAME;
|
||||
socksOptions.password = config.SOCKS5PROXY.PASSWORD;
|
||||
} else {
|
||||
// Retry with different tor circuits https://stackoverflow.com/a/64960234
|
||||
socksOptions.username = `circuit${retry}`;
|
||||
}
|
||||
|
||||
// Handle proxy agent for onion addresses
|
||||
if (isHTTP) {
|
||||
axiosOptions.httpAgent = new SocksProxyAgent(socksOptions);
|
||||
} else {
|
||||
axiosOptions.httpsAgent = new SocksProxyAgent(socksOptions);
|
||||
}
|
||||
}
|
||||
|
||||
const data: AxiosResponse = await axios.get(`${BISQ_URL}/trades/?market=bsq_btc`, axiosOptions);
|
||||
if (data.statusText === 'error' || !data.data) {
|
||||
throw new Error(`Could not fetch data from Bisq market, Error: ${data.status}`);
|
||||
}
|
||||
const prices: number[] = [];
|
||||
data.data.forEach((trade) => {
|
||||
prices.push(parseFloat(trade.price) * 100000000);
|
||||
});
|
||||
prices.sort((a, b) => a - b);
|
||||
this.price = Common.median(prices);
|
||||
if (this.priceUpdateCallbackFunction) {
|
||||
this.priceUpdateCallbackFunction(this.price);
|
||||
}
|
||||
logger.debug('Successfully updated Bisq market price');
|
||||
break;
|
||||
} catch (e) {
|
||||
logger.err('Error updating Bisq market price: ' + (e instanceof Error ? e.message : e));
|
||||
await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL);
|
||||
retry++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async loadBisqDumpFile(): Promise<void> {
|
||||
this.allBlocks = [];
|
||||
try {
|
||||
await this.loadData();
|
||||
this.buildIndex();
|
||||
this.calculateStats();
|
||||
} catch (e) {
|
||||
logger.info('Cannot load bisq dump file because: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
private buildIndex() {
|
||||
const start = new Date().getTime();
|
||||
this.transactions = [];
|
||||
this.transactionIndex = {};
|
||||
this.addressIndex = {};
|
||||
|
||||
this.allBlocks.forEach((block) => {
|
||||
/* Build block index */
|
||||
if (!this.blockIndex[block.hash]) {
|
||||
this.blockIndex[block.hash] = block;
|
||||
}
|
||||
|
||||
/* Build transactions index */
|
||||
block.txs.forEach((tx) => {
|
||||
this.transactions.push(tx);
|
||||
this.transactionIndex[tx.id] = tx;
|
||||
});
|
||||
});
|
||||
|
||||
/* Build address index */
|
||||
this.transactions.forEach((tx) => {
|
||||
tx.inputs.forEach((input) => {
|
||||
if (!this.addressIndex[input.address]) {
|
||||
this.addressIndex[input.address] = [];
|
||||
}
|
||||
if (this.addressIndex[input.address].indexOf(tx) === -1) {
|
||||
this.addressIndex[input.address].push(tx);
|
||||
}
|
||||
});
|
||||
tx.outputs.forEach((output) => {
|
||||
if (!this.addressIndex[output.address]) {
|
||||
this.addressIndex[output.address] = [];
|
||||
}
|
||||
if (this.addressIndex[output.address].indexOf(tx) === -1) {
|
||||
this.addressIndex[output.address].push(tx);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const time = new Date().getTime() - start;
|
||||
logger.debug('Bisq data index rebuilt in ' + time + ' ms');
|
||||
}
|
||||
|
||||
private calculateStats() {
|
||||
let minted = 0;
|
||||
let burned = 0;
|
||||
let unspent = 0;
|
||||
let spent = 0;
|
||||
|
||||
this.transactions.forEach((tx) => {
|
||||
tx.outputs.forEach((output) => {
|
||||
if (output.opReturn) {
|
||||
return;
|
||||
}
|
||||
if (output.txOutputType === 'GENESIS_OUTPUT' || output.txOutputType === 'ISSUANCE_CANDIDATE_OUTPUT' && output.isVerified) {
|
||||
minted += output.bsqAmount;
|
||||
}
|
||||
if (output.isUnspent) {
|
||||
unspent++;
|
||||
} else {
|
||||
spent++;
|
||||
}
|
||||
});
|
||||
burned += tx['burntFee'];
|
||||
});
|
||||
|
||||
this.stats = {
|
||||
addresses: Object.keys(this.addressIndex).length,
|
||||
minted: minted / 100,
|
||||
burnt: burned / 100,
|
||||
spent_txos: spent,
|
||||
unspent_txos: unspent,
|
||||
};
|
||||
}
|
||||
|
||||
private async loadData(): Promise<any> {
|
||||
if (!fs.existsSync(Bisq.BLOCKS_JSON_FILE_PATH)) {
|
||||
throw new Error(Bisq.BLOCKS_JSON_FILE_PATH + ` doesn't exist`);
|
||||
}
|
||||
|
||||
const readline = require('readline');
|
||||
const events = require('events');
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: fs.createReadStream(Bisq.BLOCKS_JSON_FILE_PATH),
|
||||
crlfDelay: Infinity
|
||||
});
|
||||
|
||||
let blockBuffer = '';
|
||||
let readingBlock = false;
|
||||
let lineCount = 1;
|
||||
const start = new Date().getTime();
|
||||
|
||||
logger.debug('Processing Bisq data dump...');
|
||||
|
||||
rl.on('line', (line) => {
|
||||
if (lineCount === 2) {
|
||||
line = line.replace(' "chainHeight": ', '');
|
||||
this.latestBlockHeight = parseInt(line, 10);
|
||||
}
|
||||
|
||||
if (line === ' {') {
|
||||
readingBlock = true;
|
||||
} else if (line === ' },') {
|
||||
blockBuffer += '}';
|
||||
try {
|
||||
const block: BisqBlock = JSON.parse(blockBuffer);
|
||||
this.allBlocks.push(block);
|
||||
readingBlock = false;
|
||||
blockBuffer = '';
|
||||
} catch (e) {
|
||||
logger.debug(blockBuffer);
|
||||
throw Error(`Unable to parse Bisq data dump at line ${lineCount}` + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
if (readingBlock === true) {
|
||||
blockBuffer += line;
|
||||
}
|
||||
|
||||
++lineCount;
|
||||
});
|
||||
|
||||
await events.once(rl, 'close');
|
||||
|
||||
this.allBlocks.reverse();
|
||||
this.blocks = this.allBlocks.filter((block) => block.txs.length > 0);
|
||||
|
||||
const time = new Date().getTime() - start;
|
||||
logger.debug('Bisq dump processed in ' + time + ' ms');
|
||||
}
|
||||
}
|
||||
|
||||
export default new Bisq();
|
||||
@@ -1,258 +0,0 @@
|
||||
|
||||
export interface BisqBlocks {
|
||||
chainHeight: number;
|
||||
blocks: BisqBlock[];
|
||||
}
|
||||
|
||||
export interface BisqBlock {
|
||||
height: number;
|
||||
time: number;
|
||||
hash: string;
|
||||
previousBlockHash: string;
|
||||
txs: BisqTransaction[];
|
||||
}
|
||||
|
||||
export interface BisqTransaction {
|
||||
txVersion: string;
|
||||
id: string;
|
||||
blockHeight: number;
|
||||
blockHash: string;
|
||||
time: number;
|
||||
inputs: BisqInput[];
|
||||
outputs: BisqOutput[];
|
||||
txType: string;
|
||||
txTypeDisplayString: string;
|
||||
burntFee: number;
|
||||
invalidatedBsq: number;
|
||||
unlockBlockHeight: number;
|
||||
}
|
||||
|
||||
export interface BisqStats {
|
||||
minted: number;
|
||||
burnt: number;
|
||||
addresses: number;
|
||||
unspent_txos: number;
|
||||
spent_txos: number;
|
||||
}
|
||||
|
||||
interface BisqInput {
|
||||
spendingTxOutputIndex: number;
|
||||
spendingTxId: string;
|
||||
bsqAmount: number;
|
||||
isVerified: boolean;
|
||||
address: string;
|
||||
time: number;
|
||||
}
|
||||
|
||||
interface BisqOutput {
|
||||
txVersion: string;
|
||||
txId: string;
|
||||
index: number;
|
||||
bsqAmount: number;
|
||||
btcAmount: number;
|
||||
height: number;
|
||||
isVerified: boolean;
|
||||
burntFee: number;
|
||||
invalidatedBsq: number;
|
||||
address: string;
|
||||
scriptPubKey: BisqScriptPubKey;
|
||||
time: any;
|
||||
txType: string;
|
||||
txTypeDisplayString: string;
|
||||
txOutputType: string;
|
||||
txOutputTypeDisplayString: string;
|
||||
lockTime: number;
|
||||
isUnspent: boolean;
|
||||
spentInfo: SpentInfo;
|
||||
opReturn?: string;
|
||||
}
|
||||
|
||||
interface BisqScriptPubKey {
|
||||
addresses: string[];
|
||||
asm: string;
|
||||
hex: string;
|
||||
reqSigs?: number;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface SpentInfo {
|
||||
height: number;
|
||||
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; }
|
||||
|
||||
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 {
|
||||
direction: string;
|
||||
price: string;
|
||||
amount: string;
|
||||
volume: string;
|
||||
payment_method: string;
|
||||
trade_id: string;
|
||||
trade_date: number;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -1,679 +0,0 @@
|
||||
import { Currencies, OffersData, TradesData, Depth, Currency, Interval, HighLowOpenClose,
|
||||
Markets, Offers, Offer, BisqTrade, MarketVolume, Tickers, Ticker, SummarizedIntervals, SummarizedInterval } from './interfaces';
|
||||
|
||||
const strtotime = require('./strtotime');
|
||||
|
||||
class BisqMarketsApi {
|
||||
private cryptoCurrencyData: Currency[] = [];
|
||||
private fiatCurrencyData: Currency[] = [];
|
||||
private activeCryptoCurrencyData: Currency[] = [];
|
||||
private activeFiatCurrencyData: Currency[] = [];
|
||||
private offersData: OffersData[] = [];
|
||||
private tradesData: TradesData[] = [];
|
||||
private fiatCurrenciesIndexed: { [code: string]: true } = {};
|
||||
private allCurrenciesIndexed: { [code: string]: Currency } = {};
|
||||
private tradeDataByMarket: { [market: string]: TradesData[] } = {};
|
||||
private tickersCache: Ticker | Tickers | null = null;
|
||||
|
||||
constructor() { }
|
||||
|
||||
setOffersData(offers: OffersData[]) {
|
||||
this.offersData = offers;
|
||||
}
|
||||
|
||||
setTradesData(trades: TradesData[]) {
|
||||
this.tradesData = trades;
|
||||
this.tradeDataByMarket = {};
|
||||
|
||||
this.tradesData.forEach((trade) => {
|
||||
trade._market = trade.currencyPair.toLowerCase().replace('/', '_');
|
||||
if (!this.tradeDataByMarket[trade._market]) {
|
||||
this.tradeDataByMarket[trade._market] = [];
|
||||
}
|
||||
this.tradeDataByMarket[trade._market].push(trade);
|
||||
});
|
||||
}
|
||||
|
||||
setCurrencyData(cryptoCurrency: Currency[], fiatCurrency: Currency[], activeCryptoCurrency: Currency[], activeFiatCurrency: Currency[]) {
|
||||
this.cryptoCurrencyData = cryptoCurrency,
|
||||
this.fiatCurrencyData = fiatCurrency,
|
||||
this.activeCryptoCurrencyData = activeCryptoCurrency,
|
||||
this.activeFiatCurrencyData = activeFiatCurrency;
|
||||
|
||||
this.fiatCurrenciesIndexed = {};
|
||||
this.allCurrenciesIndexed = {};
|
||||
|
||||
this.fiatCurrencyData.forEach((currency) => {
|
||||
currency._type = 'fiat';
|
||||
this.fiatCurrenciesIndexed[currency.code] = true;
|
||||
this.allCurrenciesIndexed[currency.code] = currency;
|
||||
});
|
||||
this.cryptoCurrencyData.forEach((currency) => {
|
||||
currency._type = 'crypto';
|
||||
this.allCurrenciesIndexed[currency.code] = currency;
|
||||
});
|
||||
}
|
||||
|
||||
updateCache() {
|
||||
this.tickersCache = null;
|
||||
this.tickersCache = this.getTicker();
|
||||
}
|
||||
|
||||
getCurrencies(
|
||||
type: 'crypto' | 'fiat' | 'active' | 'all' = 'all',
|
||||
): Currencies {
|
||||
let currencies: Currency[];
|
||||
|
||||
switch (type) {
|
||||
case 'fiat':
|
||||
currencies = this.fiatCurrencyData;
|
||||
break;
|
||||
case 'crypto':
|
||||
currencies = this.cryptoCurrencyData;
|
||||
break;
|
||||
case 'active':
|
||||
currencies = this.activeCryptoCurrencyData.concat(this.activeFiatCurrencyData);
|
||||
break;
|
||||
case 'all':
|
||||
default:
|
||||
currencies = this.cryptoCurrencyData.concat(this.fiatCurrencyData);
|
||||
}
|
||||
const result = {};
|
||||
currencies.forEach((currency) => {
|
||||
result[currency.code] = currency;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
getDepth(
|
||||
market: string,
|
||||
): Depth {
|
||||
const currencyPair = market.replace('_', '/').toUpperCase();
|
||||
|
||||
const buys = this.offersData
|
||||
.filter((offer) => offer.currencyPair === currencyPair && offer.primaryMarketDirection === 'BUY')
|
||||
.map((offer) => offer.price)
|
||||
.sort((a, b) => b - a)
|
||||
.map((price) => this.intToBtc(price));
|
||||
|
||||
const sells = this.offersData
|
||||
.filter((offer) => offer.currencyPair === currencyPair && offer.primaryMarketDirection === 'SELL')
|
||||
.map((offer) => offer.price)
|
||||
.sort((a, b) => a - b)
|
||||
.map((price) => this.intToBtc(price));
|
||||
|
||||
const result = {};
|
||||
result[market] = {
|
||||
'buys': buys,
|
||||
'sells': sells,
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
getOffers(
|
||||
market: string,
|
||||
direction?: 'buy' | 'sell',
|
||||
): Offers {
|
||||
const currencyPair = market.replace('_', '/').toUpperCase();
|
||||
|
||||
let buys: Offer[] | null = null;
|
||||
let sells: Offer[] | null = null;
|
||||
|
||||
if (!direction || direction === 'buy') {
|
||||
buys = this.offersData
|
||||
.filter((offer) => offer.currencyPair === currencyPair && offer.primaryMarketDirection === 'BUY')
|
||||
.sort((a, b) => b.price - a.price)
|
||||
.map((offer) => this.offerDataToOffer(offer, market));
|
||||
}
|
||||
|
||||
if (!direction || direction === 'sell') {
|
||||
sells = this.offersData
|
||||
.filter((offer) => offer.currencyPair === currencyPair && offer.primaryMarketDirection === 'SELL')
|
||||
.sort((a, b) => a.price - b.price)
|
||||
.map((offer) => this.offerDataToOffer(offer, market));
|
||||
}
|
||||
|
||||
const result: Offers = {};
|
||||
result[market] = {
|
||||
'buys': buys,
|
||||
'sells': sells,
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
getMarkets(): Markets {
|
||||
const allCurrencies = this.getCurrencies();
|
||||
const activeCurrencies = this.getCurrencies('active');
|
||||
const markets = {};
|
||||
|
||||
for (const currency of Object.keys(activeCurrencies)) {
|
||||
if (allCurrencies[currency].code === 'BTC') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isFiat = allCurrencies[currency]._type === 'fiat';
|
||||
const pmarketname = allCurrencies['BTC']['name'];
|
||||
|
||||
const lsymbol = isFiat ? 'BTC' : currency;
|
||||
const rsymbol = isFiat ? currency : 'BTC';
|
||||
const lname = isFiat ? pmarketname : allCurrencies[currency].name;
|
||||
const rname = isFiat ? allCurrencies[currency].name : pmarketname;
|
||||
const ltype = isFiat ? 'crypto' : allCurrencies[currency]._type;
|
||||
const rtype = isFiat ? 'fiat' : 'crypto';
|
||||
const lprecision = 8;
|
||||
const rprecision = isFiat ? 2 : 8;
|
||||
const pair = lsymbol.toLowerCase() + '_' + rsymbol.toLowerCase();
|
||||
|
||||
markets[pair] = {
|
||||
'pair': pair,
|
||||
'lname': lname,
|
||||
'rname': rname,
|
||||
'lsymbol': lsymbol,
|
||||
'rsymbol': rsymbol,
|
||||
'lprecision': lprecision,
|
||||
'rprecision': rprecision,
|
||||
'ltype': ltype,
|
||||
'rtype': rtype,
|
||||
'name': lname + '/' + rname,
|
||||
};
|
||||
}
|
||||
|
||||
return markets;
|
||||
}
|
||||
|
||||
getTrades(
|
||||
market: string,
|
||||
timestamp_from?: number,
|
||||
timestamp_to?: number,
|
||||
trade_id_from?: string,
|
||||
trade_id_to?: string,
|
||||
direction?: 'buy' | 'sell',
|
||||
limit: number = 100,
|
||||
sort: 'asc' | 'desc' = 'desc',
|
||||
): BisqTrade[] {
|
||||
limit = Math.min(limit, 2000);
|
||||
const _market = market === 'all' ? undefined : market;
|
||||
|
||||
if (!timestamp_from) {
|
||||
timestamp_from = new Date('2016-01-01').getTime() / 1000;
|
||||
}
|
||||
if (!timestamp_to) {
|
||||
timestamp_to = new Date().getTime() / 1000;
|
||||
}
|
||||
|
||||
const matches = this.getTradesByCriteria(_market, timestamp_to, timestamp_from,
|
||||
trade_id_to, trade_id_from, direction, sort, limit, false);
|
||||
|
||||
if (sort === 'asc') {
|
||||
matches.sort((a, b) => a.tradeDate - b.tradeDate);
|
||||
} else {
|
||||
matches.sort((a, b) => b.tradeDate - a.tradeDate);
|
||||
}
|
||||
|
||||
return matches.map((trade) => {
|
||||
const bsqTrade: BisqTrade = {
|
||||
direction: trade.primaryMarketDirection,
|
||||
price: trade._tradePriceStr,
|
||||
amount: trade._tradeAmountStr,
|
||||
volume: trade._tradeVolumeStr,
|
||||
payment_method: trade.paymentMethod,
|
||||
trade_id: trade.offerId,
|
||||
trade_date: trade.tradeDate,
|
||||
};
|
||||
if (market === 'all') {
|
||||
bsqTrade.market = trade._market;
|
||||
}
|
||||
return bsqTrade;
|
||||
});
|
||||
}
|
||||
|
||||
getVolumes(
|
||||
market?: string,
|
||||
timestamp_from?: number,
|
||||
timestamp_to?: number,
|
||||
interval: Interval = 'auto',
|
||||
milliseconds?: boolean,
|
||||
timestamp: 'no' | 'yes' = 'yes',
|
||||
): MarketVolume[] {
|
||||
if (milliseconds) {
|
||||
timestamp_from = timestamp_from ? timestamp_from / 1000 : timestamp_from;
|
||||
timestamp_to = timestamp_to ? timestamp_to / 1000 : timestamp_to;
|
||||
}
|
||||
if (!timestamp_from) {
|
||||
timestamp_from = new Date('2016-01-01').getTime() / 1000;
|
||||
}
|
||||
if (!timestamp_to) {
|
||||
timestamp_to = new Date().getTime() / 1000;
|
||||
}
|
||||
|
||||
const trades = this.getTradesByCriteria(market, timestamp_to, timestamp_from,
|
||||
undefined, undefined, undefined, 'asc', Number.MAX_SAFE_INTEGER);
|
||||
|
||||
if (interval === 'auto') {
|
||||
const range = timestamp_to - timestamp_from;
|
||||
interval = this.getIntervalFromRange(range);
|
||||
}
|
||||
|
||||
const intervals: any = {};
|
||||
const marketVolumes: MarketVolume[] = [];
|
||||
|
||||
for (const trade of trades) {
|
||||
const traded_at = trade['tradeDate'] / 1000;
|
||||
const interval_start = this.intervalStart(traded_at, interval);
|
||||
|
||||
if (!intervals[interval_start]) {
|
||||
intervals[interval_start] = {
|
||||
'volume': 0,
|
||||
'num_trades': 0,
|
||||
};
|
||||
}
|
||||
|
||||
const period = intervals[interval_start];
|
||||
period['period_start'] = interval_start;
|
||||
period['volume'] += this.fiatCurrenciesIndexed[trade.currency] ? trade._tradeAmount : trade._tradeVolume;
|
||||
period['num_trades']++;
|
||||
}
|
||||
|
||||
for (const p in intervals) {
|
||||
if (intervals.hasOwnProperty(p)) {
|
||||
const period = intervals[p];
|
||||
marketVolumes.push({
|
||||
period_start: timestamp === 'no' ? new Date(period['period_start'] * 1000).toISOString() : period['period_start'],
|
||||
num_trades: period['num_trades'],
|
||||
volume: this.intToBtc(period['volume']),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return marketVolumes;
|
||||
}
|
||||
|
||||
getTicker(
|
||||
market?: string,
|
||||
): Tickers | Ticker | null {
|
||||
if (market) {
|
||||
return this.getTickerFromMarket(market);
|
||||
}
|
||||
|
||||
if (this.tickersCache) {
|
||||
return this.tickersCache;
|
||||
}
|
||||
|
||||
const allMarkets = this.getMarkets();
|
||||
const tickers = {};
|
||||
for (const m in allMarkets) {
|
||||
if (allMarkets.hasOwnProperty(m)) {
|
||||
tickers[allMarkets[m].pair] = this.getTickerFromMarket(allMarkets[m].pair);
|
||||
}
|
||||
}
|
||||
|
||||
return tickers;
|
||||
}
|
||||
|
||||
getTickerFromMarket(market: string): Ticker | null {
|
||||
let ticker: Ticker;
|
||||
const timestamp_from = strtotime('-24 hour');
|
||||
const timestamp_to = new Date().getTime() / 1000;
|
||||
const trades = this.getTradesByCriteria(market, timestamp_to, timestamp_from,
|
||||
undefined, undefined, undefined, 'asc', Number.MAX_SAFE_INTEGER);
|
||||
|
||||
const periods: SummarizedInterval[] = Object.values(this.getTradesSummarized(trades, timestamp_from));
|
||||
|
||||
const allCurrencies = this.getCurrencies();
|
||||
const currencyRight = allCurrencies[market.split('_')[1].toUpperCase()];
|
||||
|
||||
if (periods[0]) {
|
||||
ticker = {
|
||||
'last': this.intToBtc(periods[0].close),
|
||||
'high': this.intToBtc(periods[0].high),
|
||||
'low': this.intToBtc(periods[0].low),
|
||||
'volume_left': this.intToBtc(periods[0].volume_left),
|
||||
'volume_right': this.intToBtc(periods[0].volume_right),
|
||||
'buy': null,
|
||||
'sell': null,
|
||||
};
|
||||
} else {
|
||||
const lastTrade = this.tradeDataByMarket[market];
|
||||
if (!lastTrade) {
|
||||
return null;
|
||||
}
|
||||
const tradePrice = lastTrade[0].primaryMarketTradePrice * Math.pow(10, 8 - currencyRight.precision);
|
||||
|
||||
const lastTradePrice = this.intToBtc(tradePrice);
|
||||
ticker = {
|
||||
'last': lastTradePrice,
|
||||
'high': lastTradePrice,
|
||||
'low': lastTradePrice,
|
||||
'volume_left': '0',
|
||||
'volume_right': '0',
|
||||
'buy': null,
|
||||
'sell': null,
|
||||
};
|
||||
}
|
||||
|
||||
const timestampFromMilli = timestamp_from * 1000;
|
||||
const timestampToMilli = timestamp_to * 1000;
|
||||
|
||||
const currencyPair = market.replace('_', '/').toUpperCase();
|
||||
const offersData = this.offersData.slice().sort((a, b) => a.price - b.price);
|
||||
|
||||
const buy = offersData.find((offer) => offer.currencyPair === currencyPair
|
||||
&& offer.primaryMarketDirection === 'BUY'
|
||||
&& offer.date >= timestampFromMilli
|
||||
&& offer.date <= timestampToMilli
|
||||
);
|
||||
const sell = offersData.find((offer) => offer.currencyPair === currencyPair
|
||||
&& offer.primaryMarketDirection === 'SELL'
|
||||
&& offer.date >= timestampFromMilli
|
||||
&& offer.date <= timestampToMilli
|
||||
);
|
||||
|
||||
if (buy) {
|
||||
ticker.buy = this.intToBtc(buy.primaryMarketPrice * Math.pow(10, 8 - currencyRight.precision));
|
||||
}
|
||||
if (sell) {
|
||||
ticker.sell = this.intToBtc(sell.primaryMarketPrice * Math.pow(10, 8 - currencyRight.precision));
|
||||
}
|
||||
|
||||
return ticker;
|
||||
}
|
||||
|
||||
getHloc(
|
||||
market: string,
|
||||
interval: Interval = 'auto',
|
||||
timestamp_from?: number,
|
||||
timestamp_to?: number,
|
||||
milliseconds?: boolean,
|
||||
timestamp: 'no' | 'yes' = 'yes',
|
||||
): HighLowOpenClose[] {
|
||||
if (milliseconds) {
|
||||
timestamp_from = timestamp_from ? timestamp_from / 1000 : timestamp_from;
|
||||
timestamp_to = timestamp_to ? timestamp_to / 1000 : timestamp_to;
|
||||
}
|
||||
if (!timestamp_from) {
|
||||
timestamp_from = new Date('2016-01-01').getTime() / 1000;
|
||||
}
|
||||
if (!timestamp_to) {
|
||||
timestamp_to = new Date().getTime() / 1000;
|
||||
}
|
||||
|
||||
const trades = this.getTradesByCriteria(market, timestamp_to, timestamp_from,
|
||||
undefined, undefined, undefined, 'asc', Number.MAX_SAFE_INTEGER);
|
||||
|
||||
if (interval === 'auto') {
|
||||
const range = timestamp_to - timestamp_from;
|
||||
interval = this.getIntervalFromRange(range);
|
||||
}
|
||||
|
||||
const intervals = this.getTradesSummarized(trades, timestamp_from, interval);
|
||||
|
||||
const hloc: HighLowOpenClose[] = [];
|
||||
|
||||
for (const p in intervals) {
|
||||
if (intervals.hasOwnProperty(p)) {
|
||||
const period = intervals[p];
|
||||
hloc.push({
|
||||
period_start: timestamp === 'no' ? new Date(period['period_start'] * 1000).toISOString() : period['period_start'],
|
||||
open: this.intToBtc(period['open']),
|
||||
close: this.intToBtc(period['close']),
|
||||
high: this.intToBtc(period['high']),
|
||||
low: this.intToBtc(period['low']),
|
||||
avg: this.intToBtc(period['avg']),
|
||||
volume_right: this.intToBtc(period['volume_right']),
|
||||
volume_left: this.intToBtc(period['volume_left']),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return hloc;
|
||||
}
|
||||
|
||||
private getIntervalFromRange(range: number): Interval {
|
||||
// two days range loads minute data
|
||||
if (range <= 3600) {
|
||||
// up to one hour range loads minutely data
|
||||
return 'minute';
|
||||
} else if (range <= 1 * 24 * 3600) {
|
||||
// up to one day range loads half-hourly data
|
||||
return 'half_hour';
|
||||
} else if (range <= 3 * 24 * 3600) {
|
||||
// up to 3 day range loads hourly data
|
||||
return 'hour';
|
||||
} else if (range <= 7 * 24 * 3600) {
|
||||
// up to 7 day range loads half-daily data
|
||||
return 'half_day';
|
||||
} else if (range <= 60 * 24 * 3600) {
|
||||
// up to 2 month range loads daily data
|
||||
return 'day';
|
||||
} else if (range <= 12 * 31 * 24 * 3600) {
|
||||
// up to one year range loads weekly data
|
||||
return 'week';
|
||||
} else if (range <= 12 * 31 * 24 * 3600) {
|
||||
// up to 5 year range loads monthly data
|
||||
return 'month';
|
||||
} else {
|
||||
// greater range loads yearly data
|
||||
return 'year';
|
||||
}
|
||||
}
|
||||
|
||||
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 = {};
|
||||
|
||||
for (const trade of trades) {
|
||||
const traded_at = trade.tradeDate / 1000;
|
||||
const interval_start = !interval ? timestamp_from : this.intervalStart(traded_at, interval);
|
||||
|
||||
if (!intervals[interval_start]) {
|
||||
intervals[interval_start] = {
|
||||
'open': 0,
|
||||
'close': 0,
|
||||
'high': 0,
|
||||
'low': 0,
|
||||
'avg': 0,
|
||||
'volume_right': 0,
|
||||
'volume_left': 0,
|
||||
};
|
||||
intervals_prices[interval_start] = [];
|
||||
}
|
||||
const period = intervals[interval_start];
|
||||
const price = trade._tradePrice;
|
||||
|
||||
if (!intervals_prices[interval_start]['leftvol']) {
|
||||
intervals_prices[interval_start]['leftvol'] = [];
|
||||
}
|
||||
if (!intervals_prices[interval_start]['rightvol']) {
|
||||
intervals_prices[interval_start]['rightvol'] = [];
|
||||
}
|
||||
|
||||
intervals_prices[interval_start]['leftvol'].push(trade._tradeAmount);
|
||||
intervals_prices[interval_start]['rightvol'].push(trade._tradeVolume);
|
||||
|
||||
if (price) {
|
||||
const plow = period['low'];
|
||||
period['period_start'] = interval_start;
|
||||
period['open'] = period['open'] || price;
|
||||
period['close'] = price;
|
||||
period['high'] = price > period['high'] ? price : period['high'];
|
||||
period['low'] = (plow && price > plow) ? period['low'] : price;
|
||||
period['avg'] = intervals_prices[interval_start]['rightvol'].reduce((p: number, c: number) => c + p, 0)
|
||||
/ intervals_prices[interval_start]['leftvol'].reduce((c: number, p: number) => c + p, 0) * 100000000;
|
||||
period['volume_left'] += trade._tradeAmount;
|
||||
period['volume_right'] += trade._tradeVolume;
|
||||
}
|
||||
}
|
||||
return intervals;
|
||||
}
|
||||
|
||||
private getTradesByCriteria(
|
||||
market: string | undefined,
|
||||
timestamp_to: number,
|
||||
timestamp_from: number,
|
||||
trade_id_to: string | undefined,
|
||||
trade_id_from: string | undefined,
|
||||
direction: 'buy' | 'sell' | undefined,
|
||||
sort: string,
|
||||
limit: number,
|
||||
integerAmounts: boolean = true,
|
||||
): TradesData[] {
|
||||
let trade_id_from_ts: number | null = null;
|
||||
let trade_id_to_ts: number | null = null;
|
||||
const allCurrencies = this.getCurrencies();
|
||||
|
||||
const timestampFromMilli = timestamp_from * 1000;
|
||||
const timestampToMilli = timestamp_to * 1000;
|
||||
|
||||
// note: the offer_id_from/to depends on iterating over trades in
|
||||
// descending chronological order.
|
||||
const tradesDataSorted = this.tradesData.slice();
|
||||
if (sort === 'asc') {
|
||||
tradesDataSorted.reverse();
|
||||
}
|
||||
|
||||
let matches: TradesData[] = [];
|
||||
for (const trade of tradesDataSorted) {
|
||||
if (trade_id_from === trade.offerId) {
|
||||
trade_id_from_ts = trade.tradeDate;
|
||||
}
|
||||
if (trade_id_to === trade.offerId) {
|
||||
trade_id_to_ts = trade.tradeDate;
|
||||
}
|
||||
if (trade_id_to && trade_id_to_ts === null) {
|
||||
continue;
|
||||
}
|
||||
if (trade_id_from && trade_id_from_ts != null && trade_id_from_ts !== trade.tradeDate) {
|
||||
continue;
|
||||
}
|
||||
if (market && market !== trade._market) {
|
||||
continue;
|
||||
}
|
||||
if (timestampFromMilli && timestampFromMilli > trade.tradeDate) {
|
||||
continue;
|
||||
}
|
||||
if (timestampToMilli && timestampToMilli < trade.tradeDate) {
|
||||
continue;
|
||||
}
|
||||
if (direction && direction !== trade.direction.toLowerCase()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Filter out bogus trades with BTC/BTC or XXX/XXX market.
|
||||
// See github issue: https://github.com/bitsquare/bitsquare/issues/883
|
||||
const currencyPairs = trade.currencyPair.split('/');
|
||||
if (currencyPairs[0] === currencyPairs[1]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const currencyLeft = allCurrencies[currencyPairs[0]];
|
||||
const currencyRight = allCurrencies[currencyPairs[1]];
|
||||
|
||||
if (!currencyLeft || !currencyRight) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const tradePrice = trade.primaryMarketTradePrice * Math.pow(10, 8 - currencyRight.precision);
|
||||
const tradeAmount = trade.primaryMarketTradeAmount * Math.pow(10, 8 - currencyLeft.precision);
|
||||
const tradeVolume = trade.primaryMarketTradeVolume * Math.pow(10, 8 - currencyRight.precision);
|
||||
|
||||
if (integerAmounts) {
|
||||
trade._tradePrice = tradePrice;
|
||||
trade._tradeAmount = tradeAmount;
|
||||
trade._tradeVolume = tradeVolume;
|
||||
trade._offerAmount = trade.offerAmount;
|
||||
} else {
|
||||
trade._tradePriceStr = this.intToBtc(tradePrice);
|
||||
trade._tradeAmountStr = this.intToBtc(tradeAmount);
|
||||
trade._tradeVolumeStr = this.intToBtc(tradeVolume);
|
||||
trade._offerAmountStr = this.intToBtc(trade.offerAmount);
|
||||
}
|
||||
|
||||
matches.push(trade);
|
||||
|
||||
if (matches.length >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ((trade_id_from && !trade_id_from_ts) || (trade_id_to && !trade_id_to_ts)) {
|
||||
matches = [];
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
private intervalStart(ts: number, interval: string): number {
|
||||
switch (interval) {
|
||||
case 'minute':
|
||||
return (ts - (ts % 60));
|
||||
case '10_minute':
|
||||
return (ts - (ts % 600));
|
||||
case 'half_hour':
|
||||
return (ts - (ts % 1800));
|
||||
case 'hour':
|
||||
return (ts - (ts % 3600));
|
||||
case 'half_day':
|
||||
return (ts - (ts % (3600 * 12)));
|
||||
case 'day':
|
||||
return strtotime('midnight today', ts);
|
||||
case 'week':
|
||||
return strtotime('midnight sunday last week', ts);
|
||||
case 'month':
|
||||
return strtotime('midnight first day of this month', ts);
|
||||
case 'year':
|
||||
return strtotime('midnight first day of january', ts);
|
||||
default:
|
||||
throw new Error('Unsupported interval');
|
||||
}
|
||||
}
|
||||
|
||||
private offerDataToOffer(offer: OffersData, market: string): Offer {
|
||||
const currencyPairs = market.split('_');
|
||||
const currencyRight = this.allCurrenciesIndexed[currencyPairs[1].toUpperCase()];
|
||||
const currencyLeft = this.allCurrenciesIndexed[currencyPairs[0].toUpperCase()];
|
||||
const price = offer['primaryMarketPrice'] * Math.pow( 10, 8 - currencyRight['precision']);
|
||||
const amount = offer['primaryMarketAmount'] * Math.pow( 10, 8 - currencyLeft['precision']);
|
||||
const volume = offer['primaryMarketVolume'] * Math.pow( 10, 8 - currencyRight['precision']);
|
||||
|
||||
return {
|
||||
offer_id: offer.id,
|
||||
offer_date: offer.date,
|
||||
direction: offer.primaryMarketDirection,
|
||||
min_amount: this.intToBtc(offer.minAmount),
|
||||
amount: this.intToBtc(amount),
|
||||
price: this.intToBtc(price),
|
||||
volume: this.intToBtc(volume),
|
||||
payment_method: offer.paymentMethod,
|
||||
offer_fee_txid: null,
|
||||
};
|
||||
}
|
||||
|
||||
private intToBtc(val: number): string {
|
||||
return (val / 100000000).toFixed(8);
|
||||
}
|
||||
}
|
||||
|
||||
export default new BisqMarketsApi();
|
||||
@@ -1,137 +0,0 @@
|
||||
import config from '../../config';
|
||||
import * as fs from 'fs';
|
||||
import { OffersData as OffersData, TradesData, Currency } from './interfaces';
|
||||
import bisqMarket from './markets-api';
|
||||
import logger from '../../logger';
|
||||
|
||||
class Bisq {
|
||||
private static FOLDER_WATCH_CHANGE_DETECTION_DEBOUNCE = 4000;
|
||||
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',
|
||||
cryptoCurrency: '/crypto_currency_list.json',
|
||||
fiatCurrency: '/fiat_currency_list.json',
|
||||
offers: '/offers_statistics.json',
|
||||
trades: '/trade_statistics.json',
|
||||
};
|
||||
|
||||
private cryptoCurrencyLastMtime = new Date('2016-01-01');
|
||||
private fiatCurrencyLastMtime = new Date('2016-01-01');
|
||||
private offersLastMtime = new Date('2016-01-01');
|
||||
private tradesLastMtime = new Date('2016-01-01');
|
||||
|
||||
private subdirectoryWatcher: fs.FSWatcher | undefined;
|
||||
|
||||
constructor() {}
|
||||
|
||||
startBisqService(): void {
|
||||
try {
|
||||
this.checkForBisqDataFolder();
|
||||
} catch (e) {
|
||||
logger.info('Retrying to start bisq service (markets) in 3 minutes');
|
||||
setTimeout(this.startBisqService.bind(this), 180000);
|
||||
return;
|
||||
}
|
||||
this.loadBisqDumpFile();
|
||||
this.startBisqDirectoryWatcher();
|
||||
}
|
||||
|
||||
private checkForBisqDataFolder() {
|
||||
if (!fs.existsSync(Bisq.MARKET_JSON_PATH + Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency)) {
|
||||
logger.err(Bisq.MARKET_JSON_PATH + Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency + ` doesn't exist. Make sure Bisq is running and the config is correct before starting the server.`);
|
||||
throw new Error(`Cannot load BISQ ${Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency} file`);
|
||||
}
|
||||
}
|
||||
|
||||
private startBisqDirectoryWatcher() {
|
||||
if (this.subdirectoryWatcher) {
|
||||
this.subdirectoryWatcher.close();
|
||||
}
|
||||
if (!fs.existsSync(Bisq.MARKET_JSON_PATH + Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency)) {
|
||||
logger.warn(Bisq.MARKET_JSON_PATH + Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency + ` doesn't exist. Trying to restart sub directory watcher again in 3 minutes.`);
|
||||
setTimeout(() => this.startBisqDirectoryWatcher(), 180000);
|
||||
return;
|
||||
}
|
||||
let fsWait: NodeJS.Timeout | null = null;
|
||||
this.subdirectoryWatcher = fs.watch(Bisq.MARKET_JSON_PATH, () => {
|
||||
if (fsWait) {
|
||||
clearTimeout(fsWait);
|
||||
}
|
||||
fsWait = setTimeout(() => {
|
||||
logger.debug(`Change detected in the Bisq market data folder.`);
|
||||
this.loadBisqDumpFile();
|
||||
}, Bisq.FOLDER_WATCH_CHANGE_DETECTION_DEBOUNCE);
|
||||
});
|
||||
}
|
||||
|
||||
private async loadBisqDumpFile(): Promise<void> {
|
||||
const start = new Date().getTime();
|
||||
try {
|
||||
let marketsDataUpdated = false;
|
||||
const cryptoMtime = this.getFileMtime(Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency);
|
||||
const fiatMtime = this.getFileMtime(Bisq.MARKET_JSON_FILE_PATHS.fiatCurrency);
|
||||
if (cryptoMtime > this.cryptoCurrencyLastMtime || fiatMtime > this.fiatCurrencyLastMtime) {
|
||||
const cryptoCurrencyData = await this.loadData<Currency[]>(Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency);
|
||||
const fiatCurrencyData = await this.loadData<Currency[]>(Bisq.MARKET_JSON_FILE_PATHS.fiatCurrency);
|
||||
const activeCryptoCurrencyData = await this.loadData<Currency[]>(Bisq.MARKET_JSON_FILE_PATHS.activeCryptoCurrency);
|
||||
const activeFiatCurrencyData = await this.loadData<Currency[]>(Bisq.MARKET_JSON_FILE_PATHS.activeFiatCurrency);
|
||||
logger.debug('Updating Bisq Market Currency Data');
|
||||
bisqMarket.setCurrencyData(cryptoCurrencyData, fiatCurrencyData, activeCryptoCurrencyData, activeFiatCurrencyData);
|
||||
if (cryptoMtime > this.cryptoCurrencyLastMtime) {
|
||||
this.cryptoCurrencyLastMtime = cryptoMtime;
|
||||
}
|
||||
if (fiatMtime > this.fiatCurrencyLastMtime) {
|
||||
this.fiatCurrencyLastMtime = fiatMtime;
|
||||
}
|
||||
marketsDataUpdated = true;
|
||||
}
|
||||
const offersMtime = this.getFileMtime(Bisq.MARKET_JSON_FILE_PATHS.offers);
|
||||
if (offersMtime > this.offersLastMtime) {
|
||||
const offersData = await this.loadData<OffersData[]>(Bisq.MARKET_JSON_FILE_PATHS.offers);
|
||||
logger.debug('Updating Bisq Market Offers Data');
|
||||
bisqMarket.setOffersData(offersData);
|
||||
this.offersLastMtime = offersMtime;
|
||||
marketsDataUpdated = true;
|
||||
}
|
||||
const tradesMtime = this.getFileMtime(Bisq.MARKET_JSON_FILE_PATHS.trades);
|
||||
if (tradesMtime > this.tradesLastMtime) {
|
||||
const tradesData = await this.loadData<TradesData[]>(Bisq.MARKET_JSON_FILE_PATHS.trades);
|
||||
logger.debug('Updating Bisq Market Trades Data');
|
||||
bisqMarket.setTradesData(tradesData);
|
||||
this.tradesLastMtime = tradesMtime;
|
||||
marketsDataUpdated = true;
|
||||
}
|
||||
if (marketsDataUpdated) {
|
||||
bisqMarket.updateCache();
|
||||
const time = new Date().getTime() - start;
|
||||
logger.debug('Bisq market data updated in ' + time + ' ms');
|
||||
}
|
||||
} catch (e) {
|
||||
logger.err('loadBisqMarketDataDumpFile() error.' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
private getFileMtime(path: string): Date {
|
||||
const stats = fs.statSync(Bisq.MARKET_JSON_PATH + path);
|
||||
return stats.mtime;
|
||||
}
|
||||
|
||||
private loadData<T>(path: string): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(Bisq.MARKET_JSON_PATH + path, 'utf8', (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
try {
|
||||
const parsedData = JSON.parse(data);
|
||||
resolve(parsedData);
|
||||
} catch (e) {
|
||||
reject('JSON parse error (' + path + ')');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new Bisq();
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
import { IBitcoinApi } from './bitcoin-api.interface';
|
||||
import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface';
|
||||
import { IEsploraApi } from './esplora-api.interface';
|
||||
|
||||
export interface AbstractBitcoinApi {
|
||||
@@ -22,6 +22,7 @@ export interface AbstractBitcoinApi {
|
||||
$getScriptHash(scripthash: string): Promise<IEsploraApi.ScriptHash>;
|
||||
$getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
|
||||
$sendRawTransaction(rawTransaction: string): Promise<string>;
|
||||
$testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]>;
|
||||
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
|
||||
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
|
||||
$getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
|
||||
|
||||
@@ -205,3 +205,16 @@ export namespace IBitcoinApi {
|
||||
"utxo_size_inc": number;
|
||||
}
|
||||
}
|
||||
|
||||
export interface TestMempoolAcceptResult {
|
||||
txid: string,
|
||||
wtxid: string,
|
||||
allowed?: boolean,
|
||||
vsize?: number,
|
||||
fees?: {
|
||||
base: number,
|
||||
"effective-feerate": number,
|
||||
"effective-includes": string[],
|
||||
},
|
||||
['reject-reason']?: string,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
|
||||
import { IBitcoinApi } from './bitcoin-api.interface';
|
||||
import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface';
|
||||
import { IEsploraApi } from './esplora-api.interface';
|
||||
import blocks from '../blocks';
|
||||
import mempool from '../mempool';
|
||||
@@ -174,6 +174,14 @@ class BitcoinApi implements AbstractBitcoinApi {
|
||||
return this.bitcoindClient.sendRawTransaction(rawTransaction);
|
||||
}
|
||||
|
||||
async $testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]> {
|
||||
if (rawTransactions.length) {
|
||||
return this.bitcoindClient.testMempoolAccept(rawTransactions, maxfeerate ?? undefined);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
|
||||
const txOut = await this.bitcoindClient.getTxOut(txId, vout, false);
|
||||
return {
|
||||
|
||||
@@ -37,60 +37,6 @@ class BitcoinRoutes {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'replacements', this.getRbfReplacements)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'fullrbf/replacements', this.getFullRbfReplacements)
|
||||
.post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', this.$postTransactionForm)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations`, { responseType: 'stream', timeout: 10000 });
|
||||
response.data.pipe(res);
|
||||
} catch (e) {
|
||||
res.status(500).end();
|
||||
}
|
||||
})
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'donations/images/:id', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations/images/${req.params.id}`, {
|
||||
responseType: 'stream', timeout: 10000
|
||||
});
|
||||
response.data.pipe(res);
|
||||
} catch (e) {
|
||||
res.status(500).end();
|
||||
}
|
||||
})
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'contributors', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors`, { responseType: 'stream', timeout: 10000 });
|
||||
response.data.pipe(res);
|
||||
} catch (e) {
|
||||
res.status(500).end();
|
||||
}
|
||||
})
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'contributors/images/:id', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors/images/${req.params.id}`, {
|
||||
responseType: 'stream', timeout: 10000
|
||||
});
|
||||
response.data.pipe(res);
|
||||
} catch (e) {
|
||||
res.status(500).end();
|
||||
}
|
||||
})
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'translators', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators`, { responseType: 'stream', timeout: 10000 });
|
||||
response.data.pipe(res);
|
||||
} catch (e) {
|
||||
res.status(500).end();
|
||||
}
|
||||
})
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'translators/images/:id', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators/images/${req.params.id}`, {
|
||||
responseType: 'stream', timeout: 10000
|
||||
});
|
||||
response.data.pipe(res);
|
||||
} catch (e) {
|
||||
res.status(500).end();
|
||||
}
|
||||
})
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks', this.getBlocks.bind(this))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
|
||||
@@ -109,6 +55,7 @@ class BitcoinRoutes {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mempool/recent', this.getRecentMempoolTransactions)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId', this.getTransaction)
|
||||
.post(config.MEMPOOL.API_URL_PREFIX + 'tx', this.$postTransaction)
|
||||
.post(config.MEMPOOL.API_URL_PREFIX + 'txs/test', this.$testTransactions)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/hex', this.getRawTransaction)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', this.getTransactionStatus)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', this.getTransactionOutspends)
|
||||
@@ -418,7 +365,7 @@ class BitcoinRoutes {
|
||||
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(await blocks.$getBlocks(height, 15));
|
||||
} else { // Liquid, Bisq
|
||||
} else { // Liquid
|
||||
return await this.getLegacyBlocks(req, res);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -428,7 +375,7 @@ class BitcoinRoutes {
|
||||
|
||||
private async getBlocksByBulk(req: Request, res: Response) {
|
||||
try {
|
||||
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { // Liquid, Bisq - Not implemented
|
||||
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { // Liquid - Not implemented
|
||||
return res.status(404).send(`This API is only available for Bitcoin networks`);
|
||||
}
|
||||
if (config.MEMPOOL.MAX_BLOCKS_BULK_QUERY <= 0) {
|
||||
@@ -803,6 +750,19 @@ class BitcoinRoutes {
|
||||
}
|
||||
}
|
||||
|
||||
private async $testTransactions(req: Request, res: Response) {
|
||||
try {
|
||||
const rawTxs = Common.getTransactionsFromRequest(req);
|
||||
const maxfeerate = parseFloat(req.query.maxfeerate as string);
|
||||
const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate);
|
||||
res.send(result);
|
||||
} catch (e: any) {
|
||||
res.setHeader('content-type', 'text/plain');
|
||||
res.status(400).send(e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
|
||||
: (e.message || 'Error'));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default new BitcoinRoutes();
|
||||
|
||||
@@ -5,6 +5,7 @@ import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-fact
|
||||
import { IEsploraApi } from './esplora-api.interface';
|
||||
import logger from '../../logger';
|
||||
import { Common } from '../common';
|
||||
import { TestMempoolAcceptResult } from './bitcoin-api.interface';
|
||||
|
||||
interface FailoverHost {
|
||||
host: string,
|
||||
@@ -327,6 +328,10 @@ class ElectrsApi implements AbstractBitcoinApi {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
$testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
|
||||
return this.failoverRouter.$get<IEsploraApi.Outspend>('/tx/' + txId + '/outspend/' + vout);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import websocketHandler from './websocket-handler';
|
||||
import redisCache from './redis-cache';
|
||||
import rbfCache from './rbf-cache';
|
||||
import { calcBitsDifference } from './difficulty-adjustment';
|
||||
import AccelerationRepository from '../repositories/AccelerationRepository';
|
||||
|
||||
class Blocks {
|
||||
private blocks: BlockExtended[] = [];
|
||||
@@ -838,8 +839,11 @@ class Blocks {
|
||||
} else {
|
||||
this.currentBlockHeight++;
|
||||
logger.debug(`New block found (#${this.currentBlockHeight})!`);
|
||||
this.updateTimerProgress(timer, `getting orphaned blocks for ${this.currentBlockHeight}`);
|
||||
await chainTips.updateOrphanedBlocks();
|
||||
// skip updating the orphan block cache if we've fallen behind the chain tip
|
||||
if (this.currentBlockHeight >= blockHeightTip - 2) {
|
||||
this.updateTimerProgress(timer, `getting orphaned blocks for ${this.currentBlockHeight}`);
|
||||
await chainTips.updateOrphanedBlocks();
|
||||
}
|
||||
}
|
||||
|
||||
this.updateTimerProgress(timer, `getting block data for ${this.currentBlockHeight}`);
|
||||
@@ -872,6 +876,7 @@ class Blocks {
|
||||
await BlocksRepository.$deleteBlocksFrom(lastBlock.height - 10);
|
||||
await HashratesRepository.$deleteLastEntries();
|
||||
await cpfpRepository.$deleteClustersFrom(lastBlock.height - 10);
|
||||
await AccelerationRepository.$deleteAccelerationsFrom(lastBlock.height - 10);
|
||||
this.blocks = this.blocks.slice(0, -10);
|
||||
this.updateTimerProgress(timer, `rolled back chain divergence from ${this.currentBlockHeight}`);
|
||||
for (let i = 10; i >= 0; --i) {
|
||||
@@ -974,6 +979,9 @@ class Blocks {
|
||||
if (this.blocks.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) {
|
||||
this.blocks = this.blocks.slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4);
|
||||
}
|
||||
blockSummary.transactions.forEach(tx => {
|
||||
delete tx.acc;
|
||||
});
|
||||
this.blockSummaries.push(blockSummary);
|
||||
if (this.blockSummaries.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) {
|
||||
this.blockSummaries = this.blockSummaries.slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4);
|
||||
@@ -1117,6 +1125,7 @@ class Blocks {
|
||||
}
|
||||
return {
|
||||
txid: tx.txid,
|
||||
time: tx.firstSeen,
|
||||
fee: tx.fee || 0,
|
||||
vsize: tx.vsize,
|
||||
value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0)),
|
||||
|
||||
@@ -12,32 +12,68 @@ export interface OrphanedBlock {
|
||||
height: number;
|
||||
hash: string;
|
||||
status: 'valid-fork' | 'valid-headers' | 'headers-only';
|
||||
prevhash: string;
|
||||
}
|
||||
|
||||
class ChainTips {
|
||||
private chainTips: ChainTip[] = [];
|
||||
private orphanedBlocks: OrphanedBlock[] = [];
|
||||
private orphanedBlocks: { [hash: string]: OrphanedBlock } = {};
|
||||
private blockCache: { [hash: string]: OrphanedBlock } = {};
|
||||
private orphansByHeight: { [height: number]: OrphanedBlock[] } = {};
|
||||
|
||||
public async updateOrphanedBlocks(): Promise<void> {
|
||||
try {
|
||||
this.chainTips = await bitcoinClient.getChainTips();
|
||||
this.orphanedBlocks = [];
|
||||
|
||||
const start = Date.now();
|
||||
const breakAt = start + 10000;
|
||||
let newOrphans = 0;
|
||||
this.orphanedBlocks = {};
|
||||
|
||||
for (const chain of this.chainTips) {
|
||||
if (chain.status === 'valid-fork' || chain.status === 'valid-headers') {
|
||||
let block = await bitcoinClient.getBlock(chain.hash);
|
||||
while (block && block.confirmations === -1) {
|
||||
this.orphanedBlocks.push({
|
||||
height: block.height,
|
||||
hash: block.hash,
|
||||
status: chain.status
|
||||
});
|
||||
block = await bitcoinClient.getBlock(block.previousblockhash);
|
||||
const orphans: OrphanedBlock[] = [];
|
||||
let hash = chain.hash;
|
||||
do {
|
||||
let orphan = this.blockCache[hash];
|
||||
if (!orphan) {
|
||||
const block = await bitcoinClient.getBlock(hash);
|
||||
if (block && block.confirmations === -1) {
|
||||
newOrphans++;
|
||||
orphan = {
|
||||
height: block.height,
|
||||
hash: block.hash,
|
||||
status: chain.status,
|
||||
prevhash: block.previousblockhash,
|
||||
};
|
||||
this.blockCache[hash] = orphan;
|
||||
}
|
||||
}
|
||||
if (orphan) {
|
||||
orphans.push(orphan);
|
||||
}
|
||||
hash = orphan?.prevhash;
|
||||
} while (hash && (Date.now() < breakAt));
|
||||
for (const orphan of orphans) {
|
||||
this.orphanedBlocks[orphan.hash] = orphan;
|
||||
}
|
||||
}
|
||||
if (Date.now() >= breakAt) {
|
||||
logger.debug(`Breaking orphaned blocks updater after 10s, will continue next block`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(`Updated orphaned blocks cache. Found ${this.orphanedBlocks.length} orphaned blocks`);
|
||||
this.orphansByHeight = {};
|
||||
const allOrphans = Object.values(this.orphanedBlocks);
|
||||
for (const orphan of allOrphans) {
|
||||
if (!this.orphansByHeight[orphan.height]) {
|
||||
this.orphansByHeight[orphan.height] = [];
|
||||
}
|
||||
this.orphansByHeight[orphan.height].push(orphan);
|
||||
}
|
||||
|
||||
logger.debug(`Updated orphaned blocks cache. Fetched ${newOrphans} new orphaned blocks. Total ${allOrphans.length}`);
|
||||
} catch (e) {
|
||||
logger.err(`Cannot get fetch orphaned blocks. Reason: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
@@ -48,13 +84,7 @@ class ChainTips {
|
||||
return [];
|
||||
}
|
||||
|
||||
const orphans: OrphanedBlock[] = [];
|
||||
for (const block of this.orphanedBlocks) {
|
||||
if (block.height === height) {
|
||||
orphans.push(block);
|
||||
}
|
||||
}
|
||||
return orphans;
|
||||
return this.orphansByHeight[height] || [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -373,6 +373,21 @@ export class Common {
|
||||
].includes(pubkey);
|
||||
}
|
||||
|
||||
static isInscription(vin, flags): bigint {
|
||||
// in taproot, if the last witness item begins with 0x50, it's an annex
|
||||
const hasAnnex = vin.witness?.[vin.witness.length - 1].startsWith('50');
|
||||
// script spends have more than one witness item, not counting the annex (if present)
|
||||
if (vin.witness.length > (hasAnnex ? 2 : 1)) {
|
||||
// the script itself is the second-to-last witness item, not counting the annex
|
||||
const asm = vin.inner_witnessscript_asm || transactionUtils.convertScriptSigAsm(vin.witness[vin.witness.length - (hasAnnex ? 3 : 2)]);
|
||||
// inscriptions smuggle data within an 'OP_0 OP_IF ... OP_ENDIF' envelope
|
||||
if (asm?.includes('OP_0 OP_IF')) {
|
||||
flags |= TransactionFlags.inscription;
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
static getTransactionFlags(tx: TransactionExtended): number {
|
||||
let flags = tx.flags ? BigInt(tx.flags) : 0n;
|
||||
|
||||
@@ -409,30 +424,31 @@ export class Common {
|
||||
if (vin.sequence < 0xfffffffe) {
|
||||
rbf = true;
|
||||
}
|
||||
switch (vin.prevout?.scriptpubkey_type) {
|
||||
case 'p2pk': flags |= TransactionFlags.p2pk; break;
|
||||
case 'multisig': flags |= TransactionFlags.p2ms; break;
|
||||
case 'p2pkh': flags |= TransactionFlags.p2pkh; break;
|
||||
case 'p2sh': flags |= TransactionFlags.p2sh; break;
|
||||
case 'v0_p2wpkh': flags |= TransactionFlags.p2wpkh; break;
|
||||
case 'v0_p2wsh': flags |= TransactionFlags.p2wsh; break;
|
||||
case 'v1_p2tr': {
|
||||
if (!vin.witness?.length) {
|
||||
throw new Error('Taproot input missing witness data');
|
||||
}
|
||||
flags |= TransactionFlags.p2tr;
|
||||
// in taproot, if the last witness item begins with 0x50, it's an annex
|
||||
const hasAnnex = vin.witness?.[vin.witness.length - 1].startsWith('50');
|
||||
// script spends have more than one witness item, not counting the annex (if present)
|
||||
if (vin.witness.length > (hasAnnex ? 2 : 1)) {
|
||||
// the script itself is the second-to-last witness item, not counting the annex
|
||||
const asm = vin.inner_witnessscript_asm || transactionUtils.convertScriptSigAsm(vin.witness[vin.witness.length - (hasAnnex ? 3 : 2)]);
|
||||
// inscriptions smuggle data within an 'OP_0 OP_IF ... OP_ENDIF' envelope
|
||||
if (asm?.includes('OP_0 OP_IF')) {
|
||||
flags |= TransactionFlags.inscription;
|
||||
if (vin.prevout?.scriptpubkey_type) {
|
||||
switch (vin.prevout?.scriptpubkey_type) {
|
||||
case 'p2pk': flags |= TransactionFlags.p2pk; break;
|
||||
case 'multisig': flags |= TransactionFlags.p2ms; break;
|
||||
case 'p2pkh': flags |= TransactionFlags.p2pkh; break;
|
||||
case 'p2sh': flags |= TransactionFlags.p2sh; break;
|
||||
case 'v0_p2wpkh': flags |= TransactionFlags.p2wpkh; break;
|
||||
case 'v0_p2wsh': flags |= TransactionFlags.p2wsh; break;
|
||||
case 'v1_p2tr': {
|
||||
if (!vin.witness?.length) {
|
||||
throw new Error('Taproot input missing witness data');
|
||||
}
|
||||
flags |= TransactionFlags.p2tr;
|
||||
flags = Common.isInscription(vin, flags);
|
||||
} break;
|
||||
}
|
||||
} else {
|
||||
// no prevouts, optimistically check witness-bearing inputs
|
||||
if (vin.witness?.length >= 2) {
|
||||
try {
|
||||
flags = Common.isInscription(vin, flags);
|
||||
} catch {
|
||||
// witness script parsing will fail if this isn't really a taproot output
|
||||
}
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
// sighash flags
|
||||
@@ -930,6 +946,33 @@ export class Common {
|
||||
return this.validateTransactionHex(matches[1].toLowerCase());
|
||||
}
|
||||
|
||||
static getTransactionsFromRequest(req: Request, limit: number = 25): string[] {
|
||||
if (!Array.isArray(req.body) || req.body.some(hex => typeof hex !== 'string')) {
|
||||
throw Object.assign(new Error('Invalid request body (should be an array of hexadecimal strings)'), { code: -1 });
|
||||
}
|
||||
|
||||
if (limit && req.body.length > limit) {
|
||||
throw Object.assign(new Error('Exceeded maximum of 25 transactions'), { code: -1 });
|
||||
}
|
||||
|
||||
const txs = req.body;
|
||||
|
||||
return txs.map(rawTx => {
|
||||
// Support both upper and lower case hex
|
||||
// Support both txHash= Form and direct API POST
|
||||
const reg = /^((?:[a-fA-F0-9]{2})+)$/;
|
||||
const matches = reg.exec(rawTx);
|
||||
if (!matches || !matches[1]) {
|
||||
throw Object.assign(new Error('Invalid hex string'), { code: -2 });
|
||||
}
|
||||
|
||||
// Guaranteed to be a hex string of multiple of 2
|
||||
// Guaranteed to be lower case
|
||||
// Guaranteed to pass validation (see function below)
|
||||
return this.validateTransactionHex(matches[1].toLowerCase());
|
||||
});
|
||||
}
|
||||
|
||||
private static validateTransactionHex(txhex: string): string {
|
||||
// Do not mutate txhex
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
|
||||
import { RowDataPacket } from 'mysql2';
|
||||
|
||||
class DatabaseMigration {
|
||||
private static currentVersion = 76;
|
||||
private static currentVersion = 78;
|
||||
private queryTimeout = 3600_000;
|
||||
private statisticsAddedIndexed = false;
|
||||
private uniqueLogs: string[] = [];
|
||||
@@ -652,6 +652,11 @@ class DatabaseMigration {
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `THB` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `TRY` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `ZAR` float DEFAULT "-1"');
|
||||
|
||||
await this.$executeQuery('TRUNCATE hashrates');
|
||||
await this.$executeQuery('TRUNCATE difficulty_adjustments');
|
||||
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
|
||||
|
||||
await this.updateToSchemaVersion(75);
|
||||
}
|
||||
|
||||
@@ -659,6 +664,16 @@ class DatabaseMigration {
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD prioritized_txs JSON DEFAULT "[]"');
|
||||
await this.updateToSchemaVersion(76);
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 77 && config.MEMPOOL.NETWORK === 'mainnet') {
|
||||
await this.$executeQuery('ALTER TABLE `accelerations` ADD requested datetime DEFAULT NULL');
|
||||
await this.updateToSchemaVersion(77);
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 78) {
|
||||
await this.$executeQuery('ALTER TABLE `prices` CHANGE `time` `time` datetime NOT NULL');
|
||||
await this.updateToSchemaVersion(78);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -54,9 +54,11 @@ class ChannelsRoutes {
|
||||
|
||||
if (index < -1) {
|
||||
res.status(400).send('Invalid index');
|
||||
return;
|
||||
}
|
||||
if (['open', 'active', 'closed'].includes(status) === false) {
|
||||
res.status(400).send('Invalid status');
|
||||
return;
|
||||
}
|
||||
|
||||
const channels = await channelsApi.$getChannelsForNode(req.query.public_key, index, 10, status);
|
||||
|
||||
@@ -666,7 +666,9 @@ class NodesApi {
|
||||
node.last_update = null;
|
||||
}
|
||||
|
||||
const sockets = (node.addresses?.map(a => a.addr).join(',')) ?? '';
|
||||
const uniqueAddr = [...new Set(node.addresses?.map(a => a.addr))];
|
||||
const formattedSockets = (uniqueAddr.join(',')) ?? '';
|
||||
|
||||
const query = `INSERT INTO nodes(
|
||||
public_key,
|
||||
first_seen,
|
||||
@@ -695,13 +697,13 @@ class NodesApi {
|
||||
node.alias,
|
||||
this.aliasToSearchText(node.alias),
|
||||
node.color,
|
||||
sockets,
|
||||
formattedSockets,
|
||||
JSON.stringify(node.features),
|
||||
node.last_update,
|
||||
node.alias,
|
||||
this.aliasToSearchText(node.alias),
|
||||
node.color,
|
||||
sockets,
|
||||
formattedSockets,
|
||||
JSON.stringify(node.features),
|
||||
]);
|
||||
} catch (e) {
|
||||
@@ -713,7 +715,9 @@ class NodesApi {
|
||||
* Update node sockets
|
||||
*/
|
||||
public async $updateNodeSockets(publicKey: string, sockets: {network: string; addr: string}[]): Promise<void> {
|
||||
const formattedSockets = (sockets.map(a => a.addr).join(',')) ?? '';
|
||||
const uniqueAddr = [...new Set(sockets.map(a => a.addr))];
|
||||
|
||||
const formattedSockets = (uniqueAddr.join(',')) ?? '';
|
||||
try {
|
||||
await DB.query(`UPDATE nodes SET sockets = ? WHERE public_key = ?`, [formattedSockets, publicKey]);
|
||||
} catch (e) {
|
||||
|
||||
@@ -48,6 +48,14 @@ class NodesRoutes {
|
||||
'032850492ee61a5f7006a2fda6925e4b4ec3782f2b6de2ff0e439ef5a38c3b2470',
|
||||
'022c80bace98831c44c32fb69755f2b353434e0ee9e7fbda29507f7ef8abea1421',
|
||||
'02c3559c833e6f99f9ca05fe503e0b4e7524dea9121344edfd3e811101e0c28680',
|
||||
'02b36a324fa2dd3af2a63ac65f241907882829bed5002b4e14171d25c219e0d470',
|
||||
'0231b6e8f21f9f6c057f6bf8a812f79e396ee16a66ece91939a1576ce9fb9e87a5',
|
||||
'034b6aac206bffcbd651b7ead1ab8a0991c945dfafe19ff27dcdeadc6843ebd15c',
|
||||
'039c065f7e344acd969ebdd4a94550915b6f24e8782ae2be540bb96c8a4fcfb86b',
|
||||
'03d9f9f4803fc75920f14dd13d83fbecc53229a65d4ee4cd2d86fdf211f7337576',
|
||||
'0357fe48c4dece744f70865eda66e396aab5d05e09e1145cd3b7da83f11446d4cf',
|
||||
'02bca4d642eda631f2c8659758e2a2868e518b93503f2bfcd767749c6530a10679',
|
||||
'03f32c99c0bb9f62dae53671d1d300565773455248f34134cc02779b881561174e',
|
||||
'032c7c7819276c4f706a04df1a0f1e10a5495994a7be4c1d3d28ca766e5a2b957b',
|
||||
'025a7e38c2834dd843591a4d23d5f09cdeb77ddca85f673c2d944a14220ff14cf7',
|
||||
'0395e2731a1673ef21d7a16a727c4fc4d4c35a861c428ce2c819c53d2b81c8bd55',
|
||||
@@ -60,6 +68,14 @@ class NodesRoutes {
|
||||
'039c14fdec2d958e3d14cebf657451bbd9e039196615785e82c917f274e3fb2205',
|
||||
'033589bbcb233ffc416cefd5437c7f37e9d7cb7942d405e39e72c4c846d9b37f18',
|
||||
'029293110441c6e2eacb57e1255bf6ef05c41a6a676fe474922d33c19f98a7d584',
|
||||
'038eb09bed4532ff36d12acc1279f55cbe8d95212d19f809e057bb50de00051fba',
|
||||
'027b7c0278366a0268e8bd0072b14539f6cb455a7bd588ae22d888bed541f65311',
|
||||
'02f4dd78f6eda8838029b2cdbaaea6e875e2fa373cd348ee41a7c1bb177d3fca66',
|
||||
'036b3fb692da214a3edaac5b67903b958f5ccd8712e09aa61b67ea7acfd94b40c2',
|
||||
'023bc8915d308e0b65f8de6867f95960141372436fce3edad5cec3f364d6ac948f',
|
||||
'0341690503ef21d0e203dddd9e62646380d0dfc32c499e055e7f698b9064d1c736',
|
||||
'0355d573805c018a37a5b2288378d70e9b5b438f7394abd6f467cb9b47c90eeb93',
|
||||
'0361aa68deb561a8b47b41165848edcccb98a1b56a5ea922d9d5b30a09bb7282ea',
|
||||
'0235ad0b56ed8c42c4354444c24e971c05e769ec0b5fb0ccea42880095dc02ea2c',
|
||||
'029700819a37afea630f80e6cc461f3fd3c4ace2598a21cfbbe64d1c78d0ee69a5',
|
||||
'02c2d8b2dbf87c7894af2f1d321290e2fe6db5446cd35323987cee98f06e2e0075',
|
||||
@@ -76,6 +92,14 @@ class NodesRoutes {
|
||||
'0243348cb3741cfe2d8485fa8375c29c7bc7cbb67577c363cb6987a5e5fd0052cc',
|
||||
'02cb73e631af44bee600d80f8488a9194c9dc5c7590e575c421a070d1be05bc8e9',
|
||||
'0306f55ee631aa1e2cd4d9b2bfcbc14404faec5c541cef8b2e6f779061029d09c4',
|
||||
'030bbbd8495561a894e301fe6ba5b22f8941fc661cc0e673e0206158231d8ac130',
|
||||
'03ee1f08e516ed083475f39c6cae4fa1eec686d004d2f105218269e27d7f2da5a4',
|
||||
'028c378b998f476ed22d6815c170dd2a3388a43fdf791a7cff70b9997349b8447a',
|
||||
'036f19f044d19cb1b04f14d91b6e7e5443ce337217a8c14d43861f3e86dd07bd7f',
|
||||
'03058d61869e8b88436493648b2e3e530627edf5a0b253c285cd565c1477a5c237',
|
||||
'0279dfedc87b47a941f1797f2c422c03aa3108914ea6b519d76537d60860535a9a',
|
||||
'0353486b8016761e58ec8aee7305ee58d5dc66b55ef5bd8cbaf49508f66d52d62e',
|
||||
'03df5db8eccfabcae47ff15553cfdecb2d3f56979f43a0c3578f28d056b5e35104',
|
||||
'03ddab321b760433cbf561b615ef62ac7d318630c5f51d523aaf5395b90b751956',
|
||||
'033d92c7bfd213ef1b34c90e985fb5dc77f9ec2409d391492484e57a44c4aca1de',
|
||||
'02ad010dda54253c1eb9efe38b0760657a3b43ecad62198c359c051c9d99d45781',
|
||||
@@ -88,6 +112,14 @@ class NodesRoutes {
|
||||
'038310e3a786340f2bd7770704c7ccfe560fd163d9a1c99d67894597419d12cbf7',
|
||||
'03e5e9d879b72c7d67ecd483bae023bd33e695bb32b981a4021260f7b9d62bc761',
|
||||
'028d16e1a0ace4c0c0a421536d8d32ce484dfe6e2f726b7b0e7c30f12a195f8cc7',
|
||||
'0326cf9a4ca67a5b9cdffae57293dbd6f7c5113b93010dc6f6fe4af3afde1a1739',
|
||||
'034867e16f62cebb8c2c2c22b91117c173bbece9c8a1e5bd001374a3699551cd8f',
|
||||
'038dfb1f1b637a8c27e342ffc6f9feca20e0b47be3244e09ae78df4998e2ae83b9',
|
||||
'03cb1cea3394d973355c11bc61c2f689f9d3e1c3db60d205f27770f5ad83200f77',
|
||||
'03535447b592cbdb153189b3e06a455452b1011380cb3e6511a31090c15d8efc9f',
|
||||
'028e90e9984d262ebfa3c23fb3f335a2ae061a0bdedee03f45f72b438d9e7d2ce3',
|
||||
'03ee0176289dc4a6111fa5ef22eed5273758c420fbe58cc1d2d76def75dd7e640c',
|
||||
'0370b2cd9f0eaf436d5c25c93fb39210d8cc06b31f688fc2f54418aabe394aed79',
|
||||
'02ff690d06c187ab994bf83c5a2114fe5bf50112c2c817af0f788f736be9fa2070',
|
||||
'02a9f570c51a2526a5ee85802e88f9281bed771eb66a0c8a7d898430dd5d0eae45',
|
||||
'038c3de773255d3bd7a50e31e58d423baac5c90826a74d75e64b74c95475de1097',
|
||||
@@ -104,6 +136,14 @@ class NodesRoutes {
|
||||
'03229ab4b7f692753e094b93df90530150680f86b535b5183b0cffd75b3df583fc',
|
||||
'03a696eb7acde991c1be97a58a9daef416659539ae462b897f5e9ae361f990228e',
|
||||
'0248bf26cf3a63ab8870f34dc0ec9e6c8c6288cdba96ba3f026f34ec0f13ac4055',
|
||||
'021b28ecdd782fd909705d6be354db268977b1a2ac5a5275186fc19e08bb8fca93',
|
||||
'031bec1fbd8eb7fe94d2bda108c9c3cc8c22ecfc1c3a5c11d36f5881b01b4a81a6',
|
||||
'03879c4f827a3188574d5757e002f574265a966d70aea942169785b31369b067d5',
|
||||
'0228d4b5a4fd73a03967b76f8b8cb37b9d0b6e7039126a9397bb732c15bed78e9b',
|
||||
'03f58dbb629f4427f5a1dbc02e6a7ec79345fdf13a0e4163d4f3b7aea2539cf095',
|
||||
'021cdcb8123aa670cdfc9f43909dbb297363c093883409e9e7fc82e7267f7c72bd',
|
||||
'02f2aa2c2b7b432a70dc4d0b04afa19d48715ed3b90594d49c1c8744f2e9ebb030',
|
||||
'03709a02fb3ab4857689a8ea0bd489a6ab6f56f8a397be578bc6d5ad22efbe3756',
|
||||
'03fbc17549ec667bccf397ababbcb4cdc0e3394345e4773079ab2774612ec9be61',
|
||||
'03da9a8623241ccf95f19cd645c6cecd4019ac91570e976eb0a128bebbc4d8a437',
|
||||
'03ca5340cf85cb2e7cf076e489f785410838de174e40be62723e8a60972ad75144',
|
||||
@@ -116,6 +156,14 @@ class NodesRoutes {
|
||||
'02b6b1640fe029e304c216951af9fbefdb23b0bdc9baaf327540d31b6107841fdf',
|
||||
'03694289827203a5b3156d753071ddd5bf92e371f5a462943f9555eef6d2d6606c',
|
||||
'0283d850db7c3e8ea7cc9c4abc7afaab12bbdf72b677dcba1d608350d2537d7d43',
|
||||
'03b4dda7878d3b7b71ecd6d4738322c7f9a9c1fb583374d2724f4ccc4947f37570',
|
||||
'0279a35f05b5acf159429549e56fd426685c4fec191431c58738968bbc77a39f25',
|
||||
'03cb102d796ddcf08610cd03fae8b7a1df69ff48e9e8a152af315f9edf71762eb8',
|
||||
'036b89526f4d5ac4c317f4fd23cb9f8e4ad844498bc7950a41114d060101d995d4',
|
||||
'0313eade145959d7036db009fd5b0bf1947a739c7c3c790b491ec9161b94e6ad1e',
|
||||
'02b670ca4c4bb2c5ea89c3b691da98a194cfc48fcd5c072df02a20290bddd60610',
|
||||
'02a9196d5e08598211397a83cf013a5962b84bd61198abfdd204dff987e54f7a0d',
|
||||
'036d015cd2f486fb38348182980b7e596e6c9733873102ea126fed7b4152be03b8',
|
||||
'02521287789f851268a39c9eccc9d6180d2c614315b583c9e6ae0addbd6d79df06',
|
||||
'0258c2a7b7f8af2585b4411b1ec945f70988f30412bb1df179de941f14d0b1bc3e',
|
||||
'03c3389ff1a896f84d921ed01a19fc99c6724ce8dc4b960cd3b7b2362b62cd60d7',
|
||||
|
||||
@@ -343,7 +343,7 @@ class MempoolBlocks {
|
||||
if (txid in mempool) {
|
||||
mempool[txid].cpfpDirty = (rate !== mempool[txid].effectiveFeePerVsize);
|
||||
mempool[txid].effectiveFeePerVsize = rate;
|
||||
mempool[txid].cpfpChecked = false;
|
||||
mempool[txid].cpfpChecked = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -404,6 +404,10 @@ class Mempool {
|
||||
|
||||
const newAccelerationMap: { [txid: string]: Acceleration } = {};
|
||||
for (const acceleration of newAccelerations) {
|
||||
// skip transactions we don't know about
|
||||
if (!this.mempoolCache[acceleration.txid]) {
|
||||
continue;
|
||||
}
|
||||
newAccelerationMap[acceleration.txid] = acceleration;
|
||||
if (this.accelerations[acceleration.txid] == null) {
|
||||
// new acceleration
|
||||
|
||||
@@ -5,6 +5,9 @@ import axios from 'axios';
|
||||
|
||||
export interface Acceleration {
|
||||
txid: string,
|
||||
added: number,
|
||||
effectiveVsize: number,
|
||||
effectiveFee: number,
|
||||
feeDelta: number,
|
||||
pools: number[],
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as WebSocket from 'ws';
|
||||
import {
|
||||
BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse,
|
||||
OptimizedStatistic, ILoadingIndicators, GbtCandidates, TxTrackingInfo,
|
||||
MempoolBlockDelta, MempoolDelta, MempoolDeltaTxids
|
||||
} from '../mempool.interfaces';
|
||||
import blocks from './blocks';
|
||||
import memPool from './mempool';
|
||||
@@ -44,7 +45,7 @@ const wantable = [
|
||||
];
|
||||
|
||||
class WebsocketHandler {
|
||||
private wss: WebSocket.Server | undefined;
|
||||
private webSocketServers: WebSocket.Server[] = [];
|
||||
private extraInitProperties = {};
|
||||
|
||||
private numClients = 0;
|
||||
@@ -54,11 +55,12 @@ class WebsocketHandler {
|
||||
private socketData: { [key: string]: string } = {};
|
||||
private serializedInitData: string = '{}';
|
||||
private lastRbfSummary: ReplacementInfo[] | null = null;
|
||||
private mempoolSequence: number = 0;
|
||||
|
||||
constructor() { }
|
||||
|
||||
setWebsocketServer(wss: WebSocket.Server) {
|
||||
this.wss = wss;
|
||||
addWebsocketServer(wss: WebSocket.Server) {
|
||||
this.webSocketServers.push(wss);
|
||||
}
|
||||
|
||||
setExtraInitData(property: string, value: any) {
|
||||
@@ -102,11 +104,13 @@ class WebsocketHandler {
|
||||
}
|
||||
|
||||
setupConnectionHandling() {
|
||||
if (!this.wss) {
|
||||
throw new Error('WebSocket.Server is not set');
|
||||
if (!this.webSocketServers.length) {
|
||||
throw new Error('No WebSocket.Server have been set');
|
||||
}
|
||||
|
||||
this.wss.on('connection', (client: WebSocket, req) => {
|
||||
// TODO - Fix indentation after PR is merged
|
||||
for (const server of this.webSocketServers) {
|
||||
server.on('connection', (client: WebSocket, req) => {
|
||||
this.numConnected++;
|
||||
client['remoteAddress'] = req.headers['x-forwarded-for'] || req.socket?.remoteAddress || 'unknown';
|
||||
client.on('error', (e) => {
|
||||
@@ -315,6 +319,7 @@ class WebsocketHandler {
|
||||
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
|
||||
response['projected-block-transactions'] = JSON.stringify({
|
||||
index: index,
|
||||
sequence: this.mempoolSequence,
|
||||
blockTransactions: (mBlocksWithTransactions[index]?.transactions || []).map(mempoolBlocks.compressTx),
|
||||
});
|
||||
} else {
|
||||
@@ -342,6 +347,17 @@ class WebsocketHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedMessage && parsedMessage['track-accelerations'] != null) {
|
||||
if (parsedMessage['track-accelerations']) {
|
||||
client['track-accelerations'] = true;
|
||||
response['accelerations'] = JSON.stringify({
|
||||
accelerations: Object.values(memPool.getAccelerations()),
|
||||
});
|
||||
} else {
|
||||
client['track-accelerations'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedMessage.action === 'init') {
|
||||
if (!this.socketData['blocks']?.length || !this.socketData['da'] || !this.socketData['backendInfo'] || !this.socketData['conversions']) {
|
||||
this.updateSocketData();
|
||||
@@ -360,12 +376,16 @@ 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 (parsedMessage['track-mempool-txids'] === true) {
|
||||
client['track-mempool-txids'] = true;
|
||||
} else if (parsedMessage['track-mempool-txids'] === false) {
|
||||
delete client['track-mempool-txids'];
|
||||
}
|
||||
|
||||
if (parsedMessage['track-mempool'] === true) {
|
||||
client['track-mempool'] = true;
|
||||
} else if (parsedMessage['track-mempool'] === false) {
|
||||
delete client['track-mempool'];
|
||||
}
|
||||
|
||||
if (Object.keys(response).length) {
|
||||
@@ -377,14 +397,17 @@ class WebsocketHandler {
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleNewDonation(id: string) {
|
||||
if (!this.wss) {
|
||||
throw new Error('WebSocket.Server is not set');
|
||||
if (!this.webSocketServers.length) {
|
||||
throw new Error('No WebSocket.Server have been set');
|
||||
}
|
||||
|
||||
this.wss.clients.forEach((client) => {
|
||||
// TODO - Fix indentation after PR is merged
|
||||
for (const server of this.webSocketServers) {
|
||||
server.clients.forEach((client) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
@@ -392,43 +415,50 @@ class WebsocketHandler {
|
||||
client.send(JSON.stringify({ donationConfirmed: true }));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadingChanged(indicators: ILoadingIndicators) {
|
||||
if (!this.wss) {
|
||||
throw new Error('WebSocket.Server is not set');
|
||||
if (!this.webSocketServers.length) {
|
||||
throw new Error('No WebSocket.Server have been set');
|
||||
}
|
||||
|
||||
this.updateSocketDataFields({ 'loadingIndicators': indicators });
|
||||
|
||||
const response = JSON.stringify({ loadingIndicators: indicators });
|
||||
this.wss.clients.forEach((client) => {
|
||||
// TODO - Fix indentation after PR is merged
|
||||
for (const server of this.webSocketServers) {
|
||||
server.clients.forEach((client) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
client.send(response);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleNewConversionRates(conversionRates: ApiPrice) {
|
||||
if (!this.wss) {
|
||||
throw new Error('WebSocket.Server is not set');
|
||||
if (!this.webSocketServers.length) {
|
||||
throw new Error('No WebSocket.Server have been set');
|
||||
}
|
||||
|
||||
this.updateSocketDataFields({ 'conversions': conversionRates });
|
||||
|
||||
const response = JSON.stringify({ conversions: conversionRates });
|
||||
this.wss.clients.forEach((client) => {
|
||||
// TODO - Fix indentation after PR is merged
|
||||
for (const server of this.webSocketServers) {
|
||||
server.clients.forEach((client) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
client.send(response);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleNewStatistic(stats: OptimizedStatistic) {
|
||||
if (!this.wss) {
|
||||
throw new Error('WebSocket.Server is not set');
|
||||
if (!this.webSocketServers.length) {
|
||||
throw new Error('No WebSocket.Server have been set');
|
||||
}
|
||||
|
||||
this.printLogs();
|
||||
@@ -437,7 +467,9 @@ class WebsocketHandler {
|
||||
'live-2h-chart': stats
|
||||
});
|
||||
|
||||
this.wss.clients.forEach((client) => {
|
||||
// TODO - Fix indentation after PR is merged
|
||||
for (const server of this.webSocketServers) {
|
||||
server.clients.forEach((client) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
@@ -448,11 +480,12 @@ class WebsocketHandler {
|
||||
|
||||
client.send(response);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleReorg(): void {
|
||||
if (!this.wss) {
|
||||
throw new Error('WebSocket.Server is not set');
|
||||
if (!this.webSocketServers.length) {
|
||||
throw new Error('No WebSocket.Server have been set');
|
||||
}
|
||||
|
||||
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||
@@ -463,7 +496,9 @@ class WebsocketHandler {
|
||||
'da': da?.previousTime ? da : undefined,
|
||||
});
|
||||
|
||||
this.wss.clients.forEach((client) => {
|
||||
// TODO - Fix indentation after PR is merged
|
||||
for (const server of this.webSocketServers) {
|
||||
server.clients.forEach((client) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
@@ -481,13 +516,14 @@ class WebsocketHandler {
|
||||
client.send(this.serializeResponse(response));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number,
|
||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[],
|
||||
candidates?: GbtCandidates): Promise<void> {
|
||||
if (!this.wss) {
|
||||
throw new Error('WebSocket.Server is not set');
|
||||
if (!this.webSocketServers.length) {
|
||||
throw new Error('No WebSocket.Server have been set');
|
||||
}
|
||||
|
||||
this.printLogs();
|
||||
@@ -512,6 +548,7 @@ class WebsocketHandler {
|
||||
const vBytesPerSecond = memPool.getVBytesPerSecond();
|
||||
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
|
||||
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||
const accelerations = memPool.getAccelerations();
|
||||
memPool.handleRbfTransactions(rbfTransactions);
|
||||
const rbfChanges = rbfCache.getRbfChanges();
|
||||
let rbfReplacements;
|
||||
@@ -533,6 +570,33 @@ class WebsocketHandler {
|
||||
|
||||
const latestTransactions = memPool.getLatestTransactions();
|
||||
|
||||
if (memPool.isInSync()) {
|
||||
this.mempoolSequence++;
|
||||
}
|
||||
|
||||
const replacedTransactions: { replaced: string, by: TransactionExtended }[] = [];
|
||||
for (const tx of newTransactions) {
|
||||
if (rbfTransactions[tx.txid]) {
|
||||
for (const replaced of rbfTransactions[tx.txid]) {
|
||||
replacedTransactions.push({ replaced: replaced.txid, by: tx });
|
||||
}
|
||||
}
|
||||
}
|
||||
const mempoolDeltaTxids: MempoolDeltaTxids = {
|
||||
sequence: this.mempoolSequence,
|
||||
added: newTransactions.map(tx => tx.txid),
|
||||
removed: deletedTransactions.map(tx => tx.txid),
|
||||
mined: [],
|
||||
replaced: replacedTransactions.map(replacement => ({ replaced: replacement.replaced, by: replacement.by.txid })),
|
||||
};
|
||||
const mempoolDelta: MempoolDelta = {
|
||||
sequence: this.mempoolSequence,
|
||||
added: newTransactions,
|
||||
removed: deletedTransactions.map(tx => tx.txid),
|
||||
mined: [],
|
||||
replaced: replacedTransactions,
|
||||
};
|
||||
|
||||
// update init data
|
||||
const socketDataFields = {
|
||||
'mempoolInfo': mempoolInfo,
|
||||
@@ -560,7 +624,9 @@ class WebsocketHandler {
|
||||
// pre-compute new tracked outspends
|
||||
const outspendCache: { [txid: string]: { [vout: number]: { vin: number, txid: string } } } = {};
|
||||
const trackedTxs = new Set<string>();
|
||||
this.wss.clients.forEach((client) => {
|
||||
// TODO - Fix indentation after PR is merged
|
||||
for (const server of this.webSocketServers) {
|
||||
server.clients.forEach((client) => {
|
||||
if (client['track-tx']) {
|
||||
trackedTxs.add(client['track-tx']);
|
||||
}
|
||||
@@ -570,6 +636,7 @@ class WebsocketHandler {
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (trackedTxs.size > 0) {
|
||||
for (const tx of newTransactions) {
|
||||
for (let i = 0; i < tx.vin.length; i++) {
|
||||
@@ -589,7 +656,15 @@ class WebsocketHandler {
|
||||
const addressCache = this.makeAddressCache(newTransactions);
|
||||
const removedAddressCache = this.makeAddressCache(deletedTransactions);
|
||||
|
||||
this.wss.clients.forEach(async (client) => {
|
||||
// pre-compute acceleration delta
|
||||
const accelerationUpdate = {
|
||||
added: accelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
|
||||
removed: accelerationDelta.filter(txid => !accelerations[txid]),
|
||||
};
|
||||
|
||||
// TODO - Fix indentation after PR is merged
|
||||
for (const server of this.webSocketServers) {
|
||||
server.clients.forEach(async (client) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
@@ -747,7 +822,7 @@ class WebsocketHandler {
|
||||
accelerated: mempoolTx.acceleration || undefined,
|
||||
}
|
||||
};
|
||||
if (!mempoolTx.cpfpChecked) {
|
||||
if (!mempoolTx.cpfpChecked && !mempoolTx.acceleration) {
|
||||
calculateCpfp(mempoolTx, newMempool);
|
||||
}
|
||||
if (mempoolTx.cpfpDirty) {
|
||||
@@ -810,6 +885,7 @@ class WebsocketHandler {
|
||||
if (mBlockDeltas[index]) {
|
||||
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-${index}`, {
|
||||
index: index,
|
||||
sequence: this.mempoolSequence,
|
||||
delta: mBlockDeltas[index],
|
||||
});
|
||||
}
|
||||
@@ -825,15 +901,28 @@ class WebsocketHandler {
|
||||
response['rbfLatestSummary'] = getCachedResponse('rbfLatestSummary', rbfSummary);
|
||||
}
|
||||
|
||||
if (client['track-mempool-txids']) {
|
||||
response['mempool-txids'] = getCachedResponse('mempool-txids', mempoolDeltaTxids);
|
||||
}
|
||||
|
||||
if (client['track-mempool']) {
|
||||
response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta);
|
||||
}
|
||||
|
||||
if (client['track-accelerations'] && (accelerationUpdate.added.length || accelerationUpdate.removed.length)) {
|
||||
response['accelerations'] = getCachedResponse('accelerations', accelerationUpdate);
|
||||
}
|
||||
|
||||
if (Object.keys(response).length) {
|
||||
client.send(this.serializeResponse(response));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async handleNewBlock(block: BlockExtended, txIds: string[], transactions: MempoolTransactionExtended[]): Promise<void> {
|
||||
if (!this.wss) {
|
||||
throw new Error('WebSocket.Server is not set');
|
||||
if (!this.webSocketServers.length) {
|
||||
throw new Error('No WebSocket.Server have been set');
|
||||
}
|
||||
|
||||
this.printLogs();
|
||||
@@ -969,6 +1058,31 @@ class WebsocketHandler {
|
||||
|
||||
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
|
||||
|
||||
if (memPool.isInSync()) {
|
||||
this.mempoolSequence++;
|
||||
}
|
||||
|
||||
const replacedTransactions: { replaced: string, by: TransactionExtended }[] = [];
|
||||
for (const txid of Object.keys(rbfTransactions)) {
|
||||
for (const replaced of rbfTransactions[txid].replaced) {
|
||||
replacedTransactions.push({ replaced: replaced.txid, by: rbfTransactions[txid].replacedBy });
|
||||
}
|
||||
}
|
||||
const mempoolDeltaTxids: MempoolDeltaTxids = {
|
||||
sequence: this.mempoolSequence,
|
||||
added: [],
|
||||
removed: [],
|
||||
mined: transactions.map(tx => tx.txid),
|
||||
replaced: replacedTransactions.map(replacement => ({ replaced: replacement.replaced, by: replacement.by.txid })),
|
||||
};
|
||||
const mempoolDelta: MempoolDelta = {
|
||||
sequence: this.mempoolSequence,
|
||||
added: [],
|
||||
removed: [],
|
||||
mined: transactions.map(tx => tx.txid),
|
||||
replaced: replacedTransactions,
|
||||
};
|
||||
|
||||
const responseCache = { ...this.socketData };
|
||||
function getCachedResponse(key, data): string {
|
||||
if (!responseCache[key]) {
|
||||
@@ -977,7 +1091,9 @@ class WebsocketHandler {
|
||||
return responseCache[key];
|
||||
}
|
||||
|
||||
this.wss.clients.forEach((client) => {
|
||||
// TODO - Fix indentation after PR is merged
|
||||
for (const server of this.webSocketServers) {
|
||||
server.clients.forEach((client) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
@@ -1143,21 +1259,32 @@ class WebsocketHandler {
|
||||
if (mBlockDeltas[index].added.length > (mBlocksWithTransactions[index]?.transactions.length / 2)) {
|
||||
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-full-${index}`, {
|
||||
index: index,
|
||||
sequence: this.mempoolSequence,
|
||||
blockTransactions: mBlocksWithTransactions[index].transactions.map(mempoolBlocks.compressTx),
|
||||
});
|
||||
} else {
|
||||
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-delta-${index}`, {
|
||||
index: index,
|
||||
sequence: this.mempoolSequence,
|
||||
delta: mBlockDeltas[index],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (client['track-mempool-txids']) {
|
||||
response['mempool-txids'] = getCachedResponse('mempool-txids', mempoolDeltaTxids);
|
||||
}
|
||||
|
||||
if (client['track-mempool']) {
|
||||
response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta);
|
||||
}
|
||||
|
||||
if (Object.keys(response).length) {
|
||||
client.send(this.serializeResponse(response));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await statistics.runStatistics();
|
||||
}
|
||||
@@ -1239,13 +1366,15 @@ class WebsocketHandler {
|
||||
}
|
||||
|
||||
private printLogs(): void {
|
||||
if (this.wss) {
|
||||
if (this.webSocketServers.length) {
|
||||
let numTxSubs = 0;
|
||||
let numTxsSubs = 0;
|
||||
let numProjectedSubs = 0;
|
||||
let numRbfSubs = 0;
|
||||
|
||||
this.wss.clients.forEach((client) => {
|
||||
// TODO - Fix indentation after PR is merged
|
||||
for (const server of this.webSocketServers) {
|
||||
server.clients.forEach((client) => {
|
||||
if (client['track-tx']) {
|
||||
numTxSubs++;
|
||||
}
|
||||
@@ -1259,8 +1388,12 @@ class WebsocketHandler {
|
||||
numRbfSubs++;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const count = this.wss?.clients?.size || 0;
|
||||
let count = 0;
|
||||
for (const server of this.webSocketServers) {
|
||||
count += server.clients?.size || 0;
|
||||
}
|
||||
const diff = count - this.numClients;
|
||||
this.numClients = count;
|
||||
logger.debug(`${count} websocket clients | ${this.numConnected} connected | ${this.numDisconnected} disconnected | (${diff >= 0 ? '+' : ''}${diff})`);
|
||||
|
||||
@@ -9,6 +9,7 @@ interface IConfig {
|
||||
NETWORK: 'mainnet' | 'testnet' | 'signet' | 'liquid' | 'liquidtestnet';
|
||||
BACKEND: 'esplora' | 'electrum' | 'none';
|
||||
HTTP_PORT: number;
|
||||
UNIX_SOCKET_PATH: string;
|
||||
SPAWN_CLUSTER_PROCS: number;
|
||||
API_URL_PREFIX: string;
|
||||
POLL_RATE_MS: number;
|
||||
@@ -116,10 +117,6 @@ interface IConfig {
|
||||
ENABLED: boolean;
|
||||
TX_PER_SECOND_SAMPLE_PERIOD: number;
|
||||
};
|
||||
BISQ: {
|
||||
ENABLED: boolean;
|
||||
DATA_PATH: string;
|
||||
};
|
||||
SOCKS5PROXY: {
|
||||
ENABLED: boolean;
|
||||
USE_ONION: boolean;
|
||||
@@ -133,8 +130,6 @@ interface IConfig {
|
||||
MEMPOOL_ONION: string;
|
||||
LIQUID_API: string;
|
||||
LIQUID_ONION: string;
|
||||
BISQ_URL: string;
|
||||
BISQ_ONION: string;
|
||||
};
|
||||
MAXMIND: {
|
||||
ENABLED: boolean;
|
||||
@@ -159,6 +154,7 @@ interface IConfig {
|
||||
},
|
||||
FIAT_PRICE: {
|
||||
ENABLED: boolean;
|
||||
PAID: boolean;
|
||||
API_KEY: string;
|
||||
},
|
||||
}
|
||||
@@ -170,6 +166,7 @@ const defaults: IConfig = {
|
||||
'NETWORK': 'mainnet',
|
||||
'BACKEND': 'none',
|
||||
'HTTP_PORT': 8999,
|
||||
'UNIX_SOCKET_PATH': '',
|
||||
'SPAWN_CLUSTER_PROCS': 0,
|
||||
'API_URL_PREFIX': '/api/v1/',
|
||||
'POLL_RATE_MS': 2000,
|
||||
@@ -258,10 +255,6 @@ const defaults: IConfig = {
|
||||
'ENABLED': true,
|
||||
'TX_PER_SECOND_SAMPLE_PERIOD': 150
|
||||
},
|
||||
'BISQ': {
|
||||
'ENABLED': false,
|
||||
'DATA_PATH': '/bisq/statsnode-data/btc_mainnet/db'
|
||||
},
|
||||
'LIGHTNING': {
|
||||
'ENABLED': false,
|
||||
'BACKEND': 'lnd',
|
||||
@@ -293,9 +286,7 @@ const defaults: IConfig = {
|
||||
'MEMPOOL_API': 'https://mempool.space/api/v1',
|
||||
'MEMPOOL_ONION': 'http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1',
|
||||
'LIQUID_API': 'https://liquid.network/api/v1',
|
||||
'LIQUID_ONION': 'http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1',
|
||||
'BISQ_URL': 'https://bisq.markets/api',
|
||||
'BISQ_ONION': 'http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api'
|
||||
'LIQUID_ONION': 'http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1'
|
||||
},
|
||||
'MAXMIND': {
|
||||
'ENABLED': false,
|
||||
@@ -320,6 +311,7 @@ const defaults: IConfig = {
|
||||
},
|
||||
'FIAT_PRICE': {
|
||||
'ENABLED': true,
|
||||
'PAID': false,
|
||||
'API_KEY': '',
|
||||
},
|
||||
};
|
||||
@@ -333,7 +325,6 @@ class Config implements IConfig {
|
||||
DATABASE: IConfig['DATABASE'];
|
||||
SYSLOG: IConfig['SYSLOG'];
|
||||
STATISTICS: IConfig['STATISTICS'];
|
||||
BISQ: IConfig['BISQ'];
|
||||
LIGHTNING: IConfig['LIGHTNING'];
|
||||
LND: IConfig['LND'];
|
||||
CLIGHTNING: IConfig['CLIGHTNING'];
|
||||
@@ -355,7 +346,6 @@ class Config implements IConfig {
|
||||
this.DATABASE = configs.DATABASE;
|
||||
this.SYSLOG = configs.SYSLOG;
|
||||
this.STATISTICS = configs.STATISTICS;
|
||||
this.BISQ = configs.BISQ;
|
||||
this.LIGHTNING = configs.LIGHTNING;
|
||||
this.LND = configs.LND;
|
||||
this.CLIGHTNING = configs.CLIGHTNING;
|
||||
|
||||
@@ -11,8 +11,6 @@ import memPool from './api/mempool';
|
||||
import diskCache from './api/disk-cache';
|
||||
import statistics from './api/statistics/statistics';
|
||||
import websocketHandler from './api/websocket-handler';
|
||||
import bisq from './api/bisq/bisq';
|
||||
import bisqMarkets from './api/bisq/markets';
|
||||
import logger from './logger';
|
||||
import backendInfo from './api/backend-info';
|
||||
import loadingIndicators from './api/loading-indicators';
|
||||
@@ -32,7 +30,6 @@ import networkSyncService from './tasks/lightning/network-sync.service';
|
||||
import statisticsRoutes from './api/statistics/statistics.routes';
|
||||
import pricesRoutes from './api/prices/prices.routes';
|
||||
import miningRoutes from './api/mining/mining-routes';
|
||||
import bisqRoutes from './api/bisq/bisq.routes';
|
||||
import liquidRoutes from './api/liquid/liquid.routes';
|
||||
import bitcoinRoutes from './api/bitcoin/bitcoin.routes';
|
||||
import fundingTxFetcher from './tasks/lightning/sync-tasks/funding-tx-fetcher';
|
||||
@@ -46,10 +43,14 @@ import redisCache from './api/redis-cache';
|
||||
import accelerationApi from './api/services/acceleration';
|
||||
import bitcoinCoreRoutes from './api/bitcoin/bitcoin-core.routes';
|
||||
import bitcoinSecondClient from './api/bitcoin/bitcoin-second-client';
|
||||
import accelerationRoutes from './api/acceleration/acceleration.routes';
|
||||
import aboutRoutes from './api/about.routes';
|
||||
|
||||
class Server {
|
||||
private wss: WebSocket.Server | undefined;
|
||||
private wssUnixSocket: WebSocket.Server | undefined;
|
||||
private server: http.Server | undefined;
|
||||
private serverUnixSocket: http.Server | undefined;
|
||||
private app: Application;
|
||||
private currentBackendRetryInterval = 1;
|
||||
private backendRetryCount = 0;
|
||||
@@ -130,6 +131,7 @@ class Server {
|
||||
})
|
||||
.use(express.urlencoded({ extended: true }))
|
||||
.use(express.text({ type: ['text/plain', 'application/base64'] }))
|
||||
.use(express.json())
|
||||
;
|
||||
|
||||
if (config.DATABASE.ENABLED && config.FIAT_PRICE.ENABLED) {
|
||||
@@ -138,6 +140,10 @@ class Server {
|
||||
|
||||
this.server = http.createServer(this.app);
|
||||
this.wss = new WebSocket.Server({ server: this.server });
|
||||
if (config.MEMPOOL.UNIX_SOCKET_PATH) {
|
||||
this.serverUnixSocket = http.createServer(this.app);
|
||||
this.wssUnixSocket = new WebSocket.Server({ server: this.serverUnixSocket });
|
||||
}
|
||||
|
||||
this.setUpWebsocketHandling();
|
||||
|
||||
@@ -182,13 +188,6 @@ class Server {
|
||||
|
||||
setInterval(() => { this.healthCheck(); }, 2500);
|
||||
|
||||
if (config.BISQ.ENABLED) {
|
||||
bisq.startBisqService();
|
||||
bisq.setPriceCallbackFunction((price) => websocketHandler.setExtraInitData('bsq-price', price));
|
||||
blocks.setNewBlockCallback(bisq.handleNewBitcoinBlock.bind(bisq));
|
||||
bisqMarkets.startBisqService();
|
||||
}
|
||||
|
||||
if (config.LIGHTNING.ENABLED) {
|
||||
this.$runLightningBackend();
|
||||
}
|
||||
@@ -200,6 +199,16 @@ class Server {
|
||||
logger.notice(`Mempool Server is running on port ${config.MEMPOOL.HTTP_PORT}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (this.serverUnixSocket) {
|
||||
this.serverUnixSocket.listen(config.MEMPOOL.UNIX_SOCKET_PATH, () => {
|
||||
if (worker) {
|
||||
logger.info(`Mempool Server worker #${process.pid} started`);
|
||||
} else {
|
||||
logger.notice(`Mempool Server is listening on ${config.MEMPOOL.UNIX_SOCKET_PATH}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async runMainUpdateLoop(): Promise<void> {
|
||||
@@ -273,8 +282,12 @@ class Server {
|
||||
|
||||
setUpWebsocketHandling(): void {
|
||||
if (this.wss) {
|
||||
websocketHandler.setWebsocketServer(this.wss);
|
||||
websocketHandler.addWebsocketServer(this.wss);
|
||||
}
|
||||
if (this.wssUnixSocket) {
|
||||
websocketHandler.addWebsocketServer(this.wssUnixSocket);
|
||||
}
|
||||
|
||||
if (Common.isLiquid() && config.DATABASE.ENABLED) {
|
||||
blocks.setNewBlockCallback(async () => {
|
||||
try {
|
||||
@@ -307,9 +320,6 @@ class Server {
|
||||
if (Common.indexingEnabled() && config.MEMPOOL.ENABLED) {
|
||||
miningRoutes.initRoutes(this.app);
|
||||
}
|
||||
if (config.BISQ.ENABLED) {
|
||||
bisqRoutes.initRoutes(this.app);
|
||||
}
|
||||
if (Common.isLiquid()) {
|
||||
liquidRoutes.initRoutes(this.app);
|
||||
}
|
||||
@@ -318,6 +328,10 @@ class Server {
|
||||
nodesRoutes.initRoutes(this.app);
|
||||
channelsRoutes.initRoutes(this.app);
|
||||
}
|
||||
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
|
||||
accelerationRoutes.initRoutes(this.app);
|
||||
}
|
||||
aboutRoutes.initRoutes(this.app);
|
||||
}
|
||||
|
||||
healthCheck(): void {
|
||||
@@ -345,6 +359,12 @@ class Server {
|
||||
if (config.DATABASE.ENABLED) {
|
||||
DB.releasePidLock();
|
||||
}
|
||||
this.server?.close();
|
||||
this.serverUnixSocket?.close();
|
||||
this.wss?.close();
|
||||
if (this.wssUnixSocket) {
|
||||
this.wssUnixSocket.close();
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
|
||||
@@ -86,9 +86,6 @@ class Logger {
|
||||
if (config.LIGHTNING.ENABLED) {
|
||||
return config.MEMPOOL.NETWORK === 'mainnet' ? 'lightning' : `${config.MEMPOOL.NETWORK}-lightning`;
|
||||
}
|
||||
if (config.BISQ.ENABLED) {
|
||||
return 'bisq';
|
||||
}
|
||||
if (config.MEMPOOL.NETWORK && config.MEMPOOL.NETWORK !== 'mainnet') {
|
||||
return config.MEMPOOL.NETWORK;
|
||||
}
|
||||
|
||||
@@ -71,6 +71,22 @@ export interface MempoolBlockDelta {
|
||||
changed: MempoolDeltaChange[];
|
||||
}
|
||||
|
||||
export interface MempoolDeltaTxids {
|
||||
sequence: number,
|
||||
added: string[];
|
||||
removed: string[];
|
||||
mined: string[];
|
||||
replaced: { replaced: string, by: string }[];
|
||||
}
|
||||
|
||||
export interface MempoolDelta {
|
||||
sequence: number,
|
||||
added: MempoolTransactionExtended[];
|
||||
removed: string[];
|
||||
mined: string[];
|
||||
replaced: { replaced: string, by: TransactionExtended }[];
|
||||
}
|
||||
|
||||
interface VinStrippedToScriptsig {
|
||||
scriptsig: string;
|
||||
}
|
||||
@@ -435,7 +451,6 @@ export interface WebsocketResponse {
|
||||
'track-tx': string;
|
||||
'track-address': string;
|
||||
'watch-mempool': boolean;
|
||||
'track-bisq-market': string;
|
||||
}
|
||||
|
||||
export interface VbytesPerSecond {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AccelerationInfo, makeBlockTemplate } from '../api/acceleration';
|
||||
import { AccelerationInfo, makeBlockTemplate } from '../api/acceleration/acceleration';
|
||||
import { RowDataPacket } from 'mysql2';
|
||||
import DB from '../database';
|
||||
import logger from '../logger';
|
||||
@@ -6,8 +6,8 @@ import { IEsploraApi } from '../api/bitcoin/esplora-api.interface';
|
||||
import { Common } from '../api/common';
|
||||
import config from '../config';
|
||||
import blocks from '../api/blocks';
|
||||
import accelerationApi, { Acceleration } from '../api/services/acceleration';
|
||||
import accelerationCosts from '../api/acceleration';
|
||||
import accelerationApi, { Acceleration, AccelerationHistory } from '../api/services/acceleration';
|
||||
import accelerationCosts from '../api/acceleration/acceleration';
|
||||
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
|
||||
import transactionUtils from '../api/transaction-utils';
|
||||
import { BlockExtended, MempoolTransactionExtended } from '../mempool.interfaces';
|
||||
@@ -15,6 +15,7 @@ import { BlockExtended, MempoolTransactionExtended } from '../mempool.interfaces
|
||||
export interface PublicAcceleration {
|
||||
txid: string,
|
||||
height: number,
|
||||
added: number,
|
||||
pool: {
|
||||
id: number,
|
||||
slug: string,
|
||||
@@ -29,15 +30,20 @@ export interface PublicAcceleration {
|
||||
class AccelerationRepository {
|
||||
private bidBoostV2Activated = 831580;
|
||||
|
||||
public async $saveAcceleration(acceleration: AccelerationInfo, block: IEsploraApi.Block, pool_id: number): Promise<void> {
|
||||
public async $saveAcceleration(acceleration: AccelerationInfo, block: IEsploraApi.Block, pool_id: number, accelerationData: Acceleration[]): Promise<void> {
|
||||
const accelerationMap: { [txid: string]: Acceleration } = {};
|
||||
for (const acc of accelerationData) {
|
||||
accelerationMap[acc.txid] = acc;
|
||||
}
|
||||
try {
|
||||
await DB.query(`
|
||||
INSERT INTO accelerations(txid, added, height, pool, effective_vsize, effective_fee, boost_rate, boost_cost)
|
||||
VALUE (?, FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO accelerations(txid, requested, added, height, pool, effective_vsize, effective_fee, boost_rate, boost_cost)
|
||||
VALUE (?, FROM_UNIXTIME(?), FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
height = ?
|
||||
`, [
|
||||
acceleration.txSummary.txid,
|
||||
accelerationMap[acceleration.txSummary.txid].added,
|
||||
block.timestamp,
|
||||
block.height,
|
||||
pool_id,
|
||||
@@ -64,7 +70,7 @@ class AccelerationRepository {
|
||||
}
|
||||
|
||||
let query = `
|
||||
SELECT * FROM accelerations
|
||||
SELECT *, UNIX_TIMESTAMP(requested) as requested_timestamp, UNIX_TIMESTAMP(added) as block_timestamp FROM accelerations
|
||||
JOIN pools on pools.unique_id = accelerations.pool
|
||||
`;
|
||||
let params: any[] = [];
|
||||
@@ -99,6 +105,7 @@ class AccelerationRepository {
|
||||
return rows.map(row => ({
|
||||
txid: row.txid,
|
||||
height: row.height,
|
||||
added: row.requested_timestamp || row.block_timestamp,
|
||||
pool: {
|
||||
id: row.id,
|
||||
slug: row.slug,
|
||||
@@ -202,7 +209,7 @@ class AccelerationRepository {
|
||||
const tx = blockTxs[acc.txid];
|
||||
const accelerationInfo = accelerationCosts.getAccelerationInfo(tx, boostRate, transactions);
|
||||
accelerationInfo.cost = Math.max(0, Math.min(acc.feeDelta, accelerationInfo.cost));
|
||||
this.$saveAcceleration(accelerationInfo, block, block.extras.pool.id);
|
||||
this.$saveAcceleration(accelerationInfo, block, block.extras.pool.id, successfulAccelerations);
|
||||
}
|
||||
}
|
||||
const lastSyncedHeight = await this.$getLastSyncedHeight();
|
||||
@@ -230,7 +237,7 @@ class AccelerationRepository {
|
||||
logger.debug(`Fetching accelerations between block ${lastSyncedHeight} and ${currentHeight}`);
|
||||
|
||||
// Fetch accelerations from mempool.space since the last synced block;
|
||||
const accelerationsByBlock = {};
|
||||
const accelerationsByBlock: {[height: number]: AccelerationHistory[]} = {};
|
||||
const blockHashes = {};
|
||||
let done = false;
|
||||
let page = 1;
|
||||
@@ -297,12 +304,16 @@ class AccelerationRepository {
|
||||
const feeStats = Common.calcEffectiveFeeStatistics(template);
|
||||
boostRate = feeStats.medianFee;
|
||||
}
|
||||
const accelerationSummaries = accelerations.map(acc => ({
|
||||
...acc,
|
||||
pools: acc.pools.map(pool => pool.pool_unique_id),
|
||||
}))
|
||||
for (const acc of accelerations) {
|
||||
if (blockTxs[acc.txid]) {
|
||||
const tx = blockTxs[acc.txid];
|
||||
const accelerationInfo = accelerationCosts.getAccelerationInfo(tx, boostRate, transactions);
|
||||
accelerationInfo.cost = Math.max(0, Math.min(acc.feeDelta, accelerationInfo.cost));
|
||||
await this.$saveAcceleration(accelerationInfo, block, block.extras.pool.id);
|
||||
await this.$saveAcceleration(accelerationInfo, block, block.extras.pool.id, accelerationSummaries);
|
||||
}
|
||||
}
|
||||
await this.$setLastSyncedHeight(height);
|
||||
@@ -317,6 +328,26 @@ class AccelerationRepository {
|
||||
|
||||
logger.debug(`Indexing accelerations completed`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete accelerations from the database above blockHeight
|
||||
*/
|
||||
public async $deleteAccelerationsFrom(blockHeight: number): Promise<void> {
|
||||
logger.info(`Delete newer accelerations from height ${blockHeight} from the database`);
|
||||
try {
|
||||
const currentSyncedHeight = await this.$getLastSyncedHeight();
|
||||
if (currentSyncedHeight >= blockHeight) {
|
||||
await DB.query(`
|
||||
UPDATE state
|
||||
SET number = ?
|
||||
WHERE name = 'last_acceleration_block'
|
||||
`, [blockHeight - 1]);
|
||||
}
|
||||
await DB.query(`DELETE FROM accelerations where height >= ${blockHeight}`);
|
||||
} catch (e) {
|
||||
logger.err('Cannot delete indexed accelerations. Reason: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new AccelerationRepository();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import config from '../../config';
|
||||
import { query } from '../../utils/axios-query';
|
||||
import { ConversionFeed, ConversionRates } from '../price-updater';
|
||||
|
||||
@@ -37,15 +38,26 @@ const emptyRates = {
|
||||
ZAR: -1,
|
||||
};
|
||||
|
||||
class FreeCurrencyApi implements ConversionFeed {
|
||||
private API_KEY: string;
|
||||
|
||||
constructor(apiKey: string) {
|
||||
this.API_KEY = apiKey;
|
||||
type PaidCurrencyData = {
|
||||
[key: string]: {
|
||||
code: string;
|
||||
value: number;
|
||||
}
|
||||
};
|
||||
|
||||
type FreeCurrencyData = {
|
||||
[key: string]: number;
|
||||
};
|
||||
|
||||
class FreeCurrencyApi implements ConversionFeed {
|
||||
private API_KEY = config.FIAT_PRICE.API_KEY;
|
||||
private PAID = config.FIAT_PRICE.PAID;
|
||||
private API_URL_PREFIX: string = this.PAID ? `https://api.currencyapi.com/v3/` : `https://api.freecurrencyapi.com/v1/`;
|
||||
|
||||
constructor() { }
|
||||
|
||||
public async $getQuota(): Promise<any> {
|
||||
const response = await query(`https://api.freecurrencyapi.com/v1/status?apikey=${this.API_KEY}`);
|
||||
const response = await query(`${this.API_URL_PREFIX}status?apikey=${this.API_KEY}`);
|
||||
if (response && response['quotas']) {
|
||||
return response['quotas'];
|
||||
}
|
||||
@@ -53,21 +65,36 @@ class FreeCurrencyApi implements ConversionFeed {
|
||||
}
|
||||
|
||||
public async $fetchLatestConversionRates(): Promise<ConversionRates> {
|
||||
const response = await query(`https://api.freecurrencyapi.com/v1/latest?apikey=${this.API_KEY}`);
|
||||
const response = await query(`${this.API_URL_PREFIX}latest?apikey=${this.API_KEY}`);
|
||||
if (response && response['data']) {
|
||||
if (this.PAID) {
|
||||
response['data'] = this.convertData(response['data']);
|
||||
}
|
||||
return response['data'];
|
||||
}
|
||||
return emptyRates;
|
||||
}
|
||||
|
||||
public async $fetchConversionRates(date: string): Promise<ConversionRates> {
|
||||
const response = await query(`https://api.freecurrencyapi.com/v1/historical?date=${date}&apikey=${this.API_KEY}`);
|
||||
if (response && response['data'] && response['data'][date]) {
|
||||
const response = await query(`${this.API_URL_PREFIX}historical?date=${date}&apikey=${this.API_KEY}`);
|
||||
if (response && response['data'] && (response['data'][date] || this.PAID)) {
|
||||
if (this.PAID) {
|
||||
response['data'] = this.convertData(response['data']);
|
||||
response['data'][response['meta'].last_updated_at.substr(0, 10)] = response['data'];
|
||||
}
|
||||
return response['data'][date];
|
||||
}
|
||||
return emptyRates;
|
||||
}
|
||||
|
||||
private convertData(data: PaidCurrencyData): FreeCurrencyData {
|
||||
const simplifiedData: FreeCurrencyData = {};
|
||||
for (const key in data) {
|
||||
simplifiedData[key] = data[key].value;
|
||||
}
|
||||
return simplifiedData;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default FreeCurrencyApi;
|
||||
|
||||
@@ -71,7 +71,7 @@ class PriceUpdater {
|
||||
this.feeds.push(new BitfinexApi());
|
||||
this.feeds.push(new GeminiApi());
|
||||
|
||||
this.currencyConversionFeed = new FreeCurrencyApi(config.FIAT_PRICE.API_KEY);
|
||||
this.currencyConversionFeed = new FreeCurrencyApi();
|
||||
this.setCyclePosition();
|
||||
}
|
||||
|
||||
|
||||
3
contributors/daweilv.txt
Normal file
3
contributors/daweilv.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of April 7, 2024.
|
||||
|
||||
Signed: daweilv
|
||||
3
contributors/henrialb.txt
Normal file
3
contributors/henrialb.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of April 12, 2024.
|
||||
|
||||
Signed: henrialb
|
||||
@@ -323,25 +323,6 @@ Corresponding `docker-compose.yml` overrides:
|
||||
|
||||
<br/>
|
||||
|
||||
`mempool-config.json`:
|
||||
```json
|
||||
"BISQ": {
|
||||
"ENABLED": false,
|
||||
"DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db"
|
||||
}
|
||||
```
|
||||
|
||||
Corresponding `docker-compose.yml` overrides:
|
||||
```yaml
|
||||
api:
|
||||
environment:
|
||||
BISQ_ENABLED: ""
|
||||
BISQ_DATA_PATH: ""
|
||||
...
|
||||
```
|
||||
|
||||
<br/>
|
||||
|
||||
`mempool-config.json`:
|
||||
```json
|
||||
"SOCKS5PROXY": {
|
||||
|
||||
@@ -11,10 +11,17 @@ RUN apt-get install -y build-essential python3 pkg-config curl ca-certificates
|
||||
|
||||
# Install Rust via rustup
|
||||
RUN CPU_ARCH=$(uname -m); if [ "$CPU_ARCH" = "armv7l" ]; then c_rehash; fi
|
||||
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable
|
||||
#RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable
|
||||
#Workaround to run on github actions from https://github.com/rust-lang/rustup/issues/2700#issuecomment-1367488985
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sed 's#/proc/self/exe#\/bin\/sh#g' | sh -s -- -y --default-toolchain stable
|
||||
ENV PATH="/root/.cargo/bin:$PATH"
|
||||
|
||||
COPY --from=backend . .
|
||||
COPY --from=rustgbt . ../rust/
|
||||
ENV FD=/build/rust-gbt
|
||||
RUN npm install --omit=dev --omit=optional
|
||||
|
||||
WORKDIR /build
|
||||
RUN npm run package
|
||||
|
||||
FROM node:20.12.0-buster-slim
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"OFFICIAL": __MEMPOOL_OFFICIAL__,
|
||||
"HTTP_PORT": __MEMPOOL_HTTP_PORT__,
|
||||
"SPAWN_CLUSTER_PROCS": __MEMPOOL_SPAWN_CLUSTER_PROCS__,
|
||||
"UNIX_SOCKET_PATH": "__MEMPOOL_UNIX_SOCKET_PATH__",
|
||||
"API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
|
||||
"POLL_RATE_MS": __MEMPOOL_POLL_RATE_MS__,
|
||||
"CACHE_DIR": "__MEMPOOL_CACHE_DIR__",
|
||||
@@ -93,10 +94,6 @@
|
||||
"ENABLED": __STATISTICS_ENABLED__,
|
||||
"TX_PER_SECOND_SAMPLE_PERIOD": __STATISTICS_TX_PER_SECOND_SAMPLE_PERIOD__
|
||||
},
|
||||
"BISQ": {
|
||||
"ENABLED": __BISQ_ENABLED__,
|
||||
"DATA_PATH": "__BISQ_DATA_PATH__"
|
||||
},
|
||||
"LIGHTNING": {
|
||||
"ENABLED": __LIGHTNING_ENABLED__,
|
||||
"BACKEND": "__LIGHTNING_BACKEND__",
|
||||
@@ -128,9 +125,7 @@
|
||||
"MEMPOOL_API": "__EXTERNAL_DATA_SERVER_MEMPOOL_API__",
|
||||
"MEMPOOL_ONION": "__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__",
|
||||
"LIQUID_API": "__EXTERNAL_DATA_SERVER_LIQUID_API__",
|
||||
"LIQUID_ONION": "__EXTERNAL_DATA_SERVER_LIQUID_ONION__",
|
||||
"BISQ_URL": "__EXTERNAL_DATA_SERVER_BISQ_URL__",
|
||||
"BISQ_ONION": "__EXTERNAL_DATA_SERVER_BISQ_ONION__"
|
||||
"LIQUID_ONION": "__EXTERNAL_DATA_SERVER_LIQUID_ONION__"
|
||||
},
|
||||
"MAXMIND": {
|
||||
"ENABLED": __MAXMIND_ENABLED__,
|
||||
@@ -155,6 +150,7 @@
|
||||
},
|
||||
"FIAT_PRICE": {
|
||||
"ENABLED": __FIAT_PRICE_ENABLED__,
|
||||
"PAID": __FIAT_PRICE_PAID__,
|
||||
"API_KEY": "__FIAT_PRICE_API_KEY__"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ __MEMPOOL_ENABLED__=${MEMPOOL_ENABLED:=true}
|
||||
__MEMPOOL_OFFICIAL__=${MEMPOOL_OFFICIAL:=false}
|
||||
__MEMPOOL_HTTP_PORT__=${BACKEND_HTTP_PORT:=8999}
|
||||
__MEMPOOL_SPAWN_CLUSTER_PROCS__=${MEMPOOL_SPAWN_CLUSTER_PROCS:=0}
|
||||
__MEMPOOL_UNIX_SOCKET_PATH__=${MEMPOOL_UNIX_SOCKET_PATH:=""}
|
||||
__MEMPOOL_API_URL_PREFIX__=${MEMPOOL_API_URL_PREFIX:=/api/v1/}
|
||||
__MEMPOOL_POLL_RATE_MS__=${MEMPOOL_POLL_RATE_MS:=2000}
|
||||
__MEMPOOL_CACHE_DIR__=${MEMPOOL_CACHE_DIR:=./cache}
|
||||
@@ -94,10 +95,6 @@ __SYSLOG_FACILITY__=${SYSLOG_FACILITY:=local7}
|
||||
__STATISTICS_ENABLED__=${STATISTICS_ENABLED:=true}
|
||||
__STATISTICS_TX_PER_SECOND_SAMPLE_PERIOD__=${STATISTICS_TX_PER_SECOND_SAMPLE_PERIOD:=150}
|
||||
|
||||
# BISQ
|
||||
__BISQ_ENABLED__=${BISQ_ENABLED:=false}
|
||||
__BISQ_DATA_PATH__=${BISQ_DATA_PATH:=/bisq/statsnode-data/btc_mainnet/db}
|
||||
|
||||
# SOCKS5PROXY
|
||||
__SOCKS5PROXY_ENABLED__=${SOCKS5PROXY_ENABLED:=false}
|
||||
__SOCKS5PROXY_USE_ONION__=${SOCKS5PROXY_USE_ONION:=true}
|
||||
@@ -111,8 +108,6 @@ __EXTERNAL_DATA_SERVER_MEMPOOL_API__=${EXTERNAL_DATA_SERVER_MEMPOOL_API:=https:/
|
||||
__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__=${EXTERNAL_DATA_SERVER_MEMPOOL_ONION:=http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1}
|
||||
__EXTERNAL_DATA_SERVER_LIQUID_API__=${EXTERNAL_DATA_SERVER_LIQUID_API:=https://liquid.network/api/v1}
|
||||
__EXTERNAL_DATA_SERVER_LIQUID_ONION__=${EXTERNAL_DATA_SERVER_LIQUID_ONION:=http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1}
|
||||
__EXTERNAL_DATA_SERVER_BISQ_URL__=${EXTERNAL_DATA_SERVER_BISQ_URL:=https://bisq.markets/api}
|
||||
__EXTERNAL_DATA_SERVER_BISQ_ONION__=${EXTERNAL_DATA_SERVER_BISQ_ONION:=http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api}
|
||||
|
||||
# LIGHTNING
|
||||
__LIGHTNING_ENABLED__=${LIGHTNING_ENABLED:=false}
|
||||
@@ -140,8 +135,8 @@ __MAXMIND_GEOLITE2_ASN__=${MAXMIND_GEOLITE2_ASN:="/backend/GeoIP/GeoLite2-ASN.mm
|
||||
__MAXMIND_GEOIP2_ISP__=${MAXMIND_GEOIP2_ISP:=""}
|
||||
|
||||
# REPLICATION
|
||||
__REPLICATION_ENABLED__=${REPLICATION_ENABLED:=true}
|
||||
__REPLICATION_AUDIT__=${REPLICATION_AUDIT:=true}
|
||||
__REPLICATION_ENABLED__=${REPLICATION_ENABLED:=false}
|
||||
__REPLICATION_AUDIT__=${REPLICATION_AUDIT:=false}
|
||||
__REPLICATION_AUDIT_START_HEIGHT__=${REPLICATION_AUDIT_START_HEIGHT:=774000}
|
||||
__REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
|
||||
|
||||
@@ -156,6 +151,7 @@ __REDIS_BATCH_QUERY_BASE_SIZE__=${REDIS_BATCH_QUERY_BASE_SIZE:=5000}
|
||||
|
||||
# FIAT_PRICE
|
||||
__FIAT_PRICE_ENABLED__=${FIAT_PRICE_ENABLED:=true}
|
||||
__FIAT_PRICE_PAID__=${FIAT_PRICE_PAID:=false}
|
||||
__FIAT_PRICE_API_KEY__=${FIAT_PRICE_API_KEY:=""}
|
||||
|
||||
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
|
||||
@@ -166,6 +162,7 @@ sed -i "s!__MEMPOOL_ENABLED__!${__MEMPOOL_ENABLED__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_OFFICIAL__!${__MEMPOOL_OFFICIAL__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_HTTP_PORT__!${__MEMPOOL_HTTP_PORT__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_SPAWN_CLUSTER_PROCS__!${__MEMPOOL_SPAWN_CLUSTER_PROCS__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_UNIX_SOCKET_PATH__!${__MEMPOOL_UNIX_SOCKET_PATH__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_API_URL_PREFIX__!${__MEMPOOL_API_URL_PREFIX__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_POLL_RATE_MS__!${__MEMPOOL_POLL_RATE_MS__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_CACHE_DIR__!${__MEMPOOL_CACHE_DIR__}!g" mempool-config.json
|
||||
@@ -246,9 +243,6 @@ sed -i "s!__SYSLOG_FACILITY__!${__SYSLOG_FACILITY__}!g" mempool-config.json
|
||||
sed -i "s!__STATISTICS_ENABLED__!${__STATISTICS_ENABLED__}!g" mempool-config.json
|
||||
sed -i "s!__STATISTICS_TX_PER_SECOND_SAMPLE_PERIOD__!${__STATISTICS_TX_PER_SECOND_SAMPLE_PERIOD__}!g" mempool-config.json
|
||||
|
||||
sed -i "s!__BISQ_ENABLED__!${__BISQ_ENABLED__}!g" mempool-config.json
|
||||
sed -i "s!__BISQ_DATA_PATH__!${__BISQ_DATA_PATH__}!g" mempool-config.json
|
||||
|
||||
sed -i "s!__SOCKS5PROXY_ENABLED__!${__SOCKS5PROXY_ENABLED__}!g" mempool-config.json
|
||||
sed -i "s!__SOCKS5PROXY_USE_ONION__!${__SOCKS5PROXY_USE_ONION__}!g" mempool-config.json
|
||||
sed -i "s!__SOCKS5PROXY_HOST__!${__SOCKS5PROXY_HOST__}!g" mempool-config.json
|
||||
@@ -260,8 +254,6 @@ sed -i "s!__EXTERNAL_DATA_SERVER_MEMPOOL_API__!${__EXTERNAL_DATA_SERVER_MEMPOOL_
|
||||
sed -i "s!__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__!${__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__}!g" mempool-config.json
|
||||
sed -i "s!__EXTERNAL_DATA_SERVER_LIQUID_API__!${__EXTERNAL_DATA_SERVER_LIQUID_API__}!g" mempool-config.json
|
||||
sed -i "s!__EXTERNAL_DATA_SERVER_LIQUID_ONION__!${__EXTERNAL_DATA_SERVER_LIQUID_ONION__}!g" mempool-config.json
|
||||
sed -i "s!__EXTERNAL_DATA_SERVER_BISQ_URL__!${__EXTERNAL_DATA_SERVER_BISQ_URL__}!g" mempool-config.json
|
||||
sed -i "s!__EXTERNAL_DATA_SERVER_BISQ_ONION__!${__EXTERNAL_DATA_SERVER_BISQ_ONION__}!g" mempool-config.json
|
||||
|
||||
# LIGHTNING
|
||||
sed -i "s!__LIGHTNING_ENABLED__!${__LIGHTNING_ENABLED__}!g" mempool-config.json
|
||||
@@ -305,6 +297,7 @@ sed -i "s!__REDIS_BATCH_QUERY_BASE_SIZE__!${__REDIS_BATCH_QUERY_BASE_SIZE__}!g"
|
||||
|
||||
# FIAT_PRICE
|
||||
sed -i "s!__FIAT_PRICE_ENABLED__!${__FIAT_PRICE_ENABLED__}!g" mempool-config.json
|
||||
sed -i "s!__FIAT_PRICE_PAID__!${__FIAT_PRICE_PAID__}!g" mempool-config.json
|
||||
sed -i "s!__FIAT_PRICE_API_KEY__!${__FIAT_PRICE_API_KEY__}!g" mempool-config.json
|
||||
|
||||
node /backend/package/index.js
|
||||
|
||||
@@ -13,7 +13,7 @@ RUN npm install --omit=dev --omit=optional
|
||||
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:1.25.4-alpine
|
||||
FROM nginx:1.26.0-alpine
|
||||
|
||||
WORKDIR /patch
|
||||
|
||||
|
||||
@@ -20,8 +20,6 @@ __TESTNET_ENABLED__=${TESTNET_ENABLED:=false}
|
||||
__SIGNET_ENABLED__=${SIGNET_ENABLED:=false}
|
||||
__LIQUID_ENABLED__=${LIQUID_ENABLED:=false}
|
||||
__LIQUID_TESTNET_ENABLED__=${LIQUID_TESTNET_ENABLED:=false}
|
||||
__BISQ_ENABLED__=${BISQ_ENABLED:=false}
|
||||
__BISQ_SEPARATE_BACKEND__=${BISQ_SEPARATE_BACKEND:=false}
|
||||
__ITEMS_PER_PAGE__=${ITEMS_PER_PAGE:=10}
|
||||
__KEEP_BLOCKS_AMOUNT__=${KEEP_BLOCKS_AMOUNT:=8}
|
||||
__NGINX_PROTOCOL__=${NGINX_PROTOCOL:=http}
|
||||
@@ -32,7 +30,6 @@ __MEMPOOL_BLOCKS_AMOUNT__=${MEMPOOL_BLOCKS_AMOUNT:=8}
|
||||
__BASE_MODULE__=${BASE_MODULE:=mempool}
|
||||
__MEMPOOL_WEBSITE_URL__=${MEMPOOL_WEBSITE_URL:=https://mempool.space}
|
||||
__LIQUID_WEBSITE_URL__=${LIQUID_WEBSITE_URL:=https://liquid.network}
|
||||
__BISQ_WEBSITE_URL__=${BISQ_WEBSITE_URL:=https://bisq.markets}
|
||||
__MINING_DASHBOARD__=${MINING_DASHBOARD:=true}
|
||||
__LIGHTNING__=${LIGHTNING:=false}
|
||||
__AUDIT__=${AUDIT:=false}
|
||||
@@ -40,6 +37,7 @@ __MAINNET_BLOCK_AUDIT_START_HEIGHT__=${MAINNET_BLOCK_AUDIT_START_HEIGHT:=0}
|
||||
__TESTNET_BLOCK_AUDIT_START_HEIGHT__=${TESTNET_BLOCK_AUDIT_START_HEIGHT:=0}
|
||||
__SIGNET_BLOCK_AUDIT_START_HEIGHT__=${SIGNET_BLOCK_AUDIT_START_HEIGHT:=0}
|
||||
__ACCELERATOR__=${ACCELERATOR:=false}
|
||||
__PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false}
|
||||
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
|
||||
__ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false}
|
||||
|
||||
@@ -48,8 +46,6 @@ export __TESTNET_ENABLED__
|
||||
export __SIGNET_ENABLED__
|
||||
export __LIQUID_ENABLED__
|
||||
export __LIQUID_TESTNET_ENABLED__
|
||||
export __BISQ_ENABLED__
|
||||
export __BISQ_SEPARATE_BACKEND__
|
||||
export __ITEMS_PER_PAGE__
|
||||
export __KEEP_BLOCKS_AMOUNT__
|
||||
export __NGINX_PROTOCOL__
|
||||
@@ -60,7 +56,6 @@ export __MEMPOOL_BLOCKS_AMOUNT__
|
||||
export __BASE_MODULE__
|
||||
export __MEMPOOL_WEBSITE_URL__
|
||||
export __LIQUID_WEBSITE_URL__
|
||||
export __BISQ_WEBSITE_URL__
|
||||
export __MINING_DASHBOARD__
|
||||
export __LIGHTNING__
|
||||
export __AUDIT__
|
||||
@@ -68,6 +63,7 @@ export __MAINNET_BLOCK_AUDIT_START_HEIGHT__
|
||||
export __TESTNET_BLOCK_AUDIT_START_HEIGHT__
|
||||
export __SIGNET_BLOCK_AUDIT_START_HEIGHT__
|
||||
export __ACCELERATOR__
|
||||
export __PUBLIC_ACCELERATIONS__
|
||||
export __HISTORICAL_PRICE__
|
||||
export __ADDITIONAL_CURRENCIES__
|
||||
|
||||
|
||||
1
frontend/.gitignore
vendored
1
frontend/.gitignore
vendored
@@ -63,6 +63,7 @@ src/resources/pools.json
|
||||
src/resources/mining-pools/*
|
||||
src/resources/**/*.mp4
|
||||
src/resources/**/*.vtt
|
||||
src/resources/customize.js
|
||||
|
||||
# environment config
|
||||
mempool-frontend-config.json
|
||||
|
||||
@@ -22,14 +22,13 @@ cd mempool/frontend
|
||||
|
||||
### 2. Specify Website
|
||||
|
||||
The same frontend codebase is used for https://mempool.space, https://liquid.network and https://bisq.markets.
|
||||
The same frontend codebase is used for https://mempool.space and https://liquid.network.
|
||||
|
||||
Configure the frontend for the site you want by running the corresponding command:
|
||||
|
||||
```
|
||||
$ npm run config:defaults:mempool
|
||||
$ npm run config:defaults:liquid
|
||||
$ npm run config:defaults:bisq
|
||||
```
|
||||
|
||||
### 3. Run the Frontend
|
||||
|
||||
@@ -166,10 +166,26 @@
|
||||
"src/resources",
|
||||
"src/robots.txt",
|
||||
"src/config.js",
|
||||
"src/customize.js",
|
||||
"src/config.template.js"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss",
|
||||
{
|
||||
"input": "src/theme-contrast.scss",
|
||||
"bundleName": "contrast",
|
||||
"inject": false
|
||||
},
|
||||
{
|
||||
"input": "src/theme-wiz.scss",
|
||||
"bundleName": "wiz",
|
||||
"inject": false
|
||||
},
|
||||
{
|
||||
"input": "src/theme-bukele.scss",
|
||||
"bundleName": "bukele",
|
||||
"inject": false
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-svg-core/styles.css"
|
||||
],
|
||||
"vendorChunk": true,
|
||||
|
||||
52
frontend/custom-sv-config.json
Normal file
52
frontend/custom-sv-config.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"theme": "bukele",
|
||||
"enterprise": "onbtc",
|
||||
"branding": {
|
||||
"name": "onbtc",
|
||||
"title": "Bitcoin Office",
|
||||
"site_id": 19,
|
||||
"header_img": "/resources/onbtclogo.svg",
|
||||
"footer_img": "/resources/onbtclogo.svg",
|
||||
"rounded_corner": true
|
||||
},
|
||||
"dashboard": {
|
||||
"widgets": [
|
||||
{
|
||||
"component": "fees",
|
||||
"mobileOrder": 4
|
||||
},
|
||||
{
|
||||
"component": "balance",
|
||||
"mobileOrder": 1,
|
||||
"props": {
|
||||
"address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": "twitter",
|
||||
"mobileOrder": 5,
|
||||
"props": {
|
||||
"handle": "nayibbukele"
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": "address",
|
||||
"mobileOrder": 2,
|
||||
"props": {
|
||||
"address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo",
|
||||
"period": "1m"
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": "blocks"
|
||||
},
|
||||
{
|
||||
"component": "addressTransactions",
|
||||
"mobileOrder": 3,
|
||||
"props": {
|
||||
"address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
describe('Bisq', () => {
|
||||
const baseModule = Cypress.env('BASE_MODULE');
|
||||
const basePath = '';
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
if (baseModule === 'bisq') {
|
||||
it('loads the dashboard', () => {
|
||||
cy.visit(`${basePath}`);
|
||||
cy.waitForSkeletonGone();
|
||||
});
|
||||
|
||||
describe('transactions', () => {
|
||||
it('loads the transactions screen', () => {
|
||||
cy.visit(`${basePath}`);
|
||||
cy.waitForSkeletonGone();
|
||||
cy.get('#btn-transactions').click().then(() => {
|
||||
cy.get('.table > tr').should('have.length', 50);
|
||||
});
|
||||
});
|
||||
|
||||
const filters = [
|
||||
'Asset listing fee', 'Blind vote', 'Compensation request',
|
||||
'Genesis', 'Irregular', 'Lockup', 'Pay trade fee', 'Proof of burn',
|
||||
'Proposal', 'Reimbursement request', 'Transfer BSQ', 'Unlock', 'Vote reveal'
|
||||
];
|
||||
filters.forEach((filter) => {
|
||||
it.only(`filters the transaction screen by ${filter}`, () => {
|
||||
cy.visit(`${basePath}/transactions`);
|
||||
cy.wait('@txs');
|
||||
cy.waitForSkeletonGone();
|
||||
cy.get('#filter').click();
|
||||
cy.contains(filter).find('input').click();
|
||||
cy.wait('@txs');
|
||||
cy.wait(500);
|
||||
cy.get('td:nth-of-type(2)').each(($td) => {
|
||||
expect($td.text().trim()).to.eq(filter);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('filters using multiple criteria', () => {
|
||||
const filters = ['Proposal', 'Lockup', 'Unlock'];
|
||||
cy.visit(`${basePath}/transactions`);
|
||||
cy.waitForSkeletonGone();
|
||||
cy.get('#filter').click();
|
||||
filters.forEach((filter) => {
|
||||
cy.contains(filter).find('input').click();
|
||||
//TODO: change this waiter
|
||||
cy.wait(1500);
|
||||
});
|
||||
cy.get('td:nth-of-type(2)').each(($td) => {
|
||||
const regex = new RegExp(`${filters.join('|')}`, 'g');
|
||||
expect($td.text().trim()).to.match(regex);
|
||||
});
|
||||
});
|
||||
|
||||
const transactions = [
|
||||
{ type: 'Asset listing fee', txid: '3548aa0c002b015ea700072b7d7d76d45d4f10a3573804d0d2f624c0bb255b6b' },
|
||||
{ type: 'Blind vote', txid: 'f8fabb95efa1bb81325e4c961b9fc7e3508a9b9ecd4eddf1400e58867eff8d92' },
|
||||
{ type: 'Compensation request', txid: 'a8cdc65fe6bb8730f5f89f99f779d0469b0a493e1ae570e20eb7afda696a18a9' },
|
||||
{ type: 'Genesis', txid: '4b5417ec5ab6112bedf539c3b4f5a806ed539542d8b717e1c4470aa3180edce5' },
|
||||
{ type: 'Irregular', txid: '90b06684a517388fec2237e2362a29810dc82f0e13e019c84747ec27051e6c53' },
|
||||
{ type: 'Lockup', txid: '365425b3b7487229e2ba598fb8f2a9e359e3351620383e5018548649a28b78c4' },
|
||||
{ type: 'Pay trade fee', txid: 'a66b30e9777e16572ab36723539df8f45bd5d8130d810b2c3d75b8c02a191eaf' },
|
||||
{ type: 'Proof of burn', txid: '8325ccb87065fb9243ed9ff1cbb431fc2ac5060a60433bcde474ccbd97b76dcb' },
|
||||
{ type: 'Proposal', txid: '34e2a20f045c82fbcf7cb191b42dea6fba45641777e1751ffb29d3981c4bf413' },
|
||||
{ type: 'Reimbursement request', txid: '04c16c79ca6b9ec9978880024b0d0ad3100020f33286b63c85ca7b1a319421ae' },
|
||||
{ type: 'Transfer BSQ', txid: '64500bd9220675ad30d5ace27de95a341a498d7eda08162ee0ce7feb8c56cb14' },
|
||||
{ type: 'Unlock', txid: '5a756841bbb11137d15b0082a3fcadbe102791f41a95d661d3bd0c5ad0b3b1a3' },
|
||||
{ type: 'Vote reveal', txid: 'bd7daae1d4af8837db5e47d7bd9d8b9f83dcfd35d112f85e90728b9be45191f7' }
|
||||
];
|
||||
|
||||
transactions.forEach((transaction) => {
|
||||
it(`loads a "${transaction.type}" transaction`, () => {
|
||||
cy.visit(`${basePath}/tx/${transaction.txid}`);
|
||||
cy.waitForSkeletonGone();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('blocks', () => {
|
||||
it('loads the blocks screen', () => {
|
||||
cy.visit(`${basePath}`);
|
||||
cy.waitForSkeletonGone();
|
||||
cy.get('#btn-blocks').click().then(() => {
|
||||
cy.wait('@blocks');
|
||||
cy.get('tbody tr').should('have.length', 10);
|
||||
});
|
||||
});
|
||||
|
||||
it('loads a specific block', () => {
|
||||
cy.visit(`${basePath}/block/0000000000000000000137ef33faa63bc6e809ab30932cf77d454fb36d2bd83a`);
|
||||
cy.waitForSkeletonGone();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('markets', () => {
|
||||
it('loads the markets screen', () => {
|
||||
cy.visit(`${basePath}/markets`);
|
||||
cy.waitForSkeletonGone();
|
||||
});
|
||||
|
||||
it('loads a specific market', () => {
|
||||
cy.visit(`${basePath}/market/btc_eur`);
|
||||
cy.waitForSkeletonGone();
|
||||
//Buy Offers
|
||||
cy.get('.row > :nth-child(1) td').should('have.length.at.least', 1);
|
||||
//Sell offers
|
||||
cy.get('.row > :nth-child(1) td').should('have.length.at.least', 1);
|
||||
//Trades
|
||||
cy.get('app-bisq-trades > .table-container td').should('have.length.at.least', 1);
|
||||
});
|
||||
});
|
||||
|
||||
it('loads the stats screen', () => {
|
||||
cy.visit(`${basePath}`);
|
||||
cy.waitForSkeletonGone();
|
||||
cy.get('#btn-stats').click().then(() => {
|
||||
cy.wait('@stats');
|
||||
});
|
||||
});
|
||||
|
||||
it('loads the api screen', () => {
|
||||
cy.visit(`${basePath}`);
|
||||
cy.waitForSkeletonGone();
|
||||
cy.get('#btn-docs').click().then(() => {
|
||||
cy.get('.section-header').should('have.length.at.least', 1);
|
||||
cy.get('.endpoint-container').should('have.length.at.least', 1);
|
||||
});
|
||||
});
|
||||
|
||||
it('shows blocks pagination with 5 pages (desktop)', () => {
|
||||
cy.viewport(760, 800);
|
||||
cy.visit(`${basePath}/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(`${basePath}/blocks`);
|
||||
cy.waitForSkeletonGone();
|
||||
cy.get('tbody tr').should('have.length', 10);
|
||||
// 3 pages + 4 buttons = 7 buttons
|
||||
cy.get('.pagination-container ul.pagination').first().children().should('have.length', 7);
|
||||
});
|
||||
} else {
|
||||
it.skip(`Tests cannot be run on the selected BASE_MODULE ${baseModule}`);
|
||||
}
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
describe.skip('Liquid', () => {
|
||||
describe('Liquid', () => {
|
||||
const baseModule = Cypress.env('BASE_MODULE');
|
||||
const basePath = '';
|
||||
|
||||
@@ -23,6 +23,13 @@ describe.skip('Liquid', () => {
|
||||
cy.get('#mempool-block-0 > .blockLink').should('exist');
|
||||
});
|
||||
|
||||
it('load first mempool block after skeleton loads', () => {
|
||||
cy.visit(`${basePath}`);
|
||||
cy.waitForSkeletonGone();
|
||||
cy.get('#mempool-block-0 > .blockLink').click();
|
||||
cy.waitForSkeletonGone();
|
||||
});
|
||||
|
||||
it('loads the dashboard', () => {
|
||||
cy.visit(`${basePath}`);
|
||||
cy.waitForSkeletonGone();
|
||||
@@ -84,10 +91,11 @@ describe.skip('Liquid', () => {
|
||||
cy.waitForSkeletonGone();
|
||||
//TODO: Change to an element id so we don't assert on a string
|
||||
cy.get('.table-tx-vin').should('contain', 'Peg-in');
|
||||
cy.get('.table-tx-vin a').click().then(() => {
|
||||
//Remove the target=_blank attribute so the new url opens in the same tab
|
||||
cy.get('.table-tx-vin a').invoke('removeAttr', 'target').click().then(() => {
|
||||
cy.waitForSkeletonGone();
|
||||
if (baseModule === 'liquid') {
|
||||
cy.url().should('eq', 'https://mempool.space/tx/f148c0d854db4174ea420655235f910543f0ec3680566dcfdf84fb0a1697b592');
|
||||
cy.url().should('eq', 'https://mempool.space/tx/f148c0d854db4174ea420655235f910543f0ec3680566dcfdf84fb0a1697b592#vout=0');
|
||||
} else {
|
||||
//TODO: Use an environment variable to get the hostname
|
||||
cy.url().should('eq', 'http://localhost:4200/tx/f148c0d854db4174ea420655235f910543f0ec3680566dcfdf84fb0a1697b592');
|
||||
@@ -98,7 +106,8 @@ describe.skip('Liquid', () => {
|
||||
it('loads peg out addresses', () => {
|
||||
cy.visit(`${basePath}/tx/ecf6eba04ffb3946faa172343c87162df76f1a57b07b0d6dc6ad956b13376dc8`);
|
||||
cy.waitForSkeletonGone();
|
||||
cy.get('.table-tx-vout a').first().click().then(() => {
|
||||
//Remove the target=_blank attribute so the new url opens in the same tab
|
||||
cy.get('.table-tx-vout a').first().invoke('removeAttr', 'target').click().then(() => {
|
||||
cy.waitForSkeletonGone();
|
||||
if (baseModule === 'liquid') {
|
||||
cy.url().should('eq', 'https://mempool.space/address/1BxoGcMg14oaH3CwHD2hF4gU9VcfgX5yoR');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
describe.skip('Liquid Testnet', () => {
|
||||
describe('Liquid Testnet', () => {
|
||||
const baseModule = Cypress.env('BASE_MODULE');
|
||||
const basePath = '/testnet';
|
||||
|
||||
@@ -28,6 +28,17 @@ describe.skip('Liquid Testnet', () => {
|
||||
cy.waitForSkeletonGone();
|
||||
});
|
||||
|
||||
it.skip('loads the dashboard with no scrollbars on mobile', () => {
|
||||
cy.viewport('iphone-xr');
|
||||
cy.visit(`${basePath}`);
|
||||
cy.waitForSkeletonGone();
|
||||
cy.window().then(window => {
|
||||
const htmlWidth = Cypress.$('html')[0].scrollWidth;
|
||||
const scrollBarWidth = window.innerWidth - htmlWidth;
|
||||
expect(scrollBarWidth).to.be.eq(0); //check for no horizontal scrollbar
|
||||
});
|
||||
});
|
||||
|
||||
it('loads the blocks page', () => {
|
||||
cy.visit(`${basePath}`)
|
||||
cy.get('#btn-blocks');
|
||||
@@ -57,17 +68,14 @@ describe.skip('Liquid Testnet', () => {
|
||||
cy.get('.tv-only').should('not.exist');
|
||||
});
|
||||
|
||||
it.skip('renders unconfidential addresses correctly on mobile', () => {
|
||||
cy.viewport('iphone-6');
|
||||
cy.visit(`${basePath}/address/__TODO__`);
|
||||
it.skip('renders unconfidential transactions correctly on mobile', () => {
|
||||
cy.viewport('iphone-xr');
|
||||
cy.visit(`${basePath}/tx/b119f338878416781dc285b94c0de52826341dea43566e4de4740d3ebfd1f6dc#blinded=99707,144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49,1377e4ec8eb0c89296e14ffca57e377f4b51ad8f1c881e43364434d8430dbfda,cdd6caae4c3452586cfcb107478dd2b7acaa5f82714a6a966578255e857eee60`);
|
||||
cy.waitForSkeletonGone();
|
||||
//TODO: Add proper IDs for these selectors
|
||||
const firstRowSelector = '.container-xl > :nth-child(3) > div > :nth-child(1) > .table > tbody';
|
||||
const thirdRowSelector = '.container-xl > :nth-child(3) > div > :nth-child(3)';
|
||||
cy.get(firstRowSelector).invoke('css', 'width').then(firstRowWidth => {
|
||||
cy.get(thirdRowSelector).invoke('css', 'width').then(thirdRowWidth => {
|
||||
expect(parseInt(firstRowWidth)).to.be.lessThan(parseInt(thirdRowWidth));
|
||||
});
|
||||
cy.window().then(window => {
|
||||
const htmlWidth = Cypress.$('html')[0].scrollWidth;
|
||||
const scrollBarWidth = window.innerWidth - htmlWidth;
|
||||
expect(scrollBarWidth).to.be.eq(0); //check for no horizontal scrollbar
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ describe('Mainnet', () => {
|
||||
cy.get('[id^="bitcoin-block-"]').should('have.length', 22);
|
||||
cy.get('.footer').should('be.visible');
|
||||
cy.get('.row > :nth-child(1)').invoke('text').then((text) => {
|
||||
expect(text).to.match(/Incoming transactions.* vB\/s/);
|
||||
expect(text).to.match(/Incoming Transactions.* vB\/s/);
|
||||
});
|
||||
cy.get('.row > :nth-child(2)').invoke('text').then((text) => {
|
||||
expect(text).to.match(/Unconfirmed:(.*)/);
|
||||
|
||||
@@ -72,13 +72,11 @@ Cypress.Commands.add('mockMempoolSocket', () => {
|
||||
mockWebSocket();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('changeNetwork', (network: "testnet" | "signet" | "liquid" | "bisq" | "mainnet") => {
|
||||
Cypress.Commands.add('changeNetwork', (network: "testnet" | "signet" | "liquid" | "mainnet") => {
|
||||
cy.get('.dropdown-toggle').click().then(() => {
|
||||
cy.get(`a.${network}`).click().then(() => {
|
||||
cy.waitForPageIdle();
|
||||
if (network !== 'bisq') {
|
||||
cy.waitForSkeletonGone();
|
||||
}
|
||||
cy.waitForSkeletonGone();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
2
frontend/cypress/support/index.d.ts
vendored
2
frontend/cypress/support/index.d.ts
vendored
@@ -5,6 +5,6 @@ declare namespace Cypress {
|
||||
waitForSkeletonGone(): Chainable<any>
|
||||
waitForPageIdle(): Chainable<any>
|
||||
mockMempoolSocket(): Chainable<any>
|
||||
changeNetwork(network: "testnet"|"signet"|"liquid"|"bisq"|"mainnet"): Chainable<any>
|
||||
changeNetwork(network: "testnet"|"signet"|"liquid"|"mainnet"): Chainable<any>
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,13 @@ const { spawnSync } = require('child_process');
|
||||
const CONFIG_FILE_NAME = 'mempool-frontend-config.json';
|
||||
const GENERATED_CONFIG_FILE_NAME = 'src/resources/config.js';
|
||||
const GENERATED_TEMPLATE_CONFIG_FILE_NAME = 'src/resources/config.template.js';
|
||||
const GENERATED_CUSTOMIZATION_FILE_NAME = 'src/resources/customize.js';
|
||||
|
||||
let settings = [];
|
||||
let configContent = {};
|
||||
let gitCommitHash = '';
|
||||
let packetJsonVersion = '';
|
||||
let customConfig;
|
||||
|
||||
try {
|
||||
const rawConfig = fs.readFileSync(CONFIG_FILE_NAME);
|
||||
@@ -22,7 +24,13 @@ try {
|
||||
}
|
||||
}
|
||||
|
||||
const indexFilePath = configContent.BASE_MODULE ? 'src/index.' + configContent.BASE_MODULE + '.html' : 'src/index.mempool.html';
|
||||
if (configContent && configContent.CUSTOMIZATION) {
|
||||
customConfig = readConfig(configContent.CUSTOMIZATION);
|
||||
}
|
||||
|
||||
const baseModuleName = configContent.BASE_MODULE || 'mempool';
|
||||
const customBuildName = (customConfig && configContent.enterprise) ? ('.' + configContent.enterprise) : '';
|
||||
const indexFilePath = 'src/index.' + baseModuleName + customBuildName + '.html';
|
||||
|
||||
try {
|
||||
fs.copyFileSync(indexFilePath, 'src/index.html');
|
||||
@@ -109,6 +117,17 @@ writeConfigTemplate(GENERATED_TEMPLATE_CONFIG_FILE_NAME, newConfigTemplate);
|
||||
|
||||
const currentConfig = readConfig(GENERATED_CONFIG_FILE_NAME);
|
||||
|
||||
let customConfigJs = '';
|
||||
if (customConfig) {
|
||||
console.log(`Customizing frontend using ${configContent.CUSTOMIZATION}`);
|
||||
customConfigJs = `(function (window) {
|
||||
window.__env = window.__env || {};
|
||||
window.__env.customize = ${customConfig};
|
||||
}((typeof global !== 'undefined') ? global : this));
|
||||
`;
|
||||
}
|
||||
writeConfig(GENERATED_CUSTOMIZATION_FILE_NAME, customConfigJs);
|
||||
|
||||
if (currentConfig && currentConfig === newConfig) {
|
||||
console.log(`No configuration updates, skipping ${GENERATED_CONFIG_FILE_NAME} file update`);
|
||||
return;
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
"SIGNET_ENABLED": false,
|
||||
"LIQUID_ENABLED": false,
|
||||
"LIQUID_TESTNET_ENABLED": false,
|
||||
"BISQ_ENABLED": false,
|
||||
"BISQ_SEPARATE_BACKEND": false,
|
||||
"ITEMS_PER_PAGE": 10,
|
||||
"KEEP_BLOCKS_AMOUNT": 8,
|
||||
"NGINX_PROTOCOL": "http",
|
||||
@@ -15,7 +13,6 @@
|
||||
"BASE_MODULE": "mempool",
|
||||
"MEMPOOL_WEBSITE_URL": "https://mempool.space",
|
||||
"LIQUID_WEBSITE_URL": "https://liquid.network",
|
||||
"BISQ_WEBSITE_URL": "https://bisq.markets",
|
||||
"MINING_DASHBOARD": true,
|
||||
"AUDIT": false,
|
||||
"MAINNET_BLOCK_AUDIT_START_HEIGHT": 0,
|
||||
@@ -24,5 +21,6 @@
|
||||
"LIGHTNING": false,
|
||||
"HISTORICAL_PRICE": true,
|
||||
"ADDITIONAL_CURRENCIES": false,
|
||||
"ACCELERATOR": false
|
||||
"ACCELERATOR": false,
|
||||
"PUBLIC_ACCELERATIONS": false
|
||||
}
|
||||
|
||||
409
frontend/package-lock.json
generated
409
frontend/package-lock.json
generated
@@ -32,9 +32,10 @@
|
||||
"bootstrap": "~4.6.2",
|
||||
"browserify": "^17.0.0",
|
||||
"clipboard": "^2.0.11",
|
||||
"cypress": "^13.9.0",
|
||||
"domino": "^2.1.6",
|
||||
"echarts": "~5.5.0",
|
||||
"esbuild": "^0.20.2",
|
||||
"esbuild": "^0.21.1",
|
||||
"lightweight-charts": "~3.8.0",
|
||||
"ngx-echarts": "~17.1.0",
|
||||
"ngx-infinite-scroll": "^17.0.0",
|
||||
@@ -62,7 +63,7 @@
|
||||
"optionalDependencies": {
|
||||
"@cypress/schematic": "^2.5.0",
|
||||
"@types/cypress": "^1.1.3",
|
||||
"cypress": "^13.7.0",
|
||||
"cypress": "^13.9.0",
|
||||
"cypress-fail-on-console-error": "~5.1.0",
|
||||
"cypress-wait-until": "^2.0.1",
|
||||
"mock-socket": "~9.3.1",
|
||||
@@ -3196,9 +3197,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
|
||||
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.1.tgz",
|
||||
"integrity": "sha512-O7yppwipkXvnEPjzkSXJRk2g4bS8sUx9p9oXHq9MU/U7lxUzZVsnFZMDTmeeX9bfQxrFcvOacl/ENgOh0WP9pA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -3211,9 +3212,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
|
||||
"integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.1.tgz",
|
||||
"integrity": "sha512-hh3jKWikdnTtHCglDAeVO3Oyh8MaH8xZUaWMiCCvJ9/c3NtPqZq+CACOlGTxhddypXhl+8B45SeceYBfB/e8Ow==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -3226,9 +3227,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.1.tgz",
|
||||
"integrity": "sha512-jXhccq6es+onw7x8MxoFnm820mz7sGa9J14kLADclmiEUH4fyj+FjR6t0M93RgtlI/awHWhtF0Wgfhqgf9gDZA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3241,9 +3242,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.1.tgz",
|
||||
"integrity": "sha512-NPObtlBh4jQHE01gJeucqEhdoD/4ya2owSIS8lZYS58aR0x7oZo9lB2lVFxgTANSa5MGCBeoQtr+yA9oKCGPvA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3256,9 +3257,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.1.tgz",
|
||||
"integrity": "sha512-BLT7TDzqsVlQRmJfO/FirzKlzmDpBWwmCUlyggfzUwg1cAxVxeA4O6b1XkMInlxISdfPAOunV9zXjvh5x99Heg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3271,9 +3272,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.1.tgz",
|
||||
"integrity": "sha512-D3h3wBQmeS/vp93O4B+SWsXB8HvRDwMyhTNhBd8yMbh5wN/2pPWRW5o/hM3EKgk9bdKd9594lMGoTCTiglQGRQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3286,9 +3287,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.1.tgz",
|
||||
"integrity": "sha512-/uVdqqpNKXIxT6TyS/oSK4XE4xWOqp6fh4B5tgAwozkyWdylcX+W4YF2v6SKsL4wCQ5h1bnaSNjWPXG/2hp8AQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3301,9 +3302,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.1.tgz",
|
||||
"integrity": "sha512-paAkKN1n1jJitw+dAoR27TdCzxRl1FOEITx3h201R6NoXUojpMzgMLdkXVgCvaCSCqwYkeGLoe9UVNRDKSvQgw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3316,9 +3317,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
|
||||
"integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.1.tgz",
|
||||
"integrity": "sha512-tRHnxWJnvNnDpNVnsyDhr1DIQZUfCXlHSCDohbXFqmg9W4kKR7g8LmA3kzcwbuxbRMKeit8ladnCabU5f2traA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -3331,9 +3332,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.1.tgz",
|
||||
"integrity": "sha512-G65d08YoH00TL7Xg4LaL3gLV21bpoAhQ+r31NUu013YB7KK0fyXIt05VbsJtpqh/6wWxoLJZOvQHYnodRrnbUQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3346,9 +3347,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
|
||||
"integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.1.tgz",
|
||||
"integrity": "sha512-tt/54LqNNAqCz++QhxoqB9+XqdsaZOtFD/srEhHYwBd3ZUOepmR1Eeot8bS+Q7BiEvy9vvKbtpHf+r6q8hF5UA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -3361,9 +3362,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
|
||||
"integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.1.tgz",
|
||||
"integrity": "sha512-MhNalK6r0nZD0q8VzUBPwheHzXPr9wronqmZrewLfP7ui9Fv1tdPmg6e7A8lmg0ziQCziSDHxh3cyRt4YMhGnQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -3376,9 +3377,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
|
||||
"integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.1.tgz",
|
||||
"integrity": "sha512-YCKVY7Zen5rwZV+nZczOhFmHaeIxR4Zn3jcmNH53LbgF6IKRwmrMywqDrg4SiSNApEefkAbPSIzN39FC8VsxPg==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
@@ -3391,9 +3392,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
|
||||
"integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.1.tgz",
|
||||
"integrity": "sha512-bw7bcQ+270IOzDV4mcsKAnDtAFqKO0jVv3IgRSd8iM0ac3L8amvCrujRVt1ajBTJcpDaFhIX+lCNRKteoDSLig==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -3406,9 +3407,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
|
||||
"integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.1.tgz",
|
||||
"integrity": "sha512-ARmDRNkcOGOm1AqUBSwRVDfDeD9hGYRfkudP2QdoonBz1ucWVnfBPfy7H4JPI14eYtZruRSczJxyu7SRYDVOcg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -3421,9 +3422,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
|
||||
"integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.1.tgz",
|
||||
"integrity": "sha512-o73TcUNMuoTZlhwFdsgr8SfQtmMV58sbgq6gQq9G1xUiYnHMTmJbwq65RzMx89l0iya69lR4bxBgtWiiOyDQZA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -3436,9 +3437,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.1.tgz",
|
||||
"integrity": "sha512-da4/1mBJwwgJkbj4fMH7SOXq2zapgTo0LKXX1VUZ0Dxr+e8N0WbS80nSZ5+zf3lvpf8qxrkZdqkOqFfm57gXwA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3451,9 +3452,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.1.tgz",
|
||||
"integrity": "sha512-CPWs0HTFe5woTJN5eKPvgraUoRHrCtzlYIAv9wBC+FAyagBSaf+UdZrjwYyTGnwPGkThV4OCI7XibZOnPvONVw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3466,9 +3467,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.1.tgz",
|
||||
"integrity": "sha512-xxhTm5QtzNLc24R0hEkcH+zCx/o49AsdFZ0Cy5zSd/5tOj4X2g3/2AJB625NoadUuc4A8B3TenLJoYdWYOYCew==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3481,9 +3482,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.1.tgz",
|
||||
"integrity": "sha512-CWibXszpWys1pYmbr9UiKAkX6x+Sxw8HWtw1dRESK1dLW5fFJ6rMDVw0o8MbadusvVQx1a8xuOxnHXT941Hp1A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3496,9 +3497,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.1.tgz",
|
||||
"integrity": "sha512-jb5B4k+xkytGbGUS4T+Z89cQJ9DJ4lozGRSV+hhfmCPpfJ3880O31Q1srPCimm+V6UCbnigqD10EgDNgjvjerQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3511,9 +3512,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
|
||||
"integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.1.tgz",
|
||||
"integrity": "sha512-PgyFvjJhXqHn1uxPhyN1wZ6dIomKjiLUQh1LjFvjiV1JmnkZ/oMPrfeEAZg5R/1ftz4LZWZr02kefNIQ5SKREQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -3526,9 +3527,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.1.tgz",
|
||||
"integrity": "sha512-W9NttRZQR5ehAiqHGDnvfDaGmQOm6Fi4vSlce8mjM75x//XKuVAByohlEX6N17yZnVXxQFuh4fDRunP8ca6bfA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -8028,9 +8029,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/cypress": {
|
||||
"version": "13.7.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.7.0.tgz",
|
||||
"integrity": "sha512-UimjRSJJYdTlvkChcdcfywKJ6tUYuwYuk/n1uMMglrvi+ZthNhoRYcxnWgTqUtkl17fXrPAsD5XT2rcQYN1xKA==",
|
||||
"version": "13.9.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.9.0.tgz",
|
||||
"integrity": "sha512-atNjmYfHsvTuCaxTxLZr9xGoHz53LLui3266WWxXJHY7+N6OdwJdg/feEa3T+buez9dmUXHT1izCOklqG82uCQ==",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -9196,9 +9197,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
|
||||
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.1.tgz",
|
||||
"integrity": "sha512-GPqx+FX7mdqulCeQ4TsGZQ3djBJkx5k7zBGtqt9ycVlWNg8llJ4RO9n2vciu8BN2zAEs6lPbPl0asZsAh7oWzg==",
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
@@ -9207,29 +9208,29 @@
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.20.2",
|
||||
"@esbuild/android-arm": "0.20.2",
|
||||
"@esbuild/android-arm64": "0.20.2",
|
||||
"@esbuild/android-x64": "0.20.2",
|
||||
"@esbuild/darwin-arm64": "0.20.2",
|
||||
"@esbuild/darwin-x64": "0.20.2",
|
||||
"@esbuild/freebsd-arm64": "0.20.2",
|
||||
"@esbuild/freebsd-x64": "0.20.2",
|
||||
"@esbuild/linux-arm": "0.20.2",
|
||||
"@esbuild/linux-arm64": "0.20.2",
|
||||
"@esbuild/linux-ia32": "0.20.2",
|
||||
"@esbuild/linux-loong64": "0.20.2",
|
||||
"@esbuild/linux-mips64el": "0.20.2",
|
||||
"@esbuild/linux-ppc64": "0.20.2",
|
||||
"@esbuild/linux-riscv64": "0.20.2",
|
||||
"@esbuild/linux-s390x": "0.20.2",
|
||||
"@esbuild/linux-x64": "0.20.2",
|
||||
"@esbuild/netbsd-x64": "0.20.2",
|
||||
"@esbuild/openbsd-x64": "0.20.2",
|
||||
"@esbuild/sunos-x64": "0.20.2",
|
||||
"@esbuild/win32-arm64": "0.20.2",
|
||||
"@esbuild/win32-ia32": "0.20.2",
|
||||
"@esbuild/win32-x64": "0.20.2"
|
||||
"@esbuild/aix-ppc64": "0.21.1",
|
||||
"@esbuild/android-arm": "0.21.1",
|
||||
"@esbuild/android-arm64": "0.21.1",
|
||||
"@esbuild/android-x64": "0.21.1",
|
||||
"@esbuild/darwin-arm64": "0.21.1",
|
||||
"@esbuild/darwin-x64": "0.21.1",
|
||||
"@esbuild/freebsd-arm64": "0.21.1",
|
||||
"@esbuild/freebsd-x64": "0.21.1",
|
||||
"@esbuild/linux-arm": "0.21.1",
|
||||
"@esbuild/linux-arm64": "0.21.1",
|
||||
"@esbuild/linux-ia32": "0.21.1",
|
||||
"@esbuild/linux-loong64": "0.21.1",
|
||||
"@esbuild/linux-mips64el": "0.21.1",
|
||||
"@esbuild/linux-ppc64": "0.21.1",
|
||||
"@esbuild/linux-riscv64": "0.21.1",
|
||||
"@esbuild/linux-s390x": "0.21.1",
|
||||
"@esbuild/linux-x64": "0.21.1",
|
||||
"@esbuild/netbsd-x64": "0.21.1",
|
||||
"@esbuild/openbsd-x64": "0.21.1",
|
||||
"@esbuild/sunos-x64": "0.21.1",
|
||||
"@esbuild/win32-arm64": "0.21.1",
|
||||
"@esbuild/win32-ia32": "0.21.1",
|
||||
"@esbuild/win32-x64": "0.21.1"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-wasm": {
|
||||
@@ -9860,9 +9861,9 @@
|
||||
"integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw=="
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.19.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.19.1.tgz",
|
||||
"integrity": "sha512-K4w1/Bp7y8iSiVObmCrtq8Cs79XjJc/RU2YYkZQ7wpUu5ZyZ7MtPHkqoMz4pf+mgXfNvo2qft8D9OnrH2ABk9w==",
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
|
||||
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
@@ -20562,141 +20563,141 @@
|
||||
"integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw=="
|
||||
},
|
||||
"@esbuild/aix-ppc64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
|
||||
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.1.tgz",
|
||||
"integrity": "sha512-O7yppwipkXvnEPjzkSXJRk2g4bS8sUx9p9oXHq9MU/U7lxUzZVsnFZMDTmeeX9bfQxrFcvOacl/ENgOh0WP9pA==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/android-arm": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
|
||||
"integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.1.tgz",
|
||||
"integrity": "sha512-hh3jKWikdnTtHCglDAeVO3Oyh8MaH8xZUaWMiCCvJ9/c3NtPqZq+CACOlGTxhddypXhl+8B45SeceYBfB/e8Ow==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/android-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.1.tgz",
|
||||
"integrity": "sha512-jXhccq6es+onw7x8MxoFnm820mz7sGa9J14kLADclmiEUH4fyj+FjR6t0M93RgtlI/awHWhtF0Wgfhqgf9gDZA==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/android-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.1.tgz",
|
||||
"integrity": "sha512-NPObtlBh4jQHE01gJeucqEhdoD/4ya2owSIS8lZYS58aR0x7oZo9lB2lVFxgTANSa5MGCBeoQtr+yA9oKCGPvA==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/darwin-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.1.tgz",
|
||||
"integrity": "sha512-BLT7TDzqsVlQRmJfO/FirzKlzmDpBWwmCUlyggfzUwg1cAxVxeA4O6b1XkMInlxISdfPAOunV9zXjvh5x99Heg==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/darwin-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.1.tgz",
|
||||
"integrity": "sha512-D3h3wBQmeS/vp93O4B+SWsXB8HvRDwMyhTNhBd8yMbh5wN/2pPWRW5o/hM3EKgk9bdKd9594lMGoTCTiglQGRQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/freebsd-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.1.tgz",
|
||||
"integrity": "sha512-/uVdqqpNKXIxT6TyS/oSK4XE4xWOqp6fh4B5tgAwozkyWdylcX+W4YF2v6SKsL4wCQ5h1bnaSNjWPXG/2hp8AQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/freebsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.1.tgz",
|
||||
"integrity": "sha512-paAkKN1n1jJitw+dAoR27TdCzxRl1FOEITx3h201R6NoXUojpMzgMLdkXVgCvaCSCqwYkeGLoe9UVNRDKSvQgw==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-arm": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
|
||||
"integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.1.tgz",
|
||||
"integrity": "sha512-tRHnxWJnvNnDpNVnsyDhr1DIQZUfCXlHSCDohbXFqmg9W4kKR7g8LmA3kzcwbuxbRMKeit8ladnCabU5f2traA==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.1.tgz",
|
||||
"integrity": "sha512-G65d08YoH00TL7Xg4LaL3gLV21bpoAhQ+r31NUu013YB7KK0fyXIt05VbsJtpqh/6wWxoLJZOvQHYnodRrnbUQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-ia32": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
|
||||
"integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.1.tgz",
|
||||
"integrity": "sha512-tt/54LqNNAqCz++QhxoqB9+XqdsaZOtFD/srEhHYwBd3ZUOepmR1Eeot8bS+Q7BiEvy9vvKbtpHf+r6q8hF5UA==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-loong64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
|
||||
"integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.1.tgz",
|
||||
"integrity": "sha512-MhNalK6r0nZD0q8VzUBPwheHzXPr9wronqmZrewLfP7ui9Fv1tdPmg6e7A8lmg0ziQCziSDHxh3cyRt4YMhGnQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-mips64el": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
|
||||
"integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.1.tgz",
|
||||
"integrity": "sha512-YCKVY7Zen5rwZV+nZczOhFmHaeIxR4Zn3jcmNH53LbgF6IKRwmrMywqDrg4SiSNApEefkAbPSIzN39FC8VsxPg==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-ppc64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
|
||||
"integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.1.tgz",
|
||||
"integrity": "sha512-bw7bcQ+270IOzDV4mcsKAnDtAFqKO0jVv3IgRSd8iM0ac3L8amvCrujRVt1ajBTJcpDaFhIX+lCNRKteoDSLig==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-riscv64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
|
||||
"integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.1.tgz",
|
||||
"integrity": "sha512-ARmDRNkcOGOm1AqUBSwRVDfDeD9hGYRfkudP2QdoonBz1ucWVnfBPfy7H4JPI14eYtZruRSczJxyu7SRYDVOcg==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-s390x": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
|
||||
"integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.1.tgz",
|
||||
"integrity": "sha512-o73TcUNMuoTZlhwFdsgr8SfQtmMV58sbgq6gQq9G1xUiYnHMTmJbwq65RzMx89l0iya69lR4bxBgtWiiOyDQZA==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.1.tgz",
|
||||
"integrity": "sha512-da4/1mBJwwgJkbj4fMH7SOXq2zapgTo0LKXX1VUZ0Dxr+e8N0WbS80nSZ5+zf3lvpf8qxrkZdqkOqFfm57gXwA==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/netbsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.1.tgz",
|
||||
"integrity": "sha512-CPWs0HTFe5woTJN5eKPvgraUoRHrCtzlYIAv9wBC+FAyagBSaf+UdZrjwYyTGnwPGkThV4OCI7XibZOnPvONVw==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/openbsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.1.tgz",
|
||||
"integrity": "sha512-xxhTm5QtzNLc24R0hEkcH+zCx/o49AsdFZ0Cy5zSd/5tOj4X2g3/2AJB625NoadUuc4A8B3TenLJoYdWYOYCew==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/sunos-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.1.tgz",
|
||||
"integrity": "sha512-CWibXszpWys1pYmbr9UiKAkX6x+Sxw8HWtw1dRESK1dLW5fFJ6rMDVw0o8MbadusvVQx1a8xuOxnHXT941Hp1A==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/win32-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.1.tgz",
|
||||
"integrity": "sha512-jb5B4k+xkytGbGUS4T+Z89cQJ9DJ4lozGRSV+hhfmCPpfJ3880O31Q1srPCimm+V6UCbnigqD10EgDNgjvjerQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/win32-ia32": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
|
||||
"integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.1.tgz",
|
||||
"integrity": "sha512-PgyFvjJhXqHn1uxPhyN1wZ6dIomKjiLUQh1LjFvjiV1JmnkZ/oMPrfeEAZg5R/1ftz4LZWZr02kefNIQ5SKREQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/win32-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.1.tgz",
|
||||
"integrity": "sha512-W9NttRZQR5ehAiqHGDnvfDaGmQOm6Fi4vSlce8mjM75x//XKuVAByohlEX6N17yZnVXxQFuh4fDRunP8ca6bfA==",
|
||||
"optional": true
|
||||
},
|
||||
"@eslint-community/eslint-utils": {
|
||||
@@ -24111,9 +24112,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"cypress": {
|
||||
"version": "13.7.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.7.0.tgz",
|
||||
"integrity": "sha512-UimjRSJJYdTlvkChcdcfywKJ6tUYuwYuk/n1uMMglrvi+ZthNhoRYcxnWgTqUtkl17fXrPAsD5XT2rcQYN1xKA==",
|
||||
"version": "13.9.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.9.0.tgz",
|
||||
"integrity": "sha512-atNjmYfHsvTuCaxTxLZr9xGoHz53LLui3266WWxXJHY7+N6OdwJdg/feEa3T+buez9dmUXHT1izCOklqG82uCQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@cypress/request": "^3.0.0",
|
||||
@@ -25031,33 +25032,33 @@
|
||||
}
|
||||
},
|
||||
"esbuild": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
|
||||
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.1.tgz",
|
||||
"integrity": "sha512-GPqx+FX7mdqulCeQ4TsGZQ3djBJkx5k7zBGtqt9ycVlWNg8llJ4RO9n2vciu8BN2zAEs6lPbPl0asZsAh7oWzg==",
|
||||
"requires": {
|
||||
"@esbuild/aix-ppc64": "0.20.2",
|
||||
"@esbuild/android-arm": "0.20.2",
|
||||
"@esbuild/android-arm64": "0.20.2",
|
||||
"@esbuild/android-x64": "0.20.2",
|
||||
"@esbuild/darwin-arm64": "0.20.2",
|
||||
"@esbuild/darwin-x64": "0.20.2",
|
||||
"@esbuild/freebsd-arm64": "0.20.2",
|
||||
"@esbuild/freebsd-x64": "0.20.2",
|
||||
"@esbuild/linux-arm": "0.20.2",
|
||||
"@esbuild/linux-arm64": "0.20.2",
|
||||
"@esbuild/linux-ia32": "0.20.2",
|
||||
"@esbuild/linux-loong64": "0.20.2",
|
||||
"@esbuild/linux-mips64el": "0.20.2",
|
||||
"@esbuild/linux-ppc64": "0.20.2",
|
||||
"@esbuild/linux-riscv64": "0.20.2",
|
||||
"@esbuild/linux-s390x": "0.20.2",
|
||||
"@esbuild/linux-x64": "0.20.2",
|
||||
"@esbuild/netbsd-x64": "0.20.2",
|
||||
"@esbuild/openbsd-x64": "0.20.2",
|
||||
"@esbuild/sunos-x64": "0.20.2",
|
||||
"@esbuild/win32-arm64": "0.20.2",
|
||||
"@esbuild/win32-ia32": "0.20.2",
|
||||
"@esbuild/win32-x64": "0.20.2"
|
||||
"@esbuild/aix-ppc64": "0.21.1",
|
||||
"@esbuild/android-arm": "0.21.1",
|
||||
"@esbuild/android-arm64": "0.21.1",
|
||||
"@esbuild/android-x64": "0.21.1",
|
||||
"@esbuild/darwin-arm64": "0.21.1",
|
||||
"@esbuild/darwin-x64": "0.21.1",
|
||||
"@esbuild/freebsd-arm64": "0.21.1",
|
||||
"@esbuild/freebsd-x64": "0.21.1",
|
||||
"@esbuild/linux-arm": "0.21.1",
|
||||
"@esbuild/linux-arm64": "0.21.1",
|
||||
"@esbuild/linux-ia32": "0.21.1",
|
||||
"@esbuild/linux-loong64": "0.21.1",
|
||||
"@esbuild/linux-mips64el": "0.21.1",
|
||||
"@esbuild/linux-ppc64": "0.21.1",
|
||||
"@esbuild/linux-riscv64": "0.21.1",
|
||||
"@esbuild/linux-s390x": "0.21.1",
|
||||
"@esbuild/linux-x64": "0.21.1",
|
||||
"@esbuild/netbsd-x64": "0.21.1",
|
||||
"@esbuild/openbsd-x64": "0.21.1",
|
||||
"@esbuild/sunos-x64": "0.21.1",
|
||||
"@esbuild/win32-arm64": "0.21.1",
|
||||
"@esbuild/win32-ia32": "0.21.1",
|
||||
"@esbuild/win32-x64": "0.21.1"
|
||||
}
|
||||
},
|
||||
"esbuild-wasm": {
|
||||
@@ -25526,9 +25527,9 @@
|
||||
"integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw=="
|
||||
},
|
||||
"express": {
|
||||
"version": "4.19.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.19.1.tgz",
|
||||
"integrity": "sha512-K4w1/Bp7y8iSiVObmCrtq8Cs79XjJc/RU2YYkZQ7wpUu5ZyZ7MtPHkqoMz4pf+mgXfNvo2qft8D9OnrH2ABk9w==",
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
|
||||
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
|
||||
"requires": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
|
||||
@@ -34,13 +34,12 @@
|
||||
"start:local-prod": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-prod",
|
||||
"start:local-staging": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-staging",
|
||||
"start:mixed": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c mixed",
|
||||
"build": "npm run generate-config && npm run ng -- build --configuration production --localize && npm run sync-assets && npm run build-mempool.js",
|
||||
"build": "npm run generate-config && npm run ng -- build --configuration production --localize && npm run sync-assets-dev && npm run sync-assets && npm run build-mempool.js",
|
||||
"sync-assets": "rsync -av ./src/resources ./dist/mempool/browser && node sync-assets.js 'dist/mempool/browser/resources/'",
|
||||
"sync-assets-dev": "node sync-assets.js 'src/resources/'",
|
||||
"generate-config": "node generate-config.js",
|
||||
"build-mempool.js": "npm run build-mempool-js && npm run build-mempool-liquid-js && npm run build-mempool-bisq-js",
|
||||
"build-mempool.js": "npm run build-mempool-js && npm run build-mempool-liquid-js",
|
||||
"build-mempool-js": "browserify -p tinyify ./node_modules/@mempool/mempool.js/lib/index.js --standalone mempoolJS > ./dist/mempool/browser/en-US/mempool.js",
|
||||
"build-mempool-bisq-js": "browserify -p tinyify ./node_modules/@mempool/mempool.js/lib/index-bisq.js --standalone bisqJS > ./dist/mempool/browser/en-US/bisq.js",
|
||||
"build-mempool-liquid-js": "browserify -p tinyify ./node_modules/@mempool/mempool.js/lib/index-liquid.js --standalone liquidJS > ./dist/mempool/browser/en-US/liquid.js",
|
||||
"test": "npm run ng -- test",
|
||||
"lint": "./node_modules/.bin/eslint . --ext .ts",
|
||||
@@ -51,17 +50,16 @@
|
||||
"dev:ssr": "npm run generate-config && ng run mempool:serve-ssr",
|
||||
"serve:ssr": "npm run generate-config && node server.run.js",
|
||||
"build:ssr": "npm run build && ng run mempool:server:production && ./node_modules/typescript/bin/tsc server.run.ts",
|
||||
"config:defaults:mempool": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=mempool BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
|
||||
"config:defaults:liquid": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=liquid BLOCK_WEIGHT_UNITS=300000 && npm run generate-config",
|
||||
"config:defaults:bisq": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=bisq BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
|
||||
"config:defaults:mempool": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=mempool BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
|
||||
"config:defaults:liquid": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=liquid BLOCK_WEIGHT_UNITS=300000 && npm run generate-config",
|
||||
"prerender": "npm run 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",
|
||||
"cypress:open:ci:staging": "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-staging 4200 cypress:open",
|
||||
"cypress:run:ci:staging": "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-staging 4200 cypress:run:record"
|
||||
"cypress:open:ci": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_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 ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:run:record",
|
||||
"cypress:open:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:open",
|
||||
"cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular-devkit/build-angular": "^17.3.1",
|
||||
@@ -94,7 +92,7 @@
|
||||
"ngx-infinite-scroll": "^17.0.0",
|
||||
"qrcode": "1.5.1",
|
||||
"rxjs": "~7.8.1",
|
||||
"esbuild": "^0.20.2",
|
||||
"esbuild": "^0.21.1",
|
||||
"tinyify": "^4.0.0",
|
||||
"tlite": "^0.1.9",
|
||||
"tslib": "~2.6.0",
|
||||
@@ -117,7 +115,7 @@
|
||||
"optionalDependencies": {
|
||||
"@cypress/schematic": "^2.5.0",
|
||||
"@types/cypress": "^1.1.3",
|
||||
"cypress": "^13.7.0",
|
||||
"cypress": "^13.9.0",
|
||||
"cypress-fail-on-console-error": "~5.1.0",
|
||||
"cypress-wait-until": "^2.0.1",
|
||||
"mock-socket": "~9.3.1",
|
||||
|
||||
@@ -22,7 +22,6 @@ PROXY_CONFIG = [
|
||||
{
|
||||
context: ['*',
|
||||
'/api/**', '!/api/v1/ws',
|
||||
'!/bisq', '!/bisq/**', '!/bisq/',
|
||||
'!/liquid', '!/liquid/**', '!/liquid/',
|
||||
'!/liquidtestnet', '!/liquidtestnet/**', '!/liquidtestnet/',
|
||||
'/testnet/api/**', '/signet/api/**'
|
||||
@@ -39,16 +38,6 @@ PROXY_CONFIG = [
|
||||
secure: false,
|
||||
changeOrigin: true,
|
||||
},
|
||||
{
|
||||
context: ['/api/bisq**', '/bisq/api/**'],
|
||||
target: "https://bisq.markets",
|
||||
pathRewrite: {
|
||||
"^/api/bisq/": "/bisq/api"
|
||||
},
|
||||
ws: true,
|
||||
secure: false,
|
||||
changeOrigin: true
|
||||
},
|
||||
{
|
||||
context: ['/api/liquid**', '/liquid/api/**'],
|
||||
target: "https://liquid.network",
|
||||
|
||||
@@ -67,40 +67,6 @@ if (configContent && configContent.BASE_MODULE === 'liquid') {
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
if (configContent && configContent.BASE_MODULE === 'bisq') {
|
||||
PROXY_CONFIG.push(...[
|
||||
{
|
||||
context: ['/bisq/api/v1/ws'],
|
||||
target: `http://127.0.0.1:8999`,
|
||||
secure: false,
|
||||
ws: true,
|
||||
changeOrigin: true,
|
||||
proxyTimeout: 30000,
|
||||
pathRewrite: {
|
||||
"^/bisq": ""
|
||||
},
|
||||
},
|
||||
{
|
||||
context: ['/bisq/api/v1/**'],
|
||||
target: `http://127.0.0.1:8999`,
|
||||
secure: false,
|
||||
changeOrigin: true,
|
||||
proxyTimeout: 30000,
|
||||
},
|
||||
{
|
||||
context: ['/bisq/api/**'],
|
||||
target: `http://127.0.0.1:8999`,
|
||||
secure: false,
|
||||
changeOrigin: true,
|
||||
proxyTimeout: 30000,
|
||||
pathRewrite: {
|
||||
"^/bisq/api/": "/api/v1/bisq/"
|
||||
},
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
PROXY_CONFIG.push(...[
|
||||
{
|
||||
context: ['/testnet/api/v1/lightning/**'],
|
||||
|
||||
@@ -67,40 +67,6 @@ if (configContent && configContent.BASE_MODULE === 'liquid') {
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
if (configContent && configContent.BASE_MODULE === 'bisq') {
|
||||
PROXY_CONFIG.push(...[
|
||||
{
|
||||
context: ['/bisq/api/v1/ws'],
|
||||
target: `http://localhost:8999`,
|
||||
secure: false,
|
||||
ws: true,
|
||||
changeOrigin: true,
|
||||
proxyTimeout: 30000,
|
||||
pathRewrite: {
|
||||
"^/bisq": ""
|
||||
},
|
||||
},
|
||||
{
|
||||
context: ['/bisq/api/v1/**'],
|
||||
target: `http://localhost:8999`,
|
||||
secure: false,
|
||||
changeOrigin: true,
|
||||
proxyTimeout: 30000,
|
||||
},
|
||||
{
|
||||
context: ['/bisq/api/**'],
|
||||
target: `http://localhost:8999`,
|
||||
secure: false,
|
||||
changeOrigin: true,
|
||||
proxyTimeout: 30000,
|
||||
pathRewrite: {
|
||||
"^/bisq/api/": "/api/v1/bisq/"
|
||||
},
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
PROXY_CONFIG.push(...[
|
||||
{
|
||||
context: ['/testnet/api/v1/lightning/**'],
|
||||
|
||||
@@ -61,39 +61,6 @@ if (configContent && configContent.BASE_MODULE === 'liquid') {
|
||||
]);
|
||||
}
|
||||
|
||||
if (configContent && configContent.BASE_MODULE === 'bisq') {
|
||||
PROXY_CONFIG.push(...[
|
||||
{
|
||||
context: ['/bisq/api/v1/ws'],
|
||||
target: `http://localhost:8999`,
|
||||
secure: false,
|
||||
ws: true,
|
||||
changeOrigin: true,
|
||||
proxyTimeout: 30000,
|
||||
pathRewrite: {
|
||||
"^/bisq": ""
|
||||
},
|
||||
},
|
||||
{
|
||||
context: ['/bisq/api/v1/**'],
|
||||
target: `http://localhost:8999`,
|
||||
secure: false,
|
||||
changeOrigin: true,
|
||||
proxyTimeout: 30000,
|
||||
},
|
||||
{
|
||||
context: ['/bisq/api/**'],
|
||||
target: `http://localhost:8999`,
|
||||
secure: false,
|
||||
changeOrigin: true,
|
||||
proxyTimeout: 30000,
|
||||
pathRewrite: {
|
||||
"^/bisq/api/": "/api/v1/bisq/"
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
PROXY_CONFIG.push(...[
|
||||
{
|
||||
context: ['/api/v1/services/**'],
|
||||
|
||||
@@ -5,7 +5,6 @@ let PROXY_CONFIG = require('./proxy.conf');
|
||||
PROXY_CONFIG.forEach(entry => {
|
||||
entry.target = entry.target.replace("mempool.space", "mempool-staging.fra.mempool.space");
|
||||
entry.target = entry.target.replace("liquid.network", "liquid-staging.fra.mempool.space");
|
||||
entry.target = entry.target.replace("bisq.markets", "bisq-staging.fra.mempool.space");
|
||||
});
|
||||
|
||||
module.exports = PROXY_CONFIG;
|
||||
|
||||
@@ -7,6 +7,8 @@ import { MempoolBlockViewComponent } from './components/mempool-block-view/mempo
|
||||
import { ClockComponent } from './components/clock/clock.component';
|
||||
import { StatusViewComponent } from './components/status-view/status-view.component';
|
||||
import { AddressGroupComponent } from './components/address-group/address-group.component';
|
||||
import { TrackerComponent } from './components/tracker/tracker.component';
|
||||
import { AccelerateCheckout } from './components/accelerate-checkout/accelerate-checkout.component';
|
||||
|
||||
const browserWindow = window || {};
|
||||
// @ts-ignore
|
||||
@@ -51,6 +53,44 @@ let routes: Routes = [
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'testnet4',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'wallet',
|
||||
children: [],
|
||||
component: AddressGroupComponent,
|
||||
data: {
|
||||
networkSpecific: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'status',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
component: StatusViewComponent
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: '/testnet4'
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'signet',
|
||||
children: [
|
||||
@@ -105,6 +145,10 @@ let routes: Routes = [
|
||||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'tracker/:id',
|
||||
component: TrackerComponent,
|
||||
},
|
||||
{
|
||||
path: 'wallet',
|
||||
children: [],
|
||||
@@ -124,6 +168,10 @@ let routes: Routes = [
|
||||
path: 'testnet',
|
||||
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
|
||||
},
|
||||
{
|
||||
path: 'testnet4',
|
||||
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
|
||||
},
|
||||
{
|
||||
path: 'signet',
|
||||
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
|
||||
@@ -170,13 +218,6 @@ let routes: Routes = [
|
||||
},
|
||||
];
|
||||
|
||||
if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'bisq') {
|
||||
routes = [{
|
||||
path: '',
|
||||
loadChildren: () => import('./bisq/bisq.module').then(m => m.BisqModule)
|
||||
}];
|
||||
}
|
||||
|
||||
if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
routes = [
|
||||
{
|
||||
@@ -270,7 +311,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
imports: [RouterModule.forRoot(routes, {
|
||||
initialNavigation: 'enabledBlocking',
|
||||
scrollPositionRestoration: 'enabled',
|
||||
anchorScrolling: 'enabled',
|
||||
anchorScrolling: 'disabled',
|
||||
preloadingStrategy: AppPreloadingStrategy
|
||||
})],
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const mempoolFeeColors = [
|
||||
export const defaultMempoolFeeColors = [
|
||||
'557d00',
|
||||
'5d7d01',
|
||||
'637d02',
|
||||
@@ -39,6 +39,47 @@ export const mempoolFeeColors = [
|
||||
'ae005b',
|
||||
];
|
||||
|
||||
export const contrastMempoolFeeColors = [
|
||||
'0082e6',
|
||||
'0984df',
|
||||
'1285d9',
|
||||
'1a87d2',
|
||||
'2388cb',
|
||||
'2c8ac5',
|
||||
'358bbe',
|
||||
'3e8db7',
|
||||
'468eb0',
|
||||
'4f90aa',
|
||||
'5892a3',
|
||||
'61939c',
|
||||
'6a9596',
|
||||
'72968f',
|
||||
'7b9888',
|
||||
'849982',
|
||||
'8d9b7b',
|
||||
'959c74',
|
||||
'9e9e6e',
|
||||
'a79f67',
|
||||
'b0a160',
|
||||
'b9a35a',
|
||||
'c1a453',
|
||||
'caa64c',
|
||||
'd3a745',
|
||||
'dca93f',
|
||||
'e5aa38',
|
||||
'edac31',
|
||||
'f6ad2b',
|
||||
'ffaf24',
|
||||
'ffb01e',
|
||||
'ffb118',
|
||||
'ffb212',
|
||||
'ffb30c',
|
||||
'ffb406',
|
||||
'ffb500',
|
||||
'ffb600',
|
||||
'ffb700',
|
||||
];
|
||||
|
||||
export const chartColors = [
|
||||
"#D81B60",
|
||||
"#8E24AA",
|
||||
@@ -148,22 +189,22 @@ export const specialBlocks = {
|
||||
'0': {
|
||||
labelEvent: 'Genesis',
|
||||
labelEventCompleted: 'The Genesis of Bitcoin',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'210000': {
|
||||
labelEvent: 'Bitcoin\'s 1st Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 25 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'420000': {
|
||||
labelEvent: 'Bitcoin\'s 2nd Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 12.5 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'630000': {
|
||||
labelEvent: 'Bitcoin\'s 3rd Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 6.25 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'709632': {
|
||||
labelEvent: 'Taproot 🌱 activation',
|
||||
@@ -173,62 +214,62 @@ export const specialBlocks = {
|
||||
'840000': {
|
||||
labelEvent: 'Bitcoin\'s 4th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 3.125 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'1050000': {
|
||||
labelEvent: 'Bitcoin\'s 5th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 1.5625 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'1260000': {
|
||||
labelEvent: 'Bitcoin\'s 6th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 0.78125 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'1470000': {
|
||||
labelEvent: 'Bitcoin\'s 7th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 0.390625 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'1680000': {
|
||||
labelEvent: 'Bitcoin\'s 8th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 0.1953125 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'1890000': {
|
||||
labelEvent: 'Bitcoin\'s 9th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 0.09765625 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'2100000': {
|
||||
labelEvent: 'Bitcoin\'s 10th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 0.04882812 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'2310000': {
|
||||
labelEvent: 'Bitcoin\'s 11th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 0.02441406 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'2520000': {
|
||||
labelEvent: 'Bitcoin\'s 12th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 0.01220703 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'2730000': {
|
||||
labelEvent: 'Bitcoin\'s 13th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 0.00610351 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'2940000': {
|
||||
labelEvent: 'Bitcoin\'s 14th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 0.00305175 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'3150000': {
|
||||
labelEvent: 'Bitcoin\'s 15th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 0.00152587 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import { SharedModule } from './shared/shared.module';
|
||||
import { StorageService } from './services/storage.service';
|
||||
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
|
||||
import { LanguageService } from './services/language.service';
|
||||
import { ThemeService } from './services/theme.service';
|
||||
import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe';
|
||||
import { FiatCurrencyPipe } from './shared/pipes/fiat-currency.pipe';
|
||||
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
|
||||
@@ -38,6 +39,7 @@ const providers = [
|
||||
StorageService,
|
||||
EnterpriseService,
|
||||
LanguageService,
|
||||
ThemeService,
|
||||
ShortenStringPipe,
|
||||
FiatShortenerPipe,
|
||||
FiatCurrencyPipe,
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
<div class="container-xl">
|
||||
<h1 i18n="shared.address">Address</h1>
|
||||
<span class="address-link">
|
||||
<app-truncate [text]="addressString" [lastChars]="8" [link]="['/address/' | relativeUrl, addressString]">
|
||||
<app-clipboard [text]="addressString"></app-clipboard>
|
||||
</app-truncate>
|
||||
</span>
|
||||
<br>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<ng-template [ngIf]="!isLoadingAddress && !error">
|
||||
<div class="box">
|
||||
|
||||
<div class="row">
|
||||
<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' }} <span class="symbol">BSQ</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="address.total-sent">Total sent</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' }} <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-md qrcode-col">
|
||||
<div class="qr-wrapper">
|
||||
<app-qrcode [data]="addressString"></app-qrcode>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<h2>
|
||||
<ng-container *ngTemplateOutlet="transactions.length === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: transactions.length}"></ng-container>
|
||||
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
|
||||
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
|
||||
</h2>
|
||||
|
||||
<ng-template ngFor let-tx [ngForOf]="transactions">
|
||||
|
||||
<div class="header-bg box" style="padding: 10px; margin-bottom: 10px;">
|
||||
<a [routerLink]="['/tx/' | relativeUrl, tx.id]" [state]="{ data: tx }">
|
||||
<span style="float: left;" class="d-block d-md-none">{{ tx.id | shortenString : 16 }}</span>
|
||||
<span style="float: left;" class="d-none d-md-block">{{ tx.id }}</span>
|
||||
</a>
|
||||
<div class="float-right">
|
||||
‎{{ tx.time | date:'yyyy-MM-dd HH:mm' }}
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<app-bisq-transfers [tx]="tx" [showConfirmations]="true"></app-bisq-transfers>
|
||||
|
||||
<br>
|
||||
</ng-template>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="isLoadingAddress && !error">
|
||||
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="w-100 d-block d-md-none"></div>
|
||||
<div class="col">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="error">
|
||||
<app-http-error [error]="error">
|
||||
<span i18n="address.error.loading-address-data">Error loading address data.</span>
|
||||
</app-http-error>
|
||||
</ng-template>
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
||||
@@ -1,75 +0,0 @@
|
||||
.qr-wrapper {
|
||||
background-color: #FFF;
|
||||
padding: 10px;
|
||||
padding-bottom: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.qrcode-col {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qrcode-col > div {
|
||||
margin: 20px auto 5px;
|
||||
@media (min-width: 768px) {
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { switchMap, filter, catchError } from 'rxjs/operators';
|
||||
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 '../../services/websocket.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-address',
|
||||
templateUrl: './bisq-address.component.html',
|
||||
styleUrls: ['./bisq-address.component.scss']
|
||||
})
|
||||
export class BisqAddressComponent implements OnInit, OnDestroy {
|
||||
transactions: BisqTransaction[];
|
||||
addressString: string;
|
||||
isLoadingAddress = true;
|
||||
error: any;
|
||||
mainSubscription: Subscription;
|
||||
|
||||
totalReceived = 0;
|
||||
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) => {
|
||||
this.error = undefined;
|
||||
this.isLoadingAddress = true;
|
||||
this.transactions = null;
|
||||
document.body.scrollTo(0, 0);
|
||||
this.addressString = params.get('id') || '';
|
||||
this.seoService.setTitle($localize`:@@bisq-address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bisq.address:See current balance, pending transactions, and history of confirmed transactions for BSQ address ${this.addressString}:INTERPOLATION:.`);
|
||||
|
||||
return this.bisqApiService.getAddress$(this.addressString)
|
||||
.pipe(
|
||||
catchError((err) => {
|
||||
this.isLoadingAddress = false;
|
||||
this.error = err;
|
||||
this.seoService.logSoft404();
|
||||
console.log(err);
|
||||
return of(null);
|
||||
})
|
||||
);
|
||||
}),
|
||||
filter((transactions) => transactions !== null)
|
||||
)
|
||||
.subscribe((transactions: BisqTransaction[]) => {
|
||||
this.transactions = transactions;
|
||||
this.updateChainStats();
|
||||
this.isLoadingAddress = false;
|
||||
},
|
||||
(error) => {
|
||||
console.log(error);
|
||||
this.error = error;
|
||||
this.seoService.logSoft404();
|
||||
this.isLoadingAddress = false;
|
||||
});
|
||||
}
|
||||
|
||||
updateChainStats() {
|
||||
const shortenedAddress = this.addressString.substr(1);
|
||||
|
||||
this.totalSent = this.transactions.reduce((acc, tx) =>
|
||||
acc + tx.inputs
|
||||
.filter((input) => input.address === shortenedAddress)
|
||||
.reduce((a, input) => a + input.bsqAmount, 0), 0);
|
||||
|
||||
this.totalReceived = this.transactions.reduce((acc, tx) =>
|
||||
acc + tx.outputs
|
||||
.filter((output) => output.address === shortenedAddress)
|
||||
.reduce((a, output) => a + output.bsqAmount, 0), 0);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.mainSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpResponse, HttpParams } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { BisqTransaction, BisqBlock, BisqStats, MarketVolume, Trade, Markets, Tickers, Offers, Currencies, HighLowOpenClose, SummarizedInterval } from './bisq.interfaces';
|
||||
|
||||
const API_BASE_URL = '/bisq/api';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class BisqApiService {
|
||||
apiBaseUrl: string;
|
||||
|
||||
constructor(
|
||||
private httpClient: HttpClient,
|
||||
) { }
|
||||
|
||||
getStats$(): Observable<BisqStats> {
|
||||
return this.httpClient.get<BisqStats>(API_BASE_URL + '/stats');
|
||||
}
|
||||
|
||||
getTransaction$(txId: string): Observable<BisqTransaction> {
|
||||
return this.httpClient.get<BisqTransaction>(API_BASE_URL + '/tx/' + txId);
|
||||
}
|
||||
|
||||
listTransactions$(start: number, length: number, types: string[]): Observable<HttpResponse<BisqTransaction[]>> {
|
||||
let params = new HttpParams();
|
||||
types.forEach((t: string) => {
|
||||
params = params.append('types[]', t);
|
||||
});
|
||||
return this.httpClient.get<BisqTransaction[]>(API_BASE_URL + `/txs/${start}/${length}`, { params, observe: 'response' });
|
||||
}
|
||||
|
||||
getBlock$(hash: string): Observable<BisqBlock> {
|
||||
return this.httpClient.get<BisqBlock>(API_BASE_URL + '/block/' + hash);
|
||||
}
|
||||
|
||||
listBlocks$(start: number, length: number): Observable<HttpResponse<BisqBlock[]>> {
|
||||
return this.httpClient.get<BisqBlock[]>(API_BASE_URL + `/blocks/${start}/${length}`, { observe: 'response' });
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
<div class="container-xl">
|
||||
|
||||
<div class="title-block">
|
||||
<h1><ng-template [ngIf]="blockHeight" i18n="shared.block-title">Block <ng-container *ngTemplateOutlet="blockTemplateContent"></ng-container></ng-template></h1>
|
||||
</div>
|
||||
|
||||
<ng-template #blockTemplateContent><a [routerLink]="['/block/' | relativeUrl, blockHash]">{{ blockHeight }}</a></ng-template>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<ng-template [ngIf]="!isLoading && !error">
|
||||
|
||||
<div class="box block-container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td-width" i18n="block.hash">Hash</td>
|
||||
<td><a [routerLink]="['/block/' | relativeUrl, block.hash]" title="{{ block.hash }}">{{ block.hash | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="block.hash"></app-clipboard></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="block.timestamp">Timestamp</td>
|
||||
<td>
|
||||
‎{{ block.time | date:'yyyy-MM-dd HH:mm' }}
|
||||
<div class="lg-inline">
|
||||
<i class="symbol">(<app-time kind="since" [time]="block.time / 1000" [fastRender]="true"></app-time>)</i>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td-width" i18n="block.previous_hash|Transaction Previous Hash">Previous hash</td>
|
||||
<td><a [routerLink]="['/block/' | relativeUrl, block.previousBlockHash]" title="{{ block.hash }}">{{ block.previousBlockHash | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="block.previousBlockHash"></app-clipboard></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<br>
|
||||
|
||||
<h2>
|
||||
<ng-container *ngTemplateOutlet="block.txs.length === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.txs.length| number}"></ng-container>
|
||||
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
|
||||
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
|
||||
</h2>
|
||||
|
||||
<ng-template ngFor let-tx [ngForOf]="block.txs">
|
||||
|
||||
<div class="header-bg box" style="padding: 10px; margin-bottom: 10px;">
|
||||
<a [routerLink]="['/tx/' | relativeUrl, tx.id]" [state]="{ data: tx }">
|
||||
<span style="float: left;" class="d-block d-md-none">{{ tx.id | shortenString : 16 }}</span>
|
||||
<span style="float: left;" class="d-none d-md-block">{{ tx.id }}</span>
|
||||
</a>
|
||||
<div class="float-right">
|
||||
‎{{ tx.time | date:'yyyy-MM-dd HH:mm' }}
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<app-bisq-transfers [tx]="tx" [showConfirmations]="true"></app-bisq-transfers>
|
||||
|
||||
<br>
|
||||
</ng-template>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="isLoading && !error">
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td-width" i18n="block.hash">Hash</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="block.timestamp">Timestamp</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td-width" i18n="block.previous_hash|Transaction Previous Hash">Previous hash</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="error">
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="text-center">
|
||||
Error loading block
|
||||
<br>
|
||||
<i>{{ error.status }}: {{ error.statusText }}</i>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
</div>
|
||||
@@ -1,44 +0,0 @@
|
||||
.td-width {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { BisqBlock } from '../../bisq/bisq.interfaces';
|
||||
import { Location } from '@angular/common';
|
||||
import { BisqApiService } from '../bisq-api.service';
|
||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||
import { Subscription, of } from 'rxjs';
|
||||
import { switchMap, catchError } from 'rxjs/operators';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-block',
|
||||
templateUrl: './bisq-block.component.html',
|
||||
styleUrls: ['./bisq-block.component.scss']
|
||||
})
|
||||
export class BisqBlockComponent implements OnInit, OnDestroy {
|
||||
block: BisqBlock;
|
||||
subscription: Subscription;
|
||||
blockHash = '';
|
||||
blockHeight = 0;
|
||||
isLoading = true;
|
||||
error: HttpErrorResponse | null;
|
||||
|
||||
constructor(
|
||||
private websocketService: WebsocketService,
|
||||
private bisqApiService: BisqApiService,
|
||||
private route: ActivatedRoute,
|
||||
private seoService: SeoService,
|
||||
private electrsApiService: ElectrsApiService,
|
||||
private router: Router,
|
||||
private location: Location,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
this.subscription = this.route.paramMap
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
const blockHash = params.get('id') || '';
|
||||
document.body.scrollTo(0, 0);
|
||||
this.isLoading = true;
|
||||
this.error = null;
|
||||
if (history.state.data && history.state.data.blockHeight) {
|
||||
this.blockHeight = history.state.data.blockHeight;
|
||||
}
|
||||
if (history.state.data && history.state.data.block) {
|
||||
this.blockHeight = history.state.data.block.height;
|
||||
return of(history.state.data.block);
|
||||
}
|
||||
|
||||
let isBlockHeight = false;
|
||||
if (/^[0-9]+$/.test(blockHash)) {
|
||||
isBlockHeight = true;
|
||||
} else {
|
||||
this.blockHash = blockHash;
|
||||
}
|
||||
|
||||
if (isBlockHeight) {
|
||||
return this.electrsApiService.getBlockHashFromHeight$(parseInt(blockHash, 10))
|
||||
.pipe(
|
||||
switchMap((hash) => {
|
||||
if (!hash) {
|
||||
return;
|
||||
}
|
||||
this.blockHash = hash;
|
||||
this.location.replaceState(
|
||||
this.router.createUrlTree(['/bisq/block/', hash]).toString()
|
||||
);
|
||||
this.seoService.updateCanonical(this.location.path());
|
||||
return this.bisqApiService.getBlock$(this.blockHash)
|
||||
.pipe(catchError(this.caughtHttpError.bind(this)));
|
||||
}),
|
||||
catchError(this.caughtHttpError.bind(this))
|
||||
);
|
||||
}
|
||||
|
||||
return this.bisqApiService.getBlock$(this.blockHash)
|
||||
.pipe(catchError(this.caughtHttpError.bind(this)));
|
||||
})
|
||||
)
|
||||
.subscribe((block: BisqBlock) => {
|
||||
if (!block) {
|
||||
this.seoService.logSoft404();
|
||||
return;
|
||||
}
|
||||
this.isLoading = false;
|
||||
this.blockHeight = block.height;
|
||||
this.seoService.setTitle($localize`:@@bisq-block.component.browser-title:Block ${block.height}:BLOCK_HEIGHT:: ${block.hash}:BLOCK_HASH:`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bisq.block:See all BSQ transactions in Bitcoin block ${block.height}:BLOCK_HEIGHT: (block hash ${block.hash}:BLOCK_HASH:).`);
|
||||
this.block = block;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
|
||||
caughtHttpError(err: HttpErrorResponse){
|
||||
this.error = err;
|
||||
this.seoService.logSoft404();
|
||||
return of(null);
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
<div class="container-xl" (window:resize)="onResize($event)">
|
||||
<h1 style="float: left;" i18n="Bisq blocks header">BSQ Blocks</h1>
|
||||
<br>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<ng-container *ngIf="{ value: (blocks$ | async) } as blocks">
|
||||
|
||||
<div class="table-responsive-sm">
|
||||
<table class="table table-borderless table-striped">
|
||||
<thead>
|
||||
<th style="width: 25%;" i18n="Bisq block height header">Height</th>
|
||||
<th style="width: 25%;" i18n="Bisq block confirmed time header">Confirmed</th>
|
||||
<th style="width: 25%;" i18n="Bisq block total BSQ tokens sent header">Total sent</th>
|
||||
<th class="d-none d-md-block" style="width: 25%;" i18n="Bisq block transactions title">Transactions</th>
|
||||
</thead>
|
||||
<tbody *ngIf="blocks.value; else loadingTmpl">
|
||||
<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 kind="since" [time]="block.time / 1000" [fastRender]="true"></app-time></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>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<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>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
<br>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<ng-template #loadingTmpl>
|
||||
<tr *ngFor="let i of loadingItems">
|
||||
<td *ngFor="let j of [1, 2, 3, 4]"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
@@ -1,6 +0,0 @@
|
||||
.pagination-container {
|
||||
float: none;
|
||||
@media(min-width: 400px){
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { BisqApiService } from '../bisq-api.service';
|
||||
import { switchMap, map, take, mergeMap, tap } from 'rxjs/operators';
|
||||
import { Observable } from 'rxjs';
|
||||
import { BisqBlock, BisqOutput, BisqTransaction } from '../bisq.interfaces';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-blocks',
|
||||
templateUrl: './bisq-blocks.component.html',
|
||||
styleUrls: ['./bisq-blocks.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BisqBlocksComponent implements OnInit {
|
||||
blocks$: Observable<[BisqBlock[], number]>;
|
||||
page = 1;
|
||||
itemsPerPage: number;
|
||||
contentSpace = window.innerHeight - (165 + 75);
|
||||
fiveItemsPxSize = 250;
|
||||
loadingItems: number[];
|
||||
isLoading = true;
|
||||
// @ts-ignore
|
||||
paginationSize: 'sm' | 'lg' = 'md';
|
||||
paginationMaxSize = 5;
|
||||
|
||||
constructor(
|
||||
private websocketService: WebsocketService,
|
||||
private bisqApiService: BisqApiService,
|
||||
private seoService: SeoService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.websocketService.want(['blocks']);
|
||||
this.seoService.setTitle($localize`:@@8a7b4bd44c0ac71b2e72de0398b303257f7d2f54:Blocks`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bisq.blocks:See a list of recent Bitcoin blocks with BSQ transactions, total BSQ sent per block, and more.`);
|
||||
this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10);
|
||||
this.loadingItems = Array(this.itemsPerPage);
|
||||
if (document.body.clientWidth < 670) {
|
||||
this.paginationSize = 'sm';
|
||||
this.paginationMaxSize = 3;
|
||||
}
|
||||
|
||||
this.blocks$ = this.route.queryParams
|
||||
.pipe(
|
||||
take(1),
|
||||
tap((qp) => {
|
||||
if (qp.page) {
|
||||
this.page = parseInt(qp.page, 10);
|
||||
}
|
||||
}),
|
||||
mergeMap(() => this.route.queryParams),
|
||||
map((queryParams) => {
|
||||
if (queryParams.page) {
|
||||
const newPage = parseInt(queryParams.page, 10);
|
||||
this.page = newPage;
|
||||
return newPage;
|
||||
} else {
|
||||
this.page = 1;
|
||||
}
|
||||
return 1;
|
||||
}),
|
||||
switchMap((page) => this.bisqApiService.listBlocks$((page - 1) * this.itemsPerPage, this.itemsPerPage)),
|
||||
map((response) => [response.body, parseInt(response.headers.get('x-total-count'), 10)]),
|
||||
);
|
||||
}
|
||||
|
||||
calculateTotalOutput(block: BisqBlock): number {
|
||||
return block.txs.reduce((a: number, tx: BisqTransaction) =>
|
||||
a + tx.outputs.reduce((acc: number, output: BisqOutput) => acc + output.bsqAmount, 0), 0
|
||||
);
|
||||
}
|
||||
|
||||
trackByFn(index: number) {
|
||||
return index;
|
||||
}
|
||||
|
||||
pageChange(page: number) {
|
||||
this.router.navigate([], {
|
||||
queryParams: { page: page },
|
||||
queryParamsHandling: 'merge',
|
||||
});
|
||||
}
|
||||
|
||||
onResize(event: any) {
|
||||
this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5;
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
<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.BASE_MODULE === 'bisq'" [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>
|
||||
@@ -1,22 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
|
||||
.container-info{
|
||||
overflow-x: scroll;
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { Observable, combineLatest, BehaviorSubject, of } from 'rxjs';
|
||||
import { map, share, switchMap } from 'rxjs/operators';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { WebsocketService } from '../../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($localize`:@@meta.title.bisq.markets:Markets`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bisq.markets:Explore the full Bitcoin ecosystem with The Mempool Open Source Project®. See Bisq market prices, trading activity, and more.`);
|
||||
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.BASE_MODULE !== 'bisq') {
|
||||
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.BASE_MODULE !== 'bisq') {
|
||||
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 @@
|
||||
<fa-icon [icon]="iconProp" [fixedWidth]="true" [ngStyle]="{ 'color': '#' + color }"></fa-icon>
|
||||
@@ -1,87 +0,0 @@
|
||||
import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core';
|
||||
import { IconPrefix, IconName } from '@fortawesome/fontawesome-common-types';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-icon',
|
||||
templateUrl: './bisq-icon.component.html',
|
||||
styleUrls: ['./bisq-icon.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BisqIconComponent implements OnChanges {
|
||||
@Input() txType: string;
|
||||
|
||||
iconProp: [IconPrefix, IconName] = ['fas', 'leaf'];
|
||||
color: string;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnChanges() {
|
||||
switch (this.txType) {
|
||||
case 'UNVERIFIED':
|
||||
this.iconProp[1] = 'question';
|
||||
this.color = 'ffac00';
|
||||
break;
|
||||
case 'INVALID':
|
||||
this.iconProp[1] = 'exclamation-triangle';
|
||||
this.color = 'ff4500';
|
||||
break;
|
||||
case 'GENESIS':
|
||||
this.iconProp[1] = 'rocket';
|
||||
this.color = '25B135';
|
||||
break;
|
||||
case 'TRANSFER_BSQ':
|
||||
this.iconProp[1] = 'retweet';
|
||||
this.color = 'a3a3a3';
|
||||
break;
|
||||
case 'PAY_TRADE_FEE':
|
||||
this.iconProp[1] = 'leaf';
|
||||
this.color = '689f43';
|
||||
break;
|
||||
case 'PROPOSAL':
|
||||
this.iconProp[1] = 'file-alt';
|
||||
this.color = '6c8b3b';
|
||||
break;
|
||||
case 'COMPENSATION_REQUEST':
|
||||
this.iconProp[1] = 'money-bill';
|
||||
this.color = '689f43';
|
||||
break;
|
||||
case 'REIMBURSEMENT_REQUEST':
|
||||
this.iconProp[1] = 'money-bill';
|
||||
this.color = '04a908';
|
||||
break;
|
||||
case 'BLIND_VOTE':
|
||||
this.iconProp[1] = 'eye-slash';
|
||||
this.color = '07579a';
|
||||
break;
|
||||
case 'VOTE_REVEAL':
|
||||
this.iconProp[1] = 'eye';
|
||||
this.color = '4AC5FF';
|
||||
break;
|
||||
case 'LOCKUP':
|
||||
this.iconProp[1] = 'lock';
|
||||
this.color = '0056c4';
|
||||
break;
|
||||
case 'UNLOCK':
|
||||
this.iconProp[1] = 'lock-open';
|
||||
this.color = '1d965f';
|
||||
break;
|
||||
case 'ASSET_LISTING_FEE':
|
||||
this.iconProp[1] = 'file-alt';
|
||||
this.color = '6c8b3b';
|
||||
break;
|
||||
case 'PROOF_OF_BURN':
|
||||
this.iconProp[1] = 'file-alt';
|
||||
this.color = '6c8b3b';
|
||||
break;
|
||||
case 'IRREGULAR':
|
||||
this.iconProp[1] = 'exclamation-circle';
|
||||
this.color = 'ffd700';
|
||||
break;
|
||||
default:
|
||||
this.iconProp[1] = 'question';
|
||||
this.color = 'ffac00';
|
||||
}
|
||||
// @ts-ignore
|
||||
this.iconProp = this.iconProp.slice();
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
<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.BASE_MODULE === 'bisq'" [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-more">View more »</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-more">View more »</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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]"><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>
|
||||
@@ -1,112 +0,0 @@
|
||||
#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 {
|
||||
overflow: scroll;
|
||||
scrollbar-width: none;
|
||||
font-size: 13px;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
@media(min-width: 576px){
|
||||
font-size: 16px;
|
||||
}
|
||||
thead th{
|
||||
text-align: right;
|
||||
&:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
&:nth-child(3) {
|
||||
display: none;
|
||||
@media(min-width: 1100px){
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
}
|
||||
tr {
|
||||
td {
|
||||
text-align: right;
|
||||
&:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
&:nth-child(3) {
|
||||
display: none;
|
||||
@media(min-width: 1100px){
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { Observable, combineLatest, BehaviorSubject, of } from 'rxjs';
|
||||
import { map, share, switchMap } from 'rxjs/operators';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { WebsocketService } from '../../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.resetTitle();
|
||||
this.seoService.resetDescription();
|
||||
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.BASE_MODULE !== 'bisq') {
|
||||
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.BASE_MODULE !== 'bisq') {
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
<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" name="radioBasic">
|
||||
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'half_hour'">
|
||||
<input type="radio" [value]="'half_hour'" (click)="setFragment('half_hour')" formControlName="interval"> 30M
|
||||
</label>
|
||||
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'hour'">
|
||||
<input type="radio" [value]="'hour'" (click)="setFragment('hour')" formControlName="interval"> 1H
|
||||
</label>
|
||||
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'half_day'">
|
||||
<input type="radio" [value]="'half_day'" (click)="setFragment('half_day')" formControlName="interval"> 12H
|
||||
</label>
|
||||
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'day'">
|
||||
<input type="radio" [value]="'day'" (click)="setFragment('day')" formControlName="interval"> 1D
|
||||
</label>
|
||||
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'week'">
|
||||
<input type="radio" [value]="'week'" (click)="setFragment('week')" formControlName="interval"> 1W
|
||||
</label>
|
||||
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'month'">
|
||||
<input type="radio" [value]="'month'" (click)="setFragment('month')" formControlName="interval"> 1M
|
||||
</label>
|
||||
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'year'">
|
||||
<input type="radio" [value]="'year'" (click)="setFragment('year')" formControlName="interval"> 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>
|
||||
@@ -1,46 +0,0 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { UntypedFormBuilder, UntypedFormGroup } 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 '../../services/seo.service';
|
||||
import { WebsocketService } from '../../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: UntypedFormGroup;
|
||||
defaultInterval = 'day';
|
||||
|
||||
isLoadingGraph = false;
|
||||
|
||||
constructor(
|
||||
private websocketService: WebsocketService,
|
||||
private route: ActivatedRoute,
|
||||
private bisqApiService: BisqApiService,
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
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($localize`:@@meta.title.bisq.market:Bisq market: ${pairUpperCase}`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bisq.market:See price history, current buy/sell offers, and latest trades for the ${pairUpperCase} market on Bisq.`);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
<div class="container-xl">
|
||||
<h1 style="float: left;" i18n="BSQ statistics header">BSQ statistics</h1>
|
||||
<br>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="row">
|
||||
<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' }} <span class="symbol">BSQ</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="BSQ minted amount">Minted amount</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' }} <span class="symbol">BSQ</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="BSQ addresses">Addresses</td>
|
||||
<td>{{ stats.addresses | number }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="BSQ unspent transaction outputs">Unspent TXOs</td>
|
||||
<td>{{ stats.unspent_txos | number }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="BSQ spent transaction outputs">Spent TXOs</td>
|
||||
<td>{{ stats.spent_txos | number }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n>Price</td>
|
||||
<td><app-fiat [value]="price"></app-fiat></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="BSQ token market cap">Market cap</td>
|
||||
<td><app-fiat [value]="price * (stats.minted - stats.burnt)"></app-fiat></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
<div class="col-md"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #loadingTemplate>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td-width" i18n>Existing amount</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n>Minted amount</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n>Burnt amount</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n>Addresses</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n>Unspent TXOs</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Spent TXOs</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n>Price</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n>Market cap</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</ng-template>
|
||||
@@ -1,18 +0,0 @@
|
||||
.td-width {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.td-width {
|
||||
width: 175px;
|
||||
}
|
||||
}
|
||||
|
||||
.fiat {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
@media (min-width: 768px) {
|
||||
font-size: 14px;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { BisqApiService } from '../bisq-api.service';
|
||||
import { BisqStats } from '../bisq.interfaces';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-stats',
|
||||
templateUrl: './bisq-stats.component.html',
|
||||
styleUrls: ['./bisq-stats.component.scss']
|
||||
})
|
||||
export class BisqStatsComponent implements OnInit {
|
||||
isLoading = true;
|
||||
stats: BisqStats;
|
||||
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.seoService.setDescription($localize`:@@meta.description.bisq.stats:See high-level stats on the BSQ economy: supply metrics, number of addresses, BSQ price, market cap, and more.`);
|
||||
this.stateService.bsqPrice$
|
||||
.subscribe((bsqPrice) => {
|
||||
this.price = bsqPrice;
|
||||
});
|
||||
|
||||
this.bisqApiService.getStats$()
|
||||
.subscribe((stats) => {
|
||||
this.isLoading = false;
|
||||
this.stats = stats;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
<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>
|
||||
@@ -1,38 +0,0 @@
|
||||
|
||||
.table-container {
|
||||
overflow: scroll;
|
||||
scrollbar-width: none;
|
||||
font-size: 13px;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
@media(min-width: 576px){
|
||||
font-size: 16px;
|
||||
}
|
||||
thead th{
|
||||
text-align: right;
|
||||
&:first-child{
|
||||
text-align: left;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
display: none;
|
||||
@media(min-width: 1100px){
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
}
|
||||
tr {
|
||||
td {
|
||||
text-align: right;
|
||||
&:first-child{
|
||||
text-align: left;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
display: none;
|
||||
@media(min-width: 1100px){
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user