Compare commits
479 Commits
v3.0.0-dev
...
v3.0.0-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
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 | ||
|
|
1bba1cfeb1 | ||
|
|
4a6641f544 | ||
|
|
c51159d275 | ||
|
|
d279ea3278 | ||
|
|
102cb96483 | ||
|
|
10d4e5a600 | ||
|
|
2048816c23 | ||
|
|
8d2efd4d10 | ||
|
|
ecf1db0716 | ||
|
|
1a3b8580e7 | ||
|
|
fb6aec0afe | ||
|
|
7063d9e113 | ||
|
|
0445c150a2 | ||
|
|
cfb1e1d908 | ||
|
|
3f80dbc5ce | ||
|
|
b8a48314c1 | ||
|
|
0090d36514 | ||
|
|
bf22f4515f | ||
|
|
bf5d5b98cb | ||
|
|
df491d050d | ||
|
|
91267201bf | ||
|
|
061abaa148 | ||
|
|
4132765027 | ||
|
|
e64d1be6f0 | ||
|
|
03255dd077 | ||
|
|
bd4e223aed | ||
|
|
964be2a6df | ||
|
|
cf00fa73c6 | ||
|
|
8956ef4b43 | ||
|
|
5c3f256229 | ||
|
|
56b9715fc5 | ||
|
|
e3f7f08fb4 | ||
|
|
cc63f20708 | ||
|
|
93956d0ed4 | ||
|
|
4a3aceadbf | ||
|
|
9b456954b1 | ||
|
|
855b20834e | ||
|
|
df72829fd2 | ||
|
|
855e25ccd2 | ||
|
|
150a7d9eeb | ||
|
|
1630d71e7b | ||
|
|
7deee5d5f2 | ||
|
|
19dcc9d62d | ||
|
|
9270298374 | ||
|
|
1c862d57ea | ||
|
|
ac8fdd6405 | ||
|
|
c5300c950b | ||
|
|
92b9c8f370 | ||
|
|
7969d069b2 | ||
|
|
7367991df1 | ||
|
|
8f19a376fa | ||
|
|
0b1296330b | ||
|
|
6bebc76633 | ||
|
|
6b8f08c6d7 | ||
|
|
ba8d4ccc02 | ||
|
|
e9fe50a4bb | ||
|
|
52a33d5a51 | ||
|
|
66309813d5 | ||
|
|
5a57d220cb | ||
|
|
cbcb61d3fa | ||
|
|
3d0e16857a | ||
|
|
3732406ce8 | ||
|
|
8f2e1de578 | ||
|
|
07c76d084e | ||
|
|
5411eb491f | ||
|
|
b10bd05207 | ||
|
|
0533953f54 | ||
|
|
b3e07e0c22 | ||
|
|
62653086e9 | ||
|
|
e2d3bb4cc5 | ||
|
|
70bf31c397 | ||
|
|
22d9a1cb8b | ||
|
|
6492aa6f4a | ||
|
|
c0a9a8b07a | ||
|
|
5c430e57a4 | ||
|
|
27b48b1874 | ||
|
|
33cddc6fba | ||
|
|
1e5b798af9 | ||
|
|
f91fc6f026 | ||
|
|
82e115c009 | ||
|
|
44cf63458f | ||
|
|
53aea3dcf6 | ||
|
|
7408a6f331 | ||
|
|
6e62e93bf6 | ||
|
|
8861aedea1 | ||
|
|
16f9593d2e | ||
|
|
5537e79640 | ||
|
|
1ea66595ea | ||
|
|
fa2dd3c7cb | ||
|
|
1e04afffa3 | ||
|
|
57cbf71915 | ||
|
|
78b1b1e561 | ||
|
|
6fb2a3b39c | ||
|
|
cb866d8fa2 | ||
|
|
365c89d98f | ||
|
|
f4b9301f55 | ||
|
|
6afe12c1be | ||
|
|
952a61f8f2 | ||
|
|
0825c7a6d6 | ||
|
|
0ece8bb2a6 | ||
|
|
8822c0972f | ||
|
|
6310ef7f57 | ||
|
|
d4749cfc82 | ||
|
|
60996a99f0 | ||
|
|
1b21cd89a3 | ||
|
|
13fb75fec3 | ||
|
|
484e032775 | ||
|
|
21b3e4071c | ||
|
|
48b82329cb | ||
|
|
00dcff50ee | ||
|
|
abbc8a134b | ||
|
|
4374e6e0eb | ||
|
|
15062b80d1 | ||
|
|
b4d0b75557 | ||
|
|
d6f1b51fa6 | ||
|
|
0a24a9ea7c | ||
|
|
e01df9ee32 | ||
|
|
3aa12c935b | ||
|
|
6e2f17b3d2 | ||
|
|
50e656b259 | ||
|
|
48af95d722 | ||
|
|
c926088136 | ||
|
|
b167848b9b | ||
|
|
99730d02ab | ||
|
|
ed73c1e94c | ||
|
|
ab5ee5370a | ||
|
|
4fbbff6614 | ||
|
|
9cd33825bf | ||
|
|
abd7f62b20 | ||
|
|
bcd337794a | ||
|
|
1ae5f05316 | ||
|
|
c5822b11a0 | ||
|
|
5b4132b551 | ||
|
|
29aaeef47b | ||
|
|
10e468d463 | ||
|
|
6464f6d8bb | ||
|
|
cfdbd93695 | ||
|
|
a2b9b0c89d | ||
|
|
e6fb93140c | ||
|
|
4e26e1f196 | ||
|
|
1bb625f06b | ||
|
|
68b55db872 | ||
|
|
c47822cae7 | ||
|
|
00a94f9e3c | ||
|
|
adda511869 | ||
|
|
1b971bfb05 | ||
|
|
ea94dfcd6b | ||
|
|
3dd328475e | ||
|
|
c18779b4e3 | ||
|
|
5deb8c3149 | ||
|
|
78f03bd6d5 | ||
|
|
5249fb1a8b | ||
|
|
4ce0497048 | ||
|
|
393181f753 | ||
|
|
7f2d77a73b | ||
|
|
f8a1fc6aa2 | ||
|
|
7bf7d02a18 | ||
|
|
7b6163cfee | ||
|
|
19b0c4e410 | ||
|
|
2c16dda7c0 | ||
|
|
82f1fa5110 | ||
|
|
524619f48e | ||
|
|
666165ebe9 | ||
|
|
6b9159b89c | ||
|
|
48d852fd49 | ||
|
|
f6dbe1e17f | ||
|
|
fc5b2c050e | ||
|
|
92d960a9cc | ||
|
|
482493fef0 | ||
|
|
4e45993d41 | ||
|
|
8edd2b5802 | ||
|
|
e53254fb2d | ||
|
|
40663c79c0 | ||
|
|
cca7c7e8e2 | ||
|
|
2c6f210e8e | ||
|
|
82ba80dbb1 | ||
|
|
634b461bb5 | ||
|
|
f1b9a0f536 | ||
|
|
b7d9efebd1 | ||
|
|
34d93abbf6 | ||
|
|
dbecb73f97 | ||
|
|
9fa6d232d9 | ||
|
|
30c033b403 | ||
|
|
788d1de968 | ||
|
|
a04d685f1a | ||
|
|
43232b9d0a | ||
|
|
acd9c23180 | ||
|
|
a8fa7dcb2a | ||
|
|
2dc6f6ff5a | ||
|
|
df107d34b4 | ||
|
|
2245109ae6 | ||
|
|
1c76a6091a | ||
|
|
29c0131edf | ||
|
|
c78a14c5fd | ||
|
|
1dd72418c1 | ||
|
|
f54bdace61 | ||
|
|
5ac87c095f | ||
|
|
2d8111a6e7 | ||
|
|
d5cca2903c | ||
|
|
d5e591c4af | ||
|
|
b5fa44955f | ||
|
|
9e1cf51e4a | ||
|
|
fd41dfc152 | ||
|
|
d03e7b2d46 | ||
|
|
d3c5cbde7f | ||
|
|
dd7c8f2934 | ||
|
|
0b173296c4 | ||
|
|
81c8c8dafb | ||
|
|
31d3566ed5 | ||
|
|
fb852b73e9 | ||
|
|
a44219a3c3 | ||
|
|
120ffdb0f4 | ||
|
|
8dbfa1c73c | ||
|
|
8427127545 | ||
|
|
404892174d | ||
|
|
e171c7694c | ||
|
|
7bc6ef2516 | ||
|
|
0fcfc5e78d | ||
|
|
c12e664b9a | ||
|
|
8cb040eec6 | ||
|
|
5ecb915c60 | ||
|
|
ab93a45853 | ||
|
|
a95d17950f | ||
|
|
860aac060c | ||
|
|
023b2f7ae2 | ||
|
|
9a531b0a00 | ||
|
|
0c4631359d | ||
|
|
fd04947ac0 | ||
|
|
acdc5f92cd | ||
|
|
f0f1eb06bb | ||
|
|
fc1179f7bd | ||
|
|
cbc0e44d64 | ||
|
|
f851e821b4 | ||
|
|
df51018599 | ||
|
|
11a4f4e6d9 | ||
|
|
b99e5c4160 | ||
|
|
91ec0c8382 | ||
|
|
669cf59269 | ||
|
|
8e158e1786 | ||
|
|
93f547e446 | ||
|
|
ece40c5e74 | ||
|
|
0dff902151 | ||
|
|
5ea4ef80b9 | ||
|
|
d83f483898 | ||
|
|
c5d382c351 | ||
|
|
b50d747440 | ||
|
|
e7fde2f872 | ||
|
|
c4b47e1f1c | ||
|
|
3d61e57b00 | ||
|
|
92a5fc8159 | ||
|
|
a5099fed75 | ||
|
|
b11164005c | ||
|
|
a2b0dd3a23 | ||
|
|
553c2131d9 | ||
|
|
c590a623f0 | ||
|
|
a81c0a3b0f | ||
|
|
0f6b694fa9 | ||
|
|
c55c298fb5 | ||
|
|
ede3a121c7 | ||
|
|
7bedb9488b | ||
|
|
6d3c429bf4 | ||
|
|
9a730b51d4 | ||
|
|
a7c450895b | ||
|
|
a5f864fe35 | ||
|
|
b41f196421 | ||
|
|
462c7f9c2c | ||
|
|
869aa7fea5 | ||
|
|
5b1f3d7cbe | ||
|
|
60ad0b71c5 | ||
|
|
68af577104 | ||
|
|
26be760692 | ||
|
|
a5b16cf129 | ||
|
|
a0e387efad | ||
|
|
e26e08b426 | ||
|
|
d9d92f1915 | ||
|
|
79f0ea74c8 | ||
|
|
d1c12f9f6e | ||
|
|
1b92c62353 | ||
|
|
fffffffdb3 | ||
|
|
2647e94bc8 | ||
|
|
ccf1121f19 | ||
|
|
23076172e4 | ||
|
|
52c4dd8bc3 | ||
|
|
bfde456ca8 | ||
|
|
10819fda6d | ||
|
|
c3d90d573f | ||
|
|
3c2ba74cb2 | ||
|
|
0079e5d576 | ||
|
|
3f07026229 | ||
|
|
ac971d17c7 | ||
|
|
00d8241dd7 | ||
|
|
d459e1fae3 | ||
|
|
61b84e3957 | ||
|
|
020194d37b | ||
|
|
1665b0d915 | ||
|
|
b606282fb4 | ||
|
|
dde6719306 | ||
|
|
7af185a919 | ||
|
|
c17b77fe31 | ||
|
|
faa5887c3b | ||
|
|
29bbb922c5 | ||
|
|
aac3b1aa5c | ||
|
|
e3bb7f73f9 | ||
|
|
c4ecc1626d | ||
|
|
007f424d15 | ||
|
|
1d56bfa9b8 | ||
|
|
e7788133fa | ||
|
|
7491fb512c | ||
|
|
b043d698ca | ||
|
|
f121d16544 | ||
|
|
bfaddfc345 | ||
|
|
f5e1e5f1a2 | ||
|
|
e8c5d478fa | ||
|
|
1d877a746f | ||
|
|
82716b0037 | ||
|
|
b988f042cc | ||
|
|
1df5c5df6d | ||
|
|
3178d30f2a | ||
|
|
be52fd4e46 | ||
|
|
11a6d81a17 | ||
|
|
31e320b2e2 | ||
|
|
7f18d73443 | ||
|
|
30485e7483 | ||
|
|
a2fb16c84c | ||
|
|
57a878403e | ||
|
|
faa5475ab7 | ||
|
|
28d23d4304 | ||
|
|
cef80a36a9 | ||
|
|
e0e997a3d1 | ||
|
|
c4a11309fa | ||
|
|
fa4caec73e | ||
|
|
965a135390 | ||
|
|
9e5fdbb64a | ||
|
|
71863e4f48 | ||
|
|
7cee1512a3 | ||
|
|
c9b2ce3efb | ||
|
|
0f1434e32d | ||
|
|
acb60fa1bc | ||
|
|
9e1fe71dc8 | ||
|
|
62e0cb2e57 | ||
|
|
198939da81 | ||
|
|
847b90f167 | ||
|
|
73e9c85ff1 | ||
|
|
5f3ca3a321 | ||
|
|
1518b3ccfb | ||
|
|
5898143f66 | ||
|
|
f63f1b1773 | ||
|
|
8405e5ff07 | ||
|
|
f923ee4ede | ||
|
|
bd0de745e2 | ||
|
|
130bdac58c | ||
|
|
75578ac9fa | ||
|
|
b9d46003f8 | ||
|
|
1fd07f6c9f | ||
|
|
5a5409fd1a | ||
|
|
4bddb52db1 | ||
|
|
dc9bddc6b2 | ||
|
|
8626846264 | ||
|
|
eadbc139fa | ||
|
|
882e8ec598 | ||
|
|
249c57f376 | ||
|
|
f083aa37d6 | ||
|
|
c74ce7de83 | ||
|
|
d4c01f6b1f | ||
|
|
1deacb9996 | ||
|
|
8bd939b374 | ||
|
|
cda5448f13 | ||
|
|
af6af2748c | ||
|
|
975588a862 | ||
|
|
093649136e | ||
|
|
84e91b2c90 | ||
|
|
ef209da774 | ||
|
|
4a6231c93c | ||
|
|
867e959430 | ||
|
|
3f8a4b5119 | ||
|
|
cc06f85e73 | ||
|
|
ab0c3eeab6 | ||
|
|
e26fcff063 | ||
|
|
b3b65c59dc | ||
|
|
fbebfdae04 | ||
|
|
dbe66fd4d9 | ||
|
|
3041540283 | ||
|
|
d982938366 | ||
|
|
1f14cbfcaa | ||
|
|
a7ce7121ee | ||
|
|
96a17f09a5 | ||
|
|
b8d635f257 | ||
|
|
d04952b10b | ||
|
|
df92f4d0aa | ||
|
|
08272c7f87 | ||
|
|
89d805b500 | ||
|
|
64f6775c68 | ||
|
|
24b3ca559d | ||
|
|
5edd83d496 | ||
|
|
693b845c45 | ||
|
|
fc2df35a42 | ||
|
|
d02838fb59 | ||
|
|
572f6bdbac | ||
|
|
42a222338b | ||
|
|
a23881e62c |
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -29,13 +29,13 @@ jobs:
|
||||
|
||||
- name: Read rust-toolchain file from repository
|
||||
id: gettoolchain
|
||||
run: echo "::set-output name=toolchain::$(cat rust-toolchain)"
|
||||
run: echo "::set-output name=toolchain::$(cat ./rust/gbt/rust-toolchain)"
|
||||
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}
|
||||
|
||||
- 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@be73d7920c329f220ce78e0234b8f96b7ae60248
|
||||
uses: dtolnay/rust-toolchain@dc6353516c68da0f06325f42ad880f76a5e77ec9
|
||||
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:
|
||||
|
||||
19
.github/workflows/get_backend_block_height.yml
vendored
Normal file
19
.github/workflows/get_backend_block_height.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: 'Check if servers are in sync'
|
||||
|
||||
on: [workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
print-backend-sha:
|
||||
runs-on: 'ubuntu-latest'
|
||||
name: Get block height
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: repo
|
||||
|
||||
- name: Run script
|
||||
working-directory: repo
|
||||
run: |
|
||||
chmod +x ./scripts/get_block_tip_height.sh
|
||||
sh ./scripts/get_block_tip_height.sh
|
||||
3
.github/workflows/on-tag.yml
vendored
3
.github/workflows/on-tag.yml
vendored
@@ -100,5 +100,8 @@ jobs:
|
||||
--cache-to "type=local,dest=/tmp/.buildx-cache" \
|
||||
--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
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"./backend/rust-gbt",
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
3
backend/.gitignore
vendored
3
backend/.gitignore
vendored
@@ -54,3 +54,6 @@ Thumbs.db
|
||||
|
||||
# package folder (npm run package output)
|
||||
/package
|
||||
|
||||
# Rust GBT folder (We build externally first)
|
||||
/rust-gbt
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"MEMPOOL": {
|
||||
"OFFICIAL": false,
|
||||
"NETWORK": "mainnet",
|
||||
"BACKEND": "electrum",
|
||||
"ENABLED": true,
|
||||
@@ -27,9 +28,8 @@
|
||||
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json",
|
||||
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
|
||||
"AUDIT": false,
|
||||
"ADVANCED_GBT_AUDIT": false,
|
||||
"ADVANCED_GBT_MEMPOOL": false,
|
||||
"RUST_GBT": false,
|
||||
"LIMIT_GBT": false,
|
||||
"CPFP_INDEXING": false,
|
||||
"DISK_CACHE_BLOCK_INTERVAL": 6,
|
||||
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
|
||||
@@ -97,10 +97,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 +127,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,
|
||||
@@ -152,7 +146,11 @@
|
||||
]
|
||||
},
|
||||
"MEMPOOL_SERVICES": {
|
||||
"API": "https://mempool.space/api",
|
||||
"API": "https://mempool.space/api/v1/services",
|
||||
"ACCELERATIONS": false
|
||||
},
|
||||
"FIAT_PRICE": {
|
||||
"ENABLED": true,
|
||||
"API_KEY": "your-api-key-from-freecurrencyapi.com"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,7 @@ set -e
|
||||
|
||||
# Cleaning up inside the node_modules folder
|
||||
cd package/node_modules
|
||||
rm -r \
|
||||
rm -rf \
|
||||
typescript \
|
||||
@typescript-eslint \
|
||||
@napi-rs \
|
||||
./rust-gbt/src \
|
||||
./rust-gbt/Cargo.toml \
|
||||
./rust-gbt/build.rs
|
||||
@napi-rs
|
||||
|
||||
440
backend/package-lock.json
generated
440
backend/package-lock.json
generated
@@ -7,21 +7,23 @@
|
||||
"": {
|
||||
"name": "mempool-backend",
|
||||
"version": "3.0.0-dev",
|
||||
"hasInstallScript": true,
|
||||
"license": "GNU Affero General Public License v3.0",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.24.0",
|
||||
"@mempool/electrum-client": "1.1.9",
|
||||
"@types/node": "^18.15.3",
|
||||
"axios": "~1.6.1",
|
||||
"bitcoinjs-lib": "~6.1.3",
|
||||
"crypto-js": "~4.2.0",
|
||||
"express": "~4.18.2",
|
||||
"express": "~4.19.2",
|
||||
"maxmind": "~4.3.11",
|
||||
"mysql2": "~3.9.1",
|
||||
"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.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
@@ -30,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",
|
||||
@@ -41,6 +43,13 @@
|
||||
"ts-node": "^10.9.1"
|
||||
}
|
||||
},
|
||||
"../rust/gbt": {
|
||||
"version": "3.0.1",
|
||||
"extraneous": true,
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/@aashutoshrathi/word-wrap": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
|
||||
@@ -1498,21 +1507,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/cli": {
|
||||
"version": "2.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.16.1.tgz",
|
||||
"integrity": "sha512-L0Gr5iEQIDEbvWdDr1HUaBOxBSHL1VZhWSk1oryawoT8qJIY+KGfLFelU+Qma64ivCPbxYpkfPoKYVG3rcoGIA==",
|
||||
"bin": {
|
||||
"napi": "scripts/index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.0.tgz",
|
||||
@@ -1864,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": "*"
|
||||
@@ -2535,12 +2529,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.1",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
|
||||
"integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
|
||||
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"content-type": "~1.0.4",
|
||||
"content-type": "~1.0.5",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
@@ -2548,7 +2542,7 @@
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "2.4.1",
|
||||
"qs": "6.11.0",
|
||||
"raw-body": "2.5.1",
|
||||
"raw-body": "2.5.2",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "1.0.0"
|
||||
},
|
||||
@@ -2677,12 +2671,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
|
||||
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.0.2"
|
||||
"es-define-property": "^1.0.0",
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-intrinsic": "^1.2.4",
|
||||
"set-function-length": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@@ -2866,9 +2866,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -2940,6 +2940,22 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/define-data-property": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.0",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
@@ -3070,6 +3086,25 @@
|
||||
"is-arrayish": "^0.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
|
||||
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
|
||||
"dependencies": {
|
||||
"get-intrinsic": "^1.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
||||
@@ -3465,16 +3500,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.18.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
|
||||
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
|
||||
"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",
|
||||
"body-parser": "1.20.1",
|
||||
"body-parser": "1.20.2",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.5.0",
|
||||
"cookie": "0.6.0",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
@@ -3672,9 +3707,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.5",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
|
||||
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
|
||||
"version": "1.15.6",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
||||
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -3740,9 +3775,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/generate-function": {
|
||||
"version": "2.3.1",
|
||||
@@ -3779,13 +3817,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz",
|
||||
"integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
|
||||
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
"has-symbols": "^1.0.3"
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2",
|
||||
"has-proto": "^1.0.1",
|
||||
"has-symbols": "^1.0.3",
|
||||
"hasown": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@@ -3873,6 +3916,17 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
|
||||
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
|
||||
"dependencies": {
|
||||
"get-intrinsic": "^1.1.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@@ -3889,6 +3943,7 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1"
|
||||
},
|
||||
@@ -3905,6 +3960,28 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/has-property-descriptors": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
||||
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-proto": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
|
||||
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||
@@ -3916,6 +3993,17 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
@@ -4026,9 +4114,9 @@
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"node_modules/ip": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
|
||||
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ=="
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz",
|
||||
"integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ=="
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
@@ -6218,9 +6306,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.12.3",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
|
||||
"integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
|
||||
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
@@ -6640,9 +6728,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
|
||||
"integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
|
||||
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"http-errors": "2.0.0",
|
||||
@@ -6882,6 +6970,22 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
|
||||
"dependencies": {
|
||||
"define-data-property": "^1.1.4",
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-intrinsic": "^1.2.4",
|
||||
"gopd": "^1.0.1",
|
||||
"has-property-descriptors": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
@@ -6909,13 +7013,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
|
||||
"integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.0",
|
||||
"get-intrinsic": "^1.0.2",
|
||||
"object-inspect": "^1.9.0"
|
||||
"call-bind": "^1.0.7",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.4",
|
||||
"object-inspect": "^1.13.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@@ -7582,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.16.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz",
|
||||
"integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
@@ -7665,15 +7773,7 @@
|
||||
}
|
||||
},
|
||||
"rust-gbt": {
|
||||
"name": "gbt",
|
||||
"version": "3.0.1",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@napi-rs/cli": "2.16.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
"version": "0.0.1"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -8773,11 +8873,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@mempool/electrum-client/-/electrum-client-1.1.9.tgz",
|
||||
"integrity": "sha512-mlvPiCzUlaETpYW3i6V87A24jjMYgsebaXtUo3WQyyLnYUuxs0KiXQ2mnKh3h15j8Xg/hfxeGIi+5OC9u0nftQ=="
|
||||
},
|
||||
"@napi-rs/cli": {
|
||||
"version": "2.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.16.1.tgz",
|
||||
"integrity": "sha512-L0Gr5iEQIDEbvWdDr1HUaBOxBSHL1VZhWSk1oryawoT8qJIY+KGfLFelU+Qma64ivCPbxYpkfPoKYVG3rcoGIA=="
|
||||
},
|
||||
"@noble/hashes": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.0.tgz",
|
||||
@@ -9103,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": "*"
|
||||
@@ -9579,12 +9674,12 @@
|
||||
}
|
||||
},
|
||||
"body-parser": {
|
||||
"version": "1.20.1",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
|
||||
"integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
|
||||
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
|
||||
"requires": {
|
||||
"bytes": "3.1.2",
|
||||
"content-type": "~1.0.4",
|
||||
"content-type": "~1.0.5",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
@@ -9592,7 +9687,7 @@
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "2.4.1",
|
||||
"qs": "6.11.0",
|
||||
"raw-body": "2.5.1",
|
||||
"raw-body": "2.5.2",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "1.0.0"
|
||||
},
|
||||
@@ -9690,12 +9785,15 @@
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="
|
||||
},
|
||||
"call-bind": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
|
||||
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.0.2"
|
||||
"es-define-property": "^1.0.0",
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-intrinsic": "^1.2.4",
|
||||
"set-function-length": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"callsites": {
|
||||
@@ -9822,9 +9920,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"cookie": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="
|
||||
},
|
||||
"cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
@@ -9879,6 +9977,16 @@
|
||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||
"dev": true
|
||||
},
|
||||
"define-data-property": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
||||
"requires": {
|
||||
"es-define-property": "^1.0.0",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
@@ -9972,6 +10080,19 @@
|
||||
"is-arrayish": "^0.2.1"
|
||||
}
|
||||
},
|
||||
"es-define-property": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
|
||||
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
|
||||
"requires": {
|
||||
"get-intrinsic": "^1.2.4"
|
||||
}
|
||||
},
|
||||
"es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="
|
||||
},
|
||||
"escalade": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
||||
@@ -10253,16 +10374,16 @@
|
||||
}
|
||||
},
|
||||
"express": {
|
||||
"version": "4.18.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
|
||||
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
|
||||
"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",
|
||||
"body-parser": "1.20.1",
|
||||
"body-parser": "1.20.2",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.5.0",
|
||||
"cookie": "0.6.0",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
@@ -10439,9 +10560,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.15.5",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
|
||||
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw=="
|
||||
"version": "1.15.6",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
||||
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA=="
|
||||
},
|
||||
"form-data": {
|
||||
"version": "4.0.0",
|
||||
@@ -10477,9 +10598,9 @@
|
||||
"optional": true
|
||||
},
|
||||
"function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
|
||||
},
|
||||
"generate-function": {
|
||||
"version": "2.3.1",
|
||||
@@ -10507,13 +10628,15 @@
|
||||
"dev": true
|
||||
},
|
||||
"get-intrinsic": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz",
|
||||
"integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
|
||||
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
"has-symbols": "^1.0.3"
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2",
|
||||
"has-proto": "^1.0.1",
|
||||
"has-symbols": "^1.0.3",
|
||||
"hasown": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"get-package-type": {
|
||||
@@ -10571,6 +10694,14 @@
|
||||
"slash": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"gopd": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
|
||||
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
|
||||
"requires": {
|
||||
"get-intrinsic": "^1.1.3"
|
||||
}
|
||||
},
|
||||
"graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@@ -10587,6 +10718,7 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1"
|
||||
}
|
||||
@@ -10597,11 +10729,32 @@
|
||||
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
|
||||
"dev": true
|
||||
},
|
||||
"has-property-descriptors": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
||||
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
||||
"requires": {
|
||||
"es-define-property": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"has-proto": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
|
||||
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q=="
|
||||
},
|
||||
"has-symbols": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
|
||||
},
|
||||
"hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"requires": {
|
||||
"function-bind": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
@@ -10682,9 +10835,9 @@
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"ip": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
|
||||
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ=="
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz",
|
||||
"integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ=="
|
||||
},
|
||||
"ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
@@ -12318,9 +12471,9 @@
|
||||
}
|
||||
},
|
||||
"object-inspect": {
|
||||
"version": "1.12.3",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
|
||||
"integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g=="
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
|
||||
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ=="
|
||||
},
|
||||
"on-finished": {
|
||||
"version": "2.4.1",
|
||||
@@ -12600,9 +12753,9 @@
|
||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
|
||||
},
|
||||
"raw-body": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
|
||||
"integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
|
||||
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
|
||||
"requires": {
|
||||
"bytes": "3.1.2",
|
||||
"http-errors": "2.0.0",
|
||||
@@ -12700,10 +12853,7 @@
|
||||
}
|
||||
},
|
||||
"rust-gbt": {
|
||||
"version": "file:rust-gbt",
|
||||
"requires": {
|
||||
"@napi-rs/cli": "2.16.1"
|
||||
}
|
||||
"version": "file:rust-gbt"
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
@@ -12779,6 +12929,19 @@
|
||||
"send": "0.18.0"
|
||||
}
|
||||
},
|
||||
"set-function-length": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
|
||||
"requires": {
|
||||
"define-data-property": "^1.1.4",
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-intrinsic": "^1.2.4",
|
||||
"gopd": "^1.0.1",
|
||||
"has-property-descriptors": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
@@ -12800,13 +12963,14 @@
|
||||
"dev": true
|
||||
},
|
||||
"side-channel": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
|
||||
"integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
|
||||
"requires": {
|
||||
"call-bind": "^1.0.0",
|
||||
"get-intrinsic": "^1.0.2",
|
||||
"object-inspect": "^1.9.0"
|
||||
"call-bind": "^1.0.7",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.4",
|
||||
"object-inspect": "^1.13.1"
|
||||
}
|
||||
},
|
||||
"signal-exit": {
|
||||
@@ -13260,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.16.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz",
|
||||
"integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
|
||||
"requires": {}
|
||||
},
|
||||
"y18n": {
|
||||
|
||||
@@ -22,10 +22,12 @@
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"tsc": "./node_modules/typescript/bin/tsc -p tsconfig.build.json",
|
||||
"build": "npm run rust-build && npm run tsc && npm run create-resources",
|
||||
"build": "npm run tsc && npm run create-resources",
|
||||
"clean": "rm -rf ./dist/ ./node_modules/ ./package/ ./rust-gbt/",
|
||||
"create-resources": "cp ./src/tasks/price-feeds/mtgox-weekly.json ./dist/tasks && node dist/api/fetch-version.js",
|
||||
"package": "./npm_package.sh",
|
||||
"package-rm-build-deps": "./npm_package_rm_build_deps.sh",
|
||||
"preinstall": "cd ../rust/gbt && npm run build-release && npm run to-backend",
|
||||
"start": "node --max-old-space-size=2048 dist/index.js",
|
||||
"start-production": "node --max-old-space-size=16384 dist/index.js",
|
||||
"reindex-updated-pools": "npm run start-production --update-pools",
|
||||
@@ -34,9 +36,7 @@
|
||||
"test:ci": "CI=true ./node_modules/.bin/jest --coverage",
|
||||
"lint": "./node_modules/.bin/eslint . --ext .ts",
|
||||
"lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix",
|
||||
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\"",
|
||||
"rust-clean": "cd rust-gbt && rm -f *.node index.d.ts index.js && rm -rf target && cd ../",
|
||||
"rust-build": "npm run rust-clean && cd rust-gbt && npm run build-release"
|
||||
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.24.0",
|
||||
@@ -45,14 +45,14 @@
|
||||
"axios": "~1.6.1",
|
||||
"bitcoinjs-lib": "~6.1.3",
|
||||
"crypto-js": "~4.2.0",
|
||||
"express": "~4.18.2",
|
||||
"express": "~4.19.2",
|
||||
"maxmind": "~4.3.11",
|
||||
"mysql2": "~3.9.1",
|
||||
"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.16.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",
|
||||
|
||||
50
backend/rust-gbt/index.d.ts
vendored
50
backend/rust-gbt/index.d.ts
vendored
@@ -1,50 +0,0 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
/* auto-generated by NAPI-RS */
|
||||
|
||||
export interface ThreadTransaction {
|
||||
uid: number
|
||||
order: number
|
||||
fee: number
|
||||
weight: number
|
||||
sigops: number
|
||||
effectiveFeePerVsize: number
|
||||
inputs: Array<number>
|
||||
}
|
||||
export interface ThreadAcceleration {
|
||||
uid: number
|
||||
delta: number
|
||||
}
|
||||
export class GbtGenerator {
|
||||
constructor()
|
||||
/**
|
||||
* # Errors
|
||||
*
|
||||
* Rejects if the thread panics or if the Mutex is poisoned.
|
||||
*/
|
||||
make(mempool: Array<ThreadTransaction>, accelerations: Array<ThreadAcceleration>, maxUid: number): Promise<GbtResult>
|
||||
/**
|
||||
* # Errors
|
||||
*
|
||||
* Rejects if the thread panics or if the Mutex is poisoned.
|
||||
*/
|
||||
update(newTxs: Array<ThreadTransaction>, removeTxs: Array<number>, accelerations: Array<ThreadAcceleration>, maxUid: number): Promise<GbtResult>
|
||||
}
|
||||
/**
|
||||
* The result from calling the gbt function.
|
||||
*
|
||||
* This tuple contains the following:
|
||||
* blocks: A 2D Vector of transaction IDs (u32), the inner Vecs each represent a block.
|
||||
* block_weights: A Vector of total weights per block.
|
||||
* clusters: A 2D Vector of transaction IDs representing clusters of dependent mempool transactions
|
||||
* rates: A Vector of tuples containing transaction IDs (u32) and effective fee per vsize (f64)
|
||||
*/
|
||||
export class GbtResult {
|
||||
blocks: Array<Array<number>>
|
||||
blockWeights: Array<number>
|
||||
clusters: Array<Array<number>>
|
||||
rates: Array<Array<number>>
|
||||
overflow: Array<number>
|
||||
constructor(blocks: Array<Array<number>>, blockWeights: Array<number>, clusters: Array<Array<number>>, rates: Array<Array<number>>, overflow: Array<number>)
|
||||
}
|
||||
@@ -1,258 +0,0 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
|
||||
/* auto-generated by NAPI-RS */
|
||||
|
||||
const { existsSync, readFileSync } = require('fs')
|
||||
const { join } = require('path')
|
||||
|
||||
const { platform, arch } = process
|
||||
|
||||
let nativeBinding = null
|
||||
let localFileExisted = false
|
||||
let loadError = null
|
||||
|
||||
function isMusl() {
|
||||
// For Node 10
|
||||
if (!process.report || typeof process.report.getReport !== 'function') {
|
||||
try {
|
||||
const lddPath = require('child_process').execSync('which ldd').toString().trim()
|
||||
return readFileSync(lddPath, 'utf8').includes('musl')
|
||||
} catch (e) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
const { glibcVersionRuntime } = process.report.getReport().header
|
||||
return !glibcVersionRuntime
|
||||
}
|
||||
}
|
||||
|
||||
switch (platform) {
|
||||
case 'android':
|
||||
switch (arch) {
|
||||
case 'arm64':
|
||||
localFileExisted = existsSync(join(__dirname, 'gbt.android-arm64.node'))
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./gbt.android-arm64.node')
|
||||
} else {
|
||||
nativeBinding = require('gbt-android-arm64')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'arm':
|
||||
localFileExisted = existsSync(join(__dirname, 'gbt.android-arm-eabi.node'))
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./gbt.android-arm-eabi.node')
|
||||
} else {
|
||||
nativeBinding = require('gbt-android-arm-eabi')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported architecture on Android ${arch}`)
|
||||
}
|
||||
break
|
||||
case 'win32':
|
||||
switch (arch) {
|
||||
case 'x64':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'gbt.win32-x64-msvc.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./gbt.win32-x64-msvc.node')
|
||||
} else {
|
||||
nativeBinding = require('gbt-win32-x64-msvc')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'ia32':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'gbt.win32-ia32-msvc.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./gbt.win32-ia32-msvc.node')
|
||||
} else {
|
||||
nativeBinding = require('gbt-win32-ia32-msvc')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'arm64':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'gbt.win32-arm64-msvc.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./gbt.win32-arm64-msvc.node')
|
||||
} else {
|
||||
nativeBinding = require('gbt-win32-arm64-msvc')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported architecture on Windows: ${arch}`)
|
||||
}
|
||||
break
|
||||
case 'darwin':
|
||||
localFileExisted = existsSync(join(__dirname, 'gbt.darwin-universal.node'))
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./gbt.darwin-universal.node')
|
||||
} else {
|
||||
nativeBinding = require('gbt-darwin-universal')
|
||||
}
|
||||
break
|
||||
} catch {}
|
||||
switch (arch) {
|
||||
case 'x64':
|
||||
localFileExisted = existsSync(join(__dirname, 'gbt.darwin-x64.node'))
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./gbt.darwin-x64.node')
|
||||
} else {
|
||||
nativeBinding = require('gbt-darwin-x64')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'arm64':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'gbt.darwin-arm64.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./gbt.darwin-arm64.node')
|
||||
} else {
|
||||
nativeBinding = require('gbt-darwin-arm64')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported architecture on macOS: ${arch}`)
|
||||
}
|
||||
break
|
||||
case 'freebsd':
|
||||
if (arch !== 'x64') {
|
||||
throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
|
||||
}
|
||||
localFileExisted = existsSync(join(__dirname, 'gbt.freebsd-x64.node'))
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./gbt.freebsd-x64.node')
|
||||
} else {
|
||||
nativeBinding = require('gbt-freebsd-x64')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'linux':
|
||||
switch (arch) {
|
||||
case 'x64':
|
||||
if (isMusl()) {
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'gbt.linux-x64-musl.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./gbt.linux-x64-musl.node')
|
||||
} else {
|
||||
nativeBinding = require('gbt-linux-x64-musl')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
} else {
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'gbt.linux-x64-gnu.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./gbt.linux-x64-gnu.node')
|
||||
} else {
|
||||
nativeBinding = require('gbt-linux-x64-gnu')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'arm64':
|
||||
if (isMusl()) {
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'gbt.linux-arm64-musl.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./gbt.linux-arm64-musl.node')
|
||||
} else {
|
||||
nativeBinding = require('gbt-linux-arm64-musl')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
} else {
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'gbt.linux-arm64-gnu.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./gbt.linux-arm64-gnu.node')
|
||||
} else {
|
||||
nativeBinding = require('gbt-linux-arm64-gnu')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'arm':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'gbt.linux-arm-gnueabihf.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./gbt.linux-arm-gnueabihf.node')
|
||||
} else {
|
||||
nativeBinding = require('gbt-linux-arm-gnueabihf')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported architecture on Linux: ${arch}`)
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
|
||||
}
|
||||
|
||||
if (!nativeBinding) {
|
||||
if (loadError) {
|
||||
throw loadError
|
||||
}
|
||||
throw new Error(`Failed to load native binding`)
|
||||
}
|
||||
|
||||
const { GbtGenerator, GbtResult } = nativeBinding
|
||||
|
||||
module.exports.GbtGenerator = GbtGenerator
|
||||
module.exports.GbtResult = GbtResult
|
||||
34
backend/rust-gbt/package-lock.json
generated
34
backend/rust-gbt/package-lock.json
generated
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"name": "gbt",
|
||||
"version": "3.0.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "gbt",
|
||||
"version": "3.0.1",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@napi-rs/cli": "2.16.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/cli": {
|
||||
"version": "2.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.16.1.tgz",
|
||||
"integrity": "sha512-L0Gr5iEQIDEbvWdDr1HUaBOxBSHL1VZhWSk1oryawoT8qJIY+KGfLFelU+Qma64ivCPbxYpkfPoKYVG3rcoGIA==",
|
||||
"bin": {
|
||||
"napi": "scripts/index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"MEMPOOL": {
|
||||
"ENABLED": true,
|
||||
"OFFICIAL": false,
|
||||
"NETWORK": "__MEMPOOL_NETWORK__",
|
||||
"BACKEND": "__MEMPOOL_BACKEND__",
|
||||
"BLOCKS_SUMMARIES_INDEXING": true,
|
||||
@@ -27,9 +28,8 @@
|
||||
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
|
||||
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
|
||||
"AUDIT": true,
|
||||
"ADVANCED_GBT_AUDIT": true,
|
||||
"ADVANCED_GBT_MEMPOOL": true,
|
||||
"RUST_GBT": false,
|
||||
"LIMIT_GBT": false,
|
||||
"CPFP_INDEXING": true,
|
||||
"MAX_BLOCKS_BULK_QUERY": 999,
|
||||
"DISK_CACHE_BLOCK_INTERVAL": 999,
|
||||
@@ -79,7 +79,8 @@
|
||||
"USERNAME": "__DATABASE_USERNAME__",
|
||||
"PASSWORD": "__DATABASE_PASSWORD__",
|
||||
"PID_DIR": "__DATABASE_PID_FILE__",
|
||||
"TIMEOUT": 3000
|
||||
"TIMEOUT": 3000,
|
||||
"POOL_SIZE": 100
|
||||
},
|
||||
"SYSLOG": {
|
||||
"ENABLED": false,
|
||||
@@ -92,10 +93,6 @@
|
||||
"ENABLED": false,
|
||||
"TX_PER_SECOND_SAMPLE_PERIOD": 20
|
||||
},
|
||||
"BISQ": {
|
||||
"ENABLED": true,
|
||||
"DATA_PATH": "__BISQ_DATA_PATH__"
|
||||
},
|
||||
"SOCKS5PROXY": {
|
||||
"ENABLED": true,
|
||||
"USE_ONION": true,
|
||||
@@ -108,9 +105,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,
|
||||
@@ -145,5 +140,9 @@
|
||||
"ENABLED": false,
|
||||
"UNIX_SOCKET_PATH": "/tmp/redis.sock",
|
||||
"BATCH_QUERY_BASE_SIZE": 5000
|
||||
},
|
||||
"FIAT_PRICE": {
|
||||
"ENABLED": true,
|
||||
"API_KEY": "__MEMPOOL_CURRENCY_API_KEY__"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ describe('Mempool Backend Config', () => {
|
||||
|
||||
expect(config.MEMPOOL).toStrictEqual({
|
||||
ENABLED: true,
|
||||
OFFICIAL: false,
|
||||
NETWORK: 'mainnet',
|
||||
BACKEND: 'none',
|
||||
BLOCKS_SUMMARIES_INDEXING: false,
|
||||
@@ -40,9 +41,8 @@ describe('Mempool Backend Config', () => {
|
||||
POOLS_JSON_TREE_URL: 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
|
||||
POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json',
|
||||
AUDIT: false,
|
||||
ADVANCED_GBT_AUDIT: false,
|
||||
ADVANCED_GBT_MEMPOOL: false,
|
||||
RUST_GBT: false,
|
||||
LIMIT_GBT: false,
|
||||
CPFP_INDEXING: false,
|
||||
MAX_BLOCKS_BULK_QUERY: 0,
|
||||
DISK_CACHE_BLOCK_INTERVAL: 6,
|
||||
@@ -93,7 +93,8 @@ describe('Mempool Backend Config', () => {
|
||||
USERNAME: 'mempool',
|
||||
PASSWORD: 'mempool',
|
||||
TIMEOUT: 180000,
|
||||
PID_DIR: ''
|
||||
PID_DIR: '',
|
||||
POOL_SIZE: 100,
|
||||
});
|
||||
|
||||
expect(config.SYSLOG).toStrictEqual({
|
||||
@@ -106,8 +107,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,
|
||||
@@ -121,9 +120,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({
|
||||
@@ -150,6 +147,11 @@ describe('Mempool Backend Config', () => {
|
||||
UNIX_SOCKET_PATH: '',
|
||||
BATCH_QUERY_BASE_SIZE: 5000,
|
||||
});
|
||||
|
||||
expect(config.FIAT_PRICE).toStrictEqual({
|
||||
ENABLED: true,
|
||||
API_KEY: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -176,8 +178,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();
|
||||
75
backend/src/api/acceleration/acceleration.routes.ts
Normal file
75
backend/src/api/acceleration/acceleration.routes.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Application, Request, Response } from "express";
|
||||
import config from "../../config";
|
||||
import axios from "axios";
|
||||
import logger from "../../logger";
|
||||
|
||||
class AccelerationRoutes {
|
||||
private tag = 'Accelerator';
|
||||
|
||||
public initRoutes(app: Application) {
|
||||
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) {
|
||||
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 current accelerations from ${url} in $getAcceleratorAccelerations(), ${e}`, this.tag);
|
||||
res.status(500).end();
|
||||
}
|
||||
}
|
||||
|
||||
private async $getAcceleratorAccelerationsHistory(req: Request, res: Response) {
|
||||
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 history from ${url} in $getAcceleratorAccelerationsHistory(), ${e}`, this.tag);
|
||||
res.status(500).end();
|
||||
}
|
||||
}
|
||||
|
||||
private async $getAcceleratorAccelerationsHistoryAggregated(req: Request, res: Response) {
|
||||
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) {
|
||||
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();
|
||||
738
backend/src/api/acceleration/acceleration.ts
Normal file
738
backend/src/api/acceleration/acceleration.ts
Normal file
@@ -0,0 +1,738 @@
|
||||
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;
|
||||
const MAX_RELATIVE_GRAPH_SIZE = 200;
|
||||
const BID_BOOST_WINDOW = 40_000;
|
||||
const BID_BOOST_MIN_OFFSET = 10_000;
|
||||
const BID_BOOST_MAX_OFFSET = 400_000;
|
||||
|
||||
type Acceleration = {
|
||||
txid: string;
|
||||
max_bid: number;
|
||||
};
|
||||
|
||||
interface TxSummary {
|
||||
txid: string; // txid of the current transaction
|
||||
effectiveVsize: number; // Total vsize of the dependency tree
|
||||
effectiveFee: number; // Total fee of the dependency tree in sats
|
||||
ancestorCount: number; // Number of ancestors
|
||||
}
|
||||
|
||||
export interface AccelerationInfo {
|
||||
txSummary: TxSummary;
|
||||
targetFeeRate: number; // target fee rate (recommended next block fee, or median fee for mined block)
|
||||
nextBlockFee: number; // fee in sats required to be in the next block (using recommended next block fee, or median fee for mined block)
|
||||
cost: number; // additional cost to accelerate ((cost + txSummary.effectiveFee) / txSummary.effectiveVsize) >= targetFeeRate
|
||||
}
|
||||
|
||||
interface GraphTx {
|
||||
txid: string;
|
||||
vsize: number;
|
||||
weight: number;
|
||||
fees: {
|
||||
base: number; // in sats
|
||||
};
|
||||
depends: string[];
|
||||
spentby: string[];
|
||||
}
|
||||
|
||||
interface MempoolTx extends GraphTx {
|
||||
ancestorcount: number;
|
||||
ancestorsize: number;
|
||||
fees: { // in sats
|
||||
base: number;
|
||||
ancestor: number;
|
||||
};
|
||||
|
||||
ancestors: Map<string, MempoolTx>,
|
||||
ancestorRate: number;
|
||||
individualRate: number;
|
||||
score: number;
|
||||
}
|
||||
|
||||
class AccelerationCosts {
|
||||
/**
|
||||
* Takes a list of accelerations and verbose block data
|
||||
* Returns the "fair" boost rate to charge accelerations
|
||||
*
|
||||
* @param accelerationsx
|
||||
* @param verboseBlock
|
||||
*/
|
||||
public calculateBoostRate(accelerations: Acceleration[], blockTxs: IEsploraApi.Transaction[]): number {
|
||||
// Run GBT ourselves to calculate accurate effective fee rates
|
||||
// the list of transactions comes from a mined block, so we already know everything fits within consensus limits
|
||||
const template = makeBlockTemplate(blockTxs, accelerations, 1, Infinity, Infinity);
|
||||
|
||||
// initialize working maps for fast tx lookups
|
||||
const accMap = {};
|
||||
const txMap = {};
|
||||
for (const acceleration of accelerations) {
|
||||
accMap[acceleration.txid] = acceleration;
|
||||
}
|
||||
for (const tx of template) {
|
||||
txMap[tx.txid] = tx;
|
||||
}
|
||||
|
||||
// Identify and exclude accelerated and otherwise prioritized transactions
|
||||
const excludeMap = {};
|
||||
let totalWeight = 0;
|
||||
let minAcceleratedPackage = Infinity;
|
||||
let lastEffectiveRate = 0;
|
||||
// Iterate over the mined template from bottom to top.
|
||||
// Transactions should appear in ascending order of mining priority.
|
||||
for (const blockTx of [...blockTxs].reverse()) {
|
||||
const txid = blockTx.txid;
|
||||
const tx = txMap[txid];
|
||||
totalWeight += tx.weight;
|
||||
const isAccelerated = accMap[txid] != null;
|
||||
// If a cluster has a in-band effective fee rate than the previous cluster,
|
||||
// it must have been prioritized out-of-band (in order to have a higher mining priority)
|
||||
// so exclude from the analysis.
|
||||
const isPrioritized = tx.effectiveFeePerVsize < lastEffectiveRate;
|
||||
if (isPrioritized || isAccelerated) {
|
||||
let packageWeight = 0;
|
||||
// exclude this whole CPFP cluster
|
||||
for (const clusterTxid of tx.cluster) {
|
||||
packageWeight += txMap[clusterTxid].weight;
|
||||
if (!excludeMap[clusterTxid]) {
|
||||
excludeMap[clusterTxid] = true;
|
||||
}
|
||||
}
|
||||
// keep track of the smallest accelerated CPFP cluster for later
|
||||
if (isAccelerated) {
|
||||
minAcceleratedPackage = Math.min(minAcceleratedPackage, packageWeight);
|
||||
}
|
||||
}
|
||||
if (!isPrioritized) {
|
||||
if (!isAccelerated) {
|
||||
lastEffectiveRate = tx.effectiveFeePerVsize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The Bid Boost Rate is calculated by disregarding the bottom X weight units of the block,
|
||||
// where X is the larger of BID_BOOST_MIN_OFFSET or the smallest accelerated package weight (the "offset"),
|
||||
// then taking the average fee rate of the following BID_BOOST_WINDOW weight units
|
||||
// (ignoring accelerated transactions and their ancestors).
|
||||
//
|
||||
// Transactions within the offset might pay less than the fair rate due to bin-packing effects
|
||||
// But the average rate paid by the next chunk of non-accelerated transactions provides a good
|
||||
// upper bound on the "next best rate" of alternatives to including the accelerated transactions
|
||||
// (since, if there were any better options, they would have been included instead)
|
||||
const spareWeight = BLOCK_WEIGHT_UNITS - totalWeight;
|
||||
const windowOffset = Math.min(Math.max(minAcceleratedPackage, BID_BOOST_MIN_OFFSET, spareWeight), BID_BOOST_MAX_OFFSET);
|
||||
const leftBound = windowOffset;
|
||||
const rightBound = windowOffset + BID_BOOST_WINDOW;
|
||||
let totalFeeInWindow = 0;
|
||||
let totalWeightInWindow = Math.max(0, spareWeight - leftBound);
|
||||
let txIndex = blockTxs.length - 1;
|
||||
for (let offset = spareWeight; offset < BLOCK_WEIGHT_UNITS && txIndex >= 0; txIndex--) {
|
||||
const txid = blockTxs[txIndex].txid;
|
||||
const tx = txMap[txid];
|
||||
if (excludeMap[txid]) {
|
||||
// skip prioritized transactions and their ancestors
|
||||
continue;
|
||||
}
|
||||
|
||||
const left = offset;
|
||||
const right = offset + tx.weight;
|
||||
offset += tx.weight;
|
||||
if (right < leftBound) {
|
||||
// not within window yet
|
||||
continue;
|
||||
}
|
||||
if (left > rightBound) {
|
||||
// past window
|
||||
break;
|
||||
}
|
||||
// count fees for weight units within the window
|
||||
const overlapLeft = Math.max(leftBound, left);
|
||||
const overlapRight = Math.min(rightBound, right);
|
||||
const overlapUnits = overlapRight - overlapLeft;
|
||||
totalFeeInWindow += (tx.effectiveFeePerVsize * (overlapUnits / 4));
|
||||
totalWeightInWindow += overlapUnits;
|
||||
}
|
||||
|
||||
if (totalWeightInWindow < BID_BOOST_WINDOW) {
|
||||
// not enough un-prioritized transactions to calculate a fair rate
|
||||
// just charge everyone their max bids
|
||||
return Infinity;
|
||||
}
|
||||
// Divide the total fee by the size of the BID_BOOST_WINDOW in vbytes
|
||||
const averageRate = totalFeeInWindow / (BID_BOOST_WINDOW / 4);
|
||||
return averageRate;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Takes an accelerated mined txid and a target rate
|
||||
* Returns the total vsize, fees and acceleration cost (in sats) of the tx and all same-block ancestors
|
||||
*
|
||||
* @param txid
|
||||
* @param medianFeeRate
|
||||
*/
|
||||
public getAccelerationInfo(tx: MempoolTransactionExtended, targetFeeRate: number, transactions: MempoolTransactionExtended[]): AccelerationInfo {
|
||||
// Get same-block transaction ancestors
|
||||
const allRelatives = this.getSameBlockRelatives(tx, transactions);
|
||||
const relativesMap = this.initializeRelatives(allRelatives);
|
||||
const rootTx = relativesMap.get(tx.txid) as MempoolTx;
|
||||
|
||||
// Calculate cost to boost
|
||||
return this.calculateAccelerationAncestors(rootTx, relativesMap, targetFeeRate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a raw transaction, and builds a graph of same-block relatives,
|
||||
* and returns as a MempoolTx
|
||||
*
|
||||
* @param tx
|
||||
*/
|
||||
private getSameBlockRelatives(tx: MempoolTransactionExtended, transactions: MempoolTransactionExtended[]): Map<string, GraphTx> {
|
||||
const blockTxs = new Map<string, MempoolTransactionExtended>(); // map of txs in this block
|
||||
const spendMap = new Map<string, string>(); // map of outpoints to spending txids
|
||||
for (const tx of transactions) {
|
||||
blockTxs.set(tx.txid, tx);
|
||||
for (const vin of tx.vin) {
|
||||
spendMap.set(`${vin.txid}:${vin.vout}`, tx.txid);
|
||||
}
|
||||
}
|
||||
|
||||
const relatives: Map<string, GraphTx> = new Map();
|
||||
const stack: string[] = [tx.txid];
|
||||
|
||||
// build set of same-block ancestors
|
||||
while (stack.length > 0) {
|
||||
const nextTxid = stack.pop();
|
||||
const nextTx = nextTxid ? blockTxs.get(nextTxid) : null;
|
||||
if (!nextTx || relatives.has(nextTx.txid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const mempoolTx = this.convertToGraphTx(nextTx);
|
||||
|
||||
mempoolTx.fees.base = nextTx.fee || 0;
|
||||
mempoolTx.depends = nextTx.vin.map(vin => vin.txid).filter(inTxid => inTxid && blockTxs.has(inTxid)) as string[];
|
||||
mempoolTx.spentby = nextTx.vout.map((vout, index) => spendMap.get(`${nextTx.txid}:${index}`)).filter(outTxid => outTxid && blockTxs.has(outTxid)) as string[];
|
||||
|
||||
for (const txid of [...mempoolTx.depends, ...mempoolTx.spentby]) {
|
||||
if (txid) {
|
||||
stack.push(txid);
|
||||
}
|
||||
}
|
||||
|
||||
relatives.set(mempoolTx.txid, mempoolTx);
|
||||
}
|
||||
|
||||
return relatives;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a raw transaction and converts it to MempoolTx format
|
||||
* fee and ancestor data is initialized with dummy/null values
|
||||
*
|
||||
* @param tx
|
||||
*/
|
||||
private convertToGraphTx(tx: MempoolTransactionExtended): GraphTx {
|
||||
return {
|
||||
txid: tx.txid,
|
||||
vsize: Math.ceil(tx.weight / 4),
|
||||
weight: tx.weight,
|
||||
fees: {
|
||||
base: 0, // dummy
|
||||
},
|
||||
depends: [], // dummy
|
||||
spentby: [], //dummy
|
||||
};
|
||||
}
|
||||
|
||||
private convertGraphToMempoolTx(tx: GraphTx): MempoolTx {
|
||||
return {
|
||||
...tx,
|
||||
fees: {
|
||||
base: tx.fees.base,
|
||||
ancestor: tx.fees.base,
|
||||
},
|
||||
ancestorcount: 1,
|
||||
ancestorsize: Math.ceil(tx.weight / 4),
|
||||
ancestors: new Map<string, MempoolTx>(),
|
||||
ancestorRate: 0,
|
||||
individualRate: 0,
|
||||
score: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a root transaction, a list of in-mempool ancestors, and a target fee rate,
|
||||
* Calculate the minimum set of transactions to fee-bump, their total vsize + fees
|
||||
*
|
||||
* @param tx
|
||||
* @param ancestors
|
||||
*/
|
||||
private calculateAccelerationAncestors(tx: MempoolTx, relatives: Map<string, MempoolTx>, targetFeeRate: number): AccelerationInfo {
|
||||
// add root tx to the ancestor map
|
||||
relatives.set(tx.txid, tx);
|
||||
|
||||
// Check for high-sigop transactions (not supported)
|
||||
relatives.forEach(entry => {
|
||||
if (entry.vsize > Math.ceil(entry.weight / 4)) {
|
||||
throw new Error(`high_sigop_tx`);
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize individual & ancestor fee rates
|
||||
relatives.forEach(entry => this.setAncestorScores(entry));
|
||||
|
||||
// Sort by descending ancestor score
|
||||
let sortedRelatives = Array.from(relatives.values()).sort(this.mempoolComparator);
|
||||
|
||||
let includedInCluster: Map<string, MempoolTx> | null = null;
|
||||
|
||||
// While highest score >= targetFeeRate
|
||||
let maxIterations = MAX_RELATIVE_GRAPH_SIZE;
|
||||
while (sortedRelatives.length && sortedRelatives[0].score && sortedRelatives[0].score >= targetFeeRate && maxIterations > 0) {
|
||||
maxIterations--;
|
||||
// Grab the highest scoring entry
|
||||
const best = sortedRelatives.shift();
|
||||
if (best) {
|
||||
const cluster = new Map<string, MempoolTx>(best.ancestors?.entries() || []);
|
||||
if (best.ancestors.has(tx.txid)) {
|
||||
includedInCluster = cluster;
|
||||
}
|
||||
cluster.set(best.txid, best);
|
||||
// Remove this cluster (it already pays over the target rate, so doesn't need to be boosted)
|
||||
// and update scores, ancestor totals and dependencies for the survivors
|
||||
this.removeAncestors(cluster, relatives);
|
||||
|
||||
// re-sort
|
||||
sortedRelatives = Array.from(relatives.values()).sort(this.mempoolComparator);
|
||||
}
|
||||
}
|
||||
|
||||
// sanity check for infinite loops / too many ancestors (should never happen)
|
||||
if (maxIterations <= 0) {
|
||||
logger.warn(`acceleration dependency calculation failed: calculateAccelerationAncestors loop exceeded ${MAX_RELATIVE_GRAPH_SIZE} iterations, unable to proceed`);
|
||||
throw new Error('invalid_tx_dependencies');
|
||||
}
|
||||
|
||||
let totalFee = tx.fees.ancestor;
|
||||
|
||||
// transaction is already CPFP-d above the target rate by some descendant
|
||||
if (includedInCluster) {
|
||||
let clusterSize = 0;
|
||||
let clusterFee = 0;
|
||||
includedInCluster.forEach(entry => {
|
||||
clusterSize += entry.vsize;
|
||||
clusterFee += entry.fees.base;
|
||||
});
|
||||
const clusterRate = clusterFee / clusterSize;
|
||||
totalFee = Math.ceil(tx.ancestorsize * clusterRate);
|
||||
}
|
||||
|
||||
// Whatever remains in the accelerated tx's dependencies needs to be boosted to the targetFeeRate
|
||||
// Cost = (totalVsize * targetFeeRate) - totalFee
|
||||
return {
|
||||
txSummary: {
|
||||
txid: tx.txid,
|
||||
effectiveVsize: tx.ancestorsize,
|
||||
effectiveFee: totalFee,
|
||||
ancestorCount: tx.ancestorcount,
|
||||
},
|
||||
cost: Math.max(0, Math.ceil(tx.ancestorsize * targetFeeRate) - totalFee),
|
||||
targetFeeRate,
|
||||
nextBlockFee: Math.ceil(tx.ancestorsize * targetFeeRate),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively traverses an in-mempool dependency graph, and sets a Map of in-mempool ancestors
|
||||
* for each transaction.
|
||||
*
|
||||
* @param tx
|
||||
* @param all
|
||||
*/
|
||||
private setAncestors(tx: MempoolTx, all: Map<string, MempoolTx>, visited: Map<string, Map<string, MempoolTx>>, depth: number = 0): Map<string, MempoolTx> {
|
||||
// sanity check for infinite recursion / too many ancestors (should never happen)
|
||||
if (depth >= 100) {
|
||||
logger.warn('acceleration dependency calculation failed: setAncestors reached depth of 100, unable to proceed', `Accelerator`);
|
||||
throw new Error('invalid_tx_dependencies');
|
||||
}
|
||||
|
||||
// initialize the ancestor map for this tx
|
||||
tx.ancestors = new Map<string, MempoolTx>();
|
||||
tx.depends.forEach(parentId => {
|
||||
const parent = all.get(parentId);
|
||||
if (parent) {
|
||||
// add the parent
|
||||
tx.ancestors?.set(parentId, parent);
|
||||
// check for a cached copy of this parent's ancestors
|
||||
let ancestors = visited.get(parent.txid);
|
||||
if (!ancestors) {
|
||||
// recursively fetch the parent's ancestors
|
||||
ancestors = this.setAncestors(parent, all, visited, depth + 1);
|
||||
}
|
||||
// and add to this tx's map
|
||||
ancestors.forEach((ancestor, ancestorId) => {
|
||||
tx.ancestors?.set(ancestorId, ancestor);
|
||||
});
|
||||
}
|
||||
});
|
||||
visited.set(tx.txid, tx.ancestors);
|
||||
|
||||
return tx.ancestors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Efficiently sets a Map of in-mempool ancestors for each member of an expanded relative graph
|
||||
* by running setAncestors on each leaf, and caching intermediate results.
|
||||
* then initializes ancestor data for each transaction
|
||||
*
|
||||
* @param all
|
||||
*/
|
||||
private initializeRelatives(all: Map<string, GraphTx>): Map<string, MempoolTx> {
|
||||
const mempoolTxs = new Map<string, MempoolTx>();
|
||||
all.forEach(entry => {
|
||||
mempoolTxs.set(entry.txid, this.convertGraphToMempoolTx(entry));
|
||||
});
|
||||
const visited: Map<string, Map<string, MempoolTx>> = new Map();
|
||||
const leaves: MempoolTx[] = Array.from(mempoolTxs.values()).filter(entry => entry.spentby.length === 0);
|
||||
for (const leaf of leaves) {
|
||||
this.setAncestors(leaf, mempoolTxs, visited);
|
||||
}
|
||||
mempoolTxs.forEach(entry => {
|
||||
entry.ancestors?.forEach(ancestor => {
|
||||
entry.ancestorcount++;
|
||||
entry.ancestorsize += ancestor.vsize;
|
||||
entry.fees.ancestor += ancestor.fees.base;
|
||||
});
|
||||
this.setAncestorScores(entry);
|
||||
});
|
||||
return mempoolTxs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a cluster of transactions from an in-mempool dependency graph
|
||||
* and update the survivors' scores and ancestors
|
||||
*
|
||||
* @param cluster
|
||||
* @param ancestors
|
||||
*/
|
||||
private removeAncestors(cluster: Map<string, MempoolTx>, all: Map<string, MempoolTx>): void {
|
||||
// remove
|
||||
cluster.forEach(tx => {
|
||||
all.delete(tx.txid);
|
||||
});
|
||||
|
||||
// update survivors
|
||||
all.forEach(tx => {
|
||||
cluster.forEach(remove => {
|
||||
if (tx.ancestors?.has(remove.txid)) {
|
||||
// remove as dependency
|
||||
tx.ancestors.delete(remove.txid);
|
||||
tx.depends = tx.depends.filter(parent => parent !== remove.txid);
|
||||
// update ancestor sizes and fees
|
||||
tx.ancestorsize -= remove.vsize;
|
||||
tx.fees.ancestor -= remove.fees.base;
|
||||
}
|
||||
});
|
||||
// recalculate fee rates
|
||||
this.setAncestorScores(tx);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a mempool transaction, and set the fee rates and ancestor score
|
||||
*
|
||||
* @param tx
|
||||
*/
|
||||
private setAncestorScores(tx: MempoolTx): void {
|
||||
tx.individualRate = tx.fees.base / tx.vsize;
|
||||
tx.ancestorRate = tx.fees.ancestor / tx.ancestorsize;
|
||||
tx.score = Math.min(tx.individualRate, tx.ancestorRate);
|
||||
}
|
||||
|
||||
// Sort by descending score
|
||||
private mempoolComparator(a, b): number {
|
||||
return b.score - a.score;
|
||||
}
|
||||
}
|
||||
|
||||
export default new AccelerationCosts;
|
||||
|
||||
interface TemplateTransaction {
|
||||
txid: string;
|
||||
order: number;
|
||||
weight: number;
|
||||
adjustedVsize: number; // sigop-adjusted vsize, rounded up to the nearest integer
|
||||
sigops: number;
|
||||
fee: number;
|
||||
feeDelta: number;
|
||||
ancestors: string[];
|
||||
cluster: string[];
|
||||
effectiveFeePerVsize: number;
|
||||
}
|
||||
|
||||
interface MinerTransaction extends TemplateTransaction {
|
||||
inputs: string[];
|
||||
feePerVsize: number;
|
||||
relativesSet: boolean;
|
||||
ancestorMap: Map<string, MinerTransaction>;
|
||||
children: Set<MinerTransaction>;
|
||||
ancestorFee: number;
|
||||
ancestorVsize: number;
|
||||
ancestorSigops: number;
|
||||
score: number;
|
||||
used: boolean;
|
||||
modified: boolean;
|
||||
dependencyRate: number;
|
||||
}
|
||||
|
||||
/*
|
||||
* Build a block using an approximation of the transaction selection algorithm from Bitcoin Core
|
||||
* (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp)
|
||||
*/
|
||||
export function makeBlockTemplate(candidates: IEsploraApi.Transaction[], accelerations: Acceleration[], maxBlocks: number = 8, weightLimit: number = BLOCK_WEIGHT_UNITS, sigopLimit: number = BLOCK_SIGOPS): TemplateTransaction[] {
|
||||
const auditPool: Map<string, MinerTransaction> = new Map();
|
||||
const mempoolArray: MinerTransaction[] = [];
|
||||
|
||||
candidates.forEach(tx => {
|
||||
// initializing everything up front helps V8 optimize property access later
|
||||
const adjustedVsize = Math.ceil(Math.max(tx.weight / 4, 5 * (tx.sigops || 0)));
|
||||
const feePerVsize = (tx.fee / adjustedVsize);
|
||||
auditPool.set(tx.txid, {
|
||||
txid: tx.txid,
|
||||
order: txidToOrdering(tx.txid),
|
||||
fee: tx.fee,
|
||||
feeDelta: 0,
|
||||
weight: tx.weight,
|
||||
adjustedVsize,
|
||||
feePerVsize: feePerVsize,
|
||||
effectiveFeePerVsize: feePerVsize,
|
||||
dependencyRate: feePerVsize,
|
||||
sigops: tx.sigops || 0,
|
||||
inputs: (tx.vin?.map(vin => vin.txid) || []) as string[],
|
||||
relativesSet: false,
|
||||
ancestors: [],
|
||||
cluster: [],
|
||||
ancestorMap: new Map<string, MinerTransaction>(),
|
||||
children: new Set<MinerTransaction>(),
|
||||
ancestorFee: 0,
|
||||
ancestorVsize: 0,
|
||||
ancestorSigops: 0,
|
||||
score: 0,
|
||||
used: false,
|
||||
modified: false,
|
||||
});
|
||||
mempoolArray.push(auditPool.get(tx.txid) as MinerTransaction);
|
||||
});
|
||||
|
||||
// set accelerated effective fee
|
||||
for (const acceleration of accelerations) {
|
||||
const tx = auditPool.get(acceleration.txid);
|
||||
if (tx) {
|
||||
tx.feeDelta = acceleration.max_bid;
|
||||
tx.feePerVsize = ((tx.fee + tx.feeDelta) / tx.adjustedVsize);
|
||||
tx.effectiveFeePerVsize = tx.feePerVsize;
|
||||
tx.dependencyRate = tx.feePerVsize;
|
||||
}
|
||||
}
|
||||
|
||||
// Build relatives graph & calculate ancestor scores
|
||||
for (const tx of mempoolArray) {
|
||||
if (!tx.relativesSet) {
|
||||
setRelatives(tx, auditPool);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by descending ancestor score
|
||||
mempoolArray.sort(priorityComparator);
|
||||
|
||||
// Build blocks by greedily choosing the highest feerate package
|
||||
// (i.e. the package rooted in the transaction with the best ancestor score)
|
||||
const blocks: number[][] = [];
|
||||
let blockWeight = 0;
|
||||
let blockSigops = 0;
|
||||
const transactions: MinerTransaction[] = [];
|
||||
let modified: MinerTransaction[] = [];
|
||||
const overflow: MinerTransaction[] = [];
|
||||
let failures = 0;
|
||||
while (mempoolArray.length || modified.length) {
|
||||
// skip invalid transactions
|
||||
while (mempoolArray[0].used || mempoolArray[0].modified) {
|
||||
mempoolArray.shift();
|
||||
}
|
||||
|
||||
// Select best next package
|
||||
let nextTx;
|
||||
const nextPoolTx = mempoolArray[0];
|
||||
const nextModifiedTx = modified[0];
|
||||
if (nextPoolTx && (!nextModifiedTx || (nextPoolTx.score || 0) > (nextModifiedTx.score || 0))) {
|
||||
nextTx = nextPoolTx;
|
||||
mempoolArray.shift();
|
||||
} else {
|
||||
modified.shift();
|
||||
if (nextModifiedTx) {
|
||||
nextTx = nextModifiedTx;
|
||||
}
|
||||
}
|
||||
|
||||
if (nextTx && !nextTx?.used) {
|
||||
// Check if the package fits into this block
|
||||
if (blocks.length >= (maxBlocks - 1) || ((blockWeight + (4 * nextTx.ancestorVsize) < weightLimit) && (blockSigops + nextTx.ancestorSigops <= sigopLimit))) {
|
||||
const ancestors: MinerTransaction[] = Array.from(nextTx.ancestorMap.values());
|
||||
// sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
|
||||
const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx];
|
||||
const clusterTxids = sortedTxSet.map(tx => tx.txid);
|
||||
const effectiveFeeRate = Math.min(nextTx.dependencyRate || Infinity, nextTx.ancestorFee / nextTx.ancestorVsize);
|
||||
const used: MinerTransaction[] = [];
|
||||
while (sortedTxSet.length) {
|
||||
const ancestor = sortedTxSet.pop();
|
||||
if (!ancestor) {
|
||||
continue;
|
||||
}
|
||||
ancestor.used = true;
|
||||
ancestor.usedBy = nextTx.txid;
|
||||
// update this tx with effective fee rate & relatives data
|
||||
if (ancestor.effectiveFeePerVsize !== effectiveFeeRate) {
|
||||
ancestor.effectiveFeePerVsize = effectiveFeeRate;
|
||||
}
|
||||
ancestor.cluster = clusterTxids;
|
||||
transactions.push(ancestor);
|
||||
blockWeight += ancestor.weight;
|
||||
blockSigops += ancestor.sigops;
|
||||
used.push(ancestor);
|
||||
}
|
||||
|
||||
// remove these as valid package ancestors for any descendants remaining in the mempool
|
||||
if (used.length) {
|
||||
used.forEach(tx => {
|
||||
modified = updateDescendants(tx, auditPool, modified, effectiveFeeRate);
|
||||
});
|
||||
}
|
||||
|
||||
failures = 0;
|
||||
} else {
|
||||
// hold this package in an overflow list while we check for smaller options
|
||||
overflow.push(nextTx);
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
|
||||
// this block is full
|
||||
const exceededPackageTries = failures > 1000 && blockWeight > (weightLimit - 4000);
|
||||
const queueEmpty = !mempoolArray.length && !modified.length;
|
||||
|
||||
if (exceededPackageTries || queueEmpty) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const tx of transactions) {
|
||||
tx.ancestors = Object.values(tx.ancestorMap);
|
||||
}
|
||||
|
||||
return transactions;
|
||||
}
|
||||
|
||||
// traverse in-mempool ancestors
|
||||
// recursion unavoidable, but should be limited to depth < 25 by mempool policy
|
||||
function setRelatives(
|
||||
tx: MinerTransaction,
|
||||
mempool: Map<string, MinerTransaction>,
|
||||
): void {
|
||||
for (const parent of tx.inputs) {
|
||||
const parentTx = mempool.get(parent);
|
||||
if (parentTx && !tx.ancestorMap?.has(parent)) {
|
||||
tx.ancestorMap.set(parent, parentTx);
|
||||
parentTx.children.add(tx);
|
||||
// visit each node only once
|
||||
if (!parentTx.relativesSet) {
|
||||
setRelatives(parentTx, mempool);
|
||||
}
|
||||
parentTx.ancestorMap.forEach((ancestor) => {
|
||||
tx.ancestorMap.set(ancestor.txid, ancestor);
|
||||
});
|
||||
}
|
||||
};
|
||||
tx.ancestorFee = (tx.fee + tx.feeDelta);
|
||||
tx.ancestorVsize = tx.adjustedVsize || 0;
|
||||
tx.ancestorSigops = tx.sigops || 0;
|
||||
tx.ancestorMap.forEach((ancestor) => {
|
||||
tx.ancestorFee += (ancestor.fee + ancestor.feeDelta);
|
||||
tx.ancestorVsize += ancestor.adjustedVsize;
|
||||
tx.ancestorSigops += ancestor.sigops;
|
||||
});
|
||||
tx.score = tx.ancestorFee / tx.ancestorVsize;
|
||||
tx.relativesSet = true;
|
||||
}
|
||||
|
||||
// iterate over remaining descendants, removing the root as a valid ancestor & updating the ancestor score
|
||||
// avoids recursion to limit call stack depth
|
||||
function updateDescendants(
|
||||
rootTx: MinerTransaction,
|
||||
mempool: Map<string, MinerTransaction>,
|
||||
modified: MinerTransaction[],
|
||||
clusterRate: number,
|
||||
): MinerTransaction[] {
|
||||
const descendantSet: Set<MinerTransaction> = new Set();
|
||||
// stack of nodes left to visit
|
||||
const descendants: MinerTransaction[] = [];
|
||||
let descendantTx: MinerTransaction | undefined;
|
||||
rootTx.children.forEach(childTx => {
|
||||
if (!descendantSet.has(childTx)) {
|
||||
descendants.push(childTx);
|
||||
descendantSet.add(childTx);
|
||||
}
|
||||
});
|
||||
while (descendants.length) {
|
||||
descendantTx = descendants.pop();
|
||||
if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.txid)) {
|
||||
// remove tx as ancestor
|
||||
descendantTx.ancestorMap.delete(rootTx.txid);
|
||||
descendantTx.ancestorFee -= (rootTx.fee + rootTx.feeDelta);
|
||||
descendantTx.ancestorVsize -= rootTx.adjustedVsize;
|
||||
descendantTx.ancestorSigops -= rootTx.sigops;
|
||||
descendantTx.score = descendantTx.ancestorFee / descendantTx.ancestorVsize;
|
||||
descendantTx.dependencyRate = descendantTx.dependencyRate ? Math.min(descendantTx.dependencyRate, clusterRate) : clusterRate;
|
||||
|
||||
if (!descendantTx.modified) {
|
||||
descendantTx.modified = true;
|
||||
modified.push(descendantTx);
|
||||
}
|
||||
|
||||
// add this node's children to the stack
|
||||
descendantTx.children.forEach(childTx => {
|
||||
// visit each node only once
|
||||
if (!descendantSet.has(childTx)) {
|
||||
descendants.push(childTx);
|
||||
descendantSet.add(childTx);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// return new, resorted modified list
|
||||
return modified.sort(priorityComparator);
|
||||
}
|
||||
|
||||
// Used to sort an array of MinerTransactions by descending ancestor score
|
||||
function priorityComparator(a: MinerTransaction, b: MinerTransaction): number {
|
||||
if (b.score === a.score) {
|
||||
// tie-break by txid for stability
|
||||
return a.order - b.order;
|
||||
} else {
|
||||
return b.score - a.score;
|
||||
}
|
||||
}
|
||||
|
||||
// returns the most significant 4 bytes of the txid as an integer
|
||||
function txidToOrdering(txid: string): number {
|
||||
return parseInt(
|
||||
txid.substring(62, 64) +
|
||||
txid.substring(60, 62) +
|
||||
txid.substring(58, 60) +
|
||||
txid.substring(56, 58),
|
||||
16
|
||||
);
|
||||
}
|
||||
@@ -7,13 +7,14 @@ const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first
|
||||
|
||||
class Audit {
|
||||
auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }, useAccelerations: boolean = false)
|
||||
: { censored: string[], added: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
|
||||
: { censored: string[], added: string[], prioritized: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
|
||||
if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
|
||||
return { censored: [], added: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 1, similarity: 1 };
|
||||
return { censored: [], added: [], prioritized: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 1, similarity: 1 };
|
||||
}
|
||||
|
||||
const matches: string[] = []; // present in both mined block and template
|
||||
const added: string[] = []; // present in mined block, not in template
|
||||
const prioritized: string[] = [] // present in the mined block, not in the template, but further down in the mempool
|
||||
const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
|
||||
const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
|
||||
const accelerated: string[] = []; // prioritized by the mempool accelerator
|
||||
@@ -68,20 +69,27 @@ class Audit {
|
||||
|
||||
// we can expect an honest miner to include 'displaced' transactions in place of recent arrivals and censored txs
|
||||
// these displaced transactions should occupy the first N weight units of the next projected block
|
||||
let displacedWeightRemaining = displacedWeight;
|
||||
let displacedWeightRemaining = displacedWeight + 4000;
|
||||
let index = 0;
|
||||
let lastFeeRate = Infinity;
|
||||
let failures = 0;
|
||||
while (projectedBlocks[1] && index < projectedBlocks[1].transactionIds.length && failures < 500) {
|
||||
const txid = projectedBlocks[1].transactionIds[index];
|
||||
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) {
|
||||
const fits = (tx.weight - displacedWeightRemaining) < 4000;
|
||||
const feeMatches = tx.effectiveFeePerVsize >= lastFeeRate;
|
||||
// 0.005 margin of error for any remaining vsize rounding issues
|
||||
const feeMatches = tx.effectiveFeePerVsize >= (lastFeeRate - 0.005);
|
||||
if (fits || feeMatches) {
|
||||
isDisplaced[txid] = true;
|
||||
if (fits) {
|
||||
lastFeeRate = Math.min(lastFeeRate, tx.effectiveFeePerVsize);
|
||||
// (tx.effectiveFeePerVsize * tx.vsize) / Math.ceil(tx.vsize) attempts to correct for vsize rounding in the simple non-CPFP case
|
||||
lastFeeRate = Math.min(lastFeeRate, (tx.effectiveFeePerVsize * tx.vsize) / Math.ceil(tx.vsize));
|
||||
}
|
||||
if (tx.firstSeen == null || (now - (tx?.firstSeen || 0)) > PROPAGATION_MARGIN) {
|
||||
displacedWeightRemaining -= tx.weight;
|
||||
@@ -106,7 +114,11 @@ class Audit {
|
||||
if (rbfCache.has(tx.txid)) {
|
||||
rbf.push(tx.txid);
|
||||
} else if (!isDisplaced[tx.txid]) {
|
||||
added.push(tx.txid);
|
||||
if (mempool[tx.txid]) {
|
||||
prioritized.push(tx.txid);
|
||||
} else {
|
||||
added.push(tx.txid);
|
||||
}
|
||||
}
|
||||
overflowWeight += tx.weight;
|
||||
}
|
||||
@@ -155,6 +167,7 @@ class Audit {
|
||||
return {
|
||||
censored: Object.keys(isCensored),
|
||||
added,
|
||||
prioritized,
|
||||
fresh,
|
||||
sigop: [],
|
||||
fullrbf: rbf,
|
||||
|
||||
@@ -9,8 +9,8 @@ class BackendInfo {
|
||||
|
||||
constructor() {
|
||||
// This file is created by ./fetch-version.ts during building
|
||||
const versionFile = path.join(__dirname, 'version.json')
|
||||
var versionInfo;
|
||||
const versionFile = path.join(__dirname, 'version.json');
|
||||
let versionInfo;
|
||||
if (fs.existsSync(versionFile)) {
|
||||
versionInfo = JSON.parse(fs.readFileSync(versionFile).toString());
|
||||
} else {
|
||||
@@ -24,7 +24,8 @@ class BackendInfo {
|
||||
hostname: os.hostname(),
|
||||
version: versionInfo.version,
|
||||
gitCommit: versionInfo.gitCommit,
|
||||
lightning: config.LIGHTNING.ENABLED
|
||||
lightning: config.LIGHTNING.ENABLED,
|
||||
backend: config.MEMPOOL.BACKEND,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,7 +33,7 @@ class BackendInfo {
|
||||
return this.backendInfo;
|
||||
}
|
||||
|
||||
public getShortCommitHash() {
|
||||
public getShortCommitHash(): string {
|
||||
return this.backendInfo.gitCommit.slice(0, 7);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -29,6 +29,7 @@ export interface AbstractBitcoinApi {
|
||||
$getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]>;
|
||||
|
||||
startHealthChecks(): void;
|
||||
getHealthStatus(): HealthCheckHost[];
|
||||
}
|
||||
export interface BitcoinRpcCredentials {
|
||||
host: string;
|
||||
@@ -38,3 +39,15 @@ export interface BitcoinRpcCredentials {
|
||||
timeout: number;
|
||||
cookie?: string;
|
||||
}
|
||||
|
||||
export interface HealthCheckHost {
|
||||
host: string;
|
||||
active: boolean;
|
||||
rtt: number;
|
||||
latestHeight: number;
|
||||
socket: boolean;
|
||||
outOfSync: boolean;
|
||||
unreachable: boolean;
|
||||
checked: boolean;
|
||||
lastChecked: number;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
|
||||
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
|
||||
import { IBitcoinApi } from './bitcoin-api.interface';
|
||||
import { IEsploraApi } from './esplora-api.interface';
|
||||
import blocks from '../blocks';
|
||||
@@ -382,6 +382,10 @@ class BitcoinApi implements AbstractBitcoinApi {
|
||||
}
|
||||
|
||||
public startHealthChecks(): void {};
|
||||
|
||||
public getHealthStatus() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export default BitcoinApi;
|
||||
|
||||
@@ -19,6 +19,7 @@ import bitcoinClient from './bitcoin-client';
|
||||
import difficultyAdjustment from '../difficulty-adjustment';
|
||||
import transactionRepository from '../../repositories/TransactionRepository';
|
||||
import rbfCache from '../rbf-cache';
|
||||
import { calculateCpfp } from '../cpfp';
|
||||
|
||||
class BitcoinRoutes {
|
||||
public initRoutes(app: Application) {
|
||||
@@ -36,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)
|
||||
@@ -121,8 +68,10 @@ class BitcoinRoutes {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block-height/:height', this.getBlockHeight)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address', this.getAddress)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs', this.getAddressTransactions)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs/summary', this.getAddressTransactionSummary)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'scripthash/:scripthash', this.getScriptHash)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'scripthash/:scripthash/txs', this.getScriptHashTransactions)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'scripthash/:scripthash/txs/summary', this.getScriptHashTransactionSummary)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'address-prefix/:prefix', this.getAddressPrefix)
|
||||
;
|
||||
}
|
||||
@@ -215,7 +164,7 @@ class BitcoinRoutes {
|
||||
return;
|
||||
}
|
||||
|
||||
const cpfpInfo = Common.setRelativesAndGetCpfpInfo(tx, mempool.getMempool());
|
||||
const cpfpInfo = calculateCpfp(tx, mempool.getMempool());
|
||||
|
||||
res.json(cpfpInfo);
|
||||
return;
|
||||
@@ -415,7 +364,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) {
|
||||
@@ -425,7 +374,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) {
|
||||
@@ -566,6 +515,13 @@ class BitcoinRoutes {
|
||||
}
|
||||
}
|
||||
|
||||
private async getAddressTransactionSummary(req: Request, res: Response): Promise<void> {
|
||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||
res.status(405).send('Address summary lookups require mempool/electrs backend.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private async getScriptHash(req: Request, res: Response) {
|
||||
if (config.MEMPOOL.BACKEND === 'none') {
|
||||
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
|
||||
@@ -609,6 +565,13 @@ class BitcoinRoutes {
|
||||
}
|
||||
}
|
||||
|
||||
private async getScriptHashTransactionSummary(req: Request, res: Response): Promise<void> {
|
||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||
res.status(405).send('Scripthash summary lookups require mempool/electrs backend.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private async getAddressPrefix(req: Request, res: Response) {
|
||||
try {
|
||||
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import config from '../../config';
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import axios, { AxiosResponse, isAxiosError } from 'axios';
|
||||
import http from 'http';
|
||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
|
||||
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
|
||||
import { IEsploraApi } from './esplora-api.interface';
|
||||
import logger from '../../logger';
|
||||
import { Common } from '../common';
|
||||
@@ -10,6 +10,7 @@ interface FailoverHost {
|
||||
host: string,
|
||||
rtts: number[],
|
||||
rtt: number,
|
||||
timedOut?: boolean,
|
||||
failures: number,
|
||||
latestHeight?: number,
|
||||
socket?: boolean,
|
||||
@@ -17,6 +18,7 @@ interface FailoverHost {
|
||||
unreachable?: boolean,
|
||||
preferred?: boolean,
|
||||
checked: boolean,
|
||||
lastChecked?: number,
|
||||
}
|
||||
|
||||
class FailoverRouter {
|
||||
@@ -108,14 +110,20 @@ class FailoverRouter {
|
||||
host.rtts = [];
|
||||
host.rtt = Infinity;
|
||||
}
|
||||
host.timedOut = false;
|
||||
} catch (e) {
|
||||
host.outOfSync = true;
|
||||
host.unreachable = true;
|
||||
host.rtts = [];
|
||||
host.rtt = Infinity;
|
||||
if (isAxiosError(e) && (e.code === 'ECONNABORTED' || e.code === 'ETIMEDOUT')) {
|
||||
host.timedOut = true;
|
||||
} else {
|
||||
host.timedOut = false;
|
||||
}
|
||||
}
|
||||
host.checked = true;
|
||||
|
||||
host.lastChecked = Date.now();
|
||||
|
||||
// switch if the current host is out of sync or significantly slower than the next best alternative
|
||||
const rankOrder = this.sortHosts();
|
||||
@@ -143,7 +151,7 @@ class FailoverRouter {
|
||||
|
||||
private formatRanking(index: number, host: FailoverHost, active: FailoverHost, maxHeight: number): string {
|
||||
const heightStatus = !host.checked ? '⏳' : (host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < maxHeight ? '🟧' : '✅'));
|
||||
return `${host === active ? '⭐️' : ' '} ${host.rtt < Infinity ? Math.round(host.rtt).toString().padStart(5, ' ') + 'ms' : ' - '} ${!host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅')} | block: ${host.latestHeight || '??????'} ${heightStatus} | ${host.host} ${host === active ? '⭐️' : ' '}`;
|
||||
return `${host === active ? '⭐️' : ' '} ${host.rtt < Infinity ? Math.round(host.rtt).toString().padStart(5, ' ') + 'ms' : (host.timedOut ? ' ⌛️💥 ' : ' - ')} ${!host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅')} | block: ${host.latestHeight || '??????'} ${heightStatus} | ${host.host} ${host === active ? '⭐️' : ' '}`;
|
||||
}
|
||||
|
||||
private updateFallback(): FailoverHost[] {
|
||||
@@ -157,7 +165,7 @@ class FailoverRouter {
|
||||
}
|
||||
|
||||
// sort hosts by connection quality, and update default fallback
|
||||
private sortHosts(): FailoverHost[] {
|
||||
public sortHosts(): FailoverHost[] {
|
||||
// sort by connection quality
|
||||
return this.hosts.slice().sort((a, b) => {
|
||||
if ((a.unreachable || a.outOfSync) === (b.unreachable || b.outOfSync)) {
|
||||
@@ -342,6 +350,24 @@ class ElectrsApi implements AbstractBitcoinApi {
|
||||
public startHealthChecks(): void {
|
||||
this.failoverRouter.startHealthChecks();
|
||||
}
|
||||
|
||||
public getHealthStatus(): HealthCheckHost[] {
|
||||
if (config.MEMPOOL.OFFICIAL) {
|
||||
return this.failoverRouter.sortHosts().map(host => ({
|
||||
host: host.host,
|
||||
active: host === this.failoverRouter.activeHost,
|
||||
rtt: host.rtt,
|
||||
latestHeight: host.latestHeight || 0,
|
||||
socket: !!host.socket,
|
||||
outOfSync: !!host.outOfSync,
|
||||
unreachable: !!host.unreachable,
|
||||
checked: !!host.checked,
|
||||
lastChecked: host.lastChecked || 0,
|
||||
}));
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ElectrsApi;
|
||||
|
||||
@@ -7,6 +7,24 @@ import { isIP } from 'net';
|
||||
import transactionUtils from './transaction-utils';
|
||||
import { isPoint } from '../utils/secp256k1';
|
||||
import logger from '../logger';
|
||||
import { getVarIntLength, opcodes, parseMultisigScript } from '../utils/bitcoin-script';
|
||||
|
||||
// Bitcoin Core default policy settings
|
||||
const TX_MAX_STANDARD_VERSION = 2;
|
||||
const MAX_STANDARD_TX_WEIGHT = 400_000;
|
||||
const MAX_BLOCK_SIGOPS_COST = 80_000;
|
||||
const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5);
|
||||
const MIN_STANDARD_TX_NONWITNESS_SIZE = 65;
|
||||
const MAX_P2SH_SIGOPS = 15;
|
||||
const MAX_STANDARD_P2WSH_STACK_ITEMS = 100;
|
||||
const MAX_STANDARD_P2WSH_STACK_ITEM_SIZE = 80;
|
||||
const MAX_STANDARD_TAPSCRIPT_STACK_ITEM_SIZE = 80;
|
||||
const MAX_STANDARD_P2WSH_SCRIPT_SIZE = 3600;
|
||||
const MAX_STANDARD_SCRIPTSIG_SIZE = 1650;
|
||||
const DUST_RELAY_TX_FEE = 3;
|
||||
const MAX_OP_RETURN_RELAY = 83;
|
||||
const DEFAULT_PERMIT_BAREMULTISIG = true;
|
||||
|
||||
export class Common {
|
||||
static nativeAssetId = config.MEMPOOL.NETWORK === 'liquidtestnet' ?
|
||||
'144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49'
|
||||
@@ -177,6 +195,140 @@ export class Common {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates most standardness rules
|
||||
*
|
||||
* returns true early if any standardness rule is violated, otherwise false
|
||||
* (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced)
|
||||
*/
|
||||
static isNonStandard(tx: TransactionExtended): boolean {
|
||||
// version
|
||||
if (tx.version > TX_MAX_STANDARD_VERSION) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// tx-size
|
||||
if (tx.weight > MAX_STANDARD_TX_WEIGHT) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// tx-size-small
|
||||
if (this.getNonWitnessSize(tx) < MIN_STANDARD_TX_NONWITNESS_SIZE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// bad-txns-too-many-sigops
|
||||
if (tx.sigops && tx.sigops > MAX_STANDARD_TX_SIGOPS_COST) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// input validation
|
||||
for (const vin of tx.vin) {
|
||||
if (vin.is_coinbase) {
|
||||
// standardness rules don't apply to coinbase transactions
|
||||
return false;
|
||||
}
|
||||
// scriptsig-size
|
||||
if ((vin.scriptsig.length / 2) > MAX_STANDARD_SCRIPTSIG_SIZE) {
|
||||
return true;
|
||||
}
|
||||
// scriptsig-not-pushonly
|
||||
if (vin.scriptsig_asm) {
|
||||
for (const op of vin.scriptsig_asm.split(' ')) {
|
||||
if (opcodes[op] && opcodes[op] > opcodes['OP_16']) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// bad-txns-nonstandard-inputs
|
||||
if (vin.prevout?.scriptpubkey_type === 'p2sh') {
|
||||
// TODO: evaluate script (https://github.com/bitcoin/bitcoin/blob/1ac627c485a43e50a9a49baddce186ee3ad4daad/src/policy/policy.cpp#L177)
|
||||
// countScriptSigops returns the witness-scaled sigops, so divide by 4 before comparison with MAX_P2SH_SIGOPS
|
||||
const sigops = (transactionUtils.countScriptSigops(vin.inner_redeemscript_asm) / 4);
|
||||
if (sigops > MAX_P2SH_SIGOPS) {
|
||||
return true;
|
||||
}
|
||||
} else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
|
||||
return true;
|
||||
}
|
||||
// TODO: bad-witness-nonstandard
|
||||
}
|
||||
|
||||
// output validation
|
||||
let opreturnCount = 0;
|
||||
for (const vout of tx.vout) {
|
||||
// scriptpubkey
|
||||
if (['unknown', 'provably_unspendable', 'empty'].includes(vout.scriptpubkey_type)) {
|
||||
// (non-standard output type)
|
||||
return true;
|
||||
} else if (vout.scriptpubkey_type === 'multisig') {
|
||||
if (!DEFAULT_PERMIT_BAREMULTISIG) {
|
||||
// bare-multisig
|
||||
return true;
|
||||
}
|
||||
const mOfN = parseMultisigScript(vout.scriptpubkey_asm);
|
||||
if (!mOfN || mOfN.n < 1 || mOfN.n > 3 || mOfN.m < 1 || mOfN.m > mOfN.n) {
|
||||
// (non-standard bare multisig threshold)
|
||||
return true;
|
||||
}
|
||||
} else if (vout.scriptpubkey_type === 'op_return') {
|
||||
opreturnCount++;
|
||||
if ((vout.scriptpubkey.length / 2) > MAX_OP_RETURN_RELAY) {
|
||||
// over default datacarrier limit
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// dust
|
||||
// (we could probably hardcode this for the different output types...)
|
||||
if (vout.scriptpubkey_type !== 'op_return') {
|
||||
let dustSize = (vout.scriptpubkey.length / 2);
|
||||
// add varint length overhead
|
||||
dustSize += getVarIntLength(dustSize);
|
||||
// add value size
|
||||
dustSize += 8;
|
||||
if (['v0_p2wpkh', 'v0_p2wsh', 'v1_p2tr'].includes(vout.scriptpubkey_type)) {
|
||||
dustSize += 67;
|
||||
} else {
|
||||
dustSize += 148;
|
||||
}
|
||||
if (vout.value < (dustSize * DUST_RELAY_TX_FEE)) {
|
||||
// under minimum output size
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// multi-op-return
|
||||
if (opreturnCount > 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: non-mandatory-script-verify-flag
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static getNonWitnessSize(tx: TransactionExtended): number {
|
||||
let weight = tx.weight;
|
||||
let hasWitness = false;
|
||||
for (const vin of tx.vin) {
|
||||
if (vin.witness?.length) {
|
||||
hasWitness = true;
|
||||
// witness count
|
||||
weight -= getVarIntLength(vin.witness.length);
|
||||
for (const witness of vin.witness) {
|
||||
// witness item size + content
|
||||
weight -= getVarIntLength(witness.length / 2) + (witness.length / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasWitness) {
|
||||
// marker & segwit flag
|
||||
weight -= 2;
|
||||
}
|
||||
return Math.ceil(weight / 4);
|
||||
}
|
||||
|
||||
static setSegwitSighashFlags(flags: bigint, witness: string[]): bigint {
|
||||
for (const w of witness) {
|
||||
if (this.isDERSig(w)) {
|
||||
@@ -303,6 +455,8 @@ export class Common {
|
||||
flags |= TransactionFlags.no_rbf;
|
||||
}
|
||||
let hasFakePubkey = false;
|
||||
let P2WSHCount = 0;
|
||||
let olgaSize = 0;
|
||||
for (const vout of tx.vout) {
|
||||
switch (vout.scriptpubkey_type) {
|
||||
case 'p2pk': {
|
||||
@@ -330,6 +484,20 @@ export class Common {
|
||||
if (vout.scriptpubkey_address) {
|
||||
reusedOutputAddresses[vout.scriptpubkey_address] = (reusedOutputAddresses[vout.scriptpubkey_address] || 0) + 1;
|
||||
}
|
||||
if (vout.scriptpubkey_type === 'v0_p2wsh') {
|
||||
if (!P2WSHCount) {
|
||||
olgaSize = parseInt(vout.scriptpubkey.slice(4, 8), 16);
|
||||
}
|
||||
P2WSHCount++;
|
||||
if (P2WSHCount === Math.ceil((olgaSize + 2) / 32)) {
|
||||
const nullBytes = (P2WSHCount * 32) - olgaSize - 2;
|
||||
if (vout.scriptpubkey.endsWith(''.padEnd(nullBytes * 2, '0'))) {
|
||||
flags |= TransactionFlags.fake_scripthash;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
P2WSHCount = 0;
|
||||
}
|
||||
outValues[vout.value || Math.random()] = (outValues[vout.value || Math.random()] || 0) + 1;
|
||||
}
|
||||
if (hasFakePubkey) {
|
||||
@@ -351,6 +519,10 @@ export class Common {
|
||||
flags |= TransactionFlags.batch_payout;
|
||||
}
|
||||
|
||||
if (this.isNonStandard(tx)) {
|
||||
flags |= TransactionFlags.nonstandard;
|
||||
}
|
||||
|
||||
return Number(flags);
|
||||
}
|
||||
|
||||
@@ -380,6 +552,7 @@ export class Common {
|
||||
value: tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0),
|
||||
acc: tx.acceleration || undefined,
|
||||
rate: tx.effectiveFeePerVsize,
|
||||
time: tx.firstSeen || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -402,69 +575,6 @@ export class Common {
|
||||
}
|
||||
}
|
||||
|
||||
static setRelativesAndGetCpfpInfo(tx: MempoolTransactionExtended, memPool: { [txid: string]: MempoolTransactionExtended }): CpfpInfo {
|
||||
const parents = this.findAllParents(tx, memPool);
|
||||
const lowerFeeParents = parents.filter((parent) => parent.adjustedFeePerVsize < tx.effectiveFeePerVsize);
|
||||
|
||||
let totalWeight = (tx.adjustedVsize * 4) + lowerFeeParents.reduce((prev, val) => prev + (val.adjustedVsize * 4), 0);
|
||||
let totalFees = tx.fee + lowerFeeParents.reduce((prev, val) => prev + val.fee, 0);
|
||||
|
||||
tx.ancestors = parents
|
||||
.map((t) => {
|
||||
return {
|
||||
txid: t.txid,
|
||||
weight: (t.adjustedVsize * 4),
|
||||
fee: t.fee,
|
||||
};
|
||||
});
|
||||
|
||||
// Add high (high fee) decendant weight and fees
|
||||
if (tx.bestDescendant) {
|
||||
totalWeight += tx.bestDescendant.weight;
|
||||
totalFees += tx.bestDescendant.fee;
|
||||
}
|
||||
|
||||
tx.effectiveFeePerVsize = Math.max(0, totalFees / (totalWeight / 4));
|
||||
tx.cpfpChecked = true;
|
||||
|
||||
return {
|
||||
ancestors: tx.ancestors,
|
||||
bestDescendant: tx.bestDescendant || null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private static findAllParents(tx: MempoolTransactionExtended, memPool: { [txid: string]: MempoolTransactionExtended }): MempoolTransactionExtended[] {
|
||||
let parents: MempoolTransactionExtended[] = [];
|
||||
tx.vin.forEach((parent) => {
|
||||
if (parents.find((p) => p.txid === parent.txid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parentTx = memPool[parent.txid];
|
||||
if (parentTx) {
|
||||
if (tx.bestDescendant && tx.bestDescendant.fee / (tx.bestDescendant.weight / 4) > parentTx.adjustedFeePerVsize) {
|
||||
if (parentTx.bestDescendant && parentTx.bestDescendant.fee < tx.fee + tx.bestDescendant.fee) {
|
||||
parentTx.bestDescendant = {
|
||||
weight: (tx.adjustedVsize * 4) + tx.bestDescendant.weight,
|
||||
fee: tx.fee + tx.bestDescendant.fee,
|
||||
txid: tx.txid,
|
||||
};
|
||||
}
|
||||
} else if (tx.adjustedFeePerVsize > parentTx.adjustedFeePerVsize) {
|
||||
parentTx.bestDescendant = {
|
||||
weight: (tx.adjustedVsize * 4),
|
||||
fee: tx.fee,
|
||||
txid: tx.txid
|
||||
};
|
||||
}
|
||||
parents.push(parentTx);
|
||||
parents = parents.concat(this.findAllParents(parentTx, memPool));
|
||||
}
|
||||
});
|
||||
return parents;
|
||||
}
|
||||
|
||||
// calculates the ratio of matched transactions to projected transactions by weight
|
||||
static getSimilarity(projectedBlock: MempoolBlockWithTransactions, transactions: TransactionExtended[]): number {
|
||||
let matchedWeight = 0;
|
||||
|
||||
286
backend/src/api/cpfp.ts
Normal file
286
backend/src/api/cpfp.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
import { CpfpInfo, MempoolTransactionExtended } from '../mempool.interfaces';
|
||||
import memPool from './mempool';
|
||||
|
||||
const CPFP_UPDATE_INTERVAL = 60_000; // update CPFP info at most once per 60s per transaction
|
||||
const MAX_GRAPH_SIZE = 50; // the maximum number of in-mempool relatives to consider
|
||||
|
||||
interface GraphTx extends MempoolTransactionExtended {
|
||||
depends: string[];
|
||||
spentby: string[];
|
||||
ancestorMap: Map<string, GraphTx>;
|
||||
fees: {
|
||||
base: number;
|
||||
ancestor: number;
|
||||
};
|
||||
ancestorcount: number;
|
||||
ancestorsize: number;
|
||||
ancestorRate: number;
|
||||
individualRate: number;
|
||||
score: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a mempool transaction and a copy of the current mempool, and calculates the CPFP data for
|
||||
* that transaction (and all others in the same cluster)
|
||||
*/
|
||||
export function calculateCpfp(tx: MempoolTransactionExtended, mempool: { [txid: string]: MempoolTransactionExtended }): CpfpInfo {
|
||||
if (tx.cpfpUpdated && Date.now() < (tx.cpfpUpdated + CPFP_UPDATE_INTERVAL)) {
|
||||
tx.cpfpDirty = false;
|
||||
return {
|
||||
ancestors: tx.ancestors || [],
|
||||
bestDescendant: tx.bestDescendant || null,
|
||||
descendants: tx.descendants || [],
|
||||
effectiveFeePerVsize: tx.effectiveFeePerVsize || tx.adjustedFeePerVsize || tx.feePerVsize,
|
||||
sigops: tx.sigops,
|
||||
adjustedVsize: tx.adjustedVsize,
|
||||
acceleration: tx.acceleration
|
||||
};
|
||||
}
|
||||
|
||||
const ancestorMap = new Map<string, GraphTx>();
|
||||
const graphTx = mempoolToGraphTx(tx);
|
||||
ancestorMap.set(tx.txid, graphTx);
|
||||
|
||||
const allRelatives = expandRelativesGraph(mempool, ancestorMap);
|
||||
const relativesMap = initializeRelatives(allRelatives);
|
||||
const cluster = calculateCpfpCluster(tx.txid, relativesMap);
|
||||
|
||||
let totalVsize = 0;
|
||||
let totalFee = 0;
|
||||
for (const tx of cluster.values()) {
|
||||
totalVsize += tx.adjustedVsize;
|
||||
totalFee += tx.fee;
|
||||
}
|
||||
const effectiveFeePerVsize = totalFee / totalVsize;
|
||||
for (const tx of cluster.values()) {
|
||||
mempool[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize;
|
||||
mempool[tx.txid].ancestors = Array.from(tx.ancestorMap.values()).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fee }));
|
||||
mempool[tx.txid].descendants = Array.from(cluster.values()).filter(entry => entry.txid !== tx.txid && !tx.ancestorMap.has(entry.txid)).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fee }));
|
||||
mempool[tx.txid].bestDescendant = null;
|
||||
mempool[tx.txid].cpfpChecked = true;
|
||||
mempool[tx.txid].cpfpDirty = true;
|
||||
mempool[tx.txid].cpfpUpdated = Date.now();
|
||||
}
|
||||
|
||||
tx = mempool[tx.txid];
|
||||
|
||||
return {
|
||||
ancestors: tx.ancestors || [],
|
||||
bestDescendant: tx.bestDescendant || null,
|
||||
descendants: tx.descendants || [],
|
||||
effectiveFeePerVsize: tx.effectiveFeePerVsize || tx.adjustedFeePerVsize || tx.feePerVsize,
|
||||
sigops: tx.sigops,
|
||||
adjustedVsize: tx.adjustedVsize,
|
||||
acceleration: tx.acceleration
|
||||
};
|
||||
}
|
||||
|
||||
function mempoolToGraphTx(tx: MempoolTransactionExtended): GraphTx {
|
||||
return {
|
||||
...tx,
|
||||
depends: tx.vin.map(v => v.txid),
|
||||
spentby: tx.vout.map((v, i) => memPool.getFromSpendMap(tx.txid, i)).map(tx => tx?.txid).filter(txid => txid != null) as string[],
|
||||
ancestorMap: new Map(),
|
||||
fees: {
|
||||
base: tx.fee,
|
||||
ancestor: tx.fee,
|
||||
},
|
||||
ancestorcount: 1,
|
||||
ancestorsize: tx.adjustedVsize,
|
||||
ancestorRate: 0,
|
||||
individualRate: 0,
|
||||
score: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a map of transaction ancestors, and expands it into a full graph of up to MAX_GRAPH_SIZE in-mempool relatives
|
||||
*/
|
||||
function expandRelativesGraph(mempool: { [txid: string]: MempoolTransactionExtended }, ancestors: Map<string, GraphTx>): Map<string, GraphTx> {
|
||||
const relatives: Map<string, GraphTx> = new Map();
|
||||
const stack: GraphTx[] = Array.from(ancestors.values());
|
||||
while (stack.length > 0) {
|
||||
if (relatives.size > MAX_GRAPH_SIZE) {
|
||||
return relatives;
|
||||
}
|
||||
|
||||
const nextTx = stack.pop();
|
||||
if (!nextTx) {
|
||||
continue;
|
||||
}
|
||||
relatives.set(nextTx.txid, nextTx);
|
||||
|
||||
for (const relativeTxid of [...nextTx.depends, ...nextTx.spentby]) {
|
||||
if (relatives.has(relativeTxid)) {
|
||||
// already processed this tx
|
||||
continue;
|
||||
}
|
||||
let mempoolTx = ancestors.get(relativeTxid);
|
||||
if (!mempoolTx && mempool[relativeTxid]) {
|
||||
mempoolTx = mempoolToGraphTx(mempool[relativeTxid]);
|
||||
}
|
||||
if (mempoolTx) {
|
||||
stack.push(mempoolTx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return relatives;
|
||||
}
|
||||
|
||||
/**
|
||||
* Efficiently sets a Map of in-mempool ancestors for each member of an expanded relative graph
|
||||
* by running setAncestors on each leaf, and caching intermediate results.
|
||||
* then initializes ancestor data for each transaction
|
||||
*
|
||||
* @param all
|
||||
*/
|
||||
function initializeRelatives(mempoolTxs: Map<string, GraphTx>): Map<string, GraphTx> {
|
||||
const visited: Map<string, Map<string, GraphTx>> = new Map();
|
||||
const leaves: GraphTx[] = Array.from(mempoolTxs.values()).filter(entry => entry.spentby.length === 0);
|
||||
for (const leaf of leaves) {
|
||||
setAncestors(leaf, mempoolTxs, visited);
|
||||
}
|
||||
mempoolTxs.forEach(entry => {
|
||||
entry.ancestorMap?.forEach(ancestor => {
|
||||
entry.ancestorcount++;
|
||||
entry.ancestorsize += ancestor.adjustedVsize;
|
||||
entry.fees.ancestor += ancestor.fees.base;
|
||||
});
|
||||
setAncestorScores(entry);
|
||||
});
|
||||
return mempoolTxs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a root transaction and a list of in-mempool ancestors,
|
||||
* Calculate the CPFP cluster
|
||||
*
|
||||
* @param tx
|
||||
* @param ancestors
|
||||
*/
|
||||
function calculateCpfpCluster(txid: string, graph: Map<string, GraphTx>): Map<string, GraphTx> {
|
||||
const tx = graph.get(txid);
|
||||
if (!tx) {
|
||||
return new Map<string, GraphTx>([]);
|
||||
}
|
||||
|
||||
// Initialize individual & ancestor fee rates
|
||||
graph.forEach(entry => setAncestorScores(entry));
|
||||
|
||||
// Sort by descending ancestor score
|
||||
let sortedRelatives = Array.from(graph.values()).sort(mempoolComparator);
|
||||
|
||||
// Iterate until we reach a cluster that includes our target tx
|
||||
let maxIterations = MAX_GRAPH_SIZE;
|
||||
let best = sortedRelatives.shift();
|
||||
let bestCluster = new Map<string, GraphTx>(best?.ancestorMap?.entries() || []);
|
||||
while (sortedRelatives.length && best && (best.txid !== tx.txid && !best.ancestorMap.has(tx.txid)) && maxIterations > 0) {
|
||||
maxIterations--;
|
||||
if ((best && best.txid === tx.txid) || (bestCluster && bestCluster.has(tx.txid))) {
|
||||
break;
|
||||
} else {
|
||||
// Remove this cluster (it doesn't include our target tx)
|
||||
// and update scores, ancestor totals and dependencies for the survivors
|
||||
removeAncestors(bestCluster, graph);
|
||||
|
||||
// re-sort
|
||||
sortedRelatives = Array.from(graph.values()).sort(mempoolComparator);
|
||||
|
||||
// Grab the next highest scoring entry
|
||||
best = sortedRelatives.shift();
|
||||
if (best) {
|
||||
bestCluster = new Map<string, GraphTx>(best?.ancestorMap?.entries() || []);
|
||||
bestCluster.set(best?.txid, best);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bestCluster.set(tx.txid, tx);
|
||||
|
||||
return bestCluster;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a cluster of transactions from an in-mempool dependency graph
|
||||
* and update the survivors' scores and ancestors
|
||||
*
|
||||
* @param cluster
|
||||
* @param ancestors
|
||||
*/
|
||||
function removeAncestors(cluster: Map<string, GraphTx>, all: Map<string, GraphTx>): void {
|
||||
// remove
|
||||
cluster.forEach(tx => {
|
||||
all.delete(tx.txid);
|
||||
});
|
||||
|
||||
// update survivors
|
||||
all.forEach(tx => {
|
||||
cluster.forEach(remove => {
|
||||
if (tx.ancestorMap?.has(remove.txid)) {
|
||||
// remove as dependency
|
||||
tx.ancestorMap.delete(remove.txid);
|
||||
tx.depends = tx.depends.filter(parent => parent !== remove.txid);
|
||||
// update ancestor sizes and fees
|
||||
tx.ancestorsize -= remove.adjustedVsize;
|
||||
tx.fees.ancestor -= remove.fees.base;
|
||||
}
|
||||
});
|
||||
// recalculate fee rates
|
||||
setAncestorScores(tx);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively traverses an in-mempool dependency graph, and sets a Map of in-mempool ancestors
|
||||
* for each transaction.
|
||||
*
|
||||
* @param tx
|
||||
* @param all
|
||||
*/
|
||||
function setAncestors(tx: GraphTx, all: Map<string, GraphTx>, visited: Map<string, Map<string, GraphTx>>, depth: number = 0): Map<string, GraphTx> {
|
||||
// sanity check for infinite recursion / too many ancestors (should never happen)
|
||||
if (depth > MAX_GRAPH_SIZE) {
|
||||
return tx.ancestorMap;
|
||||
}
|
||||
|
||||
// initialize the ancestor map for this tx
|
||||
tx.ancestorMap = new Map<string, GraphTx>();
|
||||
tx.depends.forEach(parentId => {
|
||||
const parent = all.get(parentId);
|
||||
if (parent) {
|
||||
// add the parent
|
||||
tx.ancestorMap?.set(parentId, parent);
|
||||
// check for a cached copy of this parent's ancestors
|
||||
let ancestors = visited.get(parent.txid);
|
||||
if (!ancestors) {
|
||||
// recursively fetch the parent's ancestors
|
||||
ancestors = setAncestors(parent, all, visited, depth + 1);
|
||||
}
|
||||
// and add to this tx's map
|
||||
ancestors.forEach((ancestor, ancestorId) => {
|
||||
tx.ancestorMap?.set(ancestorId, ancestor);
|
||||
});
|
||||
}
|
||||
});
|
||||
visited.set(tx.txid, tx.ancestorMap);
|
||||
|
||||
return tx.ancestorMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a mempool transaction, and set the fee rates and ancestor score
|
||||
*
|
||||
* @param tx
|
||||
*/
|
||||
function setAncestorScores(tx: GraphTx): GraphTx {
|
||||
tx.individualRate = (tx.fees.base * 100_000_000) / tx.adjustedVsize;
|
||||
tx.ancestorRate = (tx.fees.ancestor * 100_000_000) / tx.ancestorsize;
|
||||
tx.score = Math.min(tx.individualRate, tx.ancestorRate);
|
||||
return tx;
|
||||
}
|
||||
|
||||
// Sort by descending score
|
||||
function mempoolComparator(a: GraphTx, b: GraphTx): number {
|
||||
return b.score - a.score;
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
|
||||
import { RowDataPacket } from 'mysql2';
|
||||
|
||||
class DatabaseMigration {
|
||||
private static currentVersion = 68;
|
||||
private static currentVersion = 76;
|
||||
private queryTimeout = 3600_000;
|
||||
private statisticsAddedIndexed = false;
|
||||
private uniqueLogs: string[] = [];
|
||||
@@ -580,6 +580,90 @@ class DatabaseMigration {
|
||||
await this.$executeQuery(`INSERT INTO state VALUES('last_bitcoin_block_audit', 0, NULL);`);
|
||||
await this.updateToSchemaVersion(68);
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 69 && config.MEMPOOL.NETWORK === 'mainnet') {
|
||||
await this.$executeQuery(this.getCreateAccelerationsTableQuery(), await this.$checkIfTableExists('accelerations'));
|
||||
await this.updateToSchemaVersion(69);
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 70 && config.MEMPOOL.NETWORK === 'mainnet') {
|
||||
await this.$executeQuery('ALTER TABLE accelerations MODIFY COLUMN added DATETIME;');
|
||||
await this.updateToSchemaVersion(70);
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 71 && config.MEMPOOL.NETWORK === 'liquid') {
|
||||
await this.$executeQuery('TRUNCATE TABLE elements_pegs');
|
||||
await this.$executeQuery('TRUNCATE TABLE federation_txos');
|
||||
await this.$executeQuery('SET FOREIGN_KEY_CHECKS = 0');
|
||||
await this.$executeQuery('TRUNCATE TABLE federation_addresses');
|
||||
await this.$executeQuery('SET FOREIGN_KEY_CHECKS = 1');
|
||||
await this.$executeQuery(`INSERT INTO federation_addresses (bitcoinaddress) VALUES ('bc1qxvay4an52gcghxq5lavact7r6qe9l4laedsazz8fj2ee2cy47tlqff4aj4')`); // Federation change address
|
||||
await this.$executeQuery(`INSERT INTO federation_addresses (bitcoinaddress) VALUES ('3EiAcrzq1cELXScc98KeCswGWZaPGceT1d')`); // Federation change address
|
||||
await this.$executeQuery(`UPDATE state SET number = 0 WHERE name = 'last_elements_block';`);
|
||||
await this.$executeQuery(`UPDATE state SET number = 0 WHERE name = 'last_bitcoin_block_audit';`);
|
||||
await this.$executeQuery('ALTER TABLE `federation_txos` ADD timelock INT NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `federation_txos` ADD expiredAt INT NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `federation_txos` ADD emergencyKey TINYINT NOT NULL DEFAULT 0');
|
||||
await this.updateToSchemaVersion(71);
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 72 && isBitcoin === true) {
|
||||
// reindex Goggles flags for mined block templates above height 832000
|
||||
await this.$executeQuery('UPDATE blocks_summaries SET version = 0 WHERE height >= 832000;');
|
||||
await this.updateToSchemaVersion(72);
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 73 && config.MEMPOOL.NETWORK === 'mainnet') {
|
||||
// Clear bad data
|
||||
await this.$executeQuery(`TRUNCATE accelerations`);
|
||||
this.uniqueLog(logger.notice, `'accelerations' table has been truncated`);
|
||||
await this.updateToSchemaVersion(73);
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 74 && config.MEMPOOL.NETWORK === 'mainnet') {
|
||||
await this.$executeQuery(`INSERT INTO state(name, number) VALUE ('last_acceleration_block', 0);`);
|
||||
await this.updateToSchemaVersion(74);
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 75) {
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `BGN` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `BRL` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `CNY` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `CZK` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `DKK` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `HKD` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `HRK` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `HUF` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `IDR` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `ILS` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `INR` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `ISK` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `KRW` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `MXN` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `MYR` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `NOK` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `NZD` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `PHP` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `PLN` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `RON` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `RUB` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `SEK` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `SGD` float DEFAULT "-1"');
|
||||
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);
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 76 && isBitcoin === true) {
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD prioritized_txs JSON DEFAULT "[]"');
|
||||
await this.updateToSchemaVersion(76);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1123,6 +1207,23 @@ class DatabaseMigration {
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||
}
|
||||
|
||||
private getCreateAccelerationsTableQuery(): string {
|
||||
return `CREATE TABLE IF NOT EXISTS accelerations (
|
||||
txid varchar(65) NOT NULL,
|
||||
added datetime NOT NULL,
|
||||
height int(10) NOT NULL,
|
||||
pool smallint unsigned NULL,
|
||||
effective_vsize int(10) NOT NULL,
|
||||
effective_fee bigint(20) unsigned NOT NULL,
|
||||
boost_rate float unsigned,
|
||||
boost_cost bigint(20) unsigned NOT NULL,
|
||||
PRIMARY KEY (txid),
|
||||
INDEX (added),
|
||||
INDEX (height),
|
||||
INDEX (pool)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||
}
|
||||
|
||||
public async $blocksReindexingTruncate(): Promise<void> {
|
||||
logger.warn(`Truncating pools, blocks, hashrates and difficulty_adjustments tables for re-indexing (using '--reindex-blocks'). You can cancel this command within 5 seconds`);
|
||||
await Common.sleep$(5000);
|
||||
|
||||
@@ -92,8 +92,8 @@ class ElementsParser {
|
||||
await DB.query(`INSERT IGNORE INTO federation_addresses (bitcoinaddress) VALUES (?)`, [bitcoinaddress]);
|
||||
|
||||
// Add the UTXO to the federation txos table
|
||||
const query_utxos = `INSERT IGNORE INTO federation_txos (txid, txindex, bitcoinaddress, amount, blocknumber, blocktime, unspent, lastblockupdate, lasttimeupdate, pegtxid, pegindex, pegblocktime) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
||||
const params_utxos: (string | number)[] = [bitcointxid, bitcoinindex, bitcoinaddress, amount, bitcoinblock, bitcoinBlockTime, 1, bitcoinblock - 1, 0, txid, txindex, blockTime];
|
||||
const query_utxos = `INSERT IGNORE INTO federation_txos (txid, txindex, bitcoinaddress, amount, blocknumber, blocktime, unspent, lastblockupdate, lasttimeupdate, timelock, expiredAt, emergencyKey, pegtxid, pegindex, pegblocktime) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
||||
const params_utxos: (string | number)[] = [bitcointxid, bitcoinindex, bitcoinaddress, amount, bitcoinblock, bitcoinBlockTime, 1, bitcoinblock - 1, 0, 4032, 0, 0, txid, txindex, blockTime];
|
||||
await DB.query(query_utxos, params_utxos);
|
||||
const [minBlockUpdate] = await DB.query(`SELECT MIN(lastblockupdate) AS lastblockupdate FROM federation_txos WHERE unspent = 1`)
|
||||
await this.$saveLastBlockAuditToDatabase(minBlockUpdate[0]['lastblockupdate']);
|
||||
@@ -206,7 +206,7 @@ class ElementsParser {
|
||||
|
||||
// Get the UTXOs that need to be scanned in block height (UTXOs that were last updated in the block height - 1)
|
||||
protected async $getFederationUtxosToScan(height: number) {
|
||||
const query = `SELECT txid, txindex, bitcoinaddress, amount FROM federation_txos WHERE lastblockupdate = ? AND unspent = 1`;
|
||||
const query = `SELECT txid, txindex, bitcoinaddress, amount, blocknumber, timelock, expiredAt FROM federation_txos WHERE lastblockupdate = ? AND unspent = 1`;
|
||||
const [rows] = await DB.query(query, [height - 1]);
|
||||
return rows as any[];
|
||||
}
|
||||
@@ -227,16 +227,26 @@ class ElementsParser {
|
||||
protected async $parseBitcoinBlock(block: IBitcoinApi.Block, spentAsTip: any[], unspentAsTip: any[], confirmedTip: number, redeemAddressesData: any[] = []) {
|
||||
const redeemAddresses: string[] = redeemAddressesData.map(redeemAddress => redeemAddress.bitcoinaddress);
|
||||
for (const tx of block.tx) {
|
||||
let mightRedeemInThisTx = false; // If a Federation UTXO is spent in this block, we might find a peg-out address in the outputs...
|
||||
let mightRedeemInThisTx = false;
|
||||
// Check if the Federation UTXOs that was spent as of tip are spent in this block
|
||||
for (const input of tx.vin) {
|
||||
const txo = spentAsTip.find(txo => txo.txid === input.txid && txo.txindex === input.vout);
|
||||
if (txo) {
|
||||
mightRedeemInThisTx = true;
|
||||
await DB.query(`UPDATE federation_txos SET unspent = 0, lastblockupdate = ?, lasttimeupdate = ? WHERE txid = ? AND txindex = ?`, [block.height, block.time, txo.txid, txo.txindex]);
|
||||
mightRedeemInThisTx = true; // A Federation UTXO is spent in this block: we might find a peg-out address in the outputs
|
||||
if (txo.expiredAt > 0 ) {
|
||||
if (input.txinwitness?.length !== 13) { // Check if the witness data of the input contains the 11 signatures: if it doesn't, emergency keys are being used
|
||||
await DB.query(`UPDATE federation_txos SET unspent = 0, lastblockupdate = ?, lasttimeupdate = ?, emergencyKey = 1 WHERE txid = ? AND txindex = ?`, [block.height, block.time, txo.txid, txo.txindex]);
|
||||
logger.debug(`Expired Federation UTXO ${txo.txid}:${txo.txindex} (${txo.amount} sats) was spent in block ${block.height} using emergency keys!`);
|
||||
} else {
|
||||
await DB.query(`UPDATE federation_txos SET unspent = 0, lastblockupdate = ?, lasttimeupdate = ? WHERE txid = ? AND txindex = ?`, [block.height, block.time, txo.txid, txo.txindex]);
|
||||
logger.debug(`Expired Federation UTXO ${txo.txid}:${txo.txindex} (${txo.amount} sats) was spent in block ${block.height} using regular 11-of-15 signatures`);
|
||||
}
|
||||
} else {
|
||||
await DB.query(`UPDATE federation_txos SET unspent = 0, lastblockupdate = ?, lasttimeupdate = ? WHERE txid = ? AND txindex = ?`, [block.height, block.time, txo.txid, txo.txindex]);
|
||||
logger.debug(`Federation UTXO ${txo.txid}:${txo.txindex} (${txo.amount} sats) was spent in block ${block.height}`);
|
||||
}
|
||||
// Remove the TXO from the utxo array
|
||||
spentAsTip.splice(spentAsTip.indexOf(txo), 1);
|
||||
logger.debug(`Federation UTXO ${txo.txid}:${txo.txindex} (${txo.amount} sats) was spent in block ${block.height}`);
|
||||
}
|
||||
}
|
||||
// Check if an output is sent to a change address of the federation
|
||||
@@ -245,17 +255,21 @@ class ElementsParser {
|
||||
// Check that the UTXO was not already added in the DB by previous scans
|
||||
const [rows_check] = await DB.query(`SELECT txid FROM federation_txos WHERE txid = ? AND txindex = ?`, [tx.txid, output.n]) as any[];
|
||||
if (rows_check.length === 0) {
|
||||
const query_utxos = `INSERT INTO federation_txos (txid, txindex, bitcoinaddress, amount, blocknumber, blocktime, unspent, lastblockupdate, lasttimeupdate, pegtxid, pegindex, pegblocktime) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
||||
const params_utxos: (string | number)[] = [tx.txid, output.n, output.scriptPubKey.address, output.value * 100000000, block.height, block.time, 1, block.height, 0, '', 0, 0];
|
||||
const timelock = output.scriptPubKey.address === federationChangeAddresses[0] ? 4032 : 2016; // P2WSH change address has a 4032 timelock, P2SH change address has a 2016 timelock
|
||||
const query_utxos = `INSERT INTO federation_txos (txid, txindex, bitcoinaddress, amount, blocknumber, blocktime, unspent, lastblockupdate, lasttimeupdate, timelock, expiredAt, emergencyKey, pegtxid, pegindex, pegblocktime) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
||||
const params_utxos: (string | number)[] = [tx.txid, output.n, output.scriptPubKey.address, output.value * 100000000, block.height, block.time, 1, block.height, 0, timelock, 0, 0, '', 0, 0];
|
||||
await DB.query(query_utxos, params_utxos);
|
||||
// Add the UTXO to the utxo array
|
||||
spentAsTip.push({
|
||||
txid: tx.txid,
|
||||
txindex: output.n,
|
||||
bitcoinaddress: output.scriptPubKey.address,
|
||||
amount: output.value * 100000000
|
||||
amount: output.value * 100000000,
|
||||
blocknumber: block.height,
|
||||
timelock: timelock,
|
||||
expiredAt: 0,
|
||||
});
|
||||
logger.debug(`Added new Federation UTXO ${tx.txid}:${output.n} (${output.value * 100000000} sats), change address: ${output.scriptPubKey.address}`);
|
||||
logger.debug(`Added new Federation UTXO ${tx.txid}:${output.n} (${Math.round(output.value * 100000000)} sats), change address: ${output.scriptPubKey.address}`);
|
||||
}
|
||||
}
|
||||
if (mightRedeemInThisTx && output.scriptPubKey.address && redeemAddresses.includes(output.scriptPubKey.address)) {
|
||||
@@ -282,13 +296,22 @@ class ElementsParser {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (const utxo of spentAsTip) {
|
||||
await DB.query(`UPDATE federation_txos SET lastblockupdate = ? WHERE txid = ? AND txindex = ?`, [block.height, utxo.txid, utxo.txindex]);
|
||||
for (const utxo of spentAsTip) {
|
||||
if (utxo.expiredAt === 0 && block.height >= utxo.blocknumber + utxo.timelock) { // The UTXO is expiring in this block
|
||||
await DB.query(`UPDATE federation_txos SET lastblockupdate = ?, expiredAt = ? WHERE txid = ? AND txindex = ?`, [block.height, block.time, utxo.txid, utxo.txindex]);
|
||||
} else {
|
||||
await DB.query(`UPDATE federation_txos SET lastblockupdate = ? WHERE txid = ? AND txindex = ?`, [block.height, utxo.txid, utxo.txindex]);
|
||||
}
|
||||
}
|
||||
|
||||
for (const utxo of unspentAsTip) {
|
||||
if (utxo.expiredAt === 0 && block.height >= utxo.blocknumber + utxo.timelock) { // The UTXO is expiring in this block
|
||||
await DB.query(`UPDATE federation_txos SET unspent = 0, lastblockupdate = ?, expiredAt = ? WHERE txid = ? AND txindex = ?`, [confirmedTip, block.time, utxo.txid, utxo.txindex]);
|
||||
} else if (utxo.expiredAt === 0 && confirmedTip >= utxo.blocknumber + utxo.timelock) { // The UTXO is expiring before the tip: we need to keep track of it
|
||||
await DB.query(`UPDATE federation_txos SET lastblockupdate = ? WHERE txid = ? AND txindex = ?`, [utxo.blocknumber + utxo.timelock - 1, utxo.txid, utxo.txindex]);
|
||||
} else {
|
||||
await DB.query(`UPDATE federation_txos SET lastblockupdate = ? WHERE txid = ? AND txindex = ?`, [confirmedTip, utxo.txid, utxo.txindex]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,6 +351,10 @@ class ElementsParser {
|
||||
return rows;
|
||||
}
|
||||
|
||||
protected isDust(amount: number, feeRate: number): boolean {
|
||||
return amount <= (450 * feeRate); // A P2WSH 11-of-15 multisig input is around 450 bytes
|
||||
}
|
||||
|
||||
///////////// DATA QUERY //////////////
|
||||
|
||||
public async $getAuditStatus(): Promise<any> {
|
||||
@@ -354,6 +381,8 @@ class ElementsParser {
|
||||
(blocktime > UNIX_TIMESTAMP(LAST_DAY(FROM_UNIXTIME(blocktime) - INTERVAL 1 MONTH) + INTERVAL 1 DAY))
|
||||
AND
|
||||
((unspent = 1) OR (unspent = 0 AND lasttimeupdate > UNIX_TIMESTAMP(LAST_DAY(FROM_UNIXTIME(blocktime)) + INTERVAL 1 DAY)))
|
||||
AND
|
||||
(expiredAt = 0 OR expiredAt > UNIX_TIMESTAMP(LAST_DAY(FROM_UNIXTIME(blocktime)) + INTERVAL 1 DAY))
|
||||
GROUP BY
|
||||
date;`;
|
||||
const [rows] = await DB.query(query);
|
||||
@@ -374,7 +403,7 @@ class ElementsParser {
|
||||
|
||||
// Get the current reserves of the federation and the last Bitcoin block it was updated
|
||||
public async $getCurrentFederationReserves(): Promise<any> {
|
||||
const [rows] = await DB.query(`SELECT SUM(amount) AS total_balance FROM federation_txos WHERE unspent = 1;`);
|
||||
const [rows] = await DB.query(`SELECT SUM(amount) AS total_balance FROM federation_txos WHERE unspent = 1 AND expiredAt = 0;`);
|
||||
const lastblockaudit = await this.$getLastBlockAudit();
|
||||
const hash = await bitcoinSecondClient.getBlockHash(lastblockaudit);
|
||||
return {
|
||||
@@ -386,28 +415,53 @@ class ElementsParser {
|
||||
|
||||
// Get all of the federation addresses, most balances first
|
||||
public async $getFederationAddresses(): Promise<any> {
|
||||
const query = `SELECT bitcoinaddress, SUM(amount) AS balance FROM federation_txos WHERE unspent = 1 GROUP BY bitcoinaddress ORDER BY balance DESC;`;
|
||||
const query = `SELECT bitcoinaddress, SUM(amount) AS balance FROM federation_txos WHERE unspent = 1 AND expiredAt = 0 GROUP BY bitcoinaddress ORDER BY balance DESC;`;
|
||||
const [rows] = await DB.query(query);
|
||||
return rows;
|
||||
}
|
||||
|
||||
// Get all of the UTXOs held by the federation, most recent first
|
||||
public async $getFederationUtxos(): Promise<any> {
|
||||
const query = `SELECT txid, txindex, bitcoinaddress, amount, blocknumber, blocktime, pegtxid, pegindex, pegblocktime FROM federation_txos WHERE unspent = 1 ORDER BY blocktime DESC;`;
|
||||
const query = `SELECT txid, txindex, bitcoinaddress, amount, blocknumber, blocktime, pegtxid, pegindex, pegblocktime, timelock, expiredAt FROM federation_txos WHERE unspent = 1 AND expiredAt = 0 ORDER BY blocktime DESC;`;
|
||||
const [rows] = await DB.query(query);
|
||||
return rows;
|
||||
}
|
||||
|
||||
// Get expired UTXOs, most recent first
|
||||
public async $getExpiredUtxos(): Promise<any> {
|
||||
const query = `SELECT txid, txindex, bitcoinaddress, amount, blocknumber, blocktime, pegtxid, pegindex, pegblocktime, timelock, expiredAt FROM federation_txos WHERE unspent = 1 AND expiredAt > 0 ORDER BY blocktime DESC;`;
|
||||
const [rows]: any[] = await DB.query(query);
|
||||
const feeRate = Math.round((await bitcoinSecondClient.estimateSmartFee(1)).feerate * 100000000 / 1000);
|
||||
for (const row of rows) {
|
||||
row.isDust = this.isDust(row.amount, feeRate);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
// Get utxos that were spent using emergency keys
|
||||
public async $getEmergencySpentUtxos(): Promise<any> {
|
||||
const query = `SELECT txid, txindex, bitcoinaddress, amount, blocknumber, blocktime, pegtxid, pegindex, pegblocktime, timelock, expiredAt FROM federation_txos WHERE emergencyKey = 1 ORDER BY blocktime DESC;`;
|
||||
const [rows] = await DB.query(query);
|
||||
return rows;
|
||||
}
|
||||
|
||||
// Get the total number of federation addresses
|
||||
public async $getFederationAddressesNumber(): Promise<any> {
|
||||
const query = `SELECT COUNT(DISTINCT bitcoinaddress) AS address_count FROM federation_txos WHERE unspent = 1;`;
|
||||
const query = `SELECT COUNT(DISTINCT bitcoinaddress) AS address_count FROM federation_txos WHERE unspent = 1 AND expiredAt = 0;`;
|
||||
const [rows] = await DB.query(query);
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
// Get the total number of federation utxos
|
||||
public async $getFederationUtxosNumber(): Promise<any> {
|
||||
const query = `SELECT COUNT(*) AS utxo_count FROM federation_txos WHERE unspent = 1;`;
|
||||
const query = `SELECT COUNT(*) AS utxo_count FROM federation_txos WHERE unspent = 1 AND expiredAt = 0;`;
|
||||
const [rows] = await DB.query(query);
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
// Get the total number of emergency spent utxos and their total amount
|
||||
public async $getEmergencySpentUtxosStats(): Promise<any> {
|
||||
const query = `SELECT COUNT(*) AS utxo_count, SUM(amount) AS total_amount FROM federation_txos WHERE emergencyKey = 1;`;
|
||||
const [rows] = await DB.query(query);
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
@@ -26,6 +26,9 @@ class LiquidRoutes {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/addresses/total', this.$getFederationAddressesNumber)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/utxos', this.$getFederationUtxos)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/utxos/total', this.$getFederationUtxosNumber)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/utxos/expired', this.$getExpiredUtxos)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/utxos/emergency-spent', this.$getEmergencySpentUtxos)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/utxos/emergency-spent/stats', this.$getEmergencySpentUtxosStats)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/status', this.$getFederationAuditStatus)
|
||||
;
|
||||
}
|
||||
@@ -167,6 +170,18 @@ class LiquidRoutes {
|
||||
}
|
||||
}
|
||||
|
||||
private async $getExpiredUtxos(req: Request, res: Response) {
|
||||
try {
|
||||
const expiredUtxos = await elementsParser.$getExpiredUtxos();
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||
res.json(expiredUtxos);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async $getFederationUtxosNumber(req: Request, res: Response) {
|
||||
try {
|
||||
const federationUtxos = await elementsParser.$getFederationUtxosNumber();
|
||||
@@ -179,6 +194,30 @@ class LiquidRoutes {
|
||||
}
|
||||
}
|
||||
|
||||
private async $getEmergencySpentUtxos(req: Request, res: Response) {
|
||||
try {
|
||||
const emergencySpentUtxos = await elementsParser.$getEmergencySpentUtxos();
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||
res.json(emergencySpentUtxos);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async $getEmergencySpentUtxosStats(req: Request, res: Response) {
|
||||
try {
|
||||
const emergencySpentUtxos = await elementsParser.$getEmergencySpentUtxosStats();
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||
res.json(emergencySpentUtxos);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async $getPegsList(req: Request, res: Response) {
|
||||
try {
|
||||
const recentPegs = await elementsParser.$getPegsList(parseInt(req.params?.count));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { GbtGenerator, GbtResult, ThreadTransaction as RustThreadTransaction, ThreadAcceleration as RustThreadAcceleration } from 'rust-gbt';
|
||||
import logger from '../logger';
|
||||
import { MempoolBlock, MempoolTransactionExtended, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats, PoolTag, TransactionClassified, TransactionCompressed, MempoolDeltaChange } from '../mempool.interfaces';
|
||||
import { MempoolBlock, MempoolTransactionExtended, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats, TransactionClassified, TransactionCompressed, MempoolDeltaChange, GbtCandidates } from '../mempool.interfaces';
|
||||
import { Common, OnlineFeeStatsCalculator } from './common';
|
||||
import config from '../config';
|
||||
import { Worker } from 'worker_threads';
|
||||
@@ -18,6 +18,7 @@ class MempoolBlocks {
|
||||
|
||||
private nextUid: number = 1;
|
||||
private uidMap: Map<number, string> = new Map(); // map short numerical uids to full txids
|
||||
private txidMap: Map<string, number> = new Map(); // map full txids back to short numerical uids
|
||||
|
||||
public getMempoolBlocks(): MempoolBlock[] {
|
||||
return this.mempoolBlocks.map((block) => {
|
||||
@@ -40,132 +41,6 @@ class MempoolBlocks {
|
||||
return this.mempoolBlockDeltas;
|
||||
}
|
||||
|
||||
public updateMempoolBlocks(memPool: { [txid: string]: MempoolTransactionExtended }, saveResults: boolean = false): MempoolBlockWithTransactions[] {
|
||||
const latestMempool = memPool;
|
||||
const memPoolArray: MempoolTransactionExtended[] = [];
|
||||
for (const i in latestMempool) {
|
||||
memPoolArray.push(latestMempool[i]);
|
||||
}
|
||||
const start = new Date().getTime();
|
||||
|
||||
// Clear bestDescendants & ancestors
|
||||
memPoolArray.forEach((tx) => {
|
||||
tx.bestDescendant = null;
|
||||
tx.ancestors = [];
|
||||
tx.cpfpChecked = false;
|
||||
if (!tx.effectiveFeePerVsize) {
|
||||
tx.effectiveFeePerVsize = tx.adjustedFeePerVsize;
|
||||
}
|
||||
});
|
||||
|
||||
// First sort
|
||||
memPoolArray.sort((a, b) => {
|
||||
if (a.adjustedFeePerVsize === b.adjustedFeePerVsize) {
|
||||
// tie-break by lexicographic txid order for stability
|
||||
return a.txid < b.txid ? -1 : 1;
|
||||
} else {
|
||||
return b.adjustedFeePerVsize - a.adjustedFeePerVsize;
|
||||
}
|
||||
});
|
||||
|
||||
// Loop through and traverse all ancestors and sum up all the sizes + fees
|
||||
// Pass down size + fee to all unconfirmed children
|
||||
let sizes = 0;
|
||||
memPoolArray.forEach((tx) => {
|
||||
sizes += tx.weight;
|
||||
if (sizes > 4000000 * 8) {
|
||||
return;
|
||||
}
|
||||
Common.setRelativesAndGetCpfpInfo(tx, memPool);
|
||||
});
|
||||
|
||||
// Final sort, by effective fee
|
||||
memPoolArray.sort((a, b) => {
|
||||
if (a.effectiveFeePerVsize === b.effectiveFeePerVsize) {
|
||||
// tie-break by lexicographic txid order for stability
|
||||
return a.txid < b.txid ? -1 : 1;
|
||||
} else {
|
||||
return b.effectiveFeePerVsize - a.effectiveFeePerVsize;
|
||||
}
|
||||
});
|
||||
|
||||
const end = new Date().getTime();
|
||||
const time = end - start;
|
||||
logger.debug('Mempool blocks calculated in ' + time / 1000 + ' seconds');
|
||||
|
||||
const blocks = this.calculateMempoolBlocks(memPoolArray);
|
||||
|
||||
if (saveResults) {
|
||||
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, blocks);
|
||||
this.mempoolBlocks = blocks;
|
||||
this.mempoolBlockDeltas = deltas;
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
private calculateMempoolBlocks(transactionsSorted: MempoolTransactionExtended[]): MempoolBlockWithTransactions[] {
|
||||
const mempoolBlocks: MempoolBlockWithTransactions[] = [];
|
||||
let feeStatsCalculator: OnlineFeeStatsCalculator = new OnlineFeeStatsCalculator(config.MEMPOOL.BLOCK_WEIGHT_UNITS);
|
||||
let onlineStats = false;
|
||||
let blockSize = 0;
|
||||
let blockWeight = 0;
|
||||
let blockVsize = 0;
|
||||
let blockFees = 0;
|
||||
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
|
||||
let transactionIds: string[] = [];
|
||||
let transactions: MempoolTransactionExtended[] = [];
|
||||
transactionsSorted.forEach((tx, index) => {
|
||||
if (blockWeight + tx.weight <= config.MEMPOOL.BLOCK_WEIGHT_UNITS
|
||||
|| mempoolBlocks.length === config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT - 1) {
|
||||
tx.position = {
|
||||
block: mempoolBlocks.length,
|
||||
vsize: blockVsize + (tx.vsize / 2),
|
||||
};
|
||||
blockWeight += tx.weight;
|
||||
blockVsize += tx.vsize;
|
||||
blockSize += tx.size;
|
||||
blockFees += tx.fee;
|
||||
if (blockVsize <= sizeLimit) {
|
||||
transactions.push(tx);
|
||||
}
|
||||
transactionIds.push(tx.txid);
|
||||
if (onlineStats) {
|
||||
feeStatsCalculator.processNext(tx);
|
||||
}
|
||||
} else {
|
||||
mempoolBlocks.push(this.dataToMempoolBlocks(transactionIds, transactions, blockSize, blockWeight, blockFees));
|
||||
blockVsize = 0;
|
||||
tx.position = {
|
||||
block: mempoolBlocks.length,
|
||||
vsize: blockVsize + (tx.vsize / 2),
|
||||
};
|
||||
|
||||
if (mempoolBlocks.length === config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT - 1) {
|
||||
const stackWeight = transactionsSorted.slice(index).reduce((total, tx) => total + (tx.weight || 0), 0);
|
||||
if (stackWeight > config.MEMPOOL.BLOCK_WEIGHT_UNITS) {
|
||||
onlineStats = true;
|
||||
feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5, [10, 20, 30, 40, 50, 60, 70, 80, 90]);
|
||||
feeStatsCalculator.processNext(tx);
|
||||
}
|
||||
}
|
||||
|
||||
blockVsize += tx.vsize;
|
||||
blockWeight = tx.weight;
|
||||
blockSize = tx.size;
|
||||
blockFees = tx.fee;
|
||||
transactionIds = [tx.txid];
|
||||
transactions = [tx];
|
||||
}
|
||||
});
|
||||
if (transactions.length) {
|
||||
const feeStats = onlineStats ? feeStatsCalculator.getRawFeeStats() : undefined;
|
||||
mempoolBlocks.push(this.dataToMempoolBlocks(transactionIds, transactions, blockSize, blockWeight, blockFees, feeStats));
|
||||
}
|
||||
|
||||
return mempoolBlocks;
|
||||
}
|
||||
|
||||
private calculateMempoolDeltas(prevBlocks: MempoolBlockWithTransactions[], mempoolBlocks: MempoolBlockWithTransactions[]): MempoolBlockDelta[] {
|
||||
const mempoolBlockDeltas: MempoolBlockDelta[] = [];
|
||||
for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) {
|
||||
@@ -207,7 +82,7 @@ class MempoolBlocks {
|
||||
return mempoolBlockDeltas;
|
||||
}
|
||||
|
||||
public async $makeBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, saveResults: boolean = false, useAccelerations: boolean = false, accelerationPool?: number): Promise<MempoolBlockWithTransactions[]> {
|
||||
public async $makeBlockTemplates(transactions: string[], newMempool: { [txid: string]: MempoolTransactionExtended }, candidates: GbtCandidates | undefined, saveResults: boolean = false, useAccelerations: boolean = false, accelerationPool?: number): Promise<MempoolBlockWithTransactions[]> {
|
||||
const start = Date.now();
|
||||
|
||||
// reset mempool short ids
|
||||
@@ -215,7 +90,8 @@ class MempoolBlocks {
|
||||
this.resetUids();
|
||||
}
|
||||
// set missing short ids
|
||||
for (const tx of Object.values(newMempool)) {
|
||||
for (const txid of transactions) {
|
||||
const tx = newMempool[txid];
|
||||
this.setUid(tx, !saveResults);
|
||||
}
|
||||
|
||||
@@ -224,7 +100,8 @@ class MempoolBlocks {
|
||||
// prepare a stripped down version of the mempool with only the minimum necessary data
|
||||
// to reduce the overhead of passing this data to the worker thread
|
||||
const strippedMempool: Map<number, CompactThreadTransaction> = new Map();
|
||||
Object.values(newMempool).forEach(entry => {
|
||||
for (const txid of transactions) {
|
||||
const entry = newMempool[txid];
|
||||
if (entry.uid !== null && entry.uid !== undefined) {
|
||||
const stripped = {
|
||||
uid: entry.uid,
|
||||
@@ -237,7 +114,7 @@ class MempoolBlocks {
|
||||
};
|
||||
strippedMempool.set(entry.uid, stripped);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// (re)initialize tx selection worker thread
|
||||
if (!this.txSelectionWorker) {
|
||||
@@ -268,7 +145,7 @@ class MempoolBlocks {
|
||||
// clean up thread error listener
|
||||
this.txSelectionWorker?.removeListener('error', threadErrorListener);
|
||||
|
||||
const processed = this.processBlockTemplates(newMempool, blocks, null, Object.entries(rates), Object.values(clusters), accelerations, accelerationPool, saveResults);
|
||||
const processed = this.processBlockTemplates(newMempool, blocks, null, Object.entries(rates), Object.values(clusters), candidates, accelerations, accelerationPool, saveResults);
|
||||
|
||||
logger.debug(`makeBlockTemplates completed in ${(Date.now() - start)/1000} seconds`);
|
||||
|
||||
@@ -279,10 +156,10 @@ class MempoolBlocks {
|
||||
return this.mempoolBlocks;
|
||||
}
|
||||
|
||||
public async $updateBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, added: MempoolTransactionExtended[], removed: MempoolTransactionExtended[], accelerationDelta: string[] = [], saveResults: boolean = false, useAccelerations: boolean = false): Promise<void> {
|
||||
public async $updateBlockTemplates(transactions: string[], newMempool: { [txid: string]: MempoolTransactionExtended }, added: MempoolTransactionExtended[], removed: MempoolTransactionExtended[], candidates: GbtCandidates | undefined, accelerationDelta: string[] = [], saveResults: boolean = false, useAccelerations: boolean = false): Promise<void> {
|
||||
if (!this.txSelectionWorker) {
|
||||
// need to reset the worker
|
||||
await this.$makeBlockTemplates(newMempool, saveResults, useAccelerations);
|
||||
await this.$makeBlockTemplates(transactions, newMempool, candidates, saveResults, useAccelerations);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -292,9 +169,9 @@ class MempoolBlocks {
|
||||
const addedAndChanged: MempoolTransactionExtended[] = useAccelerations ? accelerationDelta.map(txid => newMempool[txid]).filter(tx => tx != null).concat(added) : added;
|
||||
|
||||
for (const tx of addedAndChanged) {
|
||||
this.setUid(tx, true);
|
||||
this.setUid(tx, false);
|
||||
}
|
||||
const removedUids = removed.map(tx => this.getUid(tx)).filter(uid => uid != null) as number[];
|
||||
const removedTxs = removed.filter(tx => tx.uid != null) as MempoolTransactionExtended[];
|
||||
|
||||
// prepare a stripped down version of the mempool with only the minimum necessary data
|
||||
// to reduce the overhead of passing this data to the worker thread
|
||||
@@ -320,15 +197,15 @@ class MempoolBlocks {
|
||||
});
|
||||
this.txSelectionWorker?.once('error', reject);
|
||||
});
|
||||
this.txSelectionWorker.postMessage({ type: 'update', added: addedStripped, removed: removedUids });
|
||||
this.txSelectionWorker.postMessage({ type: 'update', added: addedStripped, removed: removedTxs.map(tx => tx.uid) as number[] });
|
||||
const { blocks, rates, clusters } = this.convertResultTxids(await workerResultPromise);
|
||||
|
||||
this.removeUids(removedUids);
|
||||
this.removeUids(removedTxs);
|
||||
|
||||
// clean up thread error listener
|
||||
this.txSelectionWorker?.removeListener('error', threadErrorListener);
|
||||
|
||||
this.processBlockTemplates(newMempool, blocks, null, Object.entries(rates), Object.values(clusters), accelerations, null, saveResults);
|
||||
this.processBlockTemplates(newMempool, blocks, null, Object.entries(rates), Object.values(clusters), candidates, accelerations, null, saveResults);
|
||||
logger.debug(`updateBlockTemplates completed in ${(Date.now() - start) / 1000} seconds`);
|
||||
} catch (e) {
|
||||
logger.err('updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
|
||||
@@ -340,25 +217,28 @@ class MempoolBlocks {
|
||||
this.rustGbtGenerator = new GbtGenerator();
|
||||
}
|
||||
|
||||
public async $rustMakeBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, saveResults: boolean = false, useAccelerations: boolean = false, accelerationPool?: number): Promise<MempoolBlockWithTransactions[]> {
|
||||
public async $rustMakeBlockTemplates(txids: string[], newMempool: { [txid: string]: MempoolTransactionExtended }, candidates: GbtCandidates | undefined, saveResults: boolean = false, useAccelerations: boolean = false, accelerationPool?: number): Promise<MempoolBlockWithTransactions[]> {
|
||||
const start = Date.now();
|
||||
|
||||
// reset mempool short ids
|
||||
if (saveResults) {
|
||||
this.resetUids();
|
||||
}
|
||||
|
||||
const transactions = txids.map(txid => newMempool[txid]).filter(tx => tx != null);
|
||||
// set missing short ids
|
||||
for (const tx of Object.values(newMempool)) {
|
||||
for (const tx of transactions) {
|
||||
this.setUid(tx, !saveResults);
|
||||
}
|
||||
// set short ids for transaction inputs
|
||||
for (const tx of Object.values(newMempool)) {
|
||||
for (const tx of transactions) {
|
||||
tx.inputs = tx.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => (uid !== null && uid !== undefined)) as number[];
|
||||
}
|
||||
|
||||
const accelerations = useAccelerations ? mempool.getAccelerations() : {};
|
||||
const acceleratedList = accelerationPool ? Object.values(accelerations).filter(acc => newMempool[acc.txid] && acc.pools.includes(accelerationPool)) : Object.values(accelerations).filter(acc => newMempool[acc.txid]);
|
||||
const convertedAccelerations = acceleratedList.map(acc => {
|
||||
this.setUid(newMempool[acc.txid], true);
|
||||
return {
|
||||
uid: this.getUid(newMempool[acc.txid]),
|
||||
delta: acc.feeDelta,
|
||||
@@ -369,15 +249,15 @@ class MempoolBlocks {
|
||||
const rustGbt = saveResults ? this.rustGbtGenerator : new GbtGenerator();
|
||||
try {
|
||||
const { blocks, blockWeights, rates, clusters, overflow } = this.convertNapiResultTxids(
|
||||
await rustGbt.make(Object.values(newMempool) as RustThreadTransaction[], convertedAccelerations as RustThreadAcceleration[], this.nextUid),
|
||||
await rustGbt.make(transactions as RustThreadTransaction[], convertedAccelerations as RustThreadAcceleration[], this.nextUid),
|
||||
);
|
||||
if (saveResults) {
|
||||
this.rustInitialized = true;
|
||||
}
|
||||
const mempoolSize = Object.keys(newMempool).length;
|
||||
const expectedSize = transactions.length;
|
||||
const resultMempoolSize = blocks.reduce((total, block) => total + block.length, 0) + overflow.length;
|
||||
logger.debug(`RUST updateBlockTemplates returned ${resultMempoolSize} txs out of ${mempoolSize} in the mempool, ${overflow.length} were unmineable`);
|
||||
const processed = this.processBlockTemplates(newMempool, blocks, blockWeights, rates, clusters, accelerations, accelerationPool, saveResults);
|
||||
logger.debug(`RUST updateBlockTemplates returned ${resultMempoolSize} txs out of ${expectedSize} in the mempool, ${overflow.length} were unmineable`);
|
||||
const processed = this.processBlockTemplates(newMempool, blocks, blockWeights, rates, clusters, candidates, accelerations, accelerationPool, saveResults);
|
||||
logger.debug(`RUST makeBlockTemplates completed in ${(Date.now() - start)/1000} seconds`);
|
||||
return processed;
|
||||
} catch (e) {
|
||||
@@ -389,36 +269,37 @@ class MempoolBlocks {
|
||||
return this.mempoolBlocks;
|
||||
}
|
||||
|
||||
public async $oneOffRustBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, useAccelerations: boolean, accelerationPool?: number): Promise<MempoolBlockWithTransactions[]> {
|
||||
return this.$rustMakeBlockTemplates(newMempool, false, useAccelerations, accelerationPool);
|
||||
public async $oneOffRustBlockTemplates(transactions: string[], newMempool: { [txid: string]: MempoolTransactionExtended }, candidates: GbtCandidates | undefined, useAccelerations: boolean, accelerationPool?: number): Promise<MempoolBlockWithTransactions[]> {
|
||||
return this.$rustMakeBlockTemplates(transactions, newMempool, candidates, false, useAccelerations, accelerationPool);
|
||||
}
|
||||
|
||||
public async $rustUpdateBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number, added: MempoolTransactionExtended[], removed: MempoolTransactionExtended[], useAccelerations: boolean, accelerationPool?: number): Promise<MempoolBlockWithTransactions[]> {
|
||||
public async $rustUpdateBlockTemplates(transactions: string[], newMempool: { [txid: string]: MempoolTransactionExtended }, added: MempoolTransactionExtended[], removed: MempoolTransactionExtended[], candidates: GbtCandidates | undefined, useAccelerations: boolean, accelerationPool?: number): Promise<MempoolBlockWithTransactions[]> {
|
||||
// GBT optimization requires that uids never get too sparse
|
||||
// as a sanity check, we should also explicitly prevent uint32 uid overflow
|
||||
if (this.nextUid + added.length >= Math.min(Math.max(262144, 2 * mempoolSize), MAX_UINT32)) {
|
||||
if (this.nextUid + added.length >= Math.min(Math.max(262144, 2 * transactions.length), MAX_UINT32)) {
|
||||
this.resetRustGbt();
|
||||
}
|
||||
|
||||
if (!this.rustInitialized) {
|
||||
// need to reset the worker
|
||||
return this.$rustMakeBlockTemplates(newMempool, true, useAccelerations, accelerationPool);
|
||||
return this.$rustMakeBlockTemplates(transactions, newMempool, candidates, true, useAccelerations, accelerationPool);
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
// set missing short ids
|
||||
for (const tx of added) {
|
||||
this.setUid(tx, true);
|
||||
this.setUid(tx, false);
|
||||
}
|
||||
// set short ids for transaction inputs
|
||||
for (const tx of added) {
|
||||
tx.inputs = tx.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => (uid !== null && uid !== undefined)) as number[];
|
||||
}
|
||||
const removedUids = removed.map(tx => this.getUid(tx)).filter(uid => (uid !== null && uid !== undefined)) as number[];
|
||||
const removedTxs = removed.filter(tx => tx.uid != null) as MempoolTransactionExtended[];
|
||||
|
||||
const accelerations = useAccelerations ? mempool.getAccelerations() : {};
|
||||
const acceleratedList = accelerationPool ? Object.values(accelerations).filter(acc => newMempool[acc.txid] && acc.pools.includes(accelerationPool)) : Object.values(accelerations).filter(acc => newMempool[acc.txid]);
|
||||
const convertedAccelerations = acceleratedList.map(acc => {
|
||||
this.setUid(newMempool[acc.txid], true);
|
||||
return {
|
||||
uid: this.getUid(newMempool[acc.txid]),
|
||||
delta: acc.feeDelta,
|
||||
@@ -430,18 +311,18 @@ class MempoolBlocks {
|
||||
const { blocks, blockWeights, rates, clusters, overflow } = this.convertNapiResultTxids(
|
||||
await this.rustGbtGenerator.update(
|
||||
added as RustThreadTransaction[],
|
||||
removedUids,
|
||||
removedTxs.map(tx => tx.uid) as number[],
|
||||
convertedAccelerations as RustThreadAcceleration[],
|
||||
this.nextUid,
|
||||
),
|
||||
);
|
||||
const resultMempoolSize = blocks.reduce((total, block) => total + block.length, 0) + overflow.length;
|
||||
logger.debug(`RUST updateBlockTemplates returned ${resultMempoolSize} txs out of ${mempoolSize} in the mempool, ${overflow.length} were unmineable`);
|
||||
if (mempoolSize !== resultMempoolSize) {
|
||||
throw new Error('GBT returned wrong number of transactions , cache is probably out of sync');
|
||||
logger.debug(`RUST updateBlockTemplates returned ${resultMempoolSize} txs out of ${transactions.length} candidates, ${overflow.length} were unmineable`);
|
||||
if (transactions.length !== resultMempoolSize) {
|
||||
throw new Error(`GBT returned wrong number of transactions ${transactions.length} vs ${resultMempoolSize}, cache is probably out of sync`);
|
||||
} else {
|
||||
const processed = this.processBlockTemplates(newMempool, blocks, blockWeights, rates, clusters, accelerations, accelerationPool, true);
|
||||
this.removeUids(removedUids);
|
||||
const processed = this.processBlockTemplates(newMempool, blocks, blockWeights, rates, clusters, candidates, accelerations, accelerationPool, true);
|
||||
this.removeUids(removedTxs);
|
||||
logger.debug(`RUST updateBlockTemplates completed in ${(Date.now() - start)/1000} seconds`);
|
||||
return processed;
|
||||
}
|
||||
@@ -452,7 +333,12 @@ class MempoolBlocks {
|
||||
}
|
||||
}
|
||||
|
||||
private processBlockTemplates(mempool: { [txid: string]: MempoolTransactionExtended }, blocks: string[][], blockWeights: number[] | null, rates: [string, number][], clusters: string[][], accelerations, accelerationPool, saveResults): MempoolBlockWithTransactions[] {
|
||||
private processBlockTemplates(mempool: { [txid: string]: MempoolTransactionExtended }, blocks: string[][], blockWeights: number[] | null, rates: [string, number][], clusters: string[][], candidates: GbtCandidates | undefined, accelerations, accelerationPool, saveResults): MempoolBlockWithTransactions[] {
|
||||
for (const txid of Object.keys(candidates?.txs ?? mempool)) {
|
||||
if (txid in mempool) {
|
||||
mempool[txid].cpfpDirty = false;
|
||||
}
|
||||
}
|
||||
for (const [txid, rate] of rates) {
|
||||
if (txid in mempool) {
|
||||
mempool[txid].cpfpDirty = (rate !== mempool[txid].effectiveFeePerVsize);
|
||||
@@ -486,6 +372,9 @@ class MempoolBlocks {
|
||||
if (txid === memberTxid) {
|
||||
matched = true;
|
||||
} else {
|
||||
if (!mempool[txid]) {
|
||||
console.log('txid missing from mempool! ', txid, candidates?.txs[txid]);
|
||||
}
|
||||
const relative = {
|
||||
txid: txid,
|
||||
fee: mempool[txid].fee,
|
||||
@@ -518,6 +407,16 @@ class MempoolBlocks {
|
||||
let totalWeight = 0;
|
||||
let totalFees = 0;
|
||||
const transactions: MempoolTransactionExtended[] = [];
|
||||
|
||||
// backfill purged transactions
|
||||
if (candidates?.txs && blockIndex === blocks.length - 1) {
|
||||
for (const txid of Object.keys(mempool)) {
|
||||
if (!candidates.txs[txid]) {
|
||||
block.push(txid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const txid of block) {
|
||||
if (txid) {
|
||||
mempoolTx = mempool[txid];
|
||||
@@ -526,16 +425,6 @@ class MempoolBlocks {
|
||||
block: blockIndex,
|
||||
vsize: totalVsize + (mempoolTx.vsize / 2),
|
||||
};
|
||||
if (!mempoolTx.cpfpChecked) {
|
||||
if (mempoolTx.ancestors?.length) {
|
||||
mempoolTx.ancestors = [];
|
||||
}
|
||||
if (mempoolTx.descendants?.length) {
|
||||
mempoolTx.descendants = [];
|
||||
}
|
||||
mempoolTx.bestDescendant = null;
|
||||
mempoolTx.cpfpChecked = true;
|
||||
}
|
||||
|
||||
const acceleration = accelerations[txid];
|
||||
if (isAccelerated[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
|
||||
@@ -594,7 +483,7 @@ class MempoolBlocks {
|
||||
|
||||
private dataToMempoolBlocks(transactionIds: string[], transactions: MempoolTransactionExtended[], totalSize: number, totalWeight: number, totalFees: number, feeStats?: EffectiveFeeStats ): MempoolBlockWithTransactions {
|
||||
if (!feeStats) {
|
||||
feeStats = Common.calcEffectiveFeeStatistics(transactions.filter(tx => !tx.acceleration));
|
||||
feeStats = Common.calcEffectiveFeeStatistics(transactions);
|
||||
}
|
||||
return {
|
||||
blockSize: totalSize,
|
||||
@@ -610,30 +499,38 @@ class MempoolBlocks {
|
||||
|
||||
private resetUids(): void {
|
||||
this.uidMap.clear();
|
||||
this.txidMap.clear();
|
||||
this.nextUid = 1;
|
||||
}
|
||||
|
||||
private setUid(tx: MempoolTransactionExtended, skipSet = false): number {
|
||||
if (tx.uid === null || tx.uid === undefined || !skipSet) {
|
||||
if (!this.txidMap.has(tx.txid) || !skipSet) {
|
||||
const uid = this.nextUid;
|
||||
this.nextUid++;
|
||||
this.uidMap.set(uid, tx.txid);
|
||||
this.txidMap.set(tx.txid, uid);
|
||||
tx.uid = uid;
|
||||
return uid;
|
||||
} else {
|
||||
tx.uid = this.txidMap.get(tx.txid) as number;
|
||||
return tx.uid;
|
||||
}
|
||||
}
|
||||
|
||||
private getUid(tx: MempoolTransactionExtended): number | void {
|
||||
if (tx?.uid !== null && tx?.uid !== undefined && this.uidMap.has(tx.uid)) {
|
||||
return tx.uid;
|
||||
if (tx) {
|
||||
return this.txidMap.get(tx.txid);
|
||||
}
|
||||
}
|
||||
|
||||
private removeUids(uids: number[]): void {
|
||||
for (const uid of uids) {
|
||||
this.uidMap.delete(uid);
|
||||
private removeUids(txs: MempoolTransactionExtended[]): void {
|
||||
for (const tx of txs) {
|
||||
const uid = this.txidMap.get(tx.txid);
|
||||
if (uid != null) {
|
||||
this.uidMap.delete(uid);
|
||||
this.txidMap.delete(tx.txid);
|
||||
}
|
||||
tx.uid = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -701,7 +598,8 @@ class MempoolBlocks {
|
||||
tx.value,
|
||||
Math.round((tx.rate || (tx.fee / tx.vsize)) * 100) / 100,
|
||||
tx.flags,
|
||||
1
|
||||
tx.time || 0,
|
||||
1,
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
@@ -711,6 +609,7 @@ class MempoolBlocks {
|
||||
tx.value,
|
||||
Math.round((tx.rate || (tx.fee / tx.vsize)) * 100) / 100,
|
||||
tx.flags,
|
||||
tx.time || 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import config from '../config';
|
||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||
import { MempoolTransactionExtended, TransactionExtended, VbytesPerSecond } from '../mempool.interfaces';
|
||||
import { MempoolTransactionExtended, TransactionExtended, VbytesPerSecond, GbtCandidates } from '../mempool.interfaces';
|
||||
import logger from '../logger';
|
||||
import { Common } from './common';
|
||||
import transactionUtils from './transaction-utils';
|
||||
@@ -11,18 +11,20 @@ import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
||||
import rbfCache from './rbf-cache';
|
||||
import { Acceleration } from './services/acceleration';
|
||||
import redisCache from './redis-cache';
|
||||
import blocks from './blocks';
|
||||
|
||||
class Mempool {
|
||||
private inSync: boolean = false;
|
||||
private mempoolCacheDelta: number = -1;
|
||||
private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {};
|
||||
private mempoolCandidates: { [txid: string ]: boolean } = {};
|
||||
private spendMap = new Map<string, MempoolTransactionExtended>();
|
||||
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
|
||||
maxmempool: 300000000, mempoolminfee: Common.isLiquid() ? 0.00000100 : 0.00001000, minrelaytxfee: Common.isLiquid() ? 0.00000100 : 0.00001000 };
|
||||
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
|
||||
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void) | undefined;
|
||||
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[],
|
||||
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => Promise<void>) | undefined;
|
||||
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], candidates?: GbtCandidates) => Promise<void>) | undefined;
|
||||
|
||||
private accelerations: { [txId: string]: Acceleration } = {};
|
||||
|
||||
@@ -40,6 +42,8 @@ class Mempool {
|
||||
private missingTxCount = 0;
|
||||
private mainLoopTimeout: number = 120000;
|
||||
|
||||
public limitGBT = config.MEMPOOL.USE_SECOND_NODE_FOR_MINFEE && config.MEMPOOL.LIMIT_GBT;
|
||||
|
||||
constructor() {
|
||||
setInterval(this.updateTxPerSecond.bind(this), 1000);
|
||||
}
|
||||
@@ -74,7 +78,8 @@ class Mempool {
|
||||
}
|
||||
|
||||
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number,
|
||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => Promise<void>): void {
|
||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[],
|
||||
candidates?: GbtCandidates) => Promise<void>): void {
|
||||
this.$asyncMempoolChangedCallback = fn;
|
||||
}
|
||||
|
||||
@@ -86,6 +91,10 @@ class Mempool {
|
||||
return this.spendMap;
|
||||
}
|
||||
|
||||
public getFromSpendMap(txid, index): MempoolTransactionExtended | void {
|
||||
return this.spendMap.get(`${txid}:${index}`);
|
||||
}
|
||||
|
||||
public async $setMempool(mempoolData: { [txId: string]: MempoolTransactionExtended }) {
|
||||
this.mempoolCache = mempoolData;
|
||||
let count = 0;
|
||||
@@ -107,6 +116,10 @@ class Mempool {
|
||||
if (config.MEMPOOL.CACHE_ENABLED && config.REDIS.ENABLED) {
|
||||
await redisCache.$addTransaction(this.mempoolCache[txid]);
|
||||
}
|
||||
this.mempoolCache[txid].flags = Common.getTransactionFlags(this.mempoolCache[txid]);
|
||||
this.mempoolCache[txid].cpfpChecked = false;
|
||||
this.mempoolCache[txid].cpfpDirty = true;
|
||||
this.mempoolCache[txid].cpfpUpdated = undefined;
|
||||
}
|
||||
if (config.MEMPOOL.CACHE_ENABLED && config.REDIS.ENABLED) {
|
||||
await redisCache.$flushTransactions();
|
||||
@@ -116,7 +129,7 @@ class Mempool {
|
||||
this.mempoolChangedCallback(this.mempoolCache, [], [], []);
|
||||
}
|
||||
if (this.$asyncMempoolChangedCallback) {
|
||||
await this.$asyncMempoolChangedCallback(this.mempoolCache, count, [], [], []);
|
||||
await this.$asyncMempoolChangedCallback(this.mempoolCache, count, [], [], [], this.limitGBT ? { txs: {}, added: [], removed: [] } : undefined);
|
||||
}
|
||||
this.addToSpendMap(Object.values(this.mempoolCache));
|
||||
}
|
||||
@@ -159,6 +172,10 @@ class Mempool {
|
||||
return newTransactions;
|
||||
}
|
||||
|
||||
public getMempoolCandidates(): { [txid: string]: boolean } {
|
||||
return this.mempoolCandidates;
|
||||
}
|
||||
|
||||
public async $updateMemPoolInfo() {
|
||||
this.mempoolInfo = await this.$getMempoolInfo();
|
||||
}
|
||||
@@ -188,7 +205,7 @@ class Mempool {
|
||||
return txTimes;
|
||||
}
|
||||
|
||||
public async $updateMempool(transactions: string[], accelerations: Acceleration[] | null, pollRate: number): Promise<void> {
|
||||
public async $updateMempool(transactions: string[], accelerations: Acceleration[] | null, minFeeMempool: string[], minFeeTip: number, pollRate: number): Promise<void> {
|
||||
logger.debug(`Updating mempool...`);
|
||||
|
||||
// warn if this run stalls the main loop for more than 2 minutes
|
||||
@@ -329,6 +346,8 @@ class Mempool {
|
||||
}
|
||||
}
|
||||
|
||||
const candidates = await this.getNextCandidates(minFeeMempool, minFeeTip, deletedTransactions);
|
||||
|
||||
const newMempoolSize = currentMempoolSize + newTransactions.length - deletedTransactions.length;
|
||||
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
|
||||
this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6);
|
||||
@@ -340,12 +359,14 @@ class Mempool {
|
||||
|
||||
this.mempoolCacheDelta = Math.abs(transactions.length - newMempoolSize);
|
||||
|
||||
const candidatesChanged = candidates?.added?.length || candidates?.removed?.length;
|
||||
|
||||
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
||||
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta);
|
||||
}
|
||||
if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
||||
if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length || candidatesChanged)) {
|
||||
this.updateTimerProgress(timer, 'running async mempool callback');
|
||||
await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, deletedTransactions, accelerationDelta);
|
||||
await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, deletedTransactions, accelerationDelta, candidates);
|
||||
this.updateTimerProgress(timer, 'completed async mempool callback');
|
||||
}
|
||||
|
||||
@@ -431,6 +452,64 @@ class Mempool {
|
||||
}
|
||||
}
|
||||
|
||||
public async getNextCandidates(minFeeTransactions: string[], blockHeight: number, deletedTransactions: MempoolTransactionExtended[]): Promise<GbtCandidates | undefined> {
|
||||
if (this.limitGBT) {
|
||||
const deletedTxsMap = {};
|
||||
for (const tx of deletedTransactions) {
|
||||
deletedTxsMap[tx.txid] = tx;
|
||||
}
|
||||
const newCandidateTxMap = {};
|
||||
for (const txid of minFeeTransactions) {
|
||||
if (this.mempoolCache[txid]) {
|
||||
newCandidateTxMap[txid] = true;
|
||||
}
|
||||
}
|
||||
const accelerations = this.getAccelerations();
|
||||
for (const txid of Object.keys(accelerations)) {
|
||||
if (this.mempoolCache[txid]) {
|
||||
newCandidateTxMap[txid] = true;
|
||||
}
|
||||
}
|
||||
const removed: MempoolTransactionExtended[] = [];
|
||||
const added: MempoolTransactionExtended[] = [];
|
||||
// don't prematurely remove txs included in a new block
|
||||
if (blockHeight > blocks.getCurrentBlockHeight()) {
|
||||
for (const txid of Object.keys(this.mempoolCandidates)) {
|
||||
newCandidateTxMap[txid] = true;
|
||||
}
|
||||
} else {
|
||||
for (const txid of Object.keys(this.mempoolCandidates)) {
|
||||
if (!newCandidateTxMap[txid]) {
|
||||
if (this.mempoolCache[txid]) {
|
||||
removed.push(this.mempoolCache[txid]);
|
||||
this.mempoolCache[txid].effectiveFeePerVsize = this.mempoolCache[txid].adjustedFeePerVsize;
|
||||
this.mempoolCache[txid].ancestors = [];
|
||||
this.mempoolCache[txid].descendants = [];
|
||||
this.mempoolCache[txid].bestDescendant = null;
|
||||
this.mempoolCache[txid].cpfpChecked = false;
|
||||
this.mempoolCache[txid].cpfpUpdated = undefined;
|
||||
} else if (deletedTxsMap[txid]) {
|
||||
removed.push(deletedTxsMap[txid]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const txid of Object.keys(newCandidateTxMap)) {
|
||||
if (!this.mempoolCandidates[txid]) {
|
||||
added.push(this.mempoolCache[txid]);
|
||||
}
|
||||
}
|
||||
|
||||
this.mempoolCandidates = newCandidateTxMap;
|
||||
return {
|
||||
txs: this.mempoolCandidates,
|
||||
added,
|
||||
removed
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private startTimer() {
|
||||
const state: any = {
|
||||
start: Date.now(),
|
||||
|
||||
@@ -8,6 +8,7 @@ import HashratesRepository from '../../repositories/HashratesRepository';
|
||||
import bitcoinClient from '../bitcoin/bitcoin-client';
|
||||
import mining from "./mining";
|
||||
import PricesRepository from '../../repositories/PricesRepository';
|
||||
import AccelerationRepository from '../../repositories/AccelerationRepository';
|
||||
|
||||
class MiningRoutes {
|
||||
public initRoutes(app: Application) {
|
||||
@@ -34,6 +35,11 @@ class MiningRoutes {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/:hash', this.$getBlockAudit)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/timestamp/:timestamp', this.$getHeightFromTimestamp)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'historical-price', this.$getHistoricalPrice)
|
||||
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/pool/:slug', this.$getAccelerationsByPool)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/block/:height', this.$getAccelerationsByHeight)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/recent/:interval', this.$getRecentAccelerations)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/total', this.$getAccelerationTotals)
|
||||
;
|
||||
}
|
||||
|
||||
@@ -46,13 +52,20 @@ class MiningRoutes {
|
||||
res.status(400).send('Prices are not available on testnets.');
|
||||
return;
|
||||
}
|
||||
if (req.query.timestamp) {
|
||||
res.status(200).send(await PricesRepository.$getNearestHistoricalPrice(
|
||||
parseInt(<string>req.query.timestamp ?? 0, 10)
|
||||
));
|
||||
const timestamp = parseInt(req.query.timestamp as string, 10) || 0;
|
||||
const currency = req.query.currency as string;
|
||||
|
||||
let response;
|
||||
if (timestamp && currency) {
|
||||
response = await PricesRepository.$getNearestHistoricalPrice(timestamp, currency);
|
||||
} else if (timestamp) {
|
||||
response = await PricesRepository.$getNearestHistoricalPrice(timestamp);
|
||||
} else if (currency) {
|
||||
response = await PricesRepository.$getHistoricalPrices(currency);
|
||||
} else {
|
||||
res.status(200).send(await PricesRepository.$getHistoricalPrices());
|
||||
response = await PricesRepository.$getHistoricalPrices();
|
||||
}
|
||||
res.status(200).send(response);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
@@ -352,6 +365,67 @@ class MiningRoutes {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async $getAccelerationsByPool(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
||||
res.status(400).send('Acceleration data is not available.');
|
||||
return;
|
||||
}
|
||||
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug));
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async $getAccelerationsByHeight(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
|
||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
||||
res.status(400).send('Acceleration data is not available.');
|
||||
return;
|
||||
}
|
||||
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
|
||||
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height));
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async $getRecentAccelerations(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
||||
res.status(400).send('Acceleration data is not available.');
|
||||
return;
|
||||
}
|
||||
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval));
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async $getAccelerationTotals(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
||||
res.status(400).send('Acceleration data is not available.');
|
||||
return;
|
||||
}
|
||||
res.status(200).send(await AccelerationRepository.$getAccelerationTotals(<string>req.query.pool, <string>req.query.interval));
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new MiningRoutes();
|
||||
|
||||
@@ -7,7 +7,26 @@ export interface Acceleration {
|
||||
txid: string,
|
||||
feeDelta: number,
|
||||
pools: number[],
|
||||
}
|
||||
};
|
||||
|
||||
export interface AccelerationHistory {
|
||||
txid: string,
|
||||
status: string,
|
||||
feePaid: number,
|
||||
added: number,
|
||||
lastUpdated: number,
|
||||
baseFee: number,
|
||||
vsizeFee: number,
|
||||
effectiveFee: number,
|
||||
effectiveVsize: number,
|
||||
feeDelta: number,
|
||||
blockHash: string,
|
||||
blockHeight: number,
|
||||
pools: {
|
||||
pool_unique_id: number,
|
||||
username: string,
|
||||
}[],
|
||||
};
|
||||
|
||||
class AccelerationApi {
|
||||
public async $fetchAccelerations(): Promise<Acceleration[] | null> {
|
||||
@@ -24,6 +43,27 @@ class AccelerationApi {
|
||||
}
|
||||
}
|
||||
|
||||
public async $fetchAccelerationHistory(page?: number, status?: string): Promise<AccelerationHistory[] | null> {
|
||||
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
|
||||
try {
|
||||
const response = await axios.get(`${config.MEMPOOL_SERVICES.API}/accelerator/accelerations/history`, {
|
||||
responseType: 'json',
|
||||
timeout: 10000,
|
||||
params: {
|
||||
page,
|
||||
status,
|
||||
}
|
||||
});
|
||||
return response.data as AccelerationHistory[];
|
||||
} catch (e) {
|
||||
logger.warn('Failed to fetch acceleration history from the mempool services backend: ' + (e instanceof Error ? e.message : e));
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public isAcceleratedBlock(block: BlockExtended, accelerations: Acceleration[]): boolean {
|
||||
let anyAccelerated = false;
|
||||
for (let i = 0; i < accelerations.length && !anyAccelerated; i++) {
|
||||
|
||||
@@ -145,6 +145,10 @@ class TransactionUtils {
|
||||
}
|
||||
|
||||
public countScriptSigops(script: string, isRawScript: boolean = false, witness: boolean = false): number {
|
||||
if (!script?.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let sigops = 0;
|
||||
// count OP_CHECKSIG and OP_CHECKSIGVERIFY
|
||||
sigops += (script.match(/OP_CHECKSIG/g)?.length || 0);
|
||||
|
||||
@@ -2,7 +2,7 @@ import logger from '../logger';
|
||||
import * as WebSocket from 'ws';
|
||||
import {
|
||||
BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse,
|
||||
OptimizedStatistic, ILoadingIndicators
|
||||
OptimizedStatistic, ILoadingIndicators, GbtCandidates, TxTrackingInfo,
|
||||
} from '../mempool.interfaces';
|
||||
import blocks from './blocks';
|
||||
import memPool from './mempool';
|
||||
@@ -18,18 +18,21 @@ import feeApi from './fee-api';
|
||||
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
|
||||
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
|
||||
import Audit from './audit';
|
||||
import { deepClone } from '../utils/clone';
|
||||
import priceUpdater from '../tasks/price-updater';
|
||||
import { ApiPrice } from '../repositories/PricesRepository';
|
||||
import accelerationApi from './services/acceleration';
|
||||
import mempool from './mempool';
|
||||
import statistics from './statistics/statistics';
|
||||
import accelerationRepository from '../repositories/AccelerationRepository';
|
||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||
|
||||
interface AddressTransactions {
|
||||
mempool: MempoolTransactionExtended[],
|
||||
confirmed: MempoolTransactionExtended[],
|
||||
removed: MempoolTransactionExtended[],
|
||||
}
|
||||
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
||||
import { calculateCpfp } from './cpfp';
|
||||
|
||||
// valid 'want' subscriptions
|
||||
const wantable = [
|
||||
@@ -37,6 +40,7 @@ const wantable = [
|
||||
'mempool-blocks',
|
||||
'live-2h-chart',
|
||||
'stats',
|
||||
'tomahawk',
|
||||
];
|
||||
|
||||
class WebsocketHandler {
|
||||
@@ -49,7 +53,7 @@ class WebsocketHandler {
|
||||
|
||||
private socketData: { [key: string]: string } = {};
|
||||
private serializedInitData: string = '{}';
|
||||
private lastRbfSummary: ReplacementInfo | null = null;
|
||||
private lastRbfSummary: ReplacementInfo[] | null = null;
|
||||
|
||||
constructor() { }
|
||||
|
||||
@@ -79,6 +83,7 @@ class WebsocketHandler {
|
||||
const _blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT);
|
||||
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||
this.updateSocketDataFields({
|
||||
'backend': config.MEMPOOL.BACKEND,
|
||||
'mempoolInfo': memPool.getMempoolInfo(),
|
||||
'vBytesPerSecond': memPool.getVBytesPerSecond(),
|
||||
'blocks': _blocks,
|
||||
@@ -121,7 +126,7 @@ class WebsocketHandler {
|
||||
for (const sub of wantable) {
|
||||
const key = `want-${sub}`;
|
||||
const wants = parsedMessage.data.includes(sub);
|
||||
if (wants && client['wants'] && !client[key]) {
|
||||
if (wants && !client[key]) {
|
||||
wantNow[key] = true;
|
||||
}
|
||||
client[key] = wants;
|
||||
@@ -145,6 +150,10 @@ class WebsocketHandler {
|
||||
response['da'] = this.socketData['da'];
|
||||
}
|
||||
|
||||
if (wantNow['want-tomahawk']) {
|
||||
response['tomahawk'] = JSON.stringify(bitcoinApi.getHealthStatus());
|
||||
}
|
||||
|
||||
if (parsedMessage && parsedMessage['track-tx']) {
|
||||
if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-tx'])) {
|
||||
client['track-tx'] = parsedMessage['track-tx'];
|
||||
@@ -201,6 +210,52 @@ class WebsocketHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedMessage && parsedMessage['track-txs']) {
|
||||
const txids: string[] = [];
|
||||
if (Array.isArray(parsedMessage['track-txs'])) {
|
||||
for (const txid of parsedMessage['track-txs']) {
|
||||
if (/^[a-fA-F0-9]{64}$/.test(txid)) {
|
||||
txids.push(txid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const txs: { [txid: string]: TxTrackingInfo } = {};
|
||||
for (const txid of txids) {
|
||||
const txInfo: TxTrackingInfo = {
|
||||
confirmed: true,
|
||||
};
|
||||
const rbfCacheTxid = rbfCache.getReplacedBy(txid);
|
||||
if (rbfCacheTxid) {
|
||||
txInfo.replacedBy = rbfCacheTxid;
|
||||
txInfo.confirmed = false;
|
||||
}
|
||||
const tx = memPool.getMempool()[txid];
|
||||
if (tx && tx.position) {
|
||||
txInfo.position = {
|
||||
...tx.position
|
||||
};
|
||||
if (tx.acceleration) {
|
||||
txInfo.accelerated = tx.acceleration;
|
||||
}
|
||||
}
|
||||
if (tx) {
|
||||
txInfo.confirmed = false;
|
||||
}
|
||||
txs[txid] = txInfo;
|
||||
}
|
||||
|
||||
if (txids.length) {
|
||||
client['track-txs'] = txids;
|
||||
} else {
|
||||
client['track-txs'] = null;
|
||||
}
|
||||
|
||||
if (Object.keys(txs).length) {
|
||||
response['tracked-txs'] = JSON.stringify(txs);
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedMessage && parsedMessage['track-address']) {
|
||||
const validAddress = this.testAddress(parsedMessage['track-address']);
|
||||
if (validAddress) {
|
||||
@@ -305,14 +360,6 @@ class WebsocketHandler {
|
||||
client['track-donation'] = parsedMessage['track-donation'];
|
||||
}
|
||||
|
||||
if (parsedMessage['track-bisq-market']) {
|
||||
if (/^[a-z]{3}_[a-z]{3}$/.test(parsedMessage['track-bisq-market'])) {
|
||||
client['track-bisq-market'] = parsedMessage['track-bisq-market'];
|
||||
} else {
|
||||
client['track-bisq-market'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(response).length) {
|
||||
client.send(this.serializeResponse(response));
|
||||
}
|
||||
@@ -429,21 +476,26 @@ class WebsocketHandler {
|
||||
}
|
||||
|
||||
async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number,
|
||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]): Promise<void> {
|
||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[],
|
||||
candidates?: GbtCandidates): Promise<void> {
|
||||
if (!this.wss) {
|
||||
throw new Error('WebSocket.Server is not set');
|
||||
}
|
||||
|
||||
this.printLogs();
|
||||
|
||||
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
|
||||
if (config.MEMPOOL.RUST_GBT) {
|
||||
await mempoolBlocks.$rustUpdateBlockTemplates(newMempool, mempoolSize, newTransactions, deletedTransactions, config.MEMPOOL_SERVICES.ACCELERATIONS);
|
||||
} else {
|
||||
await mempoolBlocks.$updateBlockTemplates(newMempool, newTransactions, deletedTransactions, accelerationDelta, true, config.MEMPOOL_SERVICES.ACCELERATIONS);
|
||||
}
|
||||
const transactionIds = (memPool.limitGBT && candidates) ? Object.keys(candidates?.txs || {}) : Object.keys(newMempool);
|
||||
let added = newTransactions;
|
||||
let removed = deletedTransactions;
|
||||
if (memPool.limitGBT) {
|
||||
added = candidates?.added || [];
|
||||
removed = candidates?.removed || [];
|
||||
}
|
||||
|
||||
if (config.MEMPOOL.RUST_GBT) {
|
||||
await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, newMempool, added, removed, candidates, config.MEMPOOL_SERVICES.ACCELERATIONS);
|
||||
} else {
|
||||
mempoolBlocks.updateMempoolBlocks(newMempool, true);
|
||||
await mempoolBlocks.$updateBlockTemplates(transactionIds, newMempool, added, removed, candidates, accelerationDelta, true, config.MEMPOOL_SERVICES.ACCELERATIONS);
|
||||
}
|
||||
|
||||
const mBlocks = mempoolBlocks.getMempoolBlocks();
|
||||
@@ -457,10 +509,11 @@ class WebsocketHandler {
|
||||
let rbfReplacements;
|
||||
let fullRbfReplacements;
|
||||
let rbfSummary;
|
||||
if (Object.keys(rbfChanges.trees).length) {
|
||||
if (Object.keys(rbfChanges.trees).length || !this.lastRbfSummary) {
|
||||
rbfReplacements = rbfCache.getRbfTrees(false);
|
||||
fullRbfReplacements = rbfCache.getRbfTrees(true);
|
||||
rbfSummary = rbfCache.getLatestRbfSummary();
|
||||
rbfSummary = rbfCache.getLatestRbfSummary() || [];
|
||||
this.lastRbfSummary = rbfSummary;
|
||||
}
|
||||
|
||||
for (const deletedTx of deletedTransactions) {
|
||||
@@ -503,6 +556,11 @@ class WebsocketHandler {
|
||||
if (client['track-tx']) {
|
||||
trackedTxs.add(client['track-tx']);
|
||||
}
|
||||
if (client['track-txs']) {
|
||||
for (const txid of client['track-txs']) {
|
||||
trackedTxs.add(txid);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (trackedTxs.size > 0) {
|
||||
for (const tx of newTransactions) {
|
||||
@@ -544,6 +602,10 @@ class WebsocketHandler {
|
||||
response['mempool-blocks'] = getCachedResponse('mempool-blocks', mBlocks);
|
||||
}
|
||||
|
||||
if (client['want-tomahawk']) {
|
||||
response['tomahawk'] = getCachedResponse('tomahawk', bitcoinApi.getHealthStatus());
|
||||
}
|
||||
|
||||
if (client['track-mempool-tx']) {
|
||||
const tx = newTransactions.find((t) => t.txid === client['track-mempool-tx']);
|
||||
if (tx) {
|
||||
@@ -677,6 +739,9 @@ class WebsocketHandler {
|
||||
accelerated: mempoolTx.acceleration || undefined,
|
||||
}
|
||||
};
|
||||
if (!mempoolTx.cpfpChecked) {
|
||||
calculateCpfp(mempoolTx, newMempool);
|
||||
}
|
||||
if (mempoolTx.cpfpDirty) {
|
||||
positionData['cpfp'] = {
|
||||
ancestors: mempoolTx.ancestors,
|
||||
@@ -692,6 +757,46 @@ class WebsocketHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (client['track-txs']) {
|
||||
const txids = client['track-txs'];
|
||||
const txs: { [txid: string]: TxTrackingInfo } = {};
|
||||
for (const txid of txids) {
|
||||
const txInfo: TxTrackingInfo = {};
|
||||
const outspends = outspendCache[txid];
|
||||
if (outspends && Object.keys(outspends).length) {
|
||||
txInfo.utxoSpent = outspends;
|
||||
}
|
||||
const replacedBy = rbfChanges.map[txid] ? rbfCache.getReplacedBy(txid) : false;
|
||||
if (replacedBy) {
|
||||
txInfo.replacedBy = replacedBy;
|
||||
}
|
||||
const mempoolTx = newMempool[txid];
|
||||
if (mempoolTx && mempoolTx.position) {
|
||||
txInfo.position = {
|
||||
...mempoolTx.position,
|
||||
accelerated: mempoolTx.acceleration || undefined,
|
||||
};
|
||||
if (!mempoolTx.cpfpChecked) {
|
||||
calculateCpfp(mempoolTx, newMempool);
|
||||
}
|
||||
if (mempoolTx.cpfpDirty) {
|
||||
txInfo.cpfp = {
|
||||
ancestors: mempoolTx.ancestors,
|
||||
bestDescendant: mempoolTx.bestDescendant || null,
|
||||
descendants: mempoolTx.descendants || null,
|
||||
effectiveFeePerVsize: mempoolTx.effectiveFeePerVsize || null,
|
||||
sigops: mempoolTx.sigops,
|
||||
adjustedVsize: mempoolTx.adjustedVsize,
|
||||
};
|
||||
}
|
||||
}
|
||||
txs[txid] = txInfo;
|
||||
}
|
||||
if (Object.keys(txs).length) {
|
||||
response['tracked-txs'] = JSON.stringify(txs);
|
||||
}
|
||||
}
|
||||
|
||||
if (client['track-mempool-block'] >= 0 && memPool.isInSync()) {
|
||||
const index = client['track-mempool-block'];
|
||||
if (mBlockDeltas[index]) {
|
||||
@@ -727,6 +832,12 @@ class WebsocketHandler {
|
||||
await statistics.runStatistics();
|
||||
|
||||
const _memPool = memPool.getMempool();
|
||||
const candidateTxs = await memPool.getMempoolCandidates();
|
||||
let candidates: GbtCandidates | undefined = (memPool.limitGBT && candidateTxs) ? { txs: candidateTxs, added: [], removed: [] } : undefined;
|
||||
let transactionIds: string[] = (memPool.limitGBT) ? Object.keys(candidates?.txs || {}) : Object.keys(_memPool);
|
||||
|
||||
const accelerations = Object.values(mempool.getAccelerations());
|
||||
await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, transactions);
|
||||
|
||||
const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
|
||||
memPool.handleMinedRbfTransactions(rbfTransactions);
|
||||
@@ -734,36 +845,23 @@ class WebsocketHandler {
|
||||
|
||||
if (config.MEMPOOL.AUDIT && memPool.isInSync()) {
|
||||
let projectedBlocks;
|
||||
let auditMempool = _memPool;
|
||||
const auditMempool = _memPool;
|
||||
const isAccelerated = config.MEMPOOL_SERVICES.ACCELERATIONS && accelerationApi.isAcceleratedBlock(block, Object.values(mempool.getAccelerations()));
|
||||
// template calculation functions have mempool side effects, so calculate audits using
|
||||
// a cloned copy of the mempool if we're running a different algorithm for mempool updates
|
||||
const separateAudit = config.MEMPOOL.ADVANCED_GBT_AUDIT !== config.MEMPOOL.ADVANCED_GBT_MEMPOOL;
|
||||
if (separateAudit) {
|
||||
auditMempool = deepClone(_memPool);
|
||||
if (config.MEMPOOL.ADVANCED_GBT_AUDIT) {
|
||||
if (config.MEMPOOL.RUST_GBT) {
|
||||
projectedBlocks = await mempoolBlocks.$oneOffRustBlockTemplates(auditMempool, isAccelerated, block.extras.pool.id);
|
||||
} else {
|
||||
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false, isAccelerated, block.extras.pool.id);
|
||||
}
|
||||
|
||||
if ((config.MEMPOOL_SERVICES.ACCELERATIONS)) {
|
||||
if (config.MEMPOOL.RUST_GBT) {
|
||||
const added = memPool.limitGBT ? (candidates?.added || []) : [];
|
||||
const removed = memPool.limitGBT ? (candidates?.removed || []) : [];
|
||||
projectedBlocks = await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, auditMempool, added, removed, candidates, isAccelerated, block.extras.pool.id);
|
||||
} else {
|
||||
projectedBlocks = mempoolBlocks.updateMempoolBlocks(auditMempool, false);
|
||||
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(transactionIds, auditMempool, candidates, false, isAccelerated, block.extras.pool.id);
|
||||
}
|
||||
} else {
|
||||
if ((config.MEMPOOL_SERVICES.ACCELERATIONS)) {
|
||||
if (config.MEMPOOL.RUST_GBT) {
|
||||
projectedBlocks = await mempoolBlocks.$rustUpdateBlockTemplates(auditMempool, Object.keys(auditMempool).length, [], [], isAccelerated, block.extras.pool.id);
|
||||
} else {
|
||||
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false, isAccelerated, block.extras.pool.id);
|
||||
}
|
||||
} else {
|
||||
projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
|
||||
}
|
||||
projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
|
||||
}
|
||||
|
||||
if (Common.indexingEnabled()) {
|
||||
const { censored, added, fresh, sigop, fullrbf, accelerated, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
|
||||
const { censored, added, prioritized, fresh, sigop, fullrbf, accelerated, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
|
||||
const matchRate = Math.round(score * 100 * 100) / 100;
|
||||
|
||||
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : [];
|
||||
@@ -789,6 +887,7 @@ class WebsocketHandler {
|
||||
height: block.height,
|
||||
hash: block.id,
|
||||
addedTxs: added,
|
||||
prioritizedTxs: prioritized,
|
||||
missingTxs: censored,
|
||||
freshTxs: fresh,
|
||||
sigopTxs: sigop,
|
||||
@@ -822,14 +921,23 @@ class WebsocketHandler {
|
||||
confirmedTxids[txId] = true;
|
||||
}
|
||||
|
||||
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
|
||||
if (config.MEMPOOL.RUST_GBT) {
|
||||
await mempoolBlocks.$rustUpdateBlockTemplates(_memPool, Object.keys(_memPool).length, [], transactions, true);
|
||||
} else {
|
||||
await mempoolBlocks.$makeBlockTemplates(_memPool, true, config.MEMPOOL_SERVICES.ACCELERATIONS);
|
||||
}
|
||||
if (memPool.limitGBT) {
|
||||
const minFeeMempool = memPool.limitGBT ? await bitcoinSecondClient.getRawMemPool() : null;
|
||||
const minFeeTip = memPool.limitGBT ? await bitcoinSecondClient.getBlockCount() : -1;
|
||||
candidates = await memPool.getNextCandidates(minFeeMempool, minFeeTip, transactions);
|
||||
transactionIds = Object.keys(candidates?.txs || {});
|
||||
} else {
|
||||
mempoolBlocks.updateMempoolBlocks(_memPool, true);
|
||||
candidates = undefined;
|
||||
transactionIds = Object.keys(memPool.getMempool());
|
||||
}
|
||||
|
||||
|
||||
if (config.MEMPOOL.RUST_GBT) {
|
||||
const added = memPool.limitGBT ? (candidates?.added || []) : [];
|
||||
const removed = memPool.limitGBT ? (candidates?.removed || []) : transactions;
|
||||
await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, _memPool, added, removed, candidates, true);
|
||||
} else {
|
||||
await mempoolBlocks.$makeBlockTemplates(transactionIds, _memPool, candidates, true, config.MEMPOOL_SERVICES.ACCELERATIONS);
|
||||
}
|
||||
const mBlocks = mempoolBlocks.getMempoolBlocks();
|
||||
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
|
||||
@@ -886,6 +994,10 @@ class WebsocketHandler {
|
||||
response['mempool-blocks'] = getCachedResponse('mempool-blocks', mBlocks);
|
||||
}
|
||||
|
||||
if (client['want-tomahawk']) {
|
||||
response['tomahawk'] = getCachedResponse('tomahawk', bitcoinApi.getHealthStatus());
|
||||
}
|
||||
|
||||
if (client['track-tx']) {
|
||||
const trackTxid = client['track-tx'];
|
||||
if (trackTxid && confirmedTxids[trackTxid]) {
|
||||
@@ -904,6 +1016,28 @@ class WebsocketHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (client['track-txs']) {
|
||||
const txs: { [txid: string]: TxTrackingInfo } = {};
|
||||
for (const txid of client['track-txs']) {
|
||||
if (confirmedTxids[txid]) {
|
||||
txs[txid] = { confirmed: true };
|
||||
} else {
|
||||
const mempoolTx = _memPool[txid];
|
||||
if (mempoolTx && mempoolTx.position) {
|
||||
txs[txid] = {
|
||||
position: {
|
||||
...mempoolTx.position,
|
||||
},
|
||||
accelerated: mempoolTx.acceleration || undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Object.keys(txs).length) {
|
||||
response['tracked-txs'] = JSON.stringify(txs);
|
||||
}
|
||||
}
|
||||
|
||||
if (client['track-address']) {
|
||||
const foundTransactions: TransactionExtended[] = Array.from(addressCache[client['track-address']]?.values() || []);
|
||||
|
||||
@@ -1099,6 +1233,7 @@ class WebsocketHandler {
|
||||
private printLogs(): void {
|
||||
if (this.wss) {
|
||||
let numTxSubs = 0;
|
||||
let numTxsSubs = 0;
|
||||
let numProjectedSubs = 0;
|
||||
let numRbfSubs = 0;
|
||||
|
||||
@@ -1106,6 +1241,9 @@ class WebsocketHandler {
|
||||
if (client['track-tx']) {
|
||||
numTxSubs++;
|
||||
}
|
||||
if (client['track-txs']) {
|
||||
numTxsSubs++;
|
||||
}
|
||||
if (client['track-mempool-block'] != null && client['track-mempool-block'] >= 0) {
|
||||
numProjectedSubs++;
|
||||
}
|
||||
@@ -1118,7 +1256,7 @@ class WebsocketHandler {
|
||||
const diff = count - this.numClients;
|
||||
this.numClients = count;
|
||||
logger.debug(`${count} websocket clients | ${this.numConnected} connected | ${this.numDisconnected} disconnected | (${diff >= 0 ? '+' : ''}${diff})`);
|
||||
logger.debug(`websocket subscriptions: track-tx: ${numTxSubs}, track-mempool-block: ${numProjectedSubs} track-rbf: ${numRbfSubs}`);
|
||||
logger.debug(`websocket subscriptions: track-tx: ${numTxSubs}, track-txs: ${numTxsSubs}, track-mempool-block: ${numProjectedSubs} track-rbf: ${numRbfSubs}`);
|
||||
this.numConnected = 0;
|
||||
this.numDisconnected = 0;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ const configFromFile = require(
|
||||
interface IConfig {
|
||||
MEMPOOL: {
|
||||
ENABLED: boolean;
|
||||
OFFICIAL: boolean;
|
||||
NETWORK: 'mainnet' | 'testnet' | 'signet' | 'liquid' | 'liquidtestnet';
|
||||
BACKEND: 'esplora' | 'electrum' | 'none';
|
||||
HTTP_PORT: number;
|
||||
@@ -31,9 +32,8 @@ interface IConfig {
|
||||
POOLS_JSON_URL: string,
|
||||
POOLS_JSON_TREE_URL: string,
|
||||
AUDIT: boolean;
|
||||
ADVANCED_GBT_AUDIT: boolean;
|
||||
ADVANCED_GBT_MEMPOOL: boolean;
|
||||
RUST_GBT: boolean;
|
||||
LIMIT_GBT: boolean;
|
||||
CPFP_INDEXING: boolean;
|
||||
MAX_BLOCKS_BULK_QUERY: number;
|
||||
DISK_CACHE_BLOCK_INTERVAL: number;
|
||||
@@ -103,6 +103,7 @@ interface IConfig {
|
||||
PASSWORD: string;
|
||||
TIMEOUT: number;
|
||||
PID_DIR: string;
|
||||
POOL_SIZE: number;
|
||||
};
|
||||
SYSLOG: {
|
||||
ENABLED: boolean;
|
||||
@@ -115,10 +116,6 @@ interface IConfig {
|
||||
ENABLED: boolean;
|
||||
TX_PER_SECOND_SAMPLE_PERIOD: number;
|
||||
};
|
||||
BISQ: {
|
||||
ENABLED: boolean;
|
||||
DATA_PATH: string;
|
||||
};
|
||||
SOCKS5PROXY: {
|
||||
ENABLED: boolean;
|
||||
USE_ONION: boolean;
|
||||
@@ -132,8 +129,6 @@ interface IConfig {
|
||||
MEMPOOL_ONION: string;
|
||||
LIQUID_API: string;
|
||||
LIQUID_ONION: string;
|
||||
BISQ_URL: string;
|
||||
BISQ_ONION: string;
|
||||
};
|
||||
MAXMIND: {
|
||||
ENABLED: boolean;
|
||||
@@ -156,11 +151,16 @@ interface IConfig {
|
||||
UNIX_SOCKET_PATH: string;
|
||||
BATCH_QUERY_BASE_SIZE: number;
|
||||
},
|
||||
FIAT_PRICE: {
|
||||
ENABLED: boolean;
|
||||
API_KEY: string;
|
||||
},
|
||||
}
|
||||
|
||||
const defaults: IConfig = {
|
||||
'MEMPOOL': {
|
||||
'ENABLED': true,
|
||||
'OFFICIAL': false,
|
||||
'NETWORK': 'mainnet',
|
||||
'BACKEND': 'none',
|
||||
'HTTP_PORT': 8999,
|
||||
@@ -187,9 +187,8 @@ const defaults: IConfig = {
|
||||
'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json',
|
||||
'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
|
||||
'AUDIT': false,
|
||||
'ADVANCED_GBT_AUDIT': false,
|
||||
'ADVANCED_GBT_MEMPOOL': false,
|
||||
'RUST_GBT': false,
|
||||
'LIMIT_GBT': false,
|
||||
'CPFP_INDEXING': false,
|
||||
'MAX_BLOCKS_BULK_QUERY': 0,
|
||||
'DISK_CACHE_BLOCK_INTERVAL': 6,
|
||||
@@ -240,6 +239,7 @@ const defaults: IConfig = {
|
||||
'PASSWORD': 'mempool',
|
||||
'TIMEOUT': 180000,
|
||||
'PID_DIR': '',
|
||||
'POOL_SIZE': 100,
|
||||
},
|
||||
'SYSLOG': {
|
||||
'ENABLED': true,
|
||||
@@ -252,10 +252,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',
|
||||
@@ -287,9 +283,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,
|
||||
@@ -312,6 +306,10 @@ const defaults: IConfig = {
|
||||
'UNIX_SOCKET_PATH': '',
|
||||
'BATCH_QUERY_BASE_SIZE': 5000,
|
||||
},
|
||||
'FIAT_PRICE': {
|
||||
'ENABLED': true,
|
||||
'API_KEY': '',
|
||||
},
|
||||
};
|
||||
|
||||
class Config implements IConfig {
|
||||
@@ -323,7 +321,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'];
|
||||
@@ -333,6 +330,7 @@ class Config implements IConfig {
|
||||
REPLICATION: IConfig['REPLICATION'];
|
||||
MEMPOOL_SERVICES: IConfig['MEMPOOL_SERVICES'];
|
||||
REDIS: IConfig['REDIS'];
|
||||
FIAT_PRICE: IConfig['FIAT_PRICE'];
|
||||
|
||||
constructor() {
|
||||
const configs = this.merge(configFromFile, defaults);
|
||||
@@ -344,7 +342,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;
|
||||
@@ -354,6 +351,7 @@ class Config implements IConfig {
|
||||
this.REPLICATION = configs.REPLICATION;
|
||||
this.MEMPOOL_SERVICES = configs.MEMPOOL_SERVICES;
|
||||
this.REDIS = configs.REDIS;
|
||||
this.FIAT_PRICE = configs.FIAT_PRICE;
|
||||
}
|
||||
|
||||
merge = (...objects: object[]): IConfig => {
|
||||
|
||||
@@ -21,7 +21,7 @@ import { execSync } from 'child_process';
|
||||
database: config.DATABASE.DATABASE,
|
||||
user: config.DATABASE.USERNAME,
|
||||
password: config.DATABASE.PASSWORD,
|
||||
connectionLimit: 10,
|
||||
connectionLimit: config.DATABASE.POOL_SIZE,
|
||||
supportBigNumbers: true,
|
||||
timezone: '+00:00',
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
@@ -45,6 +42,9 @@ import { formatBytes, getBytesUnit } from './utils/format';
|
||||
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;
|
||||
@@ -131,7 +131,7 @@ class Server {
|
||||
.use(express.text({ type: ['text/plain', 'application/base64'] }))
|
||||
;
|
||||
|
||||
if (config.DATABASE.ENABLED) {
|
||||
if (config.DATABASE.ENABLED && config.FIAT_PRICE.ENABLED) {
|
||||
await priceUpdater.$initializeLatestPriceWithDb();
|
||||
}
|
||||
|
||||
@@ -155,14 +155,22 @@ class Server {
|
||||
}
|
||||
|
||||
if (Common.isLiquid()) {
|
||||
try {
|
||||
icons.loadIcons();
|
||||
} catch (e) {
|
||||
logger.err('Cannot load liquid icons. Ignoring. Reason: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
const refreshIcons = () => {
|
||||
try {
|
||||
icons.loadIcons();
|
||||
} catch (e) {
|
||||
logger.err('Cannot load liquid icons. Ignoring. Reason: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
};
|
||||
// Run once on startup.
|
||||
refreshIcons();
|
||||
// Matches crontab refresh interval for asset db.
|
||||
setInterval(refreshIcons, 3600_000);
|
||||
}
|
||||
|
||||
priceUpdater.$run();
|
||||
if (config.FIAT_PRICE.ENABLED) {
|
||||
priceUpdater.$run();
|
||||
}
|
||||
await chainTips.updateOrphanedBlocks();
|
||||
|
||||
this.setUpHttpApiRoutes();
|
||||
@@ -173,13 +181,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();
|
||||
}
|
||||
@@ -207,14 +208,18 @@ class Server {
|
||||
}
|
||||
}
|
||||
const newMempool = await bitcoinApi.$getRawMempool();
|
||||
const minFeeMempool = memPool.limitGBT ? await bitcoinSecondClient.getRawMemPool() : null;
|
||||
const minFeeTip = memPool.limitGBT ? await bitcoinSecondClient.getBlockCount() : -1;
|
||||
const newAccelerations = await accelerationApi.$fetchAccelerations();
|
||||
const numHandledBlocks = await blocks.$updateBlocks();
|
||||
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1);
|
||||
if (numHandledBlocks === 0) {
|
||||
await memPool.$updateMempool(newMempool, newAccelerations, pollRate);
|
||||
await memPool.$updateMempool(newMempool, newAccelerations, minFeeMempool, minFeeTip, pollRate);
|
||||
}
|
||||
indexer.$run();
|
||||
priceUpdater.$run();
|
||||
if (config.FIAT_PRICE.ENABLED) {
|
||||
priceUpdater.$run();
|
||||
}
|
||||
|
||||
// rerun immediately if we skipped the mempool update, otherwise wait POLL_RATE_MS
|
||||
const elapsed = Date.now() - start;
|
||||
@@ -278,7 +283,9 @@ class Server {
|
||||
memPool.setAsyncMempoolChangedCallback(websocketHandler.$handleMempoolChange.bind(websocketHandler));
|
||||
blocks.setNewAsyncBlockCallback(websocketHandler.handleNewBlock.bind(websocketHandler));
|
||||
}
|
||||
priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
|
||||
if (config.FIAT_PRICE.ENABLED) {
|
||||
priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
|
||||
}
|
||||
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
|
||||
}
|
||||
|
||||
@@ -292,9 +299,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);
|
||||
}
|
||||
@@ -303,6 +307,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 {
|
||||
|
||||
@@ -8,6 +8,7 @@ import priceUpdater from './tasks/price-updater';
|
||||
import PricesRepository from './repositories/PricesRepository';
|
||||
import config from './config';
|
||||
import auditReplicator from './replication/AuditReplication';
|
||||
import AccelerationRepository from './repositories/AccelerationRepository';
|
||||
|
||||
export interface CoreIndex {
|
||||
name: string;
|
||||
@@ -116,7 +117,7 @@ class Indexer {
|
||||
|
||||
switch (task) {
|
||||
case 'blocksPrices': {
|
||||
if (!['testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
|
||||
if (!['testnet', 'signet'].includes(config.MEMPOOL.NETWORK) && config.FIAT_PRICE.ENABLED) {
|
||||
let lastestPriceId;
|
||||
try {
|
||||
lastestPriceId = await PricesRepository.$getLatestPriceId();
|
||||
@@ -148,10 +149,12 @@ class Indexer {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await priceUpdater.$run();
|
||||
} catch (e) {
|
||||
logger.err(`Running priceUpdater failed. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
if (config.FIAT_PRICE.ENABLED) {
|
||||
try {
|
||||
await priceUpdater.$run();
|
||||
} catch (e) {
|
||||
logger.err(`Running priceUpdater failed. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
// Do not attempt to index anything unless Bitcoin Core is fully synced
|
||||
@@ -185,6 +188,7 @@ class Indexer {
|
||||
await blocks.$generateCPFPDatabase();
|
||||
await blocks.$generateAuditStats();
|
||||
await auditReplicator.$sync();
|
||||
await AccelerationRepository.$indexPastAccelerations();
|
||||
// do not wait for classify blocks to finish
|
||||
blocks.$classifyBlocks();
|
||||
} catch (e) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ export interface BlockAudit {
|
||||
sigopTxs: string[],
|
||||
fullrbfTxs: string[],
|
||||
addedTxs: string[],
|
||||
prioritizedTxs: string[],
|
||||
acceleratedTxs: string[],
|
||||
matchRate: number,
|
||||
expectedFees?: number,
|
||||
@@ -107,6 +108,7 @@ export interface MempoolTransactionExtended extends TransactionExtended {
|
||||
inputs?: number[];
|
||||
lastBoosted?: number;
|
||||
cpfpDirty?: boolean;
|
||||
cpfpUpdated?: number;
|
||||
}
|
||||
|
||||
export interface AuditTransaction {
|
||||
@@ -143,6 +145,12 @@ export interface CompactThreadTransaction {
|
||||
dirty?: boolean;
|
||||
}
|
||||
|
||||
export interface GbtCandidates {
|
||||
txs: { [txid: string ]: boolean },
|
||||
added: MempoolTransactionExtended[];
|
||||
removed: MempoolTransactionExtended[];
|
||||
}
|
||||
|
||||
export interface ThreadTransaction {
|
||||
txid: string;
|
||||
fee: number;
|
||||
@@ -181,6 +189,9 @@ export interface CpfpInfo {
|
||||
bestDescendant?: BestDescendant | null;
|
||||
descendants?: Ancestor[];
|
||||
effectiveFeePerVsize?: number;
|
||||
sigops?: number;
|
||||
adjustedVsize?: number,
|
||||
acceleration?: boolean,
|
||||
}
|
||||
|
||||
export interface TransactionStripped {
|
||||
@@ -190,6 +201,7 @@ export interface TransactionStripped {
|
||||
value: number;
|
||||
acc?: boolean;
|
||||
rate?: number; // effective fee rate
|
||||
time?: number;
|
||||
}
|
||||
|
||||
export interface TransactionClassified extends TransactionStripped {
|
||||
@@ -197,7 +209,7 @@ export interface TransactionClassified extends TransactionStripped {
|
||||
}
|
||||
|
||||
// [txid, fee, vsize, value, rate, flags, acceleration?]
|
||||
export type TransactionCompressed = [string, number, number, number, number, number, 1?];
|
||||
export type TransactionCompressed = [string, number, number, number, number, number, number, 1?];
|
||||
// [txid, rate, flags, acceleration?]
|
||||
export type MempoolDeltaChange = [string, number, number, (1|0)];
|
||||
|
||||
@@ -209,6 +221,7 @@ export const TransactionFlags = {
|
||||
v1: 0b00000100n,
|
||||
v2: 0b00001000n,
|
||||
v3: 0b00010000n,
|
||||
nonstandard: 0b00100000n,
|
||||
// address types
|
||||
p2pk: 0b00000001_00000000n,
|
||||
p2ms: 0b00000010_00000000n,
|
||||
@@ -225,6 +238,7 @@ export const TransactionFlags = {
|
||||
op_return: 0b00000001_00000000_00000000_00000000n,
|
||||
fake_pubkey: 0b00000010_00000000_00000000_00000000n,
|
||||
inscription: 0b00000100_00000000_00000000_00000000n,
|
||||
fake_scripthash: 0b00001000_00000000_00000000_00000000n,
|
||||
// heuristics
|
||||
coinjoin: 0b00000001_00000000_00000000_00000000_00000000n,
|
||||
consolidation: 0b00000010_00000000_00000000_00000000_00000000n,
|
||||
@@ -399,13 +413,28 @@ export interface OptimizedStatistic {
|
||||
vsizes: number[];
|
||||
}
|
||||
|
||||
export interface TxTrackingInfo {
|
||||
replacedBy?: string,
|
||||
position?: { block: number, vsize: number, accelerated?: boolean },
|
||||
cpfp?: {
|
||||
ancestors?: Ancestor[],
|
||||
bestDescendant?: Ancestor | null,
|
||||
descendants?: Ancestor[] | null,
|
||||
effectiveFeePerVsize?: number | null,
|
||||
sigops: number,
|
||||
adjustedVsize: number,
|
||||
},
|
||||
utxoSpent?: { [vout: number]: { vin: number, txid: string } },
|
||||
accelerated?: boolean,
|
||||
confirmed?: boolean
|
||||
}
|
||||
|
||||
export interface WebsocketResponse {
|
||||
action: string;
|
||||
data: string[];
|
||||
'track-tx': string;
|
||||
'track-address': string;
|
||||
'watch-mempool': boolean;
|
||||
'track-bisq-market': string;
|
||||
}
|
||||
|
||||
export interface VbytesPerSecond {
|
||||
@@ -427,6 +456,7 @@ export interface IBackendInfo {
|
||||
gitCommit: string;
|
||||
version: string;
|
||||
lightning: boolean;
|
||||
backend: 'esplora' | 'electrum' | 'none';
|
||||
}
|
||||
|
||||
export interface IDifficultyAdjustment {
|
||||
|
||||
@@ -114,6 +114,7 @@ class AuditReplication {
|
||||
time: auditSummary.timestamp || auditSummary.time,
|
||||
missingTxs: auditSummary.missingTxs || [],
|
||||
addedTxs: auditSummary.addedTxs || [],
|
||||
prioritizedTxs: auditSummary.prioritizedTxs || [],
|
||||
freshTxs: auditSummary.freshTxs || [],
|
||||
sigopTxs: auditSummary.sigopTxs || [],
|
||||
fullrbfTxs: auditSummary.fullrbfTxs || [],
|
||||
|
||||
322
backend/src/repositories/AccelerationRepository.ts
Normal file
322
backend/src/repositories/AccelerationRepository.ts
Normal file
@@ -0,0 +1,322 @@
|
||||
import { AccelerationInfo, makeBlockTemplate } from '../api/acceleration/acceleration';
|
||||
import { RowDataPacket } from 'mysql2';
|
||||
import DB from '../database';
|
||||
import logger from '../logger';
|
||||
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/acceleration';
|
||||
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
|
||||
import transactionUtils from '../api/transaction-utils';
|
||||
import { BlockExtended, MempoolTransactionExtended } from '../mempool.interfaces';
|
||||
|
||||
export interface PublicAcceleration {
|
||||
txid: string,
|
||||
height: number,
|
||||
pool: {
|
||||
id: number,
|
||||
slug: string,
|
||||
name: string,
|
||||
},
|
||||
effective_vsize: number,
|
||||
effective_fee: number,
|
||||
boost_rate: number,
|
||||
boost_cost: number,
|
||||
}
|
||||
|
||||
class AccelerationRepository {
|
||||
private bidBoostV2Activated = 831580;
|
||||
|
||||
public async $saveAcceleration(acceleration: AccelerationInfo, block: IEsploraApi.Block, pool_id: number): Promise<void> {
|
||||
try {
|
||||
await DB.query(`
|
||||
INSERT INTO accelerations(txid, added, height, pool, effective_vsize, effective_fee, boost_rate, boost_cost)
|
||||
VALUE (?, FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
height = ?
|
||||
`, [
|
||||
acceleration.txSummary.txid,
|
||||
block.timestamp,
|
||||
block.height,
|
||||
pool_id,
|
||||
acceleration.txSummary.effectiveVsize,
|
||||
acceleration.txSummary.effectiveFee,
|
||||
acceleration.targetFeeRate,
|
||||
acceleration.cost,
|
||||
block.height,
|
||||
]);
|
||||
} catch (e: any) {
|
||||
logger.err(`Cannot save acceleration (${acceleration.txSummary.txid}) into db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
// We don't throw, not a critical issue if we miss some accelerations
|
||||
}
|
||||
}
|
||||
|
||||
public async $getAccelerationInfo(poolSlug: string | null = null, height: number | null = null, interval: string | null = null): Promise<PublicAcceleration[]> {
|
||||
if (!interval || !['24h', '3d', '1w', '1m'].includes(interval)) {
|
||||
interval = '1m';
|
||||
}
|
||||
interval = Common.getSqlInterval(interval);
|
||||
|
||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || (interval == null && poolSlug == null && height == null)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let query = `
|
||||
SELECT * FROM accelerations
|
||||
JOIN pools on pools.unique_id = accelerations.pool
|
||||
`;
|
||||
let params: any[] = [];
|
||||
let hasFilter = false;
|
||||
|
||||
if (interval && height === null) {
|
||||
query += ` WHERE accelerations.added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW() `;
|
||||
hasFilter = true;
|
||||
}
|
||||
|
||||
if (height != null) {
|
||||
if (hasFilter) {
|
||||
query += ` AND accelerations.height = ? `;
|
||||
} else {
|
||||
query += ` WHERE accelerations.height = ? `;
|
||||
}
|
||||
params.push(height);
|
||||
} else if (poolSlug != null) {
|
||||
if (hasFilter) {
|
||||
query += ` AND pools.slug = ? `;
|
||||
} else {
|
||||
query += ` WHERE pools.slug = ? `;
|
||||
}
|
||||
params.push(poolSlug);
|
||||
}
|
||||
|
||||
query += ` ORDER BY accelerations.added DESC `;
|
||||
|
||||
try {
|
||||
const [rows] = await DB.query(query, params) as RowDataPacket[][];
|
||||
if (rows?.length) {
|
||||
return rows.map(row => ({
|
||||
txid: row.txid,
|
||||
height: row.height,
|
||||
pool: {
|
||||
id: row.id,
|
||||
slug: row.slug,
|
||||
name: row.name,
|
||||
},
|
||||
effective_vsize: row.effective_vsize,
|
||||
effective_fee: row.effective_fee,
|
||||
boost_rate: row.boost_rate,
|
||||
boost_cost: row.boost_cost,
|
||||
}));
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
} catch (e) {
|
||||
logger.err(`Cannot query acceleration info. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public async $getAccelerationTotals(poolSlug: string | null = null, interval: string | null = null): Promise<{ cost: number, count: number }> {
|
||||
interval = Common.getSqlInterval(interval);
|
||||
|
||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS) {
|
||||
return { cost: 0, count: 0 };
|
||||
}
|
||||
|
||||
let query = `
|
||||
SELECT SUM(boost_cost) as total_cost, COUNT(txid) as count FROM accelerations
|
||||
JOIN pools on pools.unique_id = accelerations.pool
|
||||
`;
|
||||
let params: any[] = [];
|
||||
let hasFilter = false;
|
||||
|
||||
if (interval) {
|
||||
query += ` WHERE accelerations.added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW() `;
|
||||
hasFilter = true;
|
||||
}
|
||||
if (poolSlug != null) {
|
||||
if (hasFilter) {
|
||||
query += ` AND pools.slug = ? `;
|
||||
} else {
|
||||
query += ` WHERE pools.slug = ? `;
|
||||
}
|
||||
params.push(poolSlug);
|
||||
}
|
||||
|
||||
try {
|
||||
const [rows] = await DB.query(query, params) as RowDataPacket[][];
|
||||
return {
|
||||
cost: rows[0]?.total_cost || 0,
|
||||
count: rows[0]?.count || 0,
|
||||
};
|
||||
} catch (e) {
|
||||
logger.err(`Cannot query acceleration totals. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public async $getLastSyncedHeight(): Promise<number> {
|
||||
try {
|
||||
const [rows] = await DB.query(`
|
||||
SELECT * FROM state
|
||||
WHERE name = 'last_acceleration_block'
|
||||
`);
|
||||
if (rows?.['length']) {
|
||||
return rows[0].number;
|
||||
}
|
||||
} catch (e: any) {
|
||||
logger.err(`Cannot find last acceleration sync height. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private async $setLastSyncedHeight(height: number): Promise<void> {
|
||||
try {
|
||||
await DB.query(`
|
||||
UPDATE state
|
||||
SET number = ?
|
||||
WHERE name = 'last_acceleration_block'
|
||||
`, [height]);
|
||||
} catch (e: any) {
|
||||
logger.err(`Cannot update last acceleration sync height. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
public async $indexAccelerationsForBlock(block: BlockExtended, accelerations: Acceleration[], transactions: MempoolTransactionExtended[]): Promise<void> {
|
||||
const blockTxs: { [txid: string]: MempoolTransactionExtended } = {};
|
||||
for (const tx of transactions) {
|
||||
blockTxs[tx.txid] = tx;
|
||||
}
|
||||
const successfulAccelerations = accelerations.filter(acc => acc.pools.includes(block.extras.pool.id));
|
||||
let boostRate: number | null = null;
|
||||
for (const acc of successfulAccelerations) {
|
||||
if (boostRate === null) {
|
||||
boostRate = accelerationCosts.calculateBoostRate(
|
||||
accelerations.map(acc => ({ txid: acc.txid, max_bid: acc.feeDelta })),
|
||||
transactions
|
||||
);
|
||||
}
|
||||
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));
|
||||
this.$saveAcceleration(accelerationInfo, block, block.extras.pool.id);
|
||||
}
|
||||
}
|
||||
const lastSyncedHeight = await this.$getLastSyncedHeight();
|
||||
// if we've missed any blocks, let the indexer catch up from the last synced height on the next run
|
||||
if (block.height === lastSyncedHeight + 1) {
|
||||
await this.$setLastSyncedHeight(block.height);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [INDEXING] Backfill missing acceleration data
|
||||
*/
|
||||
async $indexPastAccelerations(): Promise<void> {
|
||||
if (config.MEMPOOL.NETWORK !== 'mainnet' || !config.MEMPOOL_SERVICES.ACCELERATIONS) {
|
||||
// acceleration history disabled
|
||||
return;
|
||||
}
|
||||
const lastSyncedHeight = await this.$getLastSyncedHeight();
|
||||
const currentHeight = blocks.getCurrentBlockHeight();
|
||||
if (currentHeight <= lastSyncedHeight) {
|
||||
// already in sync
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(`Fetching accelerations between block ${lastSyncedHeight} and ${currentHeight}`);
|
||||
|
||||
// Fetch accelerations from mempool.space since the last synced block;
|
||||
const accelerationsByBlock = {};
|
||||
const blockHashes = {};
|
||||
let done = false;
|
||||
let page = 1;
|
||||
let count = 0;
|
||||
try {
|
||||
while (!done) {
|
||||
const accelerations = await accelerationApi.$fetchAccelerationHistory(page);
|
||||
page++;
|
||||
if (!accelerations?.length) {
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
for (const acc of accelerations) {
|
||||
if (acc.status !== 'completed_provisional' && acc.status !== 'completed') {
|
||||
continue;
|
||||
}
|
||||
if (!lastSyncedHeight || acc.blockHeight > lastSyncedHeight) {
|
||||
if (!accelerationsByBlock[acc.blockHeight]) {
|
||||
accelerationsByBlock[acc.blockHeight] = [];
|
||||
blockHashes[acc.blockHeight] = acc.blockHash;
|
||||
}
|
||||
accelerationsByBlock[acc.blockHeight].push(acc);
|
||||
count++;
|
||||
} else {
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.err(`Failed to fetch full acceleration history. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
|
||||
logger.debug(`Indexing ${count} accelerations between block ${lastSyncedHeight} and ${currentHeight}`);
|
||||
|
||||
// process accelerated blocks in order
|
||||
const heights = Object.keys(accelerationsByBlock).map(key => parseInt(key)).sort((a,b) => a - b);
|
||||
for (const height of heights) {
|
||||
const accelerations = accelerationsByBlock[height];
|
||||
try {
|
||||
const block = await blocks.$getBlock(blockHashes[height]) as BlockExtended;
|
||||
const transactions = (await bitcoinApi.$getTxsForBlock(blockHashes[height])).map(tx => transactionUtils.extendMempoolTransaction(tx));
|
||||
|
||||
const blockTxs = {};
|
||||
for (const tx of transactions) {
|
||||
blockTxs[tx.txid] = tx;
|
||||
}
|
||||
|
||||
let boostRate = 0;
|
||||
// use Bid Boost V2 if active
|
||||
if (height > this.bidBoostV2Activated) {
|
||||
boostRate = accelerationCosts.calculateBoostRate(
|
||||
accelerations.map(acc => ({ txid: acc.txid, max_bid: acc.feeDelta })),
|
||||
transactions
|
||||
);
|
||||
} else {
|
||||
// default to Bid Boost V1 (median block fee rate)
|
||||
const template = makeBlockTemplate(
|
||||
transactions,
|
||||
accelerations.map(acc => ({ txid: acc.txid, max_bid: acc.feeDelta })),
|
||||
1,
|
||||
Infinity,
|
||||
Infinity
|
||||
);
|
||||
const feeStats = Common.calcEffectiveFeeStatistics(template);
|
||||
boostRate = feeStats.medianFee;
|
||||
}
|
||||
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.$setLastSyncedHeight(height);
|
||||
} catch (e) {
|
||||
logger.err(`Failed to process accelerations for block ${height}. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
return;
|
||||
}
|
||||
logger.debug(`Indexed ${accelerations.length} accelerations in block ${height}`);
|
||||
}
|
||||
|
||||
await this.$setLastSyncedHeight(currentHeight);
|
||||
|
||||
logger.debug(`Indexing accelerations completed`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new AccelerationRepository();
|
||||
@@ -6,9 +6,9 @@ import { BlockAudit, AuditScore } from '../mempool.interfaces';
|
||||
class BlocksAuditRepositories {
|
||||
public async $saveAudit(audit: BlockAudit): Promise<void> {
|
||||
try {
|
||||
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, fresh_txs, sigop_txs, fullrbf_txs, accelerated_txs, match_rate, expected_fees, expected_weight)
|
||||
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
|
||||
JSON.stringify(audit.addedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.fullrbfTxs), JSON.stringify(audit.acceleratedTxs), audit.matchRate, audit.expectedFees, audit.expectedWeight]);
|
||||
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, prioritized_txs, fresh_txs, sigop_txs, fullrbf_txs, accelerated_txs, match_rate, expected_fees, expected_weight)
|
||||
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
|
||||
JSON.stringify(audit.addedTxs), JSON.stringify(audit.prioritizedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.fullrbfTxs), JSON.stringify(audit.acceleratedTxs), audit.matchRate, audit.expectedFees, audit.expectedWeight]);
|
||||
} catch (e: any) {
|
||||
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
|
||||
logger.debug(`Cannot save block audit for block ${audit.hash} because it has already been indexed, ignoring`);
|
||||
@@ -66,6 +66,7 @@ class BlocksAuditRepositories {
|
||||
template,
|
||||
missing_txs as missingTxs,
|
||||
added_txs as addedTxs,
|
||||
prioritized_txs as prioritizedTxs,
|
||||
fresh_txs as freshTxs,
|
||||
sigop_txs as sigopTxs,
|
||||
fullrbf_txs as fullrbfTxs,
|
||||
@@ -81,6 +82,7 @@ class BlocksAuditRepositories {
|
||||
if (rows.length) {
|
||||
rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
|
||||
rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
|
||||
rows[0].prioritizedTxs = JSON.parse(rows[0].prioritizedTxs);
|
||||
rows[0].freshTxs = JSON.parse(rows[0].freshTxs);
|
||||
rows[0].sigopTxs = JSON.parse(rows[0].sigopTxs);
|
||||
rows[0].fullrbfTxs = JSON.parse(rows[0].fullrbfTxs);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import DB from '../database';
|
||||
import logger from '../logger';
|
||||
import config from '../config';
|
||||
import priceUpdater from '../tasks/price-updater';
|
||||
|
||||
export interface ApiPrice {
|
||||
@@ -11,17 +12,81 @@ export interface ApiPrice {
|
||||
CHF: number,
|
||||
AUD: number,
|
||||
JPY: number,
|
||||
BGN: number,
|
||||
BRL: number,
|
||||
CNY: number,
|
||||
CZK: number,
|
||||
DKK: number,
|
||||
HKD: number,
|
||||
HRK: number,
|
||||
HUF: number,
|
||||
IDR: number,
|
||||
ILS: number,
|
||||
INR: number,
|
||||
ISK: number,
|
||||
KRW: number,
|
||||
MXN: number,
|
||||
MYR: number,
|
||||
NOK: number,
|
||||
NZD: number,
|
||||
PHP: number,
|
||||
PLN: number,
|
||||
RON: number,
|
||||
RUB: number,
|
||||
SEK: number,
|
||||
SGD: number,
|
||||
THB: number,
|
||||
TRY: number,
|
||||
ZAR: number,
|
||||
}
|
||||
const ApiPriceFields = `
|
||||
UNIX_TIMESTAMP(time) as time,
|
||||
USD,
|
||||
EUR,
|
||||
GBP,
|
||||
CAD,
|
||||
CHF,
|
||||
AUD,
|
||||
JPY
|
||||
`;
|
||||
|
||||
const ApiPriceFields = config.FIAT_PRICE.API_KEY ?
|
||||
`
|
||||
UNIX_TIMESTAMP(time) as time,
|
||||
USD,
|
||||
EUR,
|
||||
GBP,
|
||||
CAD,
|
||||
CHF,
|
||||
AUD,
|
||||
JPY,
|
||||
BGN,
|
||||
BRL,
|
||||
CNY,
|
||||
CZK,
|
||||
DKK,
|
||||
HKD,
|
||||
HRK,
|
||||
HUF,
|
||||
IDR,
|
||||
ILS,
|
||||
INR,
|
||||
ISK,
|
||||
KRW,
|
||||
MXN,
|
||||
MYR,
|
||||
NOK,
|
||||
NZD,
|
||||
PHP,
|
||||
PLN,
|
||||
RON,
|
||||
RUB,
|
||||
SEK,
|
||||
SGD,
|
||||
THB,
|
||||
TRY,
|
||||
ZAR
|
||||
`:
|
||||
`
|
||||
UNIX_TIMESTAMP(time) as time,
|
||||
USD,
|
||||
EUR,
|
||||
GBP,
|
||||
CAD,
|
||||
CHF,
|
||||
AUD,
|
||||
JPY
|
||||
`;
|
||||
|
||||
export interface ExchangeRates {
|
||||
USDEUR: number,
|
||||
@@ -30,6 +95,32 @@ export interface ExchangeRates {
|
||||
USDCHF: number,
|
||||
USDAUD: number,
|
||||
USDJPY: number,
|
||||
USDBGN?: number,
|
||||
USDBRL?: number,
|
||||
USDCNY?: number,
|
||||
USDCZK?: number,
|
||||
USDDKK?: number,
|
||||
USDHKD?: number,
|
||||
USDHRK?: number,
|
||||
USDHUF?: number,
|
||||
USDIDR?: number,
|
||||
USDILS?: number,
|
||||
USDINR?: number,
|
||||
USDISK?: number,
|
||||
USDKRW?: number,
|
||||
USDMXN?: number,
|
||||
USDMYR?: number,
|
||||
USDNOK?: number,
|
||||
USDNZD?: number,
|
||||
USDPHP?: number,
|
||||
USDPLN?: number,
|
||||
USDRON?: number,
|
||||
USDRUB?: number,
|
||||
USDSEK?: number,
|
||||
USDSGD?: number,
|
||||
USDTHB?: number,
|
||||
USDTRY?: number,
|
||||
USDZAR?: number,
|
||||
}
|
||||
|
||||
export interface Conversion {
|
||||
@@ -45,6 +136,32 @@ export const MAX_PRICES = {
|
||||
CHF: 100000000,
|
||||
AUD: 100000000,
|
||||
JPY: 10000000000,
|
||||
BGN: 1000000000,
|
||||
BRL: 1000000000,
|
||||
CNY: 1000000000,
|
||||
CZK: 10000000000,
|
||||
DKK: 1000000000,
|
||||
HKD: 1000000000,
|
||||
HRK: 1000000000,
|
||||
HUF: 10000000000,
|
||||
IDR: 100000000000,
|
||||
ILS: 1000000000,
|
||||
INR: 10000000000,
|
||||
ISK: 10000000000,
|
||||
KRW: 100000000000,
|
||||
MXN: 1000000000,
|
||||
MYR: 1000000000,
|
||||
NOK: 1000000000,
|
||||
NZD: 1000000000,
|
||||
PHP: 10000000000,
|
||||
PLN: 1000000000,
|
||||
RON: 1000000000,
|
||||
RUB: 10000000000,
|
||||
SEK: 1000000000,
|
||||
SGD: 100000000,
|
||||
THB: 10000000000,
|
||||
TRY: 10000000000,
|
||||
ZAR: 10000000000,
|
||||
};
|
||||
|
||||
class PricesRepository {
|
||||
@@ -64,17 +181,49 @@ class PricesRepository {
|
||||
}
|
||||
|
||||
try {
|
||||
await DB.query(`
|
||||
INSERT INTO prices(time, USD, EUR, GBP, CAD, CHF, AUD, JPY)
|
||||
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ? )`,
|
||||
[time, prices.USD, prices.EUR, prices.GBP, prices.CAD, prices.CHF, prices.AUD, prices.JPY]
|
||||
);
|
||||
if (!config.FIAT_PRICE.API_KEY) { // Store only the 7 main currencies
|
||||
await DB.query(`
|
||||
INSERT INTO prices(time, USD, EUR, GBP, CAD, CHF, AUD, JPY)
|
||||
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ? )`,
|
||||
[time, prices.USD, prices.EUR, prices.GBP, prices.CAD, prices.CHF, prices.AUD, prices.JPY]
|
||||
);
|
||||
} else { // Store all 7 main currencies + all the currencies obtained with the external API
|
||||
await DB.query(`
|
||||
INSERT INTO prices(time, USD, EUR, GBP, CAD, CHF, AUD, JPY, BGN, BRL, CNY, CZK, DKK, HKD, HRK, HUF, IDR, ILS, INR, ISK, KRW, MXN, MYR, NOK, NZD, PHP, PLN, RON, RUB, SEK, SGD, THB, TRY, ZAR)
|
||||
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ? , ?, ?, ?, ?, ?, ?, ?, ? , ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? , ? )`,
|
||||
[time, prices.USD, prices.EUR, prices.GBP, prices.CAD, prices.CHF, prices.AUD, prices.JPY, prices.BGN, prices.BRL, prices.CNY, prices.CZK, prices.DKK,
|
||||
prices.HKD, prices.HRK, prices.HUF, prices.IDR, prices.ILS, prices.INR, prices.ISK, prices.KRW, prices.MXN, prices.MYR, prices.NOK, prices.NZD,
|
||||
prices.PHP, prices.PLN, prices.RON, prices.RUB, prices.SEK, prices.SGD, prices.THB, prices.TRY, prices.ZAR]
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.err(`Cannot save exchange rate into db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public async $saveAdditionalCurrencyPrices(time: number, prices: ApiPrice, legacyCurrencies: string[]): Promise<void> {
|
||||
try {
|
||||
await DB.query(`
|
||||
UPDATE prices
|
||||
SET BGN = ?, BRL = ?, CNY = ?, CZK = ?, DKK = ?, HKD = ?, HRK = ?, HUF = ?, IDR = ?, ILS = ?, INR = ?, ISK = ?, KRW = ?, MXN = ?, MYR = ?, NOK = ?, NZD = ?, PHP = ?, PLN = ?, RON = ?, RUB = ?, SEK = ?, SGD = ?, THB = ?, TRY = ?, ZAR = ?
|
||||
WHERE UNIX_TIMESTAMP(time) = ?`,
|
||||
[prices.BGN, prices.BRL, prices.CNY, prices.CZK, prices.DKK, prices.HKD, prices.HRK, prices.HUF, prices.IDR, prices.ILS, prices.INR, prices.ISK, prices.KRW, prices.MXN, prices.MYR, prices.NOK, prices.NZD, prices.PHP, prices.PLN, prices.RON, prices.RUB, prices.SEK, prices.SGD, prices.THB, prices.TRY, prices.ZAR, time]
|
||||
);
|
||||
for (const currency of legacyCurrencies) {
|
||||
await DB.query(`
|
||||
UPDATE prices
|
||||
SET ${currency} = ?
|
||||
WHERE UNIX_TIMESTAMP(time) = ?`,
|
||||
[prices[currency], time]
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.err(`Cannot update exchange rate into db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public async $getOldestPriceTime(): Promise<number> {
|
||||
const [oldestRow] = await DB.query(`
|
||||
SELECT UNIX_TIMESTAMP(time) AS time
|
||||
@@ -118,6 +267,28 @@ class PricesRepository {
|
||||
return times.map(time => time.time);
|
||||
}
|
||||
|
||||
public async $getPricesTimesWithMissingFields(): Promise<{time: number, USD: number, eur_missing: boolean, gbp_missing: boolean, cad_missing: boolean, chf_missing: boolean, aud_missing: boolean, jpy_missing: boolean}[]> {
|
||||
const [times] = await DB.query(`
|
||||
SELECT UNIX_TIMESTAMP(time) AS time,
|
||||
USD,
|
||||
CASE WHEN EUR = -1 THEN TRUE ELSE FALSE END AS eur_missing,
|
||||
CASE WHEN GBP = -1 THEN TRUE ELSE FALSE END AS gbp_missing,
|
||||
CASE WHEN CAD = -1 THEN TRUE ELSE FALSE END AS cad_missing,
|
||||
CASE WHEN CHF = -1 THEN TRUE ELSE FALSE END AS chf_missing,
|
||||
CASE WHEN AUD = -1 THEN TRUE ELSE FALSE END AS aud_missing,
|
||||
CASE WHEN JPY = -1 THEN TRUE ELSE FALSE END AS jpy_missing
|
||||
FROM prices
|
||||
WHERE USD != -1
|
||||
AND -1 IN (EUR, GBP, CAD, CHF, AUD, JPY, BGN, BRL, CNY, CZK, DKK, HKD, HRK, HUF, IDR, ILS, INR, ISK, KRW,
|
||||
MXN, MYR, NOK, NZD, PHP, PLN, RON, RUB, SEK, SGD, THB, TRY, ZAR)
|
||||
ORDER BY time DESC
|
||||
`);
|
||||
if (!Array.isArray(times)) {
|
||||
return [];
|
||||
}
|
||||
return times as {time: number, USD: number, eur_missing: boolean, gbp_missing: boolean, cad_missing: boolean, chf_missing: boolean, aud_missing: boolean, jpy_missing: boolean}[];
|
||||
}
|
||||
|
||||
public async $getPricesTimesAndId(): Promise<{time: number, id: number, USD: number}[]> {
|
||||
const [times] = await DB.query(`
|
||||
SELECT
|
||||
@@ -144,7 +315,7 @@ class PricesRepository {
|
||||
return rates[0] as ApiPrice;
|
||||
}
|
||||
|
||||
public async $getNearestHistoricalPrice(timestamp: number | undefined): Promise<Conversion | null> {
|
||||
public async $getNearestHistoricalPrice(timestamp: number | undefined, currency?: string): Promise<Conversion | null> {
|
||||
try {
|
||||
const [rates] = await DB.query(`
|
||||
SELECT ${ApiPriceFields}
|
||||
@@ -158,24 +329,91 @@ class PricesRepository {
|
||||
throw Error(`Cannot get single historical price from the database`);
|
||||
}
|
||||
|
||||
const [latestPrices] = await DB.query(`
|
||||
SELECT ${ApiPriceFields}
|
||||
FROM prices
|
||||
ORDER BY time DESC
|
||||
LIMIT 1
|
||||
`);
|
||||
if (!Array.isArray(latestPrices)) {
|
||||
throw Error(`Cannot get single historical price from the database`);
|
||||
}
|
||||
|
||||
// Compute fiat exchange rates
|
||||
let latestPrice = rates[0] as ApiPrice;
|
||||
let latestPrice = latestPrices[0] as ApiPrice;
|
||||
if (!latestPrice || latestPrice.USD === -1) {
|
||||
latestPrice = priceUpdater.getEmptyPricesObj();
|
||||
}
|
||||
|
||||
const computeFx = (usd: number, other: number): number =>
|
||||
Math.round(Math.max(other, 0) / Math.max(usd, 1) * 100) / 100;
|
||||
const computeFx = (usd: number, other: number): number => usd <= 0.05 ? 0 : Math.round(Math.max(other, 0) / usd * 100) / 100;
|
||||
|
||||
const exchangeRates: ExchangeRates = {
|
||||
USDEUR: computeFx(latestPrice.USD, latestPrice.EUR),
|
||||
USDGBP: computeFx(latestPrice.USD, latestPrice.GBP),
|
||||
USDCAD: computeFx(latestPrice.USD, latestPrice.CAD),
|
||||
USDCHF: computeFx(latestPrice.USD, latestPrice.CHF),
|
||||
USDAUD: computeFx(latestPrice.USD, latestPrice.AUD),
|
||||
USDJPY: computeFx(latestPrice.USD, latestPrice.JPY),
|
||||
const exchangeRates: ExchangeRates = config.FIAT_PRICE.API_KEY ?
|
||||
{
|
||||
USDEUR: computeFx(latestPrice.USD, latestPrice.EUR),
|
||||
USDGBP: computeFx(latestPrice.USD, latestPrice.GBP),
|
||||
USDCAD: computeFx(latestPrice.USD, latestPrice.CAD),
|
||||
USDCHF: computeFx(latestPrice.USD, latestPrice.CHF),
|
||||
USDAUD: computeFx(latestPrice.USD, latestPrice.AUD),
|
||||
USDJPY: computeFx(latestPrice.USD, latestPrice.JPY),
|
||||
USDBGN: computeFx(latestPrice.USD, latestPrice.BGN),
|
||||
USDBRL: computeFx(latestPrice.USD, latestPrice.BRL),
|
||||
USDCNY: computeFx(latestPrice.USD, latestPrice.CNY),
|
||||
USDCZK: computeFx(latestPrice.USD, latestPrice.CZK),
|
||||
USDDKK: computeFx(latestPrice.USD, latestPrice.DKK),
|
||||
USDHKD: computeFx(latestPrice.USD, latestPrice.HKD),
|
||||
USDHRK: computeFx(latestPrice.USD, latestPrice.HRK),
|
||||
USDHUF: computeFx(latestPrice.USD, latestPrice.HUF),
|
||||
USDIDR: computeFx(latestPrice.USD, latestPrice.IDR),
|
||||
USDILS: computeFx(latestPrice.USD, latestPrice.ILS),
|
||||
USDINR: computeFx(latestPrice.USD, latestPrice.INR),
|
||||
USDISK: computeFx(latestPrice.USD, latestPrice.ISK),
|
||||
USDKRW: computeFx(latestPrice.USD, latestPrice.KRW),
|
||||
USDMXN: computeFx(latestPrice.USD, latestPrice.MXN),
|
||||
USDMYR: computeFx(latestPrice.USD, latestPrice.MYR),
|
||||
USDNOK: computeFx(latestPrice.USD, latestPrice.NOK),
|
||||
USDNZD: computeFx(latestPrice.USD, latestPrice.NZD),
|
||||
USDPHP: computeFx(latestPrice.USD, latestPrice.PHP),
|
||||
USDPLN: computeFx(latestPrice.USD, latestPrice.PLN),
|
||||
USDRON: computeFx(latestPrice.USD, latestPrice.RON),
|
||||
USDRUB: computeFx(latestPrice.USD, latestPrice.RUB),
|
||||
USDSEK: computeFx(latestPrice.USD, latestPrice.SEK),
|
||||
USDSGD: computeFx(latestPrice.USD, latestPrice.SGD),
|
||||
USDTHB: computeFx(latestPrice.USD, latestPrice.THB),
|
||||
USDTRY: computeFx(latestPrice.USD, latestPrice.TRY),
|
||||
USDZAR: computeFx(latestPrice.USD, latestPrice.ZAR),
|
||||
} : {
|
||||
USDEUR: computeFx(latestPrice.USD, latestPrice.EUR),
|
||||
USDGBP: computeFx(latestPrice.USD, latestPrice.GBP),
|
||||
USDCAD: computeFx(latestPrice.USD, latestPrice.CAD),
|
||||
USDCHF: computeFx(latestPrice.USD, latestPrice.CHF),
|
||||
USDAUD: computeFx(latestPrice.USD, latestPrice.AUD),
|
||||
USDJPY: computeFx(latestPrice.USD, latestPrice.JPY),
|
||||
};
|
||||
|
||||
if (currency) {
|
||||
if (!latestPrice[currency]) {
|
||||
return null;
|
||||
}
|
||||
const filteredRates = rates.map((rate: any) => {
|
||||
return {
|
||||
time: rate.time,
|
||||
[currency]: rate[currency],
|
||||
['USD']: rate['USD']
|
||||
};
|
||||
});
|
||||
if (filteredRates.length === 0) { // No price data before 2010-07-19: add a fake entry
|
||||
filteredRates.push({
|
||||
time: 1279497600,
|
||||
[currency]: 0,
|
||||
['USD']: 0
|
||||
});
|
||||
}
|
||||
return {
|
||||
prices: filteredRates as ApiPrice[],
|
||||
exchangeRates: exchangeRates
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
prices: rates as ApiPrice[],
|
||||
exchangeRates: exchangeRates
|
||||
@@ -186,7 +424,7 @@ class PricesRepository {
|
||||
}
|
||||
}
|
||||
|
||||
public async $getHistoricalPrices(): Promise<Conversion | null> {
|
||||
public async $getHistoricalPrices(currency?: string): Promise<Conversion | null> {
|
||||
try {
|
||||
const [rates] = await DB.query(`
|
||||
SELECT ${ApiPriceFields}
|
||||
@@ -203,18 +441,69 @@ class PricesRepository {
|
||||
latestPrice = priceUpdater.getEmptyPricesObj();
|
||||
}
|
||||
|
||||
const computeFx = (usd: number, other: number): number =>
|
||||
Math.round(Math.max(other, 0) / Math.max(usd, 1) * 100) / 100;
|
||||
const computeFx = (usd: number, other: number): number =>
|
||||
usd <= 0 ? 0 : Math.round(Math.max(other, 0) / usd * 100) / 100;
|
||||
|
||||
const exchangeRates: ExchangeRates = {
|
||||
USDEUR: computeFx(latestPrice.USD, latestPrice.EUR),
|
||||
USDGBP: computeFx(latestPrice.USD, latestPrice.GBP),
|
||||
USDCAD: computeFx(latestPrice.USD, latestPrice.CAD),
|
||||
USDCHF: computeFx(latestPrice.USD, latestPrice.CHF),
|
||||
USDAUD: computeFx(latestPrice.USD, latestPrice.AUD),
|
||||
USDJPY: computeFx(latestPrice.USD, latestPrice.JPY),
|
||||
const exchangeRates: ExchangeRates = config.FIAT_PRICE.API_KEY ?
|
||||
{
|
||||
USDEUR: computeFx(latestPrice.USD, latestPrice.EUR),
|
||||
USDGBP: computeFx(latestPrice.USD, latestPrice.GBP),
|
||||
USDCAD: computeFx(latestPrice.USD, latestPrice.CAD),
|
||||
USDCHF: computeFx(latestPrice.USD, latestPrice.CHF),
|
||||
USDAUD: computeFx(latestPrice.USD, latestPrice.AUD),
|
||||
USDJPY: computeFx(latestPrice.USD, latestPrice.JPY),
|
||||
USDBGN: computeFx(latestPrice.USD, latestPrice.BGN),
|
||||
USDBRL: computeFx(latestPrice.USD, latestPrice.BRL),
|
||||
USDCNY: computeFx(latestPrice.USD, latestPrice.CNY),
|
||||
USDCZK: computeFx(latestPrice.USD, latestPrice.CZK),
|
||||
USDDKK: computeFx(latestPrice.USD, latestPrice.DKK),
|
||||
USDHKD: computeFx(latestPrice.USD, latestPrice.HKD),
|
||||
USDHRK: computeFx(latestPrice.USD, latestPrice.HRK),
|
||||
USDHUF: computeFx(latestPrice.USD, latestPrice.HUF),
|
||||
USDIDR: computeFx(latestPrice.USD, latestPrice.IDR),
|
||||
USDILS: computeFx(latestPrice.USD, latestPrice.ILS),
|
||||
USDINR: computeFx(latestPrice.USD, latestPrice.INR),
|
||||
USDISK: computeFx(latestPrice.USD, latestPrice.ISK),
|
||||
USDKRW: computeFx(latestPrice.USD, latestPrice.KRW),
|
||||
USDMXN: computeFx(latestPrice.USD, latestPrice.MXN),
|
||||
USDMYR: computeFx(latestPrice.USD, latestPrice.MYR),
|
||||
USDNOK: computeFx(latestPrice.USD, latestPrice.NOK),
|
||||
USDNZD: computeFx(latestPrice.USD, latestPrice.NZD),
|
||||
USDPHP: computeFx(latestPrice.USD, latestPrice.PHP),
|
||||
USDPLN: computeFx(latestPrice.USD, latestPrice.PLN),
|
||||
USDRON: computeFx(latestPrice.USD, latestPrice.RON),
|
||||
USDRUB: computeFx(latestPrice.USD, latestPrice.RUB),
|
||||
USDSEK: computeFx(latestPrice.USD, latestPrice.SEK),
|
||||
USDSGD: computeFx(latestPrice.USD, latestPrice.SGD),
|
||||
USDTHB: computeFx(latestPrice.USD, latestPrice.THB),
|
||||
USDTRY: computeFx(latestPrice.USD, latestPrice.TRY),
|
||||
USDZAR: computeFx(latestPrice.USD, latestPrice.ZAR),
|
||||
} : {
|
||||
USDEUR: computeFx(latestPrice.USD, latestPrice.EUR),
|
||||
USDGBP: computeFx(latestPrice.USD, latestPrice.GBP),
|
||||
USDCAD: computeFx(latestPrice.USD, latestPrice.CAD),
|
||||
USDCHF: computeFx(latestPrice.USD, latestPrice.CHF),
|
||||
USDAUD: computeFx(latestPrice.USD, latestPrice.AUD),
|
||||
USDJPY: computeFx(latestPrice.USD, latestPrice.JPY),
|
||||
};
|
||||
|
||||
if (currency) {
|
||||
if (!latestPrice[currency]) {
|
||||
return null;
|
||||
}
|
||||
const filteredRates = rates.map((rate: any) => {
|
||||
return {
|
||||
time: rate.time,
|
||||
[currency]: rate[currency],
|
||||
['USD']: rate['USD']
|
||||
};
|
||||
});
|
||||
return {
|
||||
prices: filteredRates as ApiPrice[],
|
||||
exchangeRates: exchangeRates
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
prices: rates as ApiPrice[],
|
||||
exchangeRates: exchangeRates
|
||||
|
||||
@@ -11,6 +11,7 @@ module.exports = {
|
||||
encryptWallet: 'encryptwallet',
|
||||
estimateFee: 'estimatefee', // bitcoind v0.10.0x
|
||||
estimatePriority: 'estimatepriority', // bitcoind v0.10.0+
|
||||
estimateSmartFee: 'estimatesmartfee',
|
||||
generate: 'generate', // bitcoind v0.11.0+
|
||||
getAccount: 'getaccount',
|
||||
getAccountAddress: 'getaccountaddress',
|
||||
|
||||
73
backend/src/tasks/price-feeds/free-currency-api.ts
Normal file
73
backend/src/tasks/price-feeds/free-currency-api.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { query } from '../../utils/axios-query';
|
||||
import { ConversionFeed, ConversionRates } from '../price-updater';
|
||||
|
||||
const emptyRates = {
|
||||
AUD: -1,
|
||||
BGN: -1,
|
||||
BRL: -1,
|
||||
CAD: -1,
|
||||
CHF: -1,
|
||||
CNY: -1,
|
||||
CZK: -1,
|
||||
DKK: -1,
|
||||
EUR: -1,
|
||||
GBP: -1,
|
||||
HKD: -1,
|
||||
HRK: -1,
|
||||
HUF: -1,
|
||||
IDR: -1,
|
||||
ILS: -1,
|
||||
INR: -1,
|
||||
ISK: -1,
|
||||
JPY: -1,
|
||||
KRW: -1,
|
||||
MXN: -1,
|
||||
MYR: -1,
|
||||
NOK: -1,
|
||||
NZD: -1,
|
||||
PHP: -1,
|
||||
PLN: -1,
|
||||
RON: -1,
|
||||
RUB: -1,
|
||||
SEK: -1,
|
||||
SGD: -1,
|
||||
THB: -1,
|
||||
TRY: -1,
|
||||
USD: -1,
|
||||
ZAR: -1,
|
||||
};
|
||||
|
||||
class FreeCurrencyApi implements ConversionFeed {
|
||||
private API_KEY: string;
|
||||
|
||||
constructor(apiKey: string) {
|
||||
this.API_KEY = apiKey;
|
||||
}
|
||||
|
||||
public async $getQuota(): Promise<any> {
|
||||
const response = await query(`https://api.freecurrencyapi.com/v1/status?apikey=${this.API_KEY}`);
|
||||
if (response && response['quotas']) {
|
||||
return response['quotas'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async $fetchLatestConversionRates(): Promise<ConversionRates> {
|
||||
const response = await query(`https://api.freecurrencyapi.com/v1/latest?apikey=${this.API_KEY}`);
|
||||
if (response && 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]) {
|
||||
return response['data'][date];
|
||||
}
|
||||
return emptyRates;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default FreeCurrencyApi;
|
||||
@@ -8,6 +8,7 @@ import BitflyerApi from './price-feeds/bitflyer-api';
|
||||
import CoinbaseApi from './price-feeds/coinbase-api';
|
||||
import GeminiApi from './price-feeds/gemini-api';
|
||||
import KrakenApi from './price-feeds/kraken-api';
|
||||
import FreeCurrencyApi from './price-feeds/free-currency-api';
|
||||
|
||||
export interface PriceFeed {
|
||||
name: string;
|
||||
@@ -23,6 +24,16 @@ export interface PriceHistory {
|
||||
[timestamp: number]: ApiPrice;
|
||||
}
|
||||
|
||||
export interface ConversionFeed {
|
||||
$getQuota(): Promise<any>;
|
||||
$fetchLatestConversionRates(): Promise<ConversionRates>;
|
||||
$fetchConversionRates(date: string): Promise<ConversionRates>;
|
||||
}
|
||||
|
||||
export interface ConversionRates {
|
||||
[currency: string]: number
|
||||
}
|
||||
|
||||
function getMedian(arr: number[]): number {
|
||||
const sortedArr = arr.slice().sort((a, b) => a - b);
|
||||
const mid = Math.floor(sortedArr.length / 2);
|
||||
@@ -33,6 +44,9 @@ function getMedian(arr: number[]): number {
|
||||
|
||||
class PriceUpdater {
|
||||
public historyInserted = false;
|
||||
private additionalCurrenciesHistoryInserted = false;
|
||||
private additionalCurrenciesHistoryRunning = false;
|
||||
private lastFailedHistoricalRun = 0;
|
||||
private timeBetweenUpdatesMs = 360_0000 / config.MEMPOOL.PRICE_UPDATES_PER_HOUR;
|
||||
private cyclePosition = -1;
|
||||
private firstRun = true;
|
||||
@@ -42,6 +56,10 @@ class PriceUpdater {
|
||||
private feeds: PriceFeed[] = [];
|
||||
private currencies: string[] = ['USD', 'EUR', 'GBP', 'CAD', 'CHF', 'AUD', 'JPY'];
|
||||
private latestPrices: ApiPrice;
|
||||
private currencyConversionFeed: ConversionFeed | undefined;
|
||||
private newCurrencies: string[] = ['BGN', 'BRL', 'CNY', 'CZK', 'DKK', 'HKD', 'HRK', 'HUF', 'IDR', 'ILS', 'INR', 'ISK', 'KRW', 'MXN', 'MYR', 'NOK', 'NZD', 'PHP', 'PLN', 'RON', 'RUB', 'SEK', 'SGD', 'THB', 'TRY', 'ZAR'];
|
||||
private lastTimeConversionsRatesFetched: number = 0;
|
||||
private latestConversionsRatesFromFeed: ConversionRates = {};
|
||||
private ratesChangedCallback: ((rates: ApiPrice) => void) | undefined;
|
||||
|
||||
constructor() {
|
||||
@@ -53,6 +71,7 @@ class PriceUpdater {
|
||||
this.feeds.push(new BitfinexApi());
|
||||
this.feeds.push(new GeminiApi());
|
||||
|
||||
this.currencyConversionFeed = new FreeCurrencyApi(config.FIAT_PRICE.API_KEY);
|
||||
this.setCyclePosition();
|
||||
}
|
||||
|
||||
@@ -70,6 +89,32 @@ class PriceUpdater {
|
||||
CHF: -1,
|
||||
AUD: -1,
|
||||
JPY: -1,
|
||||
BGN: -1,
|
||||
BRL: -1,
|
||||
CNY: -1,
|
||||
CZK: -1,
|
||||
DKK: -1,
|
||||
HKD: -1,
|
||||
HRK: -1,
|
||||
HUF: -1,
|
||||
IDR: -1,
|
||||
ILS: -1,
|
||||
INR: -1,
|
||||
ISK: -1,
|
||||
KRW: -1,
|
||||
MXN: -1,
|
||||
MYR: -1,
|
||||
NOK: -1,
|
||||
NZD: -1,
|
||||
PHP: -1,
|
||||
PLN: -1,
|
||||
RON: -1,
|
||||
RUB: -1,
|
||||
SEK: -1,
|
||||
SGD: -1,
|
||||
THB: -1,
|
||||
TRY: -1,
|
||||
ZAR: -1,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -99,6 +144,23 @@ class PriceUpdater {
|
||||
if ((Math.round(new Date().getTime() / 1000) - this.lastHistoricalRun) > 3600 * 24) {
|
||||
// Once a day, look for missing prices (could happen due to network connectivity issues)
|
||||
this.historyInserted = false;
|
||||
this.additionalCurrenciesHistoryInserted = false;
|
||||
}
|
||||
|
||||
if (this.lastFailedHistoricalRun > 0 && (Math.round(new Date().getTime() / 1000) - this.lastFailedHistoricalRun) > 60) {
|
||||
// If the last attempt to insert missing prices failed, we try again after 60 seconds
|
||||
this.additionalCurrenciesHistoryInserted = false;
|
||||
}
|
||||
|
||||
if (config.FIAT_PRICE.API_KEY && this.currencyConversionFeed && (Math.round(new Date().getTime() / 1000) - this.lastTimeConversionsRatesFetched) > 3600 * 24) {
|
||||
// Once a day, fetch conversion rates from api: we don't need more granularity for fiat currencies and have a limited number of requests
|
||||
try {
|
||||
this.latestConversionsRatesFromFeed = await this.currencyConversionFeed.$fetchLatestConversionRates();
|
||||
this.lastTimeConversionsRatesFetched = Math.round(new Date().getTime() / 1000);
|
||||
logger.debug(`Fetched currencies conversion rates from external API: ${JSON.stringify(this.latestConversionsRatesFromFeed)}`);
|
||||
} catch (e) {
|
||||
logger.err(`Cannot fetch conversion rates from the API. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -106,6 +168,10 @@ class PriceUpdater {
|
||||
if (this.historyInserted === false && config.DATABASE.ENABLED === true) {
|
||||
await this.$insertHistoricalPrices();
|
||||
}
|
||||
if (this.additionalCurrenciesHistoryInserted === false && config.DATABASE.ENABLED === true && config.FIAT_PRICE.API_KEY && !this.additionalCurrenciesHistoryRunning) {
|
||||
await this.$insertMissingAdditionalPrices();
|
||||
}
|
||||
|
||||
} catch (e: any) {
|
||||
logger.err(`Cannot save BTC prices in db. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining);
|
||||
}
|
||||
@@ -185,6 +251,14 @@ class PriceUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
if (config.FIAT_PRICE.API_KEY && this.latestPrices.USD > 0 && Object.keys(this.latestConversionsRatesFromFeed).length > 0) {
|
||||
for (const conversionCurrency of this.newCurrencies) {
|
||||
if (this.latestConversionsRatesFromFeed[conversionCurrency] > 0 && this.latestPrices.USD * this.latestConversionsRatesFromFeed[conversionCurrency] < MAX_PRICES[conversionCurrency]) {
|
||||
this.latestPrices[conversionCurrency] = Math.round(this.latestPrices.USD * this.latestConversionsRatesFromFeed[conversionCurrency]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (config.DATABASE.ENABLED === true && this.cyclePosition === 0) {
|
||||
// Save everything in db
|
||||
try {
|
||||
@@ -253,7 +327,7 @@ class PriceUpdater {
|
||||
await this.$insertMissingRecentPrices('hour');
|
||||
|
||||
this.historyInserted = true;
|
||||
this.lastHistoricalRun = new Date().getTime();
|
||||
this.lastHistoricalRun = Math.round(new Date().getTime() / 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -320,6 +394,83 @@ class PriceUpdater {
|
||||
logger.debug(`Inserted ${totalInserted} ${type === 'day' ? 'dai' : 'hour'}ly historical prices into the db`, logger.tags.mining);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find missing prices for additional currencies and insert them in the database
|
||||
* We calculate the additional prices from the USD price and the conversion rates
|
||||
*/
|
||||
private async $insertMissingAdditionalPrices(): Promise<void> {
|
||||
this.lastFailedHistoricalRun = 0;
|
||||
const priceTimesToFill = await PricesRepository.$getPricesTimesWithMissingFields();
|
||||
if (priceTimesToFill.length === 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const remainingQuota = await this.currencyConversionFeed?.$getQuota();
|
||||
if (remainingQuota['month']['remaining'] < 500) { // We need some calls left for the daily updates
|
||||
logger.debug(`Not enough currency API credit to insert missing prices in ${priceTimesToFill.length} rows (${remainingQuota['month']['remaining']} calls left).`, logger.tags.mining);
|
||||
this.additionalCurrenciesHistoryInserted = true; // Do not try again until next day
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
logger.err(`Cannot fetch currency API credit, insertion of missing prices aborted. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.additionalCurrenciesHistoryRunning = true;
|
||||
logger.debug(`Fetching missing conversion rates from external API to fill ${priceTimesToFill.length} rows`, logger.tags.mining);
|
||||
|
||||
let conversionRates: { [timestamp: number]: ConversionRates } = {};
|
||||
let totalInserted = 0;
|
||||
|
||||
for (let i = 0; i < priceTimesToFill.length; i++) {
|
||||
const priceTime = priceTimesToFill[i];
|
||||
const missingLegacyCurrencies = this.getMissingLegacyCurrencies(priceTime); // In the case a legacy currency (EUR, GBP, CAD, CHF, AUD, JPY)
|
||||
const year = new Date(priceTime.time * 1000).getFullYear(); // is missing, we use the same process as for the new currencies
|
||||
const month = new Date(priceTime.time * 1000).getMonth();
|
||||
const yearMonthTimestamp = new Date(year, month, 1).getTime() / 1000;
|
||||
if (conversionRates[yearMonthTimestamp] === undefined) {
|
||||
conversionRates[yearMonthTimestamp] = await this.currencyConversionFeed?.$fetchConversionRates(`${year}-${month + 1 < 10 ? `0${month + 1}` : `${month + 1}`}-01`) || { USD: -1 };
|
||||
if (conversionRates[yearMonthTimestamp]['USD'] < 0) {
|
||||
logger.err(`Cannot fetch conversion rates from the API for ${year}-${month + 1 < 10 ? `0${month + 1}` : `${month + 1}`}-01. Aborting insertion of missing prices.`, logger.tags.mining);
|
||||
this.lastFailedHistoricalRun = Math.round(new Date().getTime() / 1000);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const prices: ApiPrice = this.getEmptyPricesObj();
|
||||
|
||||
let willInsert = false;
|
||||
for (const conversionCurrency of this.newCurrencies.concat(missingLegacyCurrencies)) {
|
||||
if (conversionRates[yearMonthTimestamp][conversionCurrency] > 0 && priceTime.USD * conversionRates[yearMonthTimestamp][conversionCurrency] < MAX_PRICES[conversionCurrency]) {
|
||||
prices[conversionCurrency] = year >= 2013 ? Math.round(priceTime.USD * conversionRates[yearMonthTimestamp][conversionCurrency]) : Math.round(priceTime.USD * conversionRates[yearMonthTimestamp][conversionCurrency] * 100) / 100;
|
||||
willInsert = true;
|
||||
} else {
|
||||
prices[conversionCurrency] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (willInsert) {
|
||||
await PricesRepository.$saveAdditionalCurrencyPrices(priceTime.time, prices, missingLegacyCurrencies);
|
||||
++totalInserted;
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(`Inserted ${totalInserted} missing additional currency prices into the db`, logger.tags.mining);
|
||||
this.additionalCurrenciesHistoryInserted = true;
|
||||
this.additionalCurrenciesHistoryRunning = false;
|
||||
}
|
||||
|
||||
// Helper function to get legacy missing currencies in a row (EUR, GBP, CAD, CHF, AUD, JPY)
|
||||
private getMissingLegacyCurrencies(priceTime: any): string[] {
|
||||
const missingCurrencies: string[] = [];
|
||||
['eur', 'gbp', 'cad', 'chf', 'aud', 'jpy'].forEach(currency => {
|
||||
if (priceTime[`${currency}_missing`]) {
|
||||
missingCurrencies.push(currency.toUpperCase());
|
||||
}
|
||||
});
|
||||
return missingCurrencies;
|
||||
}
|
||||
}
|
||||
|
||||
export default new PriceUpdater();
|
||||
|
||||
203
backend/src/utils/bitcoin-script.ts
Normal file
203
backend/src/utils/bitcoin-script.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
const opcodes = {
|
||||
OP_FALSE: 0,
|
||||
OP_0: 0,
|
||||
OP_PUSHDATA1: 76,
|
||||
OP_PUSHDATA2: 77,
|
||||
OP_PUSHDATA4: 78,
|
||||
OP_1NEGATE: 79,
|
||||
OP_PUSHNUM_NEG1: 79,
|
||||
OP_RESERVED: 80,
|
||||
OP_TRUE: 81,
|
||||
OP_1: 81,
|
||||
OP_2: 82,
|
||||
OP_3: 83,
|
||||
OP_4: 84,
|
||||
OP_5: 85,
|
||||
OP_6: 86,
|
||||
OP_7: 87,
|
||||
OP_8: 88,
|
||||
OP_9: 89,
|
||||
OP_10: 90,
|
||||
OP_11: 91,
|
||||
OP_12: 92,
|
||||
OP_13: 93,
|
||||
OP_14: 94,
|
||||
OP_15: 95,
|
||||
OP_16: 96,
|
||||
OP_PUSHNUM_1: 81,
|
||||
OP_PUSHNUM_2: 82,
|
||||
OP_PUSHNUM_3: 83,
|
||||
OP_PUSHNUM_4: 84,
|
||||
OP_PUSHNUM_5: 85,
|
||||
OP_PUSHNUM_6: 86,
|
||||
OP_PUSHNUM_7: 87,
|
||||
OP_PUSHNUM_8: 88,
|
||||
OP_PUSHNUM_9: 89,
|
||||
OP_PUSHNUM_10: 90,
|
||||
OP_PUSHNUM_11: 91,
|
||||
OP_PUSHNUM_12: 92,
|
||||
OP_PUSHNUM_13: 93,
|
||||
OP_PUSHNUM_14: 94,
|
||||
OP_PUSHNUM_15: 95,
|
||||
OP_PUSHNUM_16: 96,
|
||||
OP_NOP: 97,
|
||||
OP_VER: 98,
|
||||
OP_IF: 99,
|
||||
OP_NOTIF: 100,
|
||||
OP_VERIF: 101,
|
||||
OP_VERNOTIF: 102,
|
||||
OP_ELSE: 103,
|
||||
OP_ENDIF: 104,
|
||||
OP_VERIFY: 105,
|
||||
OP_RETURN: 106,
|
||||
OP_TOALTSTACK: 107,
|
||||
OP_FROMALTSTACK: 108,
|
||||
OP_2DROP: 109,
|
||||
OP_2DUP: 110,
|
||||
OP_3DUP: 111,
|
||||
OP_2OVER: 112,
|
||||
OP_2ROT: 113,
|
||||
OP_2SWAP: 114,
|
||||
OP_IFDUP: 115,
|
||||
OP_DEPTH: 116,
|
||||
OP_DROP: 117,
|
||||
OP_DUP: 118,
|
||||
OP_NIP: 119,
|
||||
OP_OVER: 120,
|
||||
OP_PICK: 121,
|
||||
OP_ROLL: 122,
|
||||
OP_ROT: 123,
|
||||
OP_SWAP: 124,
|
||||
OP_TUCK: 125,
|
||||
OP_CAT: 126,
|
||||
OP_SUBSTR: 127,
|
||||
OP_LEFT: 128,
|
||||
OP_RIGHT: 129,
|
||||
OP_SIZE: 130,
|
||||
OP_INVERT: 131,
|
||||
OP_AND: 132,
|
||||
OP_OR: 133,
|
||||
OP_XOR: 134,
|
||||
OP_EQUAL: 135,
|
||||
OP_EQUALVERIFY: 136,
|
||||
OP_RESERVED1: 137,
|
||||
OP_RESERVED2: 138,
|
||||
OP_1ADD: 139,
|
||||
OP_1SUB: 140,
|
||||
OP_2MUL: 141,
|
||||
OP_2DIV: 142,
|
||||
OP_NEGATE: 143,
|
||||
OP_ABS: 144,
|
||||
OP_NOT: 145,
|
||||
OP_0NOTEQUAL: 146,
|
||||
OP_ADD: 147,
|
||||
OP_SUB: 148,
|
||||
OP_MUL: 149,
|
||||
OP_DIV: 150,
|
||||
OP_MOD: 151,
|
||||
OP_LSHIFT: 152,
|
||||
OP_RSHIFT: 153,
|
||||
OP_BOOLAND: 154,
|
||||
OP_BOOLOR: 155,
|
||||
OP_NUMEQUAL: 156,
|
||||
OP_NUMEQUALVERIFY: 157,
|
||||
OP_NUMNOTEQUAL: 158,
|
||||
OP_LESSTHAN: 159,
|
||||
OP_GREATERTHAN: 160,
|
||||
OP_LESSTHANOREQUAL: 161,
|
||||
OP_GREATERTHANOREQUAL: 162,
|
||||
OP_MIN: 163,
|
||||
OP_MAX: 164,
|
||||
OP_WITHIN: 165,
|
||||
OP_RIPEMD160: 166,
|
||||
OP_SHA1: 167,
|
||||
OP_SHA256: 168,
|
||||
OP_HASH160: 169,
|
||||
OP_HASH256: 170,
|
||||
OP_CODESEPARATOR: 171,
|
||||
OP_CHECKSIG: 172,
|
||||
OP_CHECKSIGVERIFY: 173,
|
||||
OP_CHECKMULTISIG: 174,
|
||||
OP_CHECKMULTISIGVERIFY: 175,
|
||||
OP_NOP1: 176,
|
||||
OP_NOP2: 177,
|
||||
OP_CHECKLOCKTIMEVERIFY: 177,
|
||||
OP_CLTV: 177,
|
||||
OP_NOP3: 178,
|
||||
OP_CHECKSEQUENCEVERIFY: 178,
|
||||
OP_CSV: 178,
|
||||
OP_NOP4: 179,
|
||||
OP_NOP5: 180,
|
||||
OP_NOP6: 181,
|
||||
OP_NOP7: 182,
|
||||
OP_NOP8: 183,
|
||||
OP_NOP9: 184,
|
||||
OP_NOP10: 185,
|
||||
OP_CHECKSIGADD: 186,
|
||||
OP_PUBKEYHASH: 253,
|
||||
OP_PUBKEY: 254,
|
||||
OP_INVALIDOPCODE: 255,
|
||||
};
|
||||
// add unused opcodes
|
||||
for (let i = 187; i <= 255; i++) {
|
||||
opcodes[`OP_RETURN_${i}`] = i;
|
||||
}
|
||||
|
||||
export { opcodes };
|
||||
|
||||
/** extracts m and n from a multisig script (asm), returns nothing if it is not a multisig script */
|
||||
export function parseMultisigScript(script: string): void | { m: number, n: number } {
|
||||
if (!script) {
|
||||
return;
|
||||
}
|
||||
const ops = script.split(' ');
|
||||
if (ops.length < 3 || ops.pop() !== 'OP_CHECKMULTISIG') {
|
||||
return;
|
||||
}
|
||||
const opN = ops.pop();
|
||||
if (!opN) {
|
||||
return;
|
||||
}
|
||||
if (!opN.startsWith('OP_PUSHNUM_')) {
|
||||
return;
|
||||
}
|
||||
const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10);
|
||||
if (ops.length < n * 2 + 1) {
|
||||
return;
|
||||
}
|
||||
// pop n public keys
|
||||
for (let i = 0; i < n; i++) {
|
||||
if (!/^0((2|3)\w{64}|4\w{128})$/.test(ops.pop() || '')) {
|
||||
return;
|
||||
}
|
||||
if (!/^OP_PUSHBYTES_(33|65)$/.test(ops.pop() || '')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const opM = ops.pop();
|
||||
if (!opM) {
|
||||
return;
|
||||
}
|
||||
if (!opM.startsWith('OP_PUSHNUM_')) {
|
||||
return;
|
||||
}
|
||||
const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10);
|
||||
|
||||
if (ops.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
return { m, n };
|
||||
}
|
||||
|
||||
export function getVarIntLength(n: number): number {
|
||||
if (n < 0xfd) {
|
||||
return 1;
|
||||
} else if (n <= 0xffff) {
|
||||
return 3;
|
||||
} else if (n <= 0xffffffff) {
|
||||
return 5;
|
||||
} else {
|
||||
return 9;
|
||||
}
|
||||
}
|
||||
3
contributors/russeree.txt
Normal file
3
contributors/russeree.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 March 8, 2024.
|
||||
|
||||
Signed: PortlandHODL
|
||||
@@ -109,8 +109,6 @@ Below we list all settings from `mempool-config.json` and the corresponding over
|
||||
"AUTOMATIC_BLOCK_REINDEXING": false,
|
||||
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json",
|
||||
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
|
||||
"ADVANCED_GBT_AUDIT": false,
|
||||
"ADVANCED_GBT_MEMPOOL": false,
|
||||
"CPFP_INDEXING": false,
|
||||
"MAX_BLOCKS_BULK_QUERY": 0,
|
||||
"DISK_CACHE_BLOCK_INTERVAL": 6,
|
||||
@@ -142,8 +140,6 @@ Corresponding `docker-compose.yml` overrides:
|
||||
MEMPOOL_AUTOMATIC_BLOCK_REINDEXING: ""
|
||||
MEMPOOL_POOLS_JSON_URL: ""
|
||||
MEMPOOL_POOLS_JSON_TREE_URL: ""
|
||||
MEMPOOL_ADVANCED_GBT_AUDIT: ""
|
||||
MEMPOOL_ADVANCED_GBT_MEMPOOL: ""
|
||||
MEMPOOL_CPFP_INDEXING: ""
|
||||
MEMPOOL_MAX_BLOCKS_BULK_QUERY: ""
|
||||
MEMPOOL_DISK_CACHE_BLOCK_INTERVAL: ""
|
||||
@@ -151,8 +147,6 @@ Corresponding `docker-compose.yml` overrides:
|
||||
...
|
||||
```
|
||||
|
||||
`ADVANCED_GBT_AUDIT` AND `ADVANCED_GBT_MEMPOOL` enable a more accurate (but slower) block prediction algorithm for the block audit feature and the projected mempool-blocks respectively.
|
||||
|
||||
`CPFP_INDEXING` enables indexing CPFP (Child Pays For Parent) information for the last `INDEXING_BLOCKS_AMOUNT` blocks.
|
||||
|
||||
<br/>
|
||||
@@ -329,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": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:20.8.0-buster-slim AS builder
|
||||
FROM node:20.12.0-buster-slim AS builder
|
||||
|
||||
ARG commitHash
|
||||
ENV MEMPOOL_COMMIT_HASH=${commitHash}
|
||||
@@ -11,13 +11,20 @@ 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.8.0-buster-slim
|
||||
FROM node:20.12.0-buster-slim
|
||||
|
||||
WORKDIR /backend
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"NETWORK": "__MEMPOOL_NETWORK__",
|
||||
"BACKEND": "__MEMPOOL_BACKEND__",
|
||||
"ENABLED": __MEMPOOL_ENABLED__,
|
||||
"OFFICIAL": __MEMPOOL_OFFICIAL__,
|
||||
"HTTP_PORT": __MEMPOOL_HTTP_PORT__,
|
||||
"SPAWN_CLUSTER_PROCS": __MEMPOOL_SPAWN_CLUSTER_PROCS__,
|
||||
"API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
|
||||
@@ -25,9 +26,8 @@
|
||||
"GOGGLES_INDEXING": __MEMPOOL_GOGGLES_INDEXING__,
|
||||
"AUTOMATIC_BLOCK_REINDEXING": __MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__,
|
||||
"AUDIT": __MEMPOOL_AUDIT__,
|
||||
"ADVANCED_GBT_AUDIT": __MEMPOOL_ADVANCED_GBT_AUDIT__,
|
||||
"ADVANCED_GBT_MEMPOOL": __MEMPOOL_ADVANCED_GBT_MEMPOOL__,
|
||||
"RUST_GBT": __MEMPOOL_RUST_GBT__,
|
||||
"LIMIT_GBT": __MEMPOOL_LIMIT_GBT__,
|
||||
"CPFP_INDEXING": __MEMPOOL_CPFP_INDEXING__,
|
||||
"MAX_BLOCKS_BULK_QUERY": __MEMPOOL_MAX_BLOCKS_BULK_QUERY__,
|
||||
"DISK_CACHE_BLOCK_INTERVAL": __MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__,
|
||||
@@ -79,7 +79,8 @@
|
||||
"USERNAME": "__DATABASE_USERNAME__",
|
||||
"PASSWORD": "__DATABASE_PASSWORD__",
|
||||
"TIMEOUT": __DATABASE_TIMEOUT__,
|
||||
"PID_DIR": "__DATABASE_PID_DIR__"
|
||||
"PID_DIR": "__DATABASE_PID_DIR__",
|
||||
"POOL_SIZE": __DATABASE_POOL_SIZE__
|
||||
},
|
||||
"SYSLOG": {
|
||||
"ENABLED": __SYSLOG_ENABLED__,
|
||||
@@ -92,10 +93,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__",
|
||||
@@ -127,9 +124,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__,
|
||||
@@ -151,5 +146,9 @@
|
||||
"ENABLED": __REDIS_ENABLED__,
|
||||
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__",
|
||||
"BATCH_QUERY_BASE_SIZE": __REDIS_BATCH_QUERY_BASE_SIZE__
|
||||
},
|
||||
"FIAT_PRICE": {
|
||||
"ENABLED": __FIAT_PRICE_ENABLED__,
|
||||
"API_KEY": "__FIAT_PRICE_API_KEY__"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
__MEMPOOL_NETWORK__=${MEMPOOL_NETWORK:=mainnet}
|
||||
__MEMPOOL_BACKEND__=${MEMPOOL_BACKEND:=electrum}
|
||||
__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_API_URL_PREFIX__=${MEMPOOL_API_URL_PREFIX:=/api/v1/}
|
||||
@@ -28,9 +29,8 @@ __MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__=${MEMPOOL_AUTOMATIC_BLOCK_REINDEXING:=fal
|
||||
__MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json}
|
||||
__MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master}
|
||||
__MEMPOOL_AUDIT__=${MEMPOOL_AUDIT:=false}
|
||||
__MEMPOOL_ADVANCED_GBT_AUDIT__=${MEMPOOL_ADVANCED_GBT_AUDIT:=false}
|
||||
__MEMPOOL_ADVANCED_GBT_MEMPOOL__=${MEMPOOL_ADVANCED_GBT_MEMPOOL:=false}
|
||||
__MEMPOOL_RUST_GBT__=${MEMPOOL_RUST_GBT:=false}
|
||||
__MEMPOOL_LIMIT_GBT__=${MEMPOOL_LIMIT_GBT:=false}
|
||||
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
|
||||
__MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0}
|
||||
__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__=${MEMPOOL_DISK_CACHE_BLOCK_INTERVAL:=6}
|
||||
@@ -81,6 +81,7 @@ __DATABASE_USERNAME__=${DATABASE_USERNAME:=mempool}
|
||||
__DATABASE_PASSWORD__=${DATABASE_PASSWORD:=mempool}
|
||||
__DATABASE_TIMEOUT__=${DATABASE_TIMEOUT:=180000}
|
||||
__DATABASE_PID_DIR__=${DATABASE_PID_DIR:=""}
|
||||
__DATABASE_POOL_SIZE__=${DATABASE_POOL_SIZE:=100}
|
||||
|
||||
# SYSLOG
|
||||
__SYSLOG_ENABLED__=${SYSLOG_ENABLED:=false}
|
||||
@@ -93,10 +94,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}
|
||||
@@ -110,8 +107,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}
|
||||
@@ -139,8 +134,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:=[]}
|
||||
|
||||
@@ -153,11 +148,16 @@ __REDIS_ENABLED__=${REDIS_ENABLED:=false}
|
||||
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=true}
|
||||
__REDIS_BATCH_QUERY_BASE_SIZE__=${REDIS_BATCH_QUERY_BASE_SIZE:=5000}
|
||||
|
||||
# FIAT_PRICE
|
||||
__FIAT_PRICE_ENABLED__=${FIAT_PRICE_ENABLED:=true}
|
||||
__FIAT_PRICE_API_KEY__=${FIAT_PRICE_API_KEY:=""}
|
||||
|
||||
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
|
||||
|
||||
sed -i "s!__MEMPOOL_NETWORK__!${__MEMPOOL_NETWORK__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_BACKEND__!${__MEMPOOL_BACKEND__}!g" mempool-config.json
|
||||
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_API_URL_PREFIX__!${__MEMPOOL_API_URL_PREFIX__}!g" mempool-config.json
|
||||
@@ -182,9 +182,8 @@ sed -i "s!__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__!${__MEMPOOL_AUTOMATIC_BLOCK_REI
|
||||
sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_AUDIT__!${__MEMPOOL_AUDIT__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_ADVANCED_GBT_MEMPOOL__!${__MEMPOOL_ADVANCED_GBT_MEMPOOL__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_RUST_GBT__!${__MEMPOOL_RUST_GBT__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_ADVANCED_GBT_AUDIT__!${__MEMPOOL_ADVANCED_GBT_AUDIT__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_LIMIT_GBT__!${__MEMPOOL_LIMIT_GBT__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_CPFP_INDEXING__!${__MEMPOOL_CPFP_INDEXING__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_MAX_BLOCKS_BULK_QUERY__!${__MEMPOOL_MAX_BLOCKS_BULK_QUERY__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__!${__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__}!g" mempool-config.json
|
||||
@@ -230,6 +229,7 @@ sed -i "s!__DATABASE_USERNAME__!${__DATABASE_USERNAME__}!g" mempool-config.json
|
||||
sed -i "s!__DATABASE_PASSWORD__!${__DATABASE_PASSWORD__}!g" mempool-config.json
|
||||
sed -i "s!__DATABASE_TIMEOUT__!${__DATABASE_TIMEOUT__}!g" mempool-config.json
|
||||
sed -i "s!__DATABASE_PID_DIR__!${__DATABASE_PID_DIR__}!g" mempool-config.json
|
||||
sed -i "s!__DATABASE_POOL_SIZE__!${__DATABASE_POOL_SIZE__}!g" mempool-config.json
|
||||
|
||||
sed -i "s!__SYSLOG_ENABLED__!${__SYSLOG_ENABLED__}!g" mempool-config.json
|
||||
sed -i "s!__SYSLOG_HOST__!${__SYSLOG_HOST__}!g" mempool-config.json
|
||||
@@ -240,9 +240,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
|
||||
@@ -254,8 +251,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
|
||||
@@ -297,4 +292,8 @@ sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json
|
||||
sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json
|
||||
sed -i "s!__REDIS_BATCH_QUERY_BASE_SIZE__!${__REDIS_BATCH_QUERY_BASE_SIZE__}!g" mempool-config.json
|
||||
|
||||
# FIAT_PRICE
|
||||
sed -i "s!__FIAT_PRICE_ENABLED__!${__FIAT_PRICE_ENABLED__}!g" mempool-config.json
|
||||
sed -i "s!__FIAT_PRICE_API_KEY__!${__FIAT_PRICE_API_KEY__}!g" mempool-config.json
|
||||
|
||||
node /backend/package/index.js
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:20.8.0-buster-slim AS builder
|
||||
FROM node:20.12.0-buster-slim AS builder
|
||||
|
||||
ARG commitHash
|
||||
ENV DOCKER_COMMIT_HASH=${commitHash}
|
||||
@@ -13,7 +13,7 @@ RUN npm install --omit=dev --omit=optional
|
||||
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:1.24.0-alpine
|
||||
FROM nginx:1.25.4-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,15 +37,15 @@ __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}
|
||||
|
||||
# Export as environment variables to be used by envsubst
|
||||
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__
|
||||
@@ -59,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__
|
||||
@@ -67,7 +63,9 @@ 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__
|
||||
|
||||
folder=$(find /var/www/mempool -name "config.js" | xargs dirname)
|
||||
echo ${folder}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
#backend
|
||||
cp ./docker/backend/* ./backend/
|
||||
cp -r ./docker/backend/* ./backend/
|
||||
|
||||
#geoip-data
|
||||
mkdir -p ./backend/GeoIP/
|
||||
@@ -13,8 +13,8 @@ localhostIP="127.0.0.1"
|
||||
cp ./docker/frontend/* ./frontend
|
||||
cp ./nginx.conf ./frontend/
|
||||
cp ./nginx-mempool.conf ./frontend/
|
||||
sed -i "s/${localhostIP}:80/0.0.0.0:__MEMPOOL_FRONTEND_HTTP_PORT__/g" ./frontend/nginx.conf
|
||||
sed -i "s/${localhostIP}/0.0.0.0/g" ./frontend/nginx.conf
|
||||
sed -i "s/user nobody;//g" ./frontend/nginx.conf
|
||||
sed -i "s!/etc/nginx/nginx-mempool.conf!/etc/nginx/conf.d/nginx-mempool.conf!g" ./frontend/nginx.conf
|
||||
sed -i "s/${localhostIP}:8999/__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__:__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__/g" ./frontend/nginx-mempool.conf
|
||||
sed -i"" -e "s/${localhostIP}:80/0.0.0.0:__MEMPOOL_FRONTEND_HTTP_PORT__/g" ./frontend/nginx.conf
|
||||
sed -i"" -e "s/${localhostIP}/0.0.0.0/g" ./frontend/nginx.conf
|
||||
sed -i"" -e "s/user nobody;//g" ./frontend/nginx.conf
|
||||
sed -i"" -e "s!/etc/nginx/nginx-mempool.conf!/etc/nginx/conf.d/nginx-mempool.conf!g" ./frontend/nginx.conf
|
||||
sed -i"" -e "s/${localhostIP}:8999/__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__:__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__/g" ./frontend/nginx-mempool.conf
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"@typescript-eslint/no-this-alias": 1,
|
||||
"@typescript-eslint/no-var-requires": 1,
|
||||
"@typescript-eslint/explicit-function-return-type": 1,
|
||||
"@typescript-eslint/no-unused-vars": 1,
|
||||
"no-case-declarations": 1,
|
||||
"no-console": 1,
|
||||
"no-constant-condition": 1,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -223,11 +223,11 @@
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "mempool:build"
|
||||
"buildTarget": "mempool:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "mempool:build:production"
|
||||
"buildTarget": "mempool:build:production"
|
||||
},
|
||||
"local": {
|
||||
"proxyConfig": "proxy.conf.local.js",
|
||||
@@ -264,7 +264,7 @@
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "mempool:build"
|
||||
"buildTarget": "mempool:build"
|
||||
}
|
||||
},
|
||||
"e2e": {
|
||||
@@ -280,6 +280,56 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"builder": "@angular-devkit/build-angular:server",
|
||||
"options": {
|
||||
"outputPath": "dist/mempool/server",
|
||||
"main": "server.ts",
|
||||
"tsConfig": "tsconfig.server.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"outputHashing": "media",
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"sourceMap": false,
|
||||
"localize": true,
|
||||
"optimization": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve-ssr": {
|
||||
"builder": "@angular-devkit/build-angular:ssr-dev-server",
|
||||
"options": {
|
||||
"browserTarget": "mempool:build",
|
||||
"serverTarget": "mempool:server"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "mempool:build:production",
|
||||
"serverTarget": "mempool:server:production",
|
||||
"optimization": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"prerender": {
|
||||
"builder": "@angular-devkit/build-angular:prerender",
|
||||
"options": {
|
||||
"browserTarget": "mempool:build:production",
|
||||
"serverTarget": "mempool:server:production",
|
||||
"routes": [
|
||||
"/"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"production": {}
|
||||
}
|
||||
},
|
||||
"cypress-run": {
|
||||
"builder": "@cypress/schematic:cypress",
|
||||
"options": {
|
||||
|
||||
@@ -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:(.*)/);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,13 @@ export const mockWebSocket = () => {
|
||||
win.mockServer = server;
|
||||
win.mockServer.on('connection', (socket) => {
|
||||
win.mockSocket = socket;
|
||||
win.mockSocket.send('{"action":"init"}');
|
||||
win.mockSocket.send('{"conversions":{"USD":32365.338815782445}}');
|
||||
cy.readFile('cypress/fixtures/mainnet_live2hchart.json', 'ascii').then((fixture) => {
|
||||
win.mockSocket.send(JSON.stringify(fixture));
|
||||
});
|
||||
cy.readFile('cypress/fixtures/mainnet_mempoolInfo.json', 'ascii').then((fixture) => {
|
||||
win.mockSocket.send(JSON.stringify(fixture));
|
||||
});
|
||||
});
|
||||
|
||||
win.mockServer.on('message', (message) => {
|
||||
@@ -75,8 +81,6 @@ export const emitMempoolInfo = ({
|
||||
|
||||
switch (params.command) {
|
||||
case "init": {
|
||||
win.mockSocket.send('{"action":"init"}');
|
||||
win.mockSocket.send('{"action":"want","data":["blocks","stats","mempool-blocks","live-2h-chart"]}');
|
||||
win.mockSocket.send('{"conversions":{"USD":32365.338815782445}}');
|
||||
cy.readFile('cypress/fixtures/mainnet_live2hchart.json', 'ascii').then((fixture) => {
|
||||
win.mockSocket.send(JSON.stringify(fixture));
|
||||
|
||||
@@ -71,7 +71,7 @@ const newConfig = `(function (window) {
|
||||
window.__env.${obj.key} = ${typeof obj.value === 'string' ? `'${obj.value}'` : obj.value};`, '')}
|
||||
window.__env.GIT_COMMIT_HASH = '${gitCommitHash}';
|
||||
window.__env.PACKAGE_JSON_VERSION = '${packetJsonVersion}';
|
||||
}(this));`;
|
||||
}((typeof global !== 'undefined') ? global : this));`;
|
||||
|
||||
const newConfigTemplate = `(function (window) {
|
||||
window.__env = window.__env || {};${settings.reduce((str, obj) => `${str}
|
||||
|
||||
@@ -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,
|
||||
@@ -23,5 +20,7 @@
|
||||
"SIGNET_BLOCK_AUDIT_START_HEIGHT": 0,
|
||||
"LIGHTNING": false,
|
||||
"HISTORICAL_PRICE": true,
|
||||
"ACCELERATOR": false
|
||||
"ADDITIONAL_CURRENCIES": false,
|
||||
"ACCELERATOR": false,
|
||||
"PUBLIC_ACCELERATIONS": false
|
||||
}
|
||||
|
||||
12199
frontend/package-lock.json
generated
12199
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
@@ -48,37 +47,40 @@
|
||||
"prettier": "prettier --write \"src/app/**/*.{js,json,css,scss,less,md,ts,html,component.html}\"",
|
||||
"e2e": "npm run generate-config && npm run ng -- e2e",
|
||||
"e2e:ci": "npm run cypress:run:ci",
|
||||
"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",
|
||||
"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 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": "^16.2.0",
|
||||
"@angular/animations": "^16.2.2",
|
||||
"@angular/cli": "^16.2.0",
|
||||
"@angular/common": "^16.2.2",
|
||||
"@angular/compiler": "^16.2.2",
|
||||
"@angular/core": "^16.2.2",
|
||||
"@angular/forms": "^16.2.2",
|
||||
"@angular/localize": "^16.2.2",
|
||||
"@angular/platform-browser": "^16.2.2",
|
||||
"@angular/platform-browser-dynamic": "^16.2.2",
|
||||
"@angular/platform-server": "^16.2.2",
|
||||
"@angular/router": "^16.2.2",
|
||||
"@fortawesome/angular-fontawesome": "~0.13.0",
|
||||
"@angular-devkit/build-angular": "^17.3.1",
|
||||
"@angular/animations": "^17.3.1",
|
||||
"@angular/cli": "^17.3.1",
|
||||
"@angular/common": "^17.3.1",
|
||||
"@angular/compiler": "^17.3.1",
|
||||
"@angular/core": "^17.3.1",
|
||||
"@angular/forms": "^17.3.1",
|
||||
"@angular/localize": "^17.3.1",
|
||||
"@angular/platform-browser": "^17.3.1",
|
||||
"@angular/platform-browser-dynamic": "^17.3.1",
|
||||
"@angular/platform-server": "^17.3.1",
|
||||
"@angular/router": "^17.3.1",
|
||||
"@angular/ssr": "^17.3.1",
|
||||
"@fortawesome/angular-fontawesome": "~0.14.1",
|
||||
"@fortawesome/fontawesome-common-types": "~6.5.1",
|
||||
"@fortawesome/fontawesome-svg-core": "~6.5.1",
|
||||
"@fortawesome/free-solid-svg-icons": "~6.5.1",
|
||||
"@mempool/mempool.js": "2.3.0",
|
||||
"@ng-bootstrap/ng-bootstrap": "^15.1.0",
|
||||
"@ng-bootstrap/ng-bootstrap": "^16.0.0",
|
||||
"@types/qrcode": "~1.5.0",
|
||||
"bootstrap": "~4.6.2",
|
||||
"browserify": "^17.0.0",
|
||||
@@ -86,31 +88,34 @@
|
||||
"domino": "^2.1.6",
|
||||
"echarts": "~5.5.0",
|
||||
"lightweight-charts": "~3.8.0",
|
||||
"ngx-echarts": "~16.2.0",
|
||||
"ngx-infinite-scroll": "^16.0.0",
|
||||
"ngx-echarts": "~17.1.0",
|
||||
"ngx-infinite-scroll": "^17.0.0",
|
||||
"qrcode": "1.5.1",
|
||||
"rxjs": "~7.8.1",
|
||||
"tinyify": "^3.1.0",
|
||||
"esbuild": "^0.20.2",
|
||||
"tinyify": "^4.0.0",
|
||||
"tlite": "^0.1.9",
|
||||
"tslib": "~2.6.0",
|
||||
"zone.js": "~0.13.1"
|
||||
"zone.js": "~0.14.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/compiler-cli": "^16.1.5",
|
||||
"@angular/language-service": "^16.1.5",
|
||||
"@angular/compiler-cli": "^17.3.1",
|
||||
"@angular/language-service": "^17.3.1",
|
||||
"@types/node": "^18.11.9",
|
||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||
"@typescript-eslint/parser": "^5.48.1",
|
||||
"eslint": "^8.31.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.4.0",
|
||||
"@typescript-eslint/parser": "^7.4.0",
|
||||
"eslint": "^8.57.0",
|
||||
"browser-sync": "^3.0.0",
|
||||
"http-proxy-middleware": "~2.0.6",
|
||||
"prettier": "^3.0.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ts-node": "~10.9.1",
|
||||
"typescript": "~4.9.3"
|
||||
"typescript": "~5.4.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@cypress/schematic": "^2.5.0",
|
||||
"@types/cypress": "^1.1.3",
|
||||
"cypress": "^13.6.2",
|
||||
"cypress": "^13.7.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;
|
||||
|
||||
104
frontend/server.run.ts
Normal file
104
frontend/server.run.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import './src/resources/config.js';
|
||||
|
||||
import * as domino from 'domino';
|
||||
import * as express from 'express';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const {readFileSync, existsSync} = require('fs');
|
||||
const {createProxyMiddleware} = require('http-proxy-middleware');
|
||||
|
||||
const template = fs.readFileSync(path.join(process.cwd(), 'dist/mempool/browser/en-US/', 'index.html')).toString();
|
||||
const win = domino.createWindow(template);
|
||||
|
||||
// @ts-ignore
|
||||
win.__env = global.__env;
|
||||
|
||||
// @ts-ignore
|
||||
win.matchMedia = () => {
|
||||
return {
|
||||
matches: true
|
||||
};
|
||||
};
|
||||
// @ts-ignore
|
||||
win.setTimeout = (fn) => { fn(); };
|
||||
win.document.body.scrollTo = (() => {});
|
||||
// @ts-ignore
|
||||
global['window'] = win;
|
||||
global['document'] = win.document;
|
||||
// @ts-ignore
|
||||
global['history'] = { state: { } };
|
||||
|
||||
global['localStorage'] = {
|
||||
getItem: () => '',
|
||||
setItem: () => {},
|
||||
removeItem: () => {},
|
||||
clear: () => {},
|
||||
length: 0,
|
||||
key: () => '',
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the list of supported and actually active locales
|
||||
*/
|
||||
function getActiveLocales() {
|
||||
const angularConfig = JSON.parse(readFileSync('angular.json', 'utf8'));
|
||||
|
||||
const supportedLocales = [
|
||||
angularConfig.projects.mempool.i18n.sourceLocale,
|
||||
...Object.keys(angularConfig.projects.mempool.i18n.locales),
|
||||
];
|
||||
|
||||
return supportedLocales.filter(locale => locale === 'en-US' && existsSync(`./dist/mempool/server/${locale}`));
|
||||
// return supportedLocales.filter(locale => existsSync(`./dist/mempool/server/${locale}`));
|
||||
}
|
||||
|
||||
function app() {
|
||||
const server = express();
|
||||
|
||||
// proxy websocket
|
||||
server.get('/api/v1/ws', createProxyMiddleware({
|
||||
target: 'ws://localhost:4200/api/v1/ws',
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
logLevel: 'debug'
|
||||
}));
|
||||
// proxy API to nginx
|
||||
server.get('/api/**', createProxyMiddleware({
|
||||
// @ts-ignore
|
||||
target: win.__env.NGINX_PROTOCOL + '://' + win.__env.NGINX_HOSTNAME + ':' + win.__env.NGINX_PORT,
|
||||
changeOrigin: true,
|
||||
}));
|
||||
server.get('/resources/**', express.static('./src'));
|
||||
|
||||
|
||||
// map / and /en to en-US
|
||||
const defaultLocale = 'en-US';
|
||||
console.log(`serving default locale: ${defaultLocale}`);
|
||||
const appServerModule = require(`./dist/mempool/server/${defaultLocale}/main.js`);
|
||||
server.use('/', appServerModule.app(defaultLocale));
|
||||
server.use('/en', appServerModule.app(defaultLocale));
|
||||
|
||||
// map each locale to its localized main.js
|
||||
getActiveLocales().forEach(locale => {
|
||||
console.log('serving locale:', locale);
|
||||
const appServerModule = require(`./dist/mempool/server/${locale}/main.js`);
|
||||
|
||||
// map everything to itself
|
||||
server.use(`/${locale}`, appServerModule.app(locale));
|
||||
|
||||
});
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
function run() {
|
||||
const port = process.env.PORT || 4000;
|
||||
|
||||
// Start up the Node server
|
||||
app().listen(port, () => {
|
||||
console.log(`Node Express server listening on port ${port}`);
|
||||
});
|
||||
}
|
||||
|
||||
run();
|
||||
108
frontend/server.ts
Normal file
108
frontend/server.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import 'zone.js';
|
||||
import './src/resources/config.js';
|
||||
|
||||
import { CommonEngine } from '@angular/ssr';
|
||||
import * as express from 'express';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as domino from 'domino';
|
||||
|
||||
import { join } from 'path';
|
||||
import { AppServerModule } from './src/main.server';
|
||||
import { APP_BASE_HREF } from '@angular/common';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
import { ResizeObserver } from './shims';
|
||||
|
||||
const commonEngine = new CommonEngine();
|
||||
|
||||
const template = fs.readFileSync(path.join(process.cwd(), 'dist/mempool/browser/en-US/', 'index.html')).toString();
|
||||
const win = domino.createWindow(template);
|
||||
|
||||
// @ts-ignore
|
||||
win.__env = global.__env;
|
||||
|
||||
// @ts-ignore
|
||||
win.matchMedia = (media) => {
|
||||
return {
|
||||
media,
|
||||
matches: true,
|
||||
};
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
win.setTimeout = (fn) => { fn(); };
|
||||
win.document.body.scrollTo = (() => {});
|
||||
win['ResizeObserver'] = ResizeObserver;
|
||||
// @ts-ignore
|
||||
global['window'] = win;
|
||||
// @ts-ignore
|
||||
global['document'] = win.document;
|
||||
// @ts-ignore
|
||||
global['history'] = { state: { } };
|
||||
// @ts-ignore
|
||||
Object.defineProperty(global, 'navigator', {
|
||||
value: win.navigator,
|
||||
writable: true
|
||||
});
|
||||
|
||||
global['localStorage'] = {
|
||||
getItem: () => '',
|
||||
setItem: () => {},
|
||||
removeItem: () => {},
|
||||
clear: () => {},
|
||||
length: 0,
|
||||
key: () => '',
|
||||
};
|
||||
|
||||
// The Express app is exported so that it can be used by serverless Functions.
|
||||
export function app(locale: string): express.Express {
|
||||
const server = express();
|
||||
const distFolder = join(process.cwd(), `dist/mempool/browser/${locale}`);
|
||||
const indexHtml = join(distFolder, 'index.html');
|
||||
|
||||
server.set('view engine', 'html');
|
||||
server.set('views', distFolder);
|
||||
|
||||
// static file handler so we send HTTP 404 to nginx
|
||||
server.get('/**.(css|js|json|ico|webmanifest|png|jpg|jpeg|svg|mp4)*', express.static(distFolder, { maxAge: '1y', fallthrough: false }));
|
||||
// handle page routes
|
||||
server.get('*', (req, res, next) => {
|
||||
const { protocol, originalUrl, baseUrl, headers } = req;
|
||||
|
||||
commonEngine
|
||||
.render({
|
||||
bootstrap: AppServerModule,
|
||||
documentFilePath: indexHtml,
|
||||
url: `${protocol}://${headers.host}${originalUrl}`,
|
||||
publicPath: distFolder,
|
||||
providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
|
||||
})
|
||||
.then((html) => res.send(html))
|
||||
.catch((err) => next(err));
|
||||
});
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
|
||||
// only used for development mode
|
||||
function run(): void {
|
||||
const port = process.env.PORT || 4000;
|
||||
|
||||
// Start up the Node server
|
||||
const server = app('en-US');
|
||||
server.listen(port, () => {
|
||||
console.log(`Node Express server listening on port ${port}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Webpack will replace 'require' with '__webpack_require__'
|
||||
// '__non_webpack_require__' is a proxy to Node 'require'
|
||||
// The below code is to ensure that the server is run only when not requiring the bundle.
|
||||
declare const __non_webpack_require__: NodeRequire;
|
||||
const mainModule = __non_webpack_require__.main;
|
||||
const moduleFilename = mainModule && mainModule.filename || '';
|
||||
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
|
||||
run();
|
||||
}
|
||||
7
frontend/shims.ts
Normal file
7
frontend/shims.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export class ResizeObserver {
|
||||
constructor() {}
|
||||
|
||||
disconnect() {}
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { EightBlocksComponent } from './components/eight-blocks/eight-blocks.com
|
||||
import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component';
|
||||
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';
|
||||
|
||||
const browserWindow = window || {};
|
||||
// @ts-ignore
|
||||
@@ -26,6 +27,14 @@ let routes: Routes = [
|
||||
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'] },
|
||||
@@ -61,6 +70,14 @@ let routes: Routes = [
|
||||
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'] },
|
||||
@@ -88,6 +105,14 @@ let routes: Routes = [
|
||||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'wallet',
|
||||
children: [],
|
||||
component: AddressGroupComponent,
|
||||
data: {
|
||||
networkSpecific: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'preview',
|
||||
children: [
|
||||
@@ -145,13 +170,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 = [
|
||||
{
|
||||
@@ -168,6 +186,14 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'wallet',
|
||||
children: [],
|
||||
component: AddressGroupComponent,
|
||||
data: {
|
||||
networkSpecific: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'status',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
@@ -195,6 +221,14 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'wallet',
|
||||
children: [],
|
||||
component: AddressGroupComponent,
|
||||
data: {
|
||||
networkSpecific: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'preview',
|
||||
children: [
|
||||
@@ -229,7 +263,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
imports: [RouterModule.forRoot(routes, {
|
||||
initialNavigation: 'enabledBlocking',
|
||||
scrollPositionRestoration: 'enabled',
|
||||
anchorScrolling: 'enabled',
|
||||
anchorScrolling: 'disabled',
|
||||
preloadingStrategy: AppPreloadingStrategy
|
||||
})],
|
||||
})
|
||||
|
||||
@@ -268,4 +268,134 @@ export const fiatCurrencies = {
|
||||
code: 'USD',
|
||||
indexed: true,
|
||||
},
|
||||
BGN: {
|
||||
name: 'Bulgarian Lev',
|
||||
code: 'BGN',
|
||||
indexed: true,
|
||||
},
|
||||
BRL: {
|
||||
name: 'Brazilian Real',
|
||||
code: 'BRL',
|
||||
indexed: true,
|
||||
},
|
||||
CNY: {
|
||||
name: 'Chinese Yuan',
|
||||
code: 'CNY',
|
||||
indexed: true,
|
||||
},
|
||||
CZK: {
|
||||
name: 'Czech Koruna',
|
||||
code: 'CZK',
|
||||
indexed: true,
|
||||
},
|
||||
DKK: {
|
||||
name: 'Danish Krone',
|
||||
code: 'DKK',
|
||||
indexed: true,
|
||||
},
|
||||
HKD: {
|
||||
name: 'Hong Kong Dollar',
|
||||
code: 'HKD',
|
||||
indexed: true,
|
||||
},
|
||||
HRK: {
|
||||
name: 'Croatian Kuna',
|
||||
code: 'HRK',
|
||||
indexed: true,
|
||||
},
|
||||
HUF: {
|
||||
name: 'Hungarian Forint',
|
||||
code: 'HUF',
|
||||
indexed: true,
|
||||
},
|
||||
IDR: {
|
||||
name: 'Indonesian Rupiah',
|
||||
code: 'IDR',
|
||||
indexed: true,
|
||||
},
|
||||
ILS: {
|
||||
name: 'Israeli Shekel',
|
||||
code: 'ILS',
|
||||
indexed: true,
|
||||
},
|
||||
INR: {
|
||||
name: 'Indian Rupee',
|
||||
code: 'INR',
|
||||
indexed: true,
|
||||
},
|
||||
ISK: {
|
||||
name: 'Icelandic Krona',
|
||||
code: 'ISK',
|
||||
indexed: true,
|
||||
},
|
||||
KRW: {
|
||||
name: 'South Korean Won',
|
||||
code: 'KRW',
|
||||
indexed: true,
|
||||
},
|
||||
MXN: {
|
||||
name: 'Mexican Peso',
|
||||
code: 'MXN',
|
||||
indexed: true,
|
||||
},
|
||||
MYR: {
|
||||
name: 'Malaysian Ringgit',
|
||||
code: 'MYR',
|
||||
indexed: true,
|
||||
},
|
||||
NOK: {
|
||||
name: 'Norwegian Krone',
|
||||
code: 'NOK',
|
||||
indexed: true,
|
||||
},
|
||||
NZD: {
|
||||
name: 'New Zealand Dollar',
|
||||
code: 'NZD',
|
||||
indexed: true,
|
||||
},
|
||||
PHP: {
|
||||
name: 'Philippine Peso',
|
||||
code: 'PHP',
|
||||
indexed: true,
|
||||
},
|
||||
PLN: {
|
||||
name: 'Polish Zloty',
|
||||
code: 'PLN',
|
||||
indexed: true,
|
||||
},
|
||||
RON: {
|
||||
name: 'Romanian Leu',
|
||||
code: 'RON',
|
||||
indexed: true,
|
||||
},
|
||||
RUB: {
|
||||
name: 'Russian Ruble',
|
||||
code: 'RUB',
|
||||
indexed: true,
|
||||
},
|
||||
SEK: {
|
||||
name: 'Swedish Krona',
|
||||
code: 'SEK',
|
||||
indexed: true,
|
||||
},
|
||||
SGD: {
|
||||
name: 'Singapore Dollar',
|
||||
code: 'SGD',
|
||||
indexed: true,
|
||||
},
|
||||
THB: {
|
||||
name: 'Thai Baht',
|
||||
code: 'THB',
|
||||
indexed: true,
|
||||
},
|
||||
TRY: {
|
||||
name: 'Turkish Lira',
|
||||
code: 'TRY',
|
||||
indexed: true,
|
||||
},
|
||||
ZAR: {
|
||||
name: 'South African Rand',
|
||||
code: 'ZAR',
|
||||
indexed: true,
|
||||
},
|
||||
};
|
||||
@@ -1,20 +1,23 @@
|
||||
import { HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { ServerModule, ServerTransferStateModule } from '@angular/platform-server';
|
||||
import { ServerModule } from '@angular/platform-server';
|
||||
|
||||
import { ZONE_SERVICE } from './injection-tokens';
|
||||
import { AppModule } from './app.module';
|
||||
import { AppComponent } from './components/app/app.component';
|
||||
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
|
||||
import { ZoneService } from './services/zone.service';
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
AppModule,
|
||||
ServerModule,
|
||||
ServerTransferStateModule,
|
||||
],
|
||||
providers: [
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true },
|
||||
{ provide: ZONE_SERVICE, useClass: ZoneService },
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppServerModule {}
|
||||
export class AppServerModule {}
|
||||
@@ -2,6 +2,7 @@ import { BrowserModule } from '@angular/platform-browser';
|
||||
import { ModuleWithProviders, NgModule } from '@angular/core';
|
||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ZONE_SERVICE } from './injection-tokens';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './components/app/app.component';
|
||||
import { ElectrsApiService } from './services/electrs-api.service';
|
||||
@@ -13,6 +14,7 @@ import { WebsocketService } from './services/websocket.service';
|
||||
import { AudioService } from './services/audio.service';
|
||||
import { SeoService } from './services/seo.service';
|
||||
import { OpenGraphService } from './services/opengraph.service';
|
||||
import { ZoneService } from './services/zone-shim.service';
|
||||
import { SharedModule } from './shared/shared.module';
|
||||
import { StorageService } from './services/storage.service';
|
||||
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
|
||||
@@ -42,7 +44,8 @@ const providers = [
|
||||
CapAddressPipe,
|
||||
AppPreloadingStrategy,
|
||||
ServicesApiServices,
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true },
|
||||
{ provide: ZONE_SERVICE, useClass: ZoneService },
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -1,110 +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">
|
||||
<div class="text-center">
|
||||
Error loading address data.
|
||||
<br>
|
||||
<i>{{ error.error }}</i>
|
||||
</div>
|
||||
</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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user