Compare commits
203 Commits
mononaut/e
...
hunicus/30
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93dab41e9e | ||
|
|
8fe78fa12b | ||
|
|
f166cb7974 | ||
|
|
d3532eb734 | ||
|
|
4cb379bb0f | ||
|
|
316028fe66 | ||
|
|
c93f52f3a8 | ||
|
|
22e57ae95c | ||
|
|
a1af41804a | ||
|
|
b0080a5859 | ||
|
|
5c36692799 | ||
|
|
97877053bf | ||
|
|
f9a44a5fbb | ||
|
|
6b5bcaa279 | ||
|
|
8c396978a8 | ||
|
|
a51d2a6aec | ||
|
|
a863c17408 | ||
|
|
0924bb6ac0 | ||
|
|
910e67ff36 | ||
|
|
aa17f8203c | ||
|
|
b7b6548cce | ||
|
|
17f1cb8648 | ||
|
|
d5dca95fbe | ||
|
|
bed7c1b283 | ||
|
|
0d25ef0b5b | ||
|
|
6b7d8d95f7 | ||
|
|
bafc0bd9cf | ||
|
|
7a87f74b22 | ||
|
|
49db63d888 | ||
|
|
363fc1b00b | ||
|
|
36a26fc2ce | ||
|
|
73b71c4914 | ||
|
|
dcfab218fb | ||
|
|
c79a597c96 | ||
|
|
a393f42b5e | ||
|
|
6ac58f2da7 | ||
|
|
a9f8bbbcce | ||
|
|
d65bddd30b | ||
|
|
b6cb539470 | ||
|
|
aea2b1ec6b | ||
|
|
5138f9a254 | ||
|
|
8cfa4ef1a1 | ||
|
|
16401044f6 | ||
|
|
c88b7ddc77 | ||
|
|
8b012a96f3 | ||
|
|
8b6bb54efb | ||
|
|
2670589293 | ||
|
|
91eef1c4d9 | ||
|
|
562a5f6878 | ||
|
|
5c20fd71e1 | ||
|
|
adf093eca5 | ||
|
|
d14d286f24 | ||
|
|
ecd80aad6a | ||
|
|
1b248c24f1 | ||
|
|
f35f630695 | ||
|
|
8172ec9245 | ||
|
|
14c86b84b8 | ||
|
|
1f8f40011a | ||
|
|
2719be9075 | ||
|
|
354c119e99 | ||
|
|
cc27c0159e | ||
|
|
b1bdb52851 | ||
|
|
d52e2cd585 | ||
|
|
2c613195cc | ||
|
|
1ca99e9967 | ||
|
|
f9ddc3cc5f | ||
|
|
63ccecf410 | ||
|
|
5b2470955d | ||
|
|
74b87b6006 | ||
|
|
9b65fbd98c | ||
|
|
3f3f0db2f2 | ||
|
|
c993ee51cc | ||
|
|
395f47516a | ||
|
|
589adb95c3 | ||
|
|
1fd5b975f1 | ||
|
|
7edd40246c | ||
|
|
e15c0c6c7a | ||
|
|
a13c424869 | ||
|
|
8ee9f52634 | ||
|
|
b58abe4779 | ||
|
|
6d5be78dd0 | ||
|
|
07b0f24cf1 | ||
|
|
d7b874ac49 | ||
|
|
0a0978f7d7 | ||
|
|
25925751eb | ||
|
|
0ebfd6f017 | ||
|
|
81d1c0a4d5 | ||
|
|
36fe5627c7 | ||
|
|
9e43dadad8 | ||
|
|
2d463326e0 | ||
|
|
a6edfcc272 | ||
|
|
de4265a6d1 | ||
|
|
dc43a81899 | ||
|
|
e59c961f25 | ||
|
|
db715a1dba | ||
|
|
202d4122b4 | ||
|
|
f62f2341f4 | ||
|
|
e2fdacfddd | ||
|
|
c84a444f79 | ||
|
|
ee2d8f8c5a | ||
|
|
7db391d762 | ||
|
|
da4a20cb85 | ||
|
|
44f2217a68 | ||
|
|
6ce3c1d75d | ||
|
|
caa8cfbc0e | ||
|
|
02f361af73 | ||
|
|
a1e05c0c37 | ||
|
|
05affa5ad4 | ||
|
|
5e91af168b | ||
|
|
ae183210e0 | ||
|
|
56127dce6a | ||
|
|
0376467e6c | ||
|
|
48b55eed46 | ||
|
|
0ce043cca9 | ||
|
|
65dbafd2ec | ||
|
|
3f36a30d1d | ||
|
|
b021746e9e | ||
|
|
975ec772fa | ||
|
|
442a4ff6e0 | ||
|
|
cea218b81a | ||
|
|
7970df27ad | ||
|
|
a24d2ce547 | ||
|
|
95707de8ec | ||
|
|
eb37066d5d | ||
|
|
f0983844c1 | ||
|
|
a0bd4e0f63 | ||
|
|
141ab8076f | ||
|
|
267f3d4877 | ||
|
|
460a41644d | ||
|
|
ca69d19bf7 | ||
|
|
d91fa5c6ef | ||
|
|
3610aa2e20 | ||
|
|
7a6da07a61 | ||
|
|
0f77fb88bf | ||
|
|
1bd19e1d8d | ||
|
|
61eeb82694 | ||
|
|
135adfecbd | ||
|
|
20b2017908 | ||
|
|
b1345038bd | ||
|
|
7ba627e243 | ||
|
|
6b453ef018 | ||
|
|
943dc6f5e6 | ||
|
|
4192869593 | ||
|
|
e066bb1e9d | ||
|
|
6cdc97848f | ||
|
|
ade7908229 | ||
|
|
9a2ab7fe21 | ||
|
|
87e39b8389 | ||
|
|
bc508b6621 | ||
|
|
709783280a | ||
|
|
f9aa1b5b35 | ||
|
|
2fffd8b43c | ||
|
|
741571a93a | ||
|
|
07a424e6f1 | ||
|
|
943d05f680 | ||
|
|
57aa2c69ac | ||
|
|
61e28a33e0 | ||
|
|
4055128d7f | ||
|
|
7c29e51bbb | ||
|
|
fabd420586 | ||
|
|
a12316f4dc | ||
|
|
548611f13a | ||
|
|
3397edecb4 | ||
|
|
b041a145b1 | ||
|
|
7046c3d6c3 | ||
|
|
67f58a4491 | ||
|
|
d74e4b1876 | ||
|
|
e757b74c87 | ||
|
|
4b41730636 | ||
|
|
23ecf9cf41 | ||
|
|
f8cfa35552 | ||
|
|
c1d0e802d9 | ||
|
|
bde7fad1c4 | ||
|
|
8007f5a3d3 | ||
|
|
83f955e469 | ||
|
|
29c53a7852 | ||
|
|
3825a1c359 | ||
|
|
ac82a25fa2 | ||
|
|
85e071049c | ||
|
|
ae22b7b444 | ||
|
|
9aba4d4357 | ||
|
|
17866f80bd | ||
|
|
5e46176c4e | ||
|
|
69c54d7207 | ||
|
|
2d4bc9dbd6 | ||
|
|
b6a6fcd4e2 | ||
|
|
cda6567c4c | ||
|
|
a372b479b4 | ||
|
|
05f85c5201 | ||
|
|
2570357bec | ||
|
|
4ba552fe1b | ||
|
|
ec918d57b2 | ||
|
|
c89b15fdbc | ||
|
|
99af881cdd | ||
|
|
71935e29c8 | ||
|
|
123b853205 | ||
|
|
bad73c1805 | ||
|
|
7ab373ecac | ||
|
|
6cc2f20638 | ||
|
|
730c1ae2d7 | ||
|
|
f493da4eac | ||
|
|
e7ad857cc9 | ||
|
|
f968faeaf9 |
15
.github/dependabot.yml
vendored
15
.github/dependabot.yml
vendored
@@ -7,7 +7,8 @@ updates:
|
||||
open-pull-requests-limit: 10
|
||||
ignore:
|
||||
- dependency-name: "*"
|
||||
update-types: ["version-update:semver-major"]
|
||||
update-types:
|
||||
["version-update:semver-major", "version-update:semver-patch"]
|
||||
allow:
|
||||
- dependency-type: "production"
|
||||
|
||||
@@ -18,7 +19,8 @@ updates:
|
||||
open-pull-requests-limit: 10
|
||||
ignore:
|
||||
- dependency-name: "*"
|
||||
update-types: ["version-update:semver-major"]
|
||||
update-types:
|
||||
["version-update:semver-major", "version-update:semver-patch"]
|
||||
allow:
|
||||
- dependency-type: "production"
|
||||
|
||||
@@ -28,7 +30,8 @@ updates:
|
||||
interval: weekly
|
||||
ignore:
|
||||
- dependency-name: "*"
|
||||
update-types: ["version-update:semver-major"]
|
||||
update-types:
|
||||
["version-update:semver-major", "version-update:semver-patch"]
|
||||
|
||||
- package-ecosystem: docker
|
||||
directory: "/docker/frontend"
|
||||
@@ -36,7 +39,8 @@ updates:
|
||||
interval: weekly
|
||||
ignore:
|
||||
- dependency-name: "*"
|
||||
update-types: ["version-update:semver-major"]
|
||||
update-types:
|
||||
["version-update:semver-major", "version-update:semver-patch"]
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
@@ -44,4 +48,5 @@ updates:
|
||||
interval: weekly
|
||||
ignore:
|
||||
- dependency-name: "*"
|
||||
update-types: ["version-update:semver-major"]
|
||||
update-types:
|
||||
["version-update:semver-major", "version-update:semver-patch"]
|
||||
|
||||
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
||||
strategy:
|
||||
matrix:
|
||||
node: ["16", "17", "18"]
|
||||
node: ["16", "17", "18", "20"]
|
||||
flavor: ["dev", "prod"]
|
||||
fail-fast: false
|
||||
runs-on: "ubuntu-latest"
|
||||
@@ -28,9 +28,7 @@ jobs:
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Install 1.70.x Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: 1.70
|
||||
uses: dtolnay/rust-toolchain@1.70
|
||||
|
||||
- name: Install
|
||||
if: ${{ matrix.flavor == 'dev'}}
|
||||
@@ -60,7 +58,7 @@ jobs:
|
||||
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
||||
strategy:
|
||||
matrix:
|
||||
node: ["16", "17", "18"]
|
||||
node: ["16", "17", "18", "20"]
|
||||
flavor: ["dev", "prod"]
|
||||
fail-fast: false
|
||||
runs-on: "ubuntu-latest"
|
||||
@@ -99,3 +97,6 @@ jobs:
|
||||
- name: Build
|
||||
run: npm run build
|
||||
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/frontend
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -13,7 +13,7 @@ the terms of (at your option) either:
|
||||
proxy statement published on <https://mempool.space/about>.
|
||||
|
||||
However, this copyright license does not include an implied right or license to
|
||||
use our trademarks: The Mempool Open Source Project™, mempool.space™, the
|
||||
use our trademarks: The Mempool Open Source Project®, mempool.space™, the
|
||||
mempool Logo™, the mempool.space Vertical Logo™, the mempool.space Horizontal
|
||||
Logo™, the mempool Square Logo™, and the mempool Blocks logo™ are registered
|
||||
trademarks or trademarks of Mempool Space K.K in Japan, the United States,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# The Mempool Open Source Project™ [](https://dashboard.cypress.io/projects/ry4br7/runs)
|
||||
# The Mempool Open Source Project® [](https://dashboard.cypress.io/projects/ry4br7/runs)
|
||||
|
||||
https://user-images.githubusercontent.com/93150691/226236121-375ea64f-b4a1-4cc0-8fad-a6fb33226840.mp4
|
||||
|
||||
|
||||
1
backend/.dockerignore
Normal file
1
backend/.dockerignore
Normal file
@@ -0,0 +1 @@
|
||||
Dockerfile
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
These instructions are mostly intended for developers.
|
||||
|
||||
If you choose to use these instructions for a production setup, be aware that you will still probably need to do additional configuration for your specific OS, environment, use-case, etc. We do our best here to provide a good starting point, but only proceed if you know what you're doing. Mempool does not provide support for custom setups.
|
||||
If you choose to use these instructions for a production setup, be aware that you will still probably need to do additional configuration for your specific OS, environment, use-case, etc. We do our best here to provide a good starting point, but only proceed if you know what you're doing. Mempool only provides support for custom setups to [enterprise sponsors](https://mempool.space/enterprise).
|
||||
|
||||
See other ways to set up Mempool on [the main README](/../../#installation-methods).
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"API_URL_PREFIX": "/api/v1/",
|
||||
"POLL_RATE_MS": 2000,
|
||||
"CACHE_DIR": "./cache",
|
||||
"CACHE_ENABLED": true,
|
||||
"CLEAR_PROTECTION_MINUTES": 20,
|
||||
"RECOMMENDED_FEE_PERCENTILE": 50,
|
||||
"BLOCK_WEIGHT_UNITS": 4000000,
|
||||
|
||||
477
backend/package-lock.json
generated
477
backend/package-lock.json
generated
@@ -12,16 +12,16 @@
|
||||
"@babel/core": "^7.21.3",
|
||||
"@mempool/electrum-client": "1.1.9",
|
||||
"@types/node": "^18.15.3",
|
||||
"axios": "~0.27.2",
|
||||
"bitcoinjs-lib": "~6.1.0",
|
||||
"axios": "~1.4.0",
|
||||
"bitcoinjs-lib": "~6.1.3",
|
||||
"crypto-js": "~4.1.1",
|
||||
"express": "~4.18.2",
|
||||
"maxmind": "~4.3.8",
|
||||
"mysql2": "~3.2.0",
|
||||
"node-worker-threads-pool": "~1.5.1",
|
||||
"maxmind": "~4.3.11",
|
||||
"mysql2": "~3.5.2",
|
||||
"rust-gbt": "file:./rust-gbt",
|
||||
"redis": "^4.6.6",
|
||||
"socks-proxy-agent": "~7.0.0",
|
||||
"typescript": "~4.7.4",
|
||||
"typescript": "~4.9.3",
|
||||
"ws": "~8.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -29,19 +29,28 @@
|
||||
"@babel/core": "^7.21.3",
|
||||
"@types/compression": "^1.7.2",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/express": "^4.17.15",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/ws": "~8.5.4",
|
||||
"@types/ws": "~8.5.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.55.0",
|
||||
"@typescript-eslint/parser": "^5.55.0",
|
||||
"eslint": "^8.36.0",
|
||||
"eslint-config-prettier": "^8.7.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"jest": "^29.5.0",
|
||||
"prettier": "^2.8.4",
|
||||
"ts-jest": "^29.0.5",
|
||||
"prettier": "^3.0.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.9.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@aashutoshrathi/word-wrap": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
|
||||
"integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
|
||||
@@ -1490,7 +1499,6 @@
|
||||
"version": "2.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.16.1.tgz",
|
||||
"integrity": "sha512-L0Gr5iEQIDEbvWdDr1HUaBOxBSHL1VZhWSk1oryawoT8qJIY+KGfLFelU+Qma64ivCPbxYpkfPoKYVG3rcoGIA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"napi": "scripts/index.js"
|
||||
},
|
||||
@@ -1548,6 +1556,64 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/bloom": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz",
|
||||
"integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==",
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/client": {
|
||||
"version": "1.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.7.tgz",
|
||||
"integrity": "sha512-gaOBOuJPjK5fGtxSseaKgSvjiZXQCdLlGg9WYQst+/GRUjmXaiB5kVkeQMRtPc7Q2t93XZcJfBMSwzs/XS9UZw==",
|
||||
"dependencies": {
|
||||
"cluster-key-slot": "1.1.2",
|
||||
"generic-pool": "3.9.0",
|
||||
"yallist": "4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/client/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"node_modules/@redis/graph": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.0.tgz",
|
||||
"integrity": "sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==",
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/json": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.4.tgz",
|
||||
"integrity": "sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==",
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/search": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.2.tgz",
|
||||
"integrity": "sha512-/cMfstG/fOh/SsE+4/BQGeuH/JJloeWuH+qJzM8dbxuWvdWibWAOAHHCZTMPhV3xIlH4/cUEIA8OV5QnYpaVoA==",
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/time-series": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.4.tgz",
|
||||
"integrity": "sha512-ThUIgo2U/g7cCuZavucQTQzA9g9JbDDY2f64u3AbAoz/8vE2lt2U37LamDUVChhaDA3IRT9R6VvJwqnUfTJzng==",
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sinclair/typebox": {
|
||||
"version": "0.25.24",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz",
|
||||
@@ -1795,9 +1861,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz",
|
||||
"integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==",
|
||||
"version": "8.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz",
|
||||
"integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
@@ -1865,9 +1931,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": {
|
||||
"version": "7.3.8",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
|
||||
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
@@ -2009,9 +2075,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
|
||||
"version": "7.3.8",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
|
||||
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
@@ -2068,9 +2134,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils/node_modules/semver": {
|
||||
"version": "7.3.8",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
|
||||
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
@@ -2255,12 +2321,13 @@
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
|
||||
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
|
||||
"integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.9",
|
||||
"form-data": "^4.0.0"
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest": {
|
||||
@@ -2449,9 +2516,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bitcoinjs-lib": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.1.tgz",
|
||||
"integrity": "sha512-FYihfgTk29lt1eK2y48OtuarEDUnTprNBW3ctT8yHiOhvmeS3DzAVG6gI0VCvMkydz6UdlXlYNWIPqGD0SUYRQ==",
|
||||
"version": "6.1.3",
|
||||
"resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.3.tgz",
|
||||
"integrity": "sha512-TYXs/Qf+GNk2nnsB9HrXWqzFuEgCg0Gx+v3UW3B8VuceFHXVvhT+7hRnTSvwkX0i8rz2rtujeU6gFaDcFqYFDw==",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "^1.2.0",
|
||||
"bech32": "^2.0.0",
|
||||
@@ -2710,6 +2777,14 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/cluster-key-slot": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
|
||||
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/co": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||
@@ -3670,6 +3745,14 @@
|
||||
"is-property": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/generic-pool": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz",
|
||||
"integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/gensync": {
|
||||
"version": "1.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||
@@ -5367,9 +5450,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jest-snapshot/node_modules/semver": {
|
||||
"version": "7.3.8",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
|
||||
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
@@ -5892,12 +5975,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/maxmind": {
|
||||
"version": "4.3.9",
|
||||
"resolved": "https://registry.npmjs.org/maxmind/-/maxmind-4.3.9.tgz",
|
||||
"integrity": "sha512-rEfIxZ9M2P7CWQQzN5/LapCawpf2DLh+LWD/cA7lNfCbFL6dNJOKgtynp8QbRsxExutn7Ofz1P1tXEdL3gnukw==",
|
||||
"version": "4.3.11",
|
||||
"resolved": "https://registry.npmjs.org/maxmind/-/maxmind-4.3.11.tgz",
|
||||
"integrity": "sha512-tJDrKbUzN6PSA88tWgg0L2R4Ln00XwecYQJPFI+RvlF2k1sx6VQYtuQ1SVxm8+bw5tF7GWV4xyb+3/KyzEpPUw==",
|
||||
"dependencies": {
|
||||
"mmdb-lib": "2.0.2",
|
||||
"tiny-lru": "10.3.0"
|
||||
"tiny-lru": "11.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12",
|
||||
@@ -6019,15 +6102,15 @@
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/mysql2": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.2.0.tgz",
|
||||
"integrity": "sha512-0Vn6a9WSrq6fWwvPgrvIwnOCldiEcgbzapVRDAtDZ4cMTxN7pnGqCTx8EG32S/NYXl6AXkdO+9hV1tSIi/LigA==",
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.5.2.tgz",
|
||||
"integrity": "sha512-cptobmhYkYeTBIFp2c0piw2+gElpioga1rUw5UidHvo8yaHijMZoo8A3zyBVoo/K71f7ZFvrShA9iMIy9dCzCA==",
|
||||
"dependencies": {
|
||||
"denque": "^2.1.0",
|
||||
"generate-function": "^2.3.1",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"long": "^5.2.1",
|
||||
"lru-cache": "^7.14.1",
|
||||
"lru-cache": "^8.0.0",
|
||||
"named-placeholders": "^1.1.3",
|
||||
"seq-queue": "^0.0.5",
|
||||
"sqlstring": "^2.3.2"
|
||||
@@ -6048,11 +6131,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mysql2/node_modules/lru-cache": {
|
||||
"version": "7.18.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
|
||||
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
|
||||
"version": "8.0.5",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz",
|
||||
"integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=16.14"
|
||||
}
|
||||
},
|
||||
"node_modules/named-placeholders": {
|
||||
@@ -6106,11 +6189,6 @@
|
||||
"integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/node-worker-threads-pool": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/node-worker-threads-pool/-/node-worker-threads-pool-1.5.1.tgz",
|
||||
"integrity": "sha512-7TXAhpMm+jO4MfESxYLtMGSnJWv+itdNHMdaFmeZuPXxwFGU90mtEB42BciUULXOUAxYBfXILAuvrSG3rQZ7mw=="
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
@@ -6176,17 +6254,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/optionator": {
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
|
||||
"integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
|
||||
"version": "0.9.3",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
|
||||
"integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@aashutoshrathi/word-wrap": "^1.2.3",
|
||||
"deep-is": "^0.1.3",
|
||||
"fast-levenshtein": "^2.0.6",
|
||||
"levn": "^0.4.1",
|
||||
"prelude-ls": "^1.2.1",
|
||||
"type-check": "^0.4.0",
|
||||
"word-wrap": "^1.2.3"
|
||||
"type-check": "^0.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
@@ -6417,15 +6495,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "2.8.7",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz",
|
||||
"integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==",
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz",
|
||||
"integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"prettier": "bin-prettier.js"
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
@@ -6482,6 +6560,11 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
|
||||
@@ -6569,6 +6652,19 @@
|
||||
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/redis": {
|
||||
"version": "4.6.6",
|
||||
"resolved": "https://registry.npmjs.org/redis/-/redis-4.6.6.tgz",
|
||||
"integrity": "sha512-aLs2fuBFV/VJ28oLBqYykfnhGGkFxvx0HdCEBYdJ99FFbSEMZ7c1nVKwR6ZRv+7bb7JnC0mmCzaqu8frgOYhpA==",
|
||||
"dependencies": {
|
||||
"@redis/bloom": "1.2.0",
|
||||
"@redis/client": "1.5.7",
|
||||
"@redis/graph": "1.1.0",
|
||||
"@redis/json": "1.0.4",
|
||||
"@redis/search": "1.1.2",
|
||||
"@redis/time-series": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
@@ -6711,9 +6807,9 @@
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -7050,9 +7146,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tiny-lru": {
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-10.3.0.tgz",
|
||||
"integrity": "sha512-vTKRT2AEO1sViFDWAIzZVpV8KURCaMtnHa4RZB3XqtYLbrTO/fLDXKPEX9kVWq9u+nZREkwakbcmzGgvJm8QKA==",
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.0.1.tgz",
|
||||
"integrity": "sha512-iNgFugVuQgBKrqeO/mpiTTgmBsTP0WL6yeuLfLs/Ctf0pI/ixGqIRm8sDCwMcXGe9WWvt2sGXI5mNqZbValmJg==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@@ -7093,9 +7189,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ts-jest": {
|
||||
"version": "29.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.5.tgz",
|
||||
"integrity": "sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==",
|
||||
"version": "29.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz",
|
||||
"integrity": "sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"bs-logger": "0.x",
|
||||
@@ -7104,7 +7200,7 @@
|
||||
"json5": "^2.2.3",
|
||||
"lodash.memoize": "4.x",
|
||||
"make-error": "1.x",
|
||||
"semver": "7.x",
|
||||
"semver": "^7.5.3",
|
||||
"yargs-parser": "^21.0.1"
|
||||
},
|
||||
"bin": {
|
||||
@@ -7118,7 +7214,7 @@
|
||||
"@jest/types": "^29.0.0",
|
||||
"babel-jest": "^29.0.0",
|
||||
"jest": "^29.0.0",
|
||||
"typescript": ">=4.3"
|
||||
"typescript": ">=4.3 <6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@babel/core": {
|
||||
@@ -7148,9 +7244,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ts-jest/node_modules/semver": {
|
||||
"version": "7.3.8",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
|
||||
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
@@ -7283,9 +7379,9 @@
|
||||
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.7.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
|
||||
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -7405,15 +7501,6 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
||||
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
@@ -7568,7 +7655,7 @@
|
||||
},
|
||||
"rust-gbt": {
|
||||
"name": "gbt",
|
||||
"version": "0.1.0",
|
||||
"version": "3.0.0-dev",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@napi-rs/cli": "^2.16.1"
|
||||
@@ -7579,6 +7666,12 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@aashutoshrathi/word-wrap": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
|
||||
"integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
|
||||
"dev": true
|
||||
},
|
||||
"@ampproject/remapping": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
|
||||
@@ -8666,8 +8759,7 @@
|
||||
"@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==",
|
||||
"dev": true
|
||||
"integrity": "sha512-L0Gr5iEQIDEbvWdDr1HUaBOxBSHL1VZhWSk1oryawoT8qJIY+KGfLFelU+Qma64ivCPbxYpkfPoKYVG3rcoGIA=="
|
||||
},
|
||||
"@noble/hashes": {
|
||||
"version": "1.3.0",
|
||||
@@ -8700,6 +8792,53 @@
|
||||
"fastq": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"@redis/bloom": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz",
|
||||
"integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==",
|
||||
"requires": {}
|
||||
},
|
||||
"@redis/client": {
|
||||
"version": "1.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.7.tgz",
|
||||
"integrity": "sha512-gaOBOuJPjK5fGtxSseaKgSvjiZXQCdLlGg9WYQst+/GRUjmXaiB5kVkeQMRtPc7Q2t93XZcJfBMSwzs/XS9UZw==",
|
||||
"requires": {
|
||||
"cluster-key-slot": "1.1.2",
|
||||
"generic-pool": "3.9.0",
|
||||
"yallist": "4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@redis/graph": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.0.tgz",
|
||||
"integrity": "sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==",
|
||||
"requires": {}
|
||||
},
|
||||
"@redis/json": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.4.tgz",
|
||||
"integrity": "sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==",
|
||||
"requires": {}
|
||||
},
|
||||
"@redis/search": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.2.tgz",
|
||||
"integrity": "sha512-/cMfstG/fOh/SsE+4/BQGeuH/JJloeWuH+qJzM8dbxuWvdWibWAOAHHCZTMPhV3xIlH4/cUEIA8OV5QnYpaVoA==",
|
||||
"requires": {}
|
||||
},
|
||||
"@redis/time-series": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.4.tgz",
|
||||
"integrity": "sha512-ThUIgo2U/g7cCuZavucQTQzA9g9JbDDY2f64u3AbAoz/8vE2lt2U37LamDUVChhaDA3IRT9R6VvJwqnUfTJzng==",
|
||||
"requires": {}
|
||||
},
|
||||
"@sinclair/typebox": {
|
||||
"version": "0.25.24",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz",
|
||||
@@ -8947,9 +9086,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@types/ws": {
|
||||
"version": "8.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz",
|
||||
"integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==",
|
||||
"version": "8.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz",
|
||||
"integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
@@ -8998,9 +9137,9 @@
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.3.8",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
|
||||
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
@@ -9079,9 +9218,9 @@
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.3.8",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
|
||||
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
@@ -9121,9 +9260,9 @@
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.3.8",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
|
||||
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
@@ -9258,12 +9397,13 @@
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
|
||||
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
|
||||
"integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.14.9",
|
||||
"form-data": "^4.0.0"
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"babel-jest": {
|
||||
@@ -9409,9 +9549,9 @@
|
||||
"integrity": "sha512-lkc0XyiX9E9KiVAS1ZiOqK1xfiwvf4FXDDdkDq5crcDzOq+xGytY+14qCsqz7kCiy8rpN1CRNfacRhf9G3JNSA=="
|
||||
},
|
||||
"bitcoinjs-lib": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.1.tgz",
|
||||
"integrity": "sha512-FYihfgTk29lt1eK2y48OtuarEDUnTprNBW3ctT8yHiOhvmeS3DzAVG6gI0VCvMkydz6UdlXlYNWIPqGD0SUYRQ==",
|
||||
"version": "6.1.3",
|
||||
"resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.3.tgz",
|
||||
"integrity": "sha512-TYXs/Qf+GNk2nnsB9HrXWqzFuEgCg0Gx+v3UW3B8VuceFHXVvhT+7hRnTSvwkX0i8rz2rtujeU6gFaDcFqYFDw==",
|
||||
"requires": {
|
||||
"@noble/hashes": "^1.2.0",
|
||||
"bech32": "^2.0.0",
|
||||
@@ -9599,6 +9739,11 @@
|
||||
"wrap-ansi": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"cluster-key-slot": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
|
||||
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="
|
||||
},
|
||||
"co": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||
@@ -10327,6 +10472,11 @@
|
||||
"is-property": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"generic-pool": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz",
|
||||
"integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g=="
|
||||
},
|
||||
"gensync": {
|
||||
"version": "1.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||
@@ -11577,9 +11727,9 @@
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.3.8",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
|
||||
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
@@ -11973,12 +12123,12 @@
|
||||
}
|
||||
},
|
||||
"maxmind": {
|
||||
"version": "4.3.9",
|
||||
"resolved": "https://registry.npmjs.org/maxmind/-/maxmind-4.3.9.tgz",
|
||||
"integrity": "sha512-rEfIxZ9M2P7CWQQzN5/LapCawpf2DLh+LWD/cA7lNfCbFL6dNJOKgtynp8QbRsxExutn7Ofz1P1tXEdL3gnukw==",
|
||||
"version": "4.3.11",
|
||||
"resolved": "https://registry.npmjs.org/maxmind/-/maxmind-4.3.11.tgz",
|
||||
"integrity": "sha512-tJDrKbUzN6PSA88tWgg0L2R4Ln00XwecYQJPFI+RvlF2k1sx6VQYtuQ1SVxm8+bw5tF7GWV4xyb+3/KyzEpPUw==",
|
||||
"requires": {
|
||||
"mmdb-lib": "2.0.2",
|
||||
"tiny-lru": "10.3.0"
|
||||
"tiny-lru": "11.0.1"
|
||||
}
|
||||
},
|
||||
"media-typer": {
|
||||
@@ -12062,15 +12212,15 @@
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"mysql2": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.2.0.tgz",
|
||||
"integrity": "sha512-0Vn6a9WSrq6fWwvPgrvIwnOCldiEcgbzapVRDAtDZ4cMTxN7pnGqCTx8EG32S/NYXl6AXkdO+9hV1tSIi/LigA==",
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.5.2.tgz",
|
||||
"integrity": "sha512-cptobmhYkYeTBIFp2c0piw2+gElpioga1rUw5UidHvo8yaHijMZoo8A3zyBVoo/K71f7ZFvrShA9iMIy9dCzCA==",
|
||||
"requires": {
|
||||
"denque": "^2.1.0",
|
||||
"generate-function": "^2.3.1",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"long": "^5.2.1",
|
||||
"lru-cache": "^7.14.1",
|
||||
"lru-cache": "^8.0.0",
|
||||
"named-placeholders": "^1.1.3",
|
||||
"seq-queue": "^0.0.5",
|
||||
"sqlstring": "^2.3.2"
|
||||
@@ -12085,9 +12235,9 @@
|
||||
}
|
||||
},
|
||||
"lru-cache": {
|
||||
"version": "7.18.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
|
||||
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="
|
||||
"version": "8.0.5",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz",
|
||||
"integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -12135,11 +12285,6 @@
|
||||
"integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==",
|
||||
"dev": true
|
||||
},
|
||||
"node-worker-threads-pool": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/node-worker-threads-pool/-/node-worker-threads-pool-1.5.1.tgz",
|
||||
"integrity": "sha512-7TXAhpMm+jO4MfESxYLtMGSnJWv+itdNHMdaFmeZuPXxwFGU90mtEB42BciUULXOUAxYBfXILAuvrSG3rQZ7mw=="
|
||||
},
|
||||
"normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
@@ -12187,17 +12332,17 @@
|
||||
}
|
||||
},
|
||||
"optionator": {
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
|
||||
"integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
|
||||
"version": "0.9.3",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
|
||||
"integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@aashutoshrathi/word-wrap": "^1.2.3",
|
||||
"deep-is": "^0.1.3",
|
||||
"fast-levenshtein": "^2.0.6",
|
||||
"levn": "^0.4.1",
|
||||
"prelude-ls": "^1.2.1",
|
||||
"type-check": "^0.4.0",
|
||||
"word-wrap": "^1.2.3"
|
||||
"type-check": "^0.4.0"
|
||||
}
|
||||
},
|
||||
"p-limit": {
|
||||
@@ -12358,9 +12503,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"prettier": {
|
||||
"version": "2.8.7",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz",
|
||||
"integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==",
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz",
|
||||
"integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==",
|
||||
"dev": true
|
||||
},
|
||||
"pretty-format": {
|
||||
@@ -12401,6 +12546,11 @@
|
||||
"ipaddr.js": "1.9.1"
|
||||
}
|
||||
},
|
||||
"proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"punycode": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
|
||||
@@ -12449,6 +12599,19 @@
|
||||
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
|
||||
"dev": true
|
||||
},
|
||||
"redis": {
|
||||
"version": "4.6.6",
|
||||
"resolved": "https://registry.npmjs.org/redis/-/redis-4.6.6.tgz",
|
||||
"integrity": "sha512-aLs2fuBFV/VJ28oLBqYykfnhGGkFxvx0HdCEBYdJ99FFbSEMZ7c1nVKwR6ZRv+7bb7JnC0mmCzaqu8frgOYhpA==",
|
||||
"requires": {
|
||||
"@redis/bloom": "1.2.0",
|
||||
"@redis/client": "1.5.7",
|
||||
"@redis/graph": "1.1.0",
|
||||
"@redis/json": "1.0.4",
|
||||
"@redis/search": "1.1.2",
|
||||
"@redis/time-series": "1.0.4"
|
||||
}
|
||||
},
|
||||
"require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
@@ -12536,9 +12699,9 @@
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true
|
||||
},
|
||||
"send": {
|
||||
@@ -12801,9 +12964,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"tiny-lru": {
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-10.3.0.tgz",
|
||||
"integrity": "sha512-vTKRT2AEO1sViFDWAIzZVpV8KURCaMtnHa4RZB3XqtYLbrTO/fLDXKPEX9kVWq9u+nZREkwakbcmzGgvJm8QKA=="
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.0.1.tgz",
|
||||
"integrity": "sha512-iNgFugVuQgBKrqeO/mpiTTgmBsTP0WL6yeuLfLs/Ctf0pI/ixGqIRm8sDCwMcXGe9WWvt2sGXI5mNqZbValmJg=="
|
||||
},
|
||||
"tmpl": {
|
||||
"version": "1.0.5",
|
||||
@@ -12832,9 +12995,9 @@
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
|
||||
},
|
||||
"ts-jest": {
|
||||
"version": "29.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.5.tgz",
|
||||
"integrity": "sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==",
|
||||
"version": "29.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz",
|
||||
"integrity": "sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"bs-logger": "0.x",
|
||||
@@ -12843,7 +13006,7 @@
|
||||
"json5": "^2.2.3",
|
||||
"lodash.memoize": "4.x",
|
||||
"make-error": "1.x",
|
||||
"semver": "7.x",
|
||||
"semver": "^7.5.3",
|
||||
"yargs-parser": "^21.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -12857,9 +13020,9 @@
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.3.8",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
|
||||
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
@@ -12945,9 +13108,9 @@
|
||||
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.7.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
|
||||
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ=="
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g=="
|
||||
},
|
||||
"unpipe": {
|
||||
"version": "1.0.0",
|
||||
@@ -13026,12 +13189,6 @@
|
||||
"isexe": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"word-wrap": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
||||
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
|
||||
"dev": true
|
||||
},
|
||||
"wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
|
||||
@@ -40,33 +40,33 @@
|
||||
"@babel/core": "^7.21.3",
|
||||
"@mempool/electrum-client": "1.1.9",
|
||||
"@types/node": "^18.15.3",
|
||||
"axios": "~0.27.2",
|
||||
"bitcoinjs-lib": "~6.1.0",
|
||||
"axios": "~1.4.0",
|
||||
"bitcoinjs-lib": "~6.1.3",
|
||||
"crypto-js": "~4.1.1",
|
||||
"express": "~4.18.2",
|
||||
"maxmind": "~4.3.8",
|
||||
"mysql2": "~3.2.0",
|
||||
"node-worker-threads-pool": "~1.5.1",
|
||||
"maxmind": "~4.3.11",
|
||||
"mysql2": "~3.5.2",
|
||||
"rust-gbt": "file:./rust-gbt",
|
||||
"redis": "^4.6.6",
|
||||
"socks-proxy-agent": "~7.0.0",
|
||||
"typescript": "~4.7.4",
|
||||
"typescript": "~4.9.3",
|
||||
"ws": "~8.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.3",
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
"@babel/core": "^7.21.3",
|
||||
"@types/compression": "^1.7.2",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/express": "^4.17.15",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/ws": "~8.5.4",
|
||||
"@types/ws": "~8.5.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.55.0",
|
||||
"@typescript-eslint/parser": "^5.55.0",
|
||||
"eslint": "^8.36.0",
|
||||
"eslint-config-prettier": "^8.7.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"jest": "^29.5.0",
|
||||
"prettier": "^2.8.4",
|
||||
"ts-jest": "^29.0.5",
|
||||
"prettier": "^3.0.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.9.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"AUTOMATIC_BLOCK_REINDEXING": false,
|
||||
"POLL_RATE_MS": 3,
|
||||
"CACHE_DIR": "__MEMPOOL_CACHE_DIR__",
|
||||
"CACHE_ENABLED": true,
|
||||
"CLEAR_PROTECTION_MINUTES": 4,
|
||||
"RECOMMENDED_FEE_PERCENTILE": 5,
|
||||
"BLOCK_WEIGHT_UNITS": 6,
|
||||
@@ -127,5 +128,9 @@
|
||||
"AUDIT": false,
|
||||
"AUDIT_START_HEIGHT": 774000,
|
||||
"SERVERS": []
|
||||
},
|
||||
"REDIS": {
|
||||
"ENABLED": false,
|
||||
"UNIX_SOCKET_PATH": "/tmp/redis.sock"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ describe('Mempool Backend Config', () => {
|
||||
AUTOMATIC_BLOCK_REINDEXING: false,
|
||||
POLL_RATE_MS: 2000,
|
||||
CACHE_DIR: './cache',
|
||||
CACHE_ENABLED: true,
|
||||
CLEAR_PROTECTION_MINUTES: 20,
|
||||
RECOMMENDED_FEE_PERCENTILE: 50,
|
||||
BLOCK_WEIGHT_UNITS: 4000000,
|
||||
@@ -127,6 +128,11 @@ describe('Mempool Backend Config', () => {
|
||||
AUDIT_START_HEIGHT: 774000,
|
||||
SERVERS: []
|
||||
});
|
||||
|
||||
expect(config.REDIS).toStrictEqual({
|
||||
ENABLED: false,
|
||||
UNIX_SOCKET_PATH: ''
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -160,6 +166,8 @@ describe('Mempool Backend Config', () => {
|
||||
expect(config.PRICE_DATA_SERVER).toStrictEqual(fixture.PRICE_DATA_SERVER);
|
||||
|
||||
expect(config.EXTERNAL_DATA_SERVER).toStrictEqual(fixture.EXTERNAL_DATA_SERVER);
|
||||
|
||||
expect(config.REDIS).toStrictEqual(fixture.REDIS);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -173,12 +181,12 @@ describe('Mempool Backend Config', () => {
|
||||
// We have a few cases where we can't follow the pattern
|
||||
if (root === 'MEMPOOL' && key === 'HTTP_PORT') {
|
||||
console.log('skipping check for MEMPOOL_HTTP_PORT');
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
switch (typeof value) {
|
||||
case 'object': {
|
||||
if (Array.isArray(value)) {
|
||||
return;
|
||||
continue;
|
||||
} else {
|
||||
parseJson(value, key);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ class Audit {
|
||||
const matches: string[] = []; // present in both mined block and template
|
||||
const added: string[] = []; // present in mined block, not in template
|
||||
const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
|
||||
const fullrbf: string[] = []; // either missing or present, and part of a fullrbf replacement
|
||||
const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
|
||||
const isCensored = {}; // missing, without excuse
|
||||
const isDisplaced = {};
|
||||
let displacedWeight = 0;
|
||||
@@ -36,8 +36,9 @@ class Audit {
|
||||
// look for transactions that were expected in the template, but missing from the mined block
|
||||
for (const txid of projectedBlocks[0].transactionIds) {
|
||||
if (!inBlock[txid]) {
|
||||
if (rbfCache.isFullRbf(txid)) {
|
||||
fullrbf.push(txid);
|
||||
// allow missing transactions which either belong to a full rbf tree, or conflict with any transaction in the mined block
|
||||
if (rbfCache.has(txid) && (rbfCache.isFullRbf(txid) || rbfCache.anyInSameTree(txid, (tx) => inBlock[tx.txid]))) {
|
||||
rbf.push(txid);
|
||||
} else if (mempool[txid]?.firstSeen != null && (now - (mempool[txid]?.firstSeen || 0)) <= PROPAGATION_MARGIN) {
|
||||
// tx is recent, may have reached the miner too late for inclusion
|
||||
fresh.push(txid);
|
||||
@@ -98,8 +99,8 @@ class Audit {
|
||||
if (inTemplate[tx.txid]) {
|
||||
matches.push(tx.txid);
|
||||
} else {
|
||||
if (rbfCache.isFullRbf(tx.txid)) {
|
||||
fullrbf.push(tx.txid);
|
||||
if (rbfCache.has(tx.txid)) {
|
||||
rbf.push(tx.txid);
|
||||
} else if (!isDisplaced[tx.txid]) {
|
||||
added.push(tx.txid);
|
||||
}
|
||||
@@ -147,7 +148,7 @@ class Audit {
|
||||
added,
|
||||
fresh,
|
||||
sigop: [],
|
||||
fullrbf,
|
||||
fullrbf: rbf,
|
||||
score,
|
||||
similarity,
|
||||
};
|
||||
|
||||
@@ -7,7 +7,6 @@ import { SocksProxyAgent } from 'socks-proxy-agent';
|
||||
import { BisqBlocks, BisqBlock, BisqTransaction, BisqStats, BisqTrade } from './interfaces';
|
||||
import { Common } from '../common';
|
||||
import { BlockExtended } from '../../mempool.interfaces';
|
||||
import { StaticPool } from 'node-worker-threads-pool';
|
||||
import backendInfo from '../backend-info';
|
||||
import logger from '../../logger';
|
||||
|
||||
@@ -31,10 +30,6 @@ class Bisq {
|
||||
private priceUpdateCallbackFunction: ((price: number) => void) | undefined;
|
||||
private topDirectoryWatcher: fs.FSWatcher | undefined;
|
||||
private subdirectoryWatcher: fs.FSWatcher | undefined;
|
||||
private jsonParsePool = new StaticPool({
|
||||
size: 4,
|
||||
task: (blob: string) => JSON.parse(blob),
|
||||
});
|
||||
|
||||
constructor() {}
|
||||
|
||||
|
||||
@@ -3,10 +3,12 @@ import { IEsploraApi } from './esplora-api.interface';
|
||||
export interface AbstractBitcoinApi {
|
||||
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>;
|
||||
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>;
|
||||
$getMempoolTransactions(lastTxid: string);
|
||||
$getTransactionHex(txId: string): Promise<string>;
|
||||
$getBlockHeightTip(): Promise<number>;
|
||||
$getBlockHashTip(): Promise<string>;
|
||||
$getTxIdsForBlock(hash: string): Promise<string[]>;
|
||||
$getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]>;
|
||||
$getBlockHash(height: number): Promise<string>;
|
||||
$getBlockHeader(hash: string): Promise<string>;
|
||||
$getBlock(hash: string): Promise<IEsploraApi.Block>;
|
||||
@@ -14,6 +16,8 @@ export interface AbstractBitcoinApi {
|
||||
$getAddress(address: string): Promise<IEsploraApi.Address>;
|
||||
$getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
|
||||
$getAddressPrefix(prefix: string): string[];
|
||||
$getScriptHash(scripthash: string): Promise<IEsploraApi.ScriptHash>;
|
||||
$getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
|
||||
$sendRawTransaction(rawTransaction: string): Promise<string>;
|
||||
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
|
||||
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { IEsploraApi } from './esplora-api.interface';
|
||||
import blocks from '../blocks';
|
||||
import mempool from '../mempool';
|
||||
import { TransactionExtended } from '../../mempool.interfaces';
|
||||
import transactionUtils from '../transaction-utils';
|
||||
|
||||
class BitcoinApi implements AbstractBitcoinApi {
|
||||
private rawMempoolCache: IBitcoinApi.RawMempool | null = null;
|
||||
@@ -59,9 +60,20 @@ class BitcoinApi implements AbstractBitcoinApi {
|
||||
});
|
||||
}
|
||||
|
||||
$getTransactionHex(txId: string): Promise<string> {
|
||||
return this.$getRawTransaction(txId, true)
|
||||
.then((tx) => tx.hex || '');
|
||||
$getMempoolTransactions(lastTxid: string): Promise<IEsploraApi.Transaction[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
async $getTransactionHex(txId: string): Promise<string> {
|
||||
const txInMempool = mempool.getMempool()[txId];
|
||||
if (txInMempool && txInMempool.hex) {
|
||||
return txInMempool.hex;
|
||||
}
|
||||
|
||||
return this.bitcoindClient.getRawTransaction(txId, true)
|
||||
.then((transaction: IBitcoinApi.Transaction) => {
|
||||
return transaction.hex;
|
||||
});
|
||||
}
|
||||
|
||||
$getBlockHeightTip(): Promise<number> {
|
||||
@@ -77,6 +89,10 @@ class BitcoinApi implements AbstractBitcoinApi {
|
||||
.then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx);
|
||||
}
|
||||
|
||||
$getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]> {
|
||||
throw new Error('Method getTxsForBlock not supported by the Bitcoin RPC API.');
|
||||
}
|
||||
|
||||
$getRawBlock(hash: string): Promise<Buffer> {
|
||||
return this.bitcoindClient.getBlock(hash, 0)
|
||||
.then((raw: string) => Buffer.from(raw, "hex"));
|
||||
@@ -108,6 +124,14 @@ class BitcoinApi implements AbstractBitcoinApi {
|
||||
throw new Error('Method getAddressTransactions not supported by the Bitcoin RPC API.');
|
||||
}
|
||||
|
||||
$getScriptHash(scripthash: string): Promise<IEsploraApi.ScriptHash> {
|
||||
throw new Error('Method getScriptHash not supported by the Bitcoin RPC API.');
|
||||
}
|
||||
|
||||
$getScriptHashTransactions(scripthash: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]> {
|
||||
throw new Error('Method getScriptHashTransactions not supported by the Bitcoin RPC API.');
|
||||
}
|
||||
|
||||
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> {
|
||||
return this.bitcoindClient.getRawMemPool();
|
||||
}
|
||||
@@ -193,7 +217,7 @@ class BitcoinApi implements AbstractBitcoinApi {
|
||||
scriptpubkey: vout.scriptPubKey.hex,
|
||||
scriptpubkey_address: vout.scriptPubKey && vout.scriptPubKey.address ? vout.scriptPubKey.address
|
||||
: vout.scriptPubKey.addresses ? vout.scriptPubKey.addresses[0] : '',
|
||||
scriptpubkey_asm: vout.scriptPubKey.asm ? this.convertScriptSigAsm(vout.scriptPubKey.hex) : '',
|
||||
scriptpubkey_asm: vout.scriptPubKey.asm ? transactionUtils.convertScriptSigAsm(vout.scriptPubKey.hex) : '',
|
||||
scriptpubkey_type: this.translateScriptPubKeyType(vout.scriptPubKey.type),
|
||||
};
|
||||
});
|
||||
@@ -203,7 +227,7 @@ class BitcoinApi implements AbstractBitcoinApi {
|
||||
is_coinbase: !!vin.coinbase,
|
||||
prevout: null,
|
||||
scriptsig: vin.scriptSig && vin.scriptSig.hex || vin.coinbase || '',
|
||||
scriptsig_asm: vin.scriptSig && this.convertScriptSigAsm(vin.scriptSig.hex) || '',
|
||||
scriptsig_asm: vin.scriptSig && transactionUtils.convertScriptSigAsm(vin.scriptSig.hex) || '',
|
||||
sequence: vin.sequence,
|
||||
txid: vin.txid || '',
|
||||
vout: vin.vout || 0,
|
||||
@@ -275,7 +299,7 @@ class BitcoinApi implements AbstractBitcoinApi {
|
||||
}
|
||||
const innerTx = await this.$getRawTransaction(vin.txid, false, false);
|
||||
vin.prevout = innerTx.vout[vin.vout];
|
||||
this.addInnerScriptsToVin(vin);
|
||||
transactionUtils.addInnerScriptsToVin(vin);
|
||||
}
|
||||
return transaction;
|
||||
}
|
||||
@@ -314,7 +338,7 @@ class BitcoinApi implements AbstractBitcoinApi {
|
||||
}
|
||||
const innerTx = await this.$getRawTransaction(transaction.vin[i].txid, false, false);
|
||||
transaction.vin[i].prevout = innerTx.vout[transaction.vin[i].vout];
|
||||
this.addInnerScriptsToVin(transaction.vin[i]);
|
||||
transactionUtils.addInnerScriptsToVin(transaction.vin[i]);
|
||||
totalIn += innerTx.vout[transaction.vin[i].vout].value;
|
||||
}
|
||||
if (lazyPrevouts && transaction.vin.length > 12) {
|
||||
@@ -326,122 +350,6 @@ class BitcoinApi implements AbstractBitcoinApi {
|
||||
return transaction;
|
||||
}
|
||||
|
||||
private convertScriptSigAsm(hex: string): string {
|
||||
const buf = Buffer.from(hex, 'hex');
|
||||
|
||||
const b: string[] = [];
|
||||
|
||||
let i = 0;
|
||||
while (i < buf.length) {
|
||||
const op = buf[i];
|
||||
if (op >= 0x01 && op <= 0x4e) {
|
||||
i++;
|
||||
let push: number;
|
||||
if (op === 0x4c) {
|
||||
push = buf.readUInt8(i);
|
||||
b.push('OP_PUSHDATA1');
|
||||
i += 1;
|
||||
} else if (op === 0x4d) {
|
||||
push = buf.readUInt16LE(i);
|
||||
b.push('OP_PUSHDATA2');
|
||||
i += 2;
|
||||
} else if (op === 0x4e) {
|
||||
push = buf.readUInt32LE(i);
|
||||
b.push('OP_PUSHDATA4');
|
||||
i += 4;
|
||||
} else {
|
||||
push = op;
|
||||
b.push('OP_PUSHBYTES_' + push);
|
||||
}
|
||||
|
||||
const data = buf.slice(i, i + push);
|
||||
if (data.length !== push) {
|
||||
break;
|
||||
}
|
||||
|
||||
b.push(data.toString('hex'));
|
||||
i += data.length;
|
||||
} else {
|
||||
if (op === 0x00) {
|
||||
b.push('OP_0');
|
||||
} else if (op === 0x4f) {
|
||||
b.push('OP_PUSHNUM_NEG1');
|
||||
} else if (op === 0xb1) {
|
||||
b.push('OP_CLTV');
|
||||
} else if (op === 0xb2) {
|
||||
b.push('OP_CSV');
|
||||
} else if (op === 0xba) {
|
||||
b.push('OP_CHECKSIGADD');
|
||||
} else {
|
||||
const opcode = bitcoinjs.script.toASM([ op ]);
|
||||
if (opcode && op < 0xfd) {
|
||||
if (/^OP_(\d+)$/.test(opcode)) {
|
||||
b.push(opcode.replace(/^OP_(\d+)$/, 'OP_PUSHNUM_$1'));
|
||||
} else {
|
||||
b.push(opcode);
|
||||
}
|
||||
} else {
|
||||
b.push('OP_RETURN_' + op);
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return b.join(' ');
|
||||
}
|
||||
|
||||
private addInnerScriptsToVin(vin: IEsploraApi.Vin): void {
|
||||
if (!vin.prevout) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (vin.prevout.scriptpubkey_type === 'p2sh') {
|
||||
const redeemScript = vin.scriptsig_asm.split(' ').reverse()[0];
|
||||
vin.inner_redeemscript_asm = this.convertScriptSigAsm(redeemScript);
|
||||
if (vin.witness && vin.witness.length > 2) {
|
||||
const witnessScript = vin.witness[vin.witness.length - 1];
|
||||
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
|
||||
}
|
||||
}
|
||||
|
||||
if (vin.prevout.scriptpubkey_type === 'v0_p2wsh' && vin.witness) {
|
||||
const witnessScript = vin.witness[vin.witness.length - 1];
|
||||
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
|
||||
}
|
||||
|
||||
if (vin.prevout.scriptpubkey_type === 'v1_p2tr' && vin.witness) {
|
||||
const witnessScript = this.witnessToP2TRScript(vin.witness);
|
||||
if (witnessScript !== null) {
|
||||
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function must only be called when we know the witness we are parsing
|
||||
* is a taproot witness.
|
||||
* @param witness An array of hex strings that represents the witness stack of
|
||||
* the input.
|
||||
* @returns null if the witness is not a script spend, and the hex string of
|
||||
* the script item if it is a script spend.
|
||||
*/
|
||||
private witnessToP2TRScript(witness: string[]): string | null {
|
||||
if (witness.length < 2) return null;
|
||||
// Note: see BIP341 for parsing details of witness stack
|
||||
|
||||
// If there are at least two witness elements, and the first byte of the
|
||||
// last element is 0x50, this last element is called annex a and
|
||||
// is removed from the witness stack.
|
||||
const hasAnnex = witness[witness.length - 1].substring(0, 2) === '50';
|
||||
// If there are at least two witness elements left, script path spending is used.
|
||||
// Call the second-to-last stack element s, the script.
|
||||
// (Note: this phrasing from BIP341 assumes we've *removed* the annex from the stack)
|
||||
if (hasAnnex && witness.length < 3) return null;
|
||||
const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2;
|
||||
return witness[positionOfScript];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default BitcoinApi;
|
||||
|
||||
@@ -6,7 +6,7 @@ import websocketHandler from '../websocket-handler';
|
||||
import mempool from '../mempool';
|
||||
import feeApi from '../fee-api';
|
||||
import mempoolBlocks from '../mempool-blocks';
|
||||
import bitcoinApi, { bitcoinCoreApi } from './bitcoin-api-factory';
|
||||
import bitcoinApi from './bitcoin-api-factory';
|
||||
import { Common } from '../common';
|
||||
import backendInfo from '../backend-info';
|
||||
import transactionUtils from '../transaction-utils';
|
||||
@@ -121,6 +121,8 @@ 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 + 'scripthash/:scripthash', this.getScriptHash)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'scripthash/:scripthash/txs', this.getScriptHashTransactions)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'address-prefix/:prefix', this.getAddressPrefix)
|
||||
;
|
||||
}
|
||||
@@ -481,7 +483,7 @@ class BitcoinRoutes {
|
||||
returnBlocks.push(localBlock);
|
||||
nextHash = localBlock.previousblockhash;
|
||||
} else {
|
||||
const block = await bitcoinCoreApi.$getBlock(nextHash);
|
||||
const block = await bitcoinApi.$getBlock(nextHash);
|
||||
returnBlocks.push(block);
|
||||
nextHash = block.previousblockhash;
|
||||
}
|
||||
@@ -567,6 +569,45 @@ class BitcoinRoutes {
|
||||
}
|
||||
}
|
||||
|
||||
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.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const addressData = await bitcoinApi.$getScriptHash(req.params.scripthash);
|
||||
res.json(addressData);
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
||||
return res.status(413).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async getScriptHashTransactions(req: Request, res: Response): Promise<void> {
|
||||
if (config.MEMPOOL.BACKEND === 'none') {
|
||||
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let lastTxId: string = '';
|
||||
if (req.query.after_txid && typeof req.query.after_txid === 'string') {
|
||||
lastTxId = req.query.after_txid;
|
||||
}
|
||||
const transactions = await bitcoinApi.$getScriptHashTransactions(req.params.scripthash, lastTxId);
|
||||
res.json(transactions);
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
||||
res.status(413).send(e instanceof Error ? e.message : e);
|
||||
return;
|
||||
}
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async getAddressPrefix(req: Request, res: Response) {
|
||||
try {
|
||||
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
|
||||
|
||||
@@ -126,6 +126,77 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
|
||||
}
|
||||
}
|
||||
|
||||
async $getScriptHash(scripthash: string): Promise<IEsploraApi.ScriptHash> {
|
||||
try {
|
||||
const balance = await this.electrumClient.blockchainScripthash_getBalance(scripthash);
|
||||
let history = memoryCache.get<IElectrumApi.ScriptHashHistory[]>('Scripthash_getHistory', scripthash);
|
||||
if (!history) {
|
||||
history = await this.electrumClient.blockchainScripthash_getHistory(scripthash);
|
||||
memoryCache.set('Scripthash_getHistory', scripthash, history, 2);
|
||||
}
|
||||
|
||||
const unconfirmed = history ? history.filter((h) => h.fee).length : 0;
|
||||
|
||||
return {
|
||||
'scripthash': scripthash,
|
||||
'chain_stats': {
|
||||
'funded_txo_count': 0,
|
||||
'funded_txo_sum': balance.confirmed ? balance.confirmed : 0,
|
||||
'spent_txo_count': 0,
|
||||
'spent_txo_sum': balance.confirmed < 0 ? balance.confirmed : 0,
|
||||
'tx_count': (history?.length || 0) - unconfirmed,
|
||||
},
|
||||
'mempool_stats': {
|
||||
'funded_txo_count': 0,
|
||||
'funded_txo_sum': balance.unconfirmed > 0 ? balance.unconfirmed : 0,
|
||||
'spent_txo_count': 0,
|
||||
'spent_txo_sum': balance.unconfirmed < 0 ? -balance.unconfirmed : 0,
|
||||
'tx_count': unconfirmed,
|
||||
},
|
||||
'electrum': true,
|
||||
};
|
||||
} catch (e: any) {
|
||||
throw new Error(typeof e === 'string' ? e : e && e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
async $getScriptHashTransactions(scripthash: string, lastSeenTxId?: string): Promise<IEsploraApi.Transaction[]> {
|
||||
try {
|
||||
loadingIndicators.setProgress('address-' + scripthash, 0);
|
||||
|
||||
const transactions: IEsploraApi.Transaction[] = [];
|
||||
let history = memoryCache.get<IElectrumApi.ScriptHashHistory[]>('Scripthash_getHistory', scripthash);
|
||||
if (!history) {
|
||||
history = await this.electrumClient.blockchainScripthash_getHistory(scripthash);
|
||||
memoryCache.set('Scripthash_getHistory', scripthash, history, 2);
|
||||
}
|
||||
if (!history) {
|
||||
throw new Error('failed to get scripthash history');
|
||||
}
|
||||
history.sort((a, b) => (b.height || 9999999) - (a.height || 9999999));
|
||||
|
||||
let startingIndex = 0;
|
||||
if (lastSeenTxId) {
|
||||
const pos = history.findIndex((historicalTx) => historicalTx.tx_hash === lastSeenTxId);
|
||||
if (pos) {
|
||||
startingIndex = pos + 1;
|
||||
}
|
||||
}
|
||||
const endIndex = Math.min(startingIndex + 10, history.length);
|
||||
|
||||
for (let i = startingIndex; i < endIndex; i++) {
|
||||
const tx = await this.$getRawTransaction(history[i].tx_hash, false, true);
|
||||
transactions.push(tx);
|
||||
loadingIndicators.setProgress('address-' + scripthash, (i + 1) / endIndex * 100);
|
||||
}
|
||||
|
||||
return transactions;
|
||||
} catch (e: any) {
|
||||
loadingIndicators.setProgress('address-' + scripthash, 100);
|
||||
throw new Error(typeof e === 'string' ? e : e && e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
private $getScriptHashBalance(scriptHash: string): Promise<IElectrumApi.ScriptHashBalance> {
|
||||
return this.electrumClient.blockchainScripthash_getBalance(this.encodeScriptHash(scriptHash));
|
||||
}
|
||||
|
||||
@@ -99,6 +99,13 @@ export namespace IEsploraApi {
|
||||
electrum?: boolean;
|
||||
}
|
||||
|
||||
export interface ScriptHash {
|
||||
scripthash: string;
|
||||
chain_stats: ChainStats;
|
||||
mempool_stats: MempoolStats;
|
||||
electrum?: boolean;
|
||||
}
|
||||
|
||||
export interface ChainStats {
|
||||
funded_txo_count: number;
|
||||
funded_txo_sum: number;
|
||||
|
||||
@@ -69,6 +69,10 @@ class ElectrsApi implements AbstractBitcoinApi {
|
||||
return this.$queryWrapper<IEsploraApi.Transaction>(config.ESPLORA.REST_API_URL + '/tx/' + txId);
|
||||
}
|
||||
|
||||
async $getMempoolTransactions(lastSeenTxid?: string): Promise<IEsploraApi.Transaction[]> {
|
||||
return this.$queryWrapper<IEsploraApi.Transaction[]>(config.ESPLORA.REST_API_URL + '/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : ''));
|
||||
}
|
||||
|
||||
$getTransactionHex(txId: string): Promise<string> {
|
||||
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/hex');
|
||||
}
|
||||
@@ -85,6 +89,10 @@ class ElectrsApi implements AbstractBitcoinApi {
|
||||
return this.$queryWrapper<string[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids');
|
||||
}
|
||||
|
||||
$getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]> {
|
||||
return this.$queryWrapper<IEsploraApi.Transaction[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txs');
|
||||
}
|
||||
|
||||
$getBlockHash(height: number): Promise<string> {
|
||||
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/block-height/' + height);
|
||||
}
|
||||
@@ -110,6 +118,14 @@ class ElectrsApi implements AbstractBitcoinApi {
|
||||
throw new Error('Method getAddressTransactions not implemented.');
|
||||
}
|
||||
|
||||
$getScriptHash(scripthash: string): Promise<IEsploraApi.ScriptHash> {
|
||||
throw new Error('Method getScriptHash not implemented.');
|
||||
}
|
||||
|
||||
$getScriptHashTransactions(scripthash: string, txId?: string): Promise<IEsploraApi.Transaction[]> {
|
||||
throw new Error('Method getScriptHashTransactions not implemented.');
|
||||
}
|
||||
|
||||
$getAddressPrefix(prefix: string): string[] {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ import PricesRepository from '../repositories/PricesRepository';
|
||||
import priceUpdater from '../tasks/price-updater';
|
||||
import chainTips from './chain-tips';
|
||||
import websocketHandler from './websocket-handler';
|
||||
import redisCache from './redis-cache';
|
||||
import rbfCache from './rbf-cache';
|
||||
|
||||
class Blocks {
|
||||
private blocks: BlockExtended[] = [];
|
||||
@@ -70,6 +72,9 @@ class Blocks {
|
||||
* @param blockHash
|
||||
* @param blockHeight
|
||||
* @param onlyCoinbase - Set to true if you only need the coinbase transaction
|
||||
* @param txIds - optional ordered list of transaction ids if already known
|
||||
* @param quiet - don't print non-essential logs
|
||||
* @param addMempoolData - calculate sigops etc
|
||||
* @returns Promise<TransactionExtended[]>
|
||||
*/
|
||||
private async $getTransactionsExtended(
|
||||
@@ -80,62 +85,98 @@ class Blocks {
|
||||
quiet: boolean = false,
|
||||
addMempoolData: boolean = false,
|
||||
): Promise<TransactionExtended[]> {
|
||||
const transactions: TransactionExtended[] = [];
|
||||
const isEsplora = config.MEMPOOL.BACKEND === 'esplora';
|
||||
const transactionMap: { [txid: string]: TransactionExtended } = {};
|
||||
|
||||
if (!txIds) {
|
||||
txIds = await bitcoinApi.$getTxIdsForBlock(blockHash);
|
||||
}
|
||||
|
||||
const mempool = memPool.getMempool();
|
||||
let transactionsFound = 0;
|
||||
let transactionsFetched = 0;
|
||||
let foundInMempool = 0;
|
||||
let totalFound = 0;
|
||||
|
||||
for (let i = 0; i < txIds.length; i++) {
|
||||
if (mempool[txIds[i]]) {
|
||||
// We update blocks before the mempool (index.ts), therefore we can
|
||||
// optimize here by directly fetching txs in the "outdated" mempool
|
||||
transactions.push(mempool[txIds[i]]);
|
||||
transactionsFound++;
|
||||
} else if (config.MEMPOOL.BACKEND === 'esplora' || !memPool.hasPriority() || i === 0) {
|
||||
// Otherwise we fetch the tx data through backend services (esplora, electrum, core rpc...)
|
||||
if (!quiet && (i % (Math.round((txIds.length) / 10)) === 0 || i + 1 === txIds.length)) { // Avoid log spam
|
||||
logger.debug(`Indexing tx ${i + 1} of ${txIds.length} in block #${blockHeight}`);
|
||||
}
|
||||
try {
|
||||
const tx = await transactionUtils.$getTransactionExtended(txIds[i], false, false, false, addMempoolData);
|
||||
transactions.push(tx);
|
||||
transactionsFetched++;
|
||||
} catch (e) {
|
||||
try {
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
// Try again with core
|
||||
const tx = await transactionUtils.$getTransactionExtended(txIds[i], false, false, true, addMempoolData);
|
||||
transactions.push(tx);
|
||||
transactionsFetched++;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
} catch (e) {
|
||||
if (i === 0) {
|
||||
const msg = `Cannot fetch coinbase tx ${txIds[i]}. Reason: ` + (e instanceof Error ? e.message : e);
|
||||
logger.err(msg);
|
||||
throw new Error(msg);
|
||||
} else {
|
||||
logger.err(`Cannot fetch tx ${txIds[i]}. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
// Copy existing transactions from the mempool
|
||||
if (!onlyCoinbase) {
|
||||
for (const txid of txIds) {
|
||||
if (mempool[txid]) {
|
||||
transactionMap[txid] = mempool[txid];
|
||||
foundInMempool++;
|
||||
totalFound++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (onlyCoinbase === true) {
|
||||
break; // Fetch the first transaction and exit
|
||||
if (onlyCoinbase) {
|
||||
try {
|
||||
const coinbase = await transactionUtils.$getTransactionExtendedRetry(txIds[0], false, false, false, addMempoolData);
|
||||
if (coinbase && coinbase.vin[0].is_coinbase) {
|
||||
return [coinbase];
|
||||
} else {
|
||||
const msg = `Expected a coinbase tx, but the backend API returned something else`;
|
||||
logger.err(msg);
|
||||
throw new Error(msg);
|
||||
}
|
||||
} catch (e) {
|
||||
const msg = `Cannot fetch coinbase tx ${txIds[0]}. Reason: ` + (e instanceof Error ? e.message : e);
|
||||
logger.err(msg);
|
||||
throw new Error(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch remaining txs in bulk
|
||||
if (isEsplora && (txIds.length - totalFound > 500)) {
|
||||
try {
|
||||
const rawTransactions = await bitcoinApi.$getTxsForBlock(blockHash);
|
||||
for (const tx of rawTransactions) {
|
||||
if (!transactionMap[tx.txid]) {
|
||||
transactionMap[tx.txid] = addMempoolData ? transactionUtils.extendMempoolTransaction(tx) : transactionUtils.extendTransaction(tx);
|
||||
totalFound++;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.err(`Cannot fetch bulk txs for block ${blockHash}. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch remaining txs individually
|
||||
for (const txid of txIds.filter(txid => !transactionMap[txid])) {
|
||||
if (!quiet && (totalFound % (Math.round((txIds.length) / 10)) === 0 || totalFound + 1 === txIds.length)) { // Avoid log spam
|
||||
logger.debug(`Indexing tx ${totalFound + 1} of ${txIds.length} in block #${blockHeight}`);
|
||||
}
|
||||
try {
|
||||
const tx = await transactionUtils.$getTransactionExtendedRetry(txid, false, false, false, addMempoolData);
|
||||
transactionMap[txid] = tx;
|
||||
totalFound++;
|
||||
} catch (e) {
|
||||
const msg = `Cannot fetch tx ${txid}. Reason: ` + (e instanceof Error ? e.message : e);
|
||||
logger.err(msg);
|
||||
throw new Error(msg);
|
||||
}
|
||||
}
|
||||
|
||||
if (!quiet) {
|
||||
logger.debug(`${transactionsFound} of ${txIds.length} found in mempool. ${transactionsFetched} fetched through backend service.`);
|
||||
logger.debug(`${foundInMempool} of ${txIds.length} found in mempool. ${totalFound - foundInMempool} fetched through backend service.`);
|
||||
}
|
||||
|
||||
return transactions;
|
||||
// Require the first transaction to be a coinbase
|
||||
const coinbase = transactionMap[txIds[0]];
|
||||
if (!coinbase || !coinbase.vin[0].is_coinbase) {
|
||||
const msg = `Expected first tx in a block to be a coinbase, but found something else`;
|
||||
logger.err(msg);
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
// Require all transactions to be present
|
||||
// (we should have thrown an error already if a tx request failed)
|
||||
if (txIds.some(txid => !transactionMap[txid])) {
|
||||
const msg = `Failed to fetch ${txIds.length - totalFound} transactions from block`;
|
||||
logger.err(msg);
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
// Return list of transactions, preserving block order
|
||||
return txIds.map(txid => transactionMap[txid]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -171,7 +212,9 @@ class Blocks {
|
||||
|
||||
private convertLiquidFees(block: IBitcoinApi.VerboseBlock): IBitcoinApi.VerboseBlock {
|
||||
block.tx.forEach(tx => {
|
||||
tx.fee = Object.values(tx.fee || {}).reduce((total, output) => total + output, 0);
|
||||
if (!isFinite(Number(tx.fee))) {
|
||||
tx.fee = Object.values(tx.fee || {}).reduce((total, output) => total + output, 0);
|
||||
}
|
||||
});
|
||||
return block;
|
||||
}
|
||||
@@ -376,8 +419,8 @@ class Blocks {
|
||||
let newlyIndexed = 0;
|
||||
let totalIndexed = indexedBlockSummariesHashesArray.length;
|
||||
let indexedThisRun = 0;
|
||||
let timer = new Date().getTime() / 1000;
|
||||
const startedAt = new Date().getTime() / 1000;
|
||||
let timer = Date.now() / 1000;
|
||||
const startedAt = Date.now() / 1000;
|
||||
|
||||
for (const block of indexedBlocks) {
|
||||
if (indexedBlockSummariesHashes[block.hash] === true) {
|
||||
@@ -385,17 +428,24 @@ class Blocks {
|
||||
}
|
||||
|
||||
// Logging
|
||||
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
|
||||
const elapsedSeconds = (Date.now() / 1000) - timer;
|
||||
if (elapsedSeconds > 5) {
|
||||
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
||||
const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds);
|
||||
const runningFor = (Date.now() / 1000) - startedAt;
|
||||
const blockPerSeconds = indexedThisRun / elapsedSeconds;
|
||||
const progress = Math.round(totalIndexed / indexedBlocks.length * 10000) / 100;
|
||||
logger.debug(`Indexing block summary for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexedBlocks.length} (${progress}%) | elapsed: ${runningFor} seconds`, logger.tags.mining);
|
||||
timer = new Date().getTime() / 1000;
|
||||
logger.debug(`Indexing block summary for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexedBlocks.length} (${progress}%) | elapsed: ${runningFor.toFixed(2)} seconds`, logger.tags.mining);
|
||||
timer = Date.now() / 1000;
|
||||
indexedThisRun = 0;
|
||||
}
|
||||
|
||||
await this.$getStrippedBlockTransactions(block.hash, true, true); // This will index the block summary
|
||||
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||
const cpfpSummary = await this.$indexCPFP(block.hash, block.height, txs);
|
||||
await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary
|
||||
} else {
|
||||
await this.$getStrippedBlockTransactions(block.hash, true, true); // This will index the block summary
|
||||
}
|
||||
|
||||
// Logging
|
||||
indexedThisRun++;
|
||||
@@ -434,18 +484,18 @@ class Blocks {
|
||||
// Logging
|
||||
let count = 0;
|
||||
let countThisRun = 0;
|
||||
let timer = new Date().getTime() / 1000;
|
||||
const startedAt = new Date().getTime() / 1000;
|
||||
let timer = Date.now() / 1000;
|
||||
const startedAt = Date.now() / 1000;
|
||||
for (const height of unindexedBlockHeights) {
|
||||
// Logging
|
||||
const hash = await bitcoinApi.$getBlockHash(height);
|
||||
const elapsedSeconds = Math.max(1, new Date().getTime() / 1000 - timer);
|
||||
const elapsedSeconds = (Date.now() / 1000) - timer;
|
||||
if (elapsedSeconds > 5) {
|
||||
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
||||
const blockPerSeconds = (countThisRun / elapsedSeconds);
|
||||
const runningFor = (Date.now() / 1000) - startedAt;
|
||||
const blockPerSeconds = countThisRun / elapsedSeconds;
|
||||
const progress = Math.round(count / unindexedBlockHeights.length * 10000) / 100;
|
||||
logger.debug(`Indexing cpfp clusters for #${height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${count}/${unindexedBlockHeights.length} (${progress}%) | elapsed: ${runningFor} seconds`);
|
||||
timer = new Date().getTime() / 1000;
|
||||
logger.debug(`Indexing cpfp clusters for #${height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${count}/${unindexedBlockHeights.length} (${progress}%) | elapsed: ${runningFor.toFixed(2)} seconds`);
|
||||
timer = Date.now() / 1000;
|
||||
countThisRun = 0;
|
||||
}
|
||||
|
||||
@@ -524,8 +574,8 @@ class Blocks {
|
||||
let totalIndexed = await blocksRepository.$blockCountBetweenHeight(currentBlockHeight, lastBlockToIndex);
|
||||
let indexedThisRun = 0;
|
||||
let newlyIndexed = 0;
|
||||
const startedAt = new Date().getTime() / 1000;
|
||||
let timer = new Date().getTime() / 1000;
|
||||
const startedAt = Date.now() / 1000;
|
||||
let timer = Date.now() / 1000;
|
||||
|
||||
while (currentBlockHeight >= lastBlockToIndex) {
|
||||
const endBlock = Math.max(0, lastBlockToIndex, currentBlockHeight - chunkSize + 1);
|
||||
@@ -545,18 +595,18 @@ class Blocks {
|
||||
}
|
||||
++indexedThisRun;
|
||||
++totalIndexed;
|
||||
const elapsedSeconds = Math.max(1, new Date().getTime() / 1000 - timer);
|
||||
const elapsedSeconds = (Date.now() / 1000) - timer;
|
||||
if (elapsedSeconds > 5 || blockHeight === lastBlockToIndex) {
|
||||
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
||||
const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds);
|
||||
const runningFor = (Date.now() / 1000) - startedAt;
|
||||
const blockPerSeconds = indexedThisRun / elapsedSeconds;
|
||||
const progress = Math.round(totalIndexed / indexingBlockAmount * 10000) / 100;
|
||||
logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress}%) | elapsed: ${runningFor} seconds`, logger.tags.mining);
|
||||
timer = new Date().getTime() / 1000;
|
||||
logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress.toFixed(2)}%) | elapsed: ${runningFor.toFixed(2)} seconds`, logger.tags.mining);
|
||||
timer = Date.now() / 1000;
|
||||
indexedThisRun = 0;
|
||||
loadingIndicators.setProgress('block-indexing', progress, false);
|
||||
}
|
||||
const blockHash = await bitcoinApi.$getBlockHash(blockHeight);
|
||||
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(blockHash);
|
||||
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(blockHash);
|
||||
const transactions = await this.$getTransactionsExtended(blockHash, block.height, true, null, true);
|
||||
const blockExtended = await this.$getBlockExtended(block, transactions);
|
||||
|
||||
@@ -613,7 +663,7 @@ class Blocks {
|
||||
const heightDiff = blockHeightTip % 2016;
|
||||
const blockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff);
|
||||
this.updateTimerProgress(timer, 'got block hash for initial difficulty adjustment');
|
||||
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(blockHash);
|
||||
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(blockHash);
|
||||
this.updateTimerProgress(timer, 'got block for initial difficulty adjustment');
|
||||
this.lastDifficultyAdjustmentTime = block.timestamp;
|
||||
this.currentDifficulty = block.difficulty;
|
||||
@@ -621,7 +671,7 @@ class Blocks {
|
||||
if (blockHeightTip >= 2016) {
|
||||
const previousPeriodBlockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff - 2016);
|
||||
this.updateTimerProgress(timer, 'got previous block hash for initial difficulty adjustment');
|
||||
const previousPeriodBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(previousPeriodBlockHash);
|
||||
const previousPeriodBlock: IEsploraApi.Block = await bitcoinApi.$getBlock(previousPeriodBlockHash);
|
||||
this.updateTimerProgress(timer, 'got previous block for initial difficulty adjustment');
|
||||
this.previousDifficultyRetarget = (block.difficulty - previousPeriodBlock.difficulty) / previousPeriodBlock.difficulty * 100;
|
||||
logger.debug(`Initial difficulty adjustment data set.`);
|
||||
@@ -647,14 +697,14 @@ class Blocks {
|
||||
const block = BitcoinApi.convertBlock(verboseBlock);
|
||||
const txIds: string[] = verboseBlock.tx.map(tx => tx.txid);
|
||||
const transactions = await this.$getTransactionsExtended(blockHash, block.height, false, txIds, false, true) as MempoolTransactionExtended[];
|
||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||
// fill in missing transaction fee data from verboseBlock
|
||||
for (let i = 0; i < transactions.length; i++) {
|
||||
if (!transactions[i].fee && transactions[i].txid === verboseBlock.tx[i].txid) {
|
||||
transactions[i].fee = verboseBlock.tx[i].fee * 100_000_000;
|
||||
}
|
||||
|
||||
// fill in missing transaction fee data from verboseBlock
|
||||
for (let i = 0; i < transactions.length; i++) {
|
||||
if (!transactions[i].fee && transactions[i].txid === verboseBlock.tx[i].txid) {
|
||||
transactions[i].fee = verboseBlock.tx[i].fee * 100_000_000;
|
||||
}
|
||||
}
|
||||
|
||||
const cpfpSummary: CpfpSummary = Common.calculateCpfp(block.height, transactions);
|
||||
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
|
||||
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions);
|
||||
@@ -763,10 +813,18 @@ class Blocks {
|
||||
if (this.newBlockCallbacks.length) {
|
||||
this.newBlockCallbacks.forEach((cb) => cb(blockExtended, txIds, transactions));
|
||||
}
|
||||
if (!memPool.hasPriority() && (block.height % config.MEMPOOL.DISK_CACHE_BLOCK_INTERVAL === 0)) {
|
||||
if (config.MEMPOOL.CACHE_ENABLED && !memPool.hasPriority() && (block.height % config.MEMPOOL.DISK_CACHE_BLOCK_INTERVAL === 0)) {
|
||||
diskCache.$saveCacheToDisk();
|
||||
}
|
||||
|
||||
// Update Redis cache
|
||||
if (config.REDIS.ENABLED) {
|
||||
await redisCache.$updateBlocks(this.blocks);
|
||||
await redisCache.$updateBlockSummaries(this.blockSummaries);
|
||||
await redisCache.$removeTransactions(txIds);
|
||||
await rbfCache.updateCache();
|
||||
}
|
||||
|
||||
handledBlocks++;
|
||||
}
|
||||
|
||||
@@ -811,7 +869,7 @@ class Blocks {
|
||||
}
|
||||
|
||||
const blockHash = await bitcoinApi.$getBlockHash(height);
|
||||
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(blockHash);
|
||||
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(blockHash);
|
||||
const transactions = await this.$getTransactionsExtended(blockHash, block.height, true);
|
||||
const blockExtended = await this.$getBlockExtended(block, transactions);
|
||||
|
||||
@@ -823,7 +881,7 @@ class Blocks {
|
||||
}
|
||||
|
||||
public async $indexStaleBlock(hash: string): Promise<BlockExtended> {
|
||||
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(hash);
|
||||
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(hash);
|
||||
const transactions = await this.$getTransactionsExtended(hash, block.height, true);
|
||||
const blockExtended = await this.$getBlockExtended(block, transactions);
|
||||
|
||||
@@ -848,7 +906,7 @@ class Blocks {
|
||||
}
|
||||
|
||||
// Bitcoin network, add our custom data on top
|
||||
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(hash);
|
||||
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(hash);
|
||||
if (block.stale) {
|
||||
return await this.$indexStaleBlock(hash);
|
||||
} else {
|
||||
@@ -877,7 +935,7 @@ class Blocks {
|
||||
|
||||
let height = blockHeight;
|
||||
let summary: BlockSummary;
|
||||
if (cpfpSummary) {
|
||||
if (cpfpSummary && !Common.isLiquid()) {
|
||||
summary = {
|
||||
id: hash,
|
||||
transactions: cpfpSummary.transactions.map(tx => {
|
||||
@@ -891,10 +949,15 @@ class Blocks {
|
||||
}),
|
||||
};
|
||||
} else {
|
||||
// Call Core RPC
|
||||
const block = await bitcoinClient.getBlock(hash, 2);
|
||||
summary = this.summarizeBlock(block);
|
||||
height = block.height;
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||
summary = this.summarizeBlockTransactions(hash, txs);
|
||||
} else {
|
||||
// Call Core RPC
|
||||
const block = await bitcoinClient.getBlock(hash, 2);
|
||||
summary = this.summarizeBlock(block);
|
||||
height = block.height;
|
||||
}
|
||||
}
|
||||
if (height == null) {
|
||||
const block = await bitcoinApi.$getBlock(hash);
|
||||
@@ -1017,8 +1080,17 @@ class Blocks {
|
||||
if (Common.blocksSummariesIndexingEnabled() && cleanBlock.fee_amt_percentiles === null) {
|
||||
cleanBlock.fee_amt_percentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(cleanBlock.hash);
|
||||
if (cleanBlock.fee_amt_percentiles === null) {
|
||||
const block = await bitcoinClient.getBlock(cleanBlock.hash, 2);
|
||||
const summary = this.summarizeBlock(block);
|
||||
|
||||
let summary;
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||
summary = this.summarizeBlockTransactions(cleanBlock.hash, txs);
|
||||
} else {
|
||||
// Call Core RPC
|
||||
const block = await bitcoinClient.getBlock(cleanBlock.hash, 2);
|
||||
summary = this.summarizeBlock(block);
|
||||
}
|
||||
|
||||
await BlocksSummariesRepository.$saveTransactions(cleanBlock.height, cleanBlock.hash, summary.transactions);
|
||||
cleanBlock.fee_amt_percentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(cleanBlock.hash);
|
||||
}
|
||||
@@ -1078,19 +1150,29 @@ class Blocks {
|
||||
return this.currentBlockHeight;
|
||||
}
|
||||
|
||||
public async $indexCPFP(hash: string, height: number): Promise<void> {
|
||||
const block = await bitcoinClient.getBlock(hash, 2);
|
||||
const transactions = block.tx.map(tx => {
|
||||
tx.fee *= 100_000_000;
|
||||
return tx;
|
||||
});
|
||||
public async $indexCPFP(hash: string, height: number, txs?: TransactionExtended[]): Promise<CpfpSummary> {
|
||||
let transactions = txs;
|
||||
if (!transactions) {
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
transactions = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||
}
|
||||
if (!transactions) {
|
||||
const block = await bitcoinClient.getBlock(hash, 2);
|
||||
transactions = block.tx.map(tx => {
|
||||
tx.fee *= 100_000_000;
|
||||
return tx;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const summary = Common.calculateCpfp(height, transactions);
|
||||
const summary = Common.calculateCpfp(height, transactions as TransactionExtended[]);
|
||||
|
||||
await this.$saveCpfp(hash, height, summary);
|
||||
|
||||
const effectiveFeeStats = Common.calcEffectiveFeeStatistics(summary.transactions);
|
||||
await blocksRepository.$saveEffectiveFeeStats(hash, effectiveFeeStats);
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
public async $saveCpfp(hash: string, height: number, cpfpSummary: CpfpSummary): Promise<void> {
|
||||
|
||||
@@ -29,7 +29,7 @@ class DiskCache {
|
||||
};
|
||||
|
||||
constructor() {
|
||||
if (!cluster.isPrimary) {
|
||||
if (!cluster.isPrimary || !config.MEMPOOL.CACHE_ENABLED) {
|
||||
return;
|
||||
}
|
||||
process.on('SIGINT', (e) => {
|
||||
@@ -39,7 +39,7 @@ class DiskCache {
|
||||
}
|
||||
|
||||
async $saveCacheToDisk(sync: boolean = false): Promise<void> {
|
||||
if (!cluster.isPrimary) {
|
||||
if (!cluster.isPrimary || !config.MEMPOOL.CACHE_ENABLED) {
|
||||
return;
|
||||
}
|
||||
if (this.isWritingCache) {
|
||||
@@ -175,10 +175,11 @@ class DiskCache {
|
||||
}
|
||||
|
||||
async $loadMempoolCache(): Promise<void> {
|
||||
if (!fs.existsSync(DiskCache.FILE_NAME)) {
|
||||
if (!config.MEMPOOL.CACHE_ENABLED || !fs.existsSync(DiskCache.FILE_NAME)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const start = Date.now();
|
||||
let data: any = {};
|
||||
const cacheData = fs.readFileSync(DiskCache.FILE_NAME, 'utf8');
|
||||
if (cacheData) {
|
||||
@@ -220,6 +221,8 @@ class DiskCache {
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Loaded mempool from disk cache in ${Date.now() - start} ms`);
|
||||
|
||||
await memPool.$setMempool(data.mempool);
|
||||
if (!this.ignoreBlocksCache) {
|
||||
blocks.setBlocks(data.blocks);
|
||||
|
||||
@@ -80,7 +80,7 @@ class ChannelsApi {
|
||||
|
||||
public async $searchChannelsById(search: string): Promise<any[]> {
|
||||
try {
|
||||
const searchStripped = search.replace('%', '') + '%';
|
||||
const searchStripped = search.replace(/[^0-9x]/g, '') + '%';
|
||||
const query = `SELECT id, short_id, capacity, status FROM channels WHERE id LIKE ? OR short_id LIKE ? LIMIT 10`;
|
||||
const [rows]: any = await DB.query(query, [searchStripped, searchStripped]);
|
||||
return rows;
|
||||
@@ -117,6 +117,26 @@ class ChannelsApi {
|
||||
}
|
||||
}
|
||||
|
||||
public async $getPenaltyClosedChannels(): Promise<any[]> {
|
||||
try {
|
||||
const query = `
|
||||
SELECT n1.alias AS alias_left,
|
||||
n2.alias AS alias_right,
|
||||
channels.*
|
||||
FROM channels
|
||||
LEFT JOIN nodes AS n1 ON n1.public_key = channels.node1_public_key
|
||||
LEFT JOIN nodes AS n2 ON n2.public_key = channels.node2_public_key
|
||||
WHERE channels.status = 2 AND channels.closing_reason = 3
|
||||
ORDER BY closing_date DESC
|
||||
`;
|
||||
const [rows]: any = await DB.query(query);
|
||||
return rows;
|
||||
} catch (e) {
|
||||
logger.err('$getPenaltyClosedChannels error: ' + (e instanceof Error ? e.message : e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public async $getUnresolvedClosedChannels(): Promise<any[]> {
|
||||
try {
|
||||
const query = `SELECT * FROM channels WHERE status = 2 AND closing_reason = 2 AND closing_resolved = 0 AND closing_transaction_id != ''`;
|
||||
|
||||
@@ -11,6 +11,7 @@ class ChannelsRoutes {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels/search/:search', this.$searchChannelsById)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels/:short_id', this.$getChannel)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels', this.$getChannelsForNode)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/penalties', this.$getPenaltyClosedChannels)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels-geo', this.$getAllChannelsGeo)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels-geo/:publicKey', this.$getAllChannelsGeo)
|
||||
;
|
||||
@@ -108,6 +109,18 @@ class ChannelsRoutes {
|
||||
}
|
||||
}
|
||||
|
||||
private async $getPenaltyClosedChannels(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const channels = await channelsApi.$getPenaltyClosedChannels();
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(channels);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async $getAllChannelsGeo(req: Request, res: Response) {
|
||||
try {
|
||||
const style: string = typeof req.query.style === 'string' ? req.query.style : '';
|
||||
|
||||
@@ -217,7 +217,7 @@ async function buildFullChannel(clChannelA: any, clChannelB: any): Promise<ILigh
|
||||
|
||||
return {
|
||||
channel_id: Common.channelShortIdToIntegerId(clChannelA.short_channel_id),
|
||||
capacity: clChannelA.satoshis,
|
||||
capacity: (clChannelA.amount_msat / 1000).toString(),
|
||||
last_update: lastUpdate,
|
||||
node1_policy: convertPolicy(clChannelA),
|
||||
node2_policy: convertPolicy(clChannelB),
|
||||
@@ -241,7 +241,7 @@ async function buildIncompleteChannel(clChannel: any): Promise<ILightningApi.Cha
|
||||
|
||||
return {
|
||||
channel_id: Common.channelShortIdToIntegerId(clChannel.short_channel_id),
|
||||
capacity: clChannel.satoshis,
|
||||
capacity: (clChannel.amount_msat / 1000).toString(),
|
||||
last_update: clChannel.last_update ?? 0,
|
||||
node1_policy: convertPolicy(clChannel),
|
||||
node2_policy: null,
|
||||
@@ -257,8 +257,8 @@ async function buildIncompleteChannel(clChannel: any): Promise<ILightningApi.Cha
|
||||
function convertPolicy(clChannel: any): ILightningApi.RoutingPolicy {
|
||||
return {
|
||||
time_lock_delta: clChannel.delay,
|
||||
min_htlc: clChannel.htlc_minimum_msat.slice(0, -4),
|
||||
max_htlc_msat: clChannel.htlc_maximum_msat.slice(0, -4),
|
||||
min_htlc: clChannel.htlc_minimum_msat.toString(),
|
||||
max_htlc_msat: clChannel.htlc_maximum_msat.toString(),
|
||||
fee_base_msat: clChannel.base_fee_millisatoshi,
|
||||
fee_rate_milli_msat: clChannel.fee_per_millionth,
|
||||
disabled: !clChannel.active,
|
||||
|
||||
@@ -9,6 +9,7 @@ import loadingIndicators from './loading-indicators';
|
||||
import bitcoinClient from './bitcoin/bitcoin-client';
|
||||
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
||||
import rbfCache from './rbf-cache';
|
||||
import redisCache from './redis-cache';
|
||||
|
||||
class Mempool {
|
||||
private inSync: boolean = false;
|
||||
@@ -85,14 +86,25 @@ class Mempool {
|
||||
public async $setMempool(mempoolData: { [txId: string]: MempoolTransactionExtended }) {
|
||||
this.mempoolCache = mempoolData;
|
||||
let count = 0;
|
||||
const redisTimer = Date.now();
|
||||
if (config.MEMPOOL.CACHE_ENABLED && config.REDIS.ENABLED) {
|
||||
logger.debug(`Migrating ${Object.keys(this.mempoolCache).length} transactions from disk cache to Redis cache`);
|
||||
}
|
||||
for (const txid of Object.keys(this.mempoolCache)) {
|
||||
if (this.mempoolCache[txid].sigops == null || this.mempoolCache[txid].effectiveFeePerVsize == null) {
|
||||
if (!this.mempoolCache[txid].sigops || this.mempoolCache[txid].effectiveFeePerVsize == null) {
|
||||
this.mempoolCache[txid] = transactionUtils.extendMempoolTransaction(this.mempoolCache[txid]);
|
||||
}
|
||||
if (this.mempoolCache[txid].order == null) {
|
||||
this.mempoolCache[txid].order = transactionUtils.txidToOrdering(txid);
|
||||
}
|
||||
count++;
|
||||
if (config.MEMPOOL.CACHE_ENABLED && config.REDIS.ENABLED) {
|
||||
await redisCache.$addTransaction(this.mempoolCache[txid]);
|
||||
}
|
||||
}
|
||||
if (config.MEMPOOL.CACHE_ENABLED && config.REDIS.ENABLED) {
|
||||
await redisCache.$flushTransactions();
|
||||
logger.debug(`Finished migrating cache transactions in ${((Date.now() - redisTimer) / 1000).toFixed(2)} seconds`);
|
||||
}
|
||||
if (this.mempoolChangedCallback) {
|
||||
this.mempoolChangedCallback(this.mempoolCache, [], []);
|
||||
@@ -103,6 +115,44 @@ class Mempool {
|
||||
this.addToSpendMap(Object.values(this.mempoolCache));
|
||||
}
|
||||
|
||||
public async $reloadMempool(expectedCount: number): Promise<MempoolTransactionExtended[]> {
|
||||
let count = 0;
|
||||
let done = false;
|
||||
let last_txid;
|
||||
const newTransactions: MempoolTransactionExtended[] = [];
|
||||
loadingIndicators.setProgress('mempool', count / expectedCount * 100);
|
||||
while (!done) {
|
||||
try {
|
||||
const result = await bitcoinApi.$getMempoolTransactions(last_txid);
|
||||
if (result) {
|
||||
for (const tx of result) {
|
||||
const extendedTransaction = transactionUtils.extendMempoolTransaction(tx);
|
||||
if (!this.mempoolCache[extendedTransaction.txid]) {
|
||||
newTransactions.push(extendedTransaction);
|
||||
this.mempoolCache[extendedTransaction.txid] = extendedTransaction;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
logger.info(`Fetched ${count} of ${expectedCount} mempool transactions from esplora`);
|
||||
if (result.length > 0) {
|
||||
last_txid = result[result.length - 1].txid;
|
||||
} else {
|
||||
done = true;
|
||||
}
|
||||
if (Math.floor((count / expectedCount) * 100) < 100) {
|
||||
loadingIndicators.setProgress('mempool', count / expectedCount * 100);
|
||||
}
|
||||
} else {
|
||||
done = true;
|
||||
}
|
||||
} catch(err) {
|
||||
logger.err('failed to fetch bulk mempool transactions from esplora');
|
||||
}
|
||||
}
|
||||
logger.info(`Done inserting loaded mempool transactions into local cache`);
|
||||
return newTransactions;
|
||||
}
|
||||
|
||||
public async $updateMemPoolInfo() {
|
||||
this.mempoolInfo = await this.$getMempoolInfo();
|
||||
}
|
||||
@@ -132,7 +182,7 @@ class Mempool {
|
||||
return txTimes;
|
||||
}
|
||||
|
||||
public async $updateMempool(transactions: string[]): Promise<void> {
|
||||
public async $updateMempool(transactions: string[], pollRate: number): Promise<void> {
|
||||
logger.debug(`Updating mempool...`);
|
||||
|
||||
// warn if this run stalls the main loop for more than 2 minutes
|
||||
@@ -143,7 +193,7 @@ class Mempool {
|
||||
const currentMempoolSize = Object.keys(this.mempoolCache).length;
|
||||
this.updateTimerProgress(timer, 'got raw mempool');
|
||||
const diff = transactions.length - currentMempoolSize;
|
||||
const newTransactions: MempoolTransactionExtended[] = [];
|
||||
let newTransactions: MempoolTransactionExtended[] = [];
|
||||
|
||||
this.mempoolCacheDelta = Math.abs(diff);
|
||||
|
||||
@@ -162,41 +212,66 @@ class Mempool {
|
||||
};
|
||||
|
||||
let intervalTimer = Date.now();
|
||||
for (const txid of transactions) {
|
||||
if (!this.mempoolCache[txid]) {
|
||||
try {
|
||||
const transaction = await transactionUtils.$getMempoolTransactionExtended(txid, false, false, false);
|
||||
this.updateTimerProgress(timer, 'fetched new transaction');
|
||||
this.mempoolCache[txid] = transaction;
|
||||
if (this.inSync) {
|
||||
this.txPerSecondArray.push(new Date().getTime());
|
||||
this.vBytesPerSecondArray.push({
|
||||
unixTime: new Date().getTime(),
|
||||
vSize: transaction.vsize,
|
||||
});
|
||||
}
|
||||
hasChange = true;
|
||||
newTransactions.push(transaction);
|
||||
} catch (e: any) {
|
||||
if (config.MEMPOOL.BACKEND === 'esplora' && e.response?.status === 404) {
|
||||
this.missingTxCount++;
|
||||
}
|
||||
logger.debug(`Error finding transaction '${txid}' in the mempool: ` + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
if (Date.now() - intervalTimer > 5_000) {
|
||||
|
||||
if (this.inSync) {
|
||||
// Break and restart mempool loop if we spend too much time processing
|
||||
// new transactions that may lead to falling behind on block height
|
||||
logger.debug('Breaking mempool loop because the 5s time limit exceeded.');
|
||||
break;
|
||||
} else {
|
||||
const progress = (currentMempoolSize + newTransactions.length) / transactions.length * 100;
|
||||
logger.debug(`Mempool is synchronizing. Processed ${newTransactions.length}/${diff} txs (${Math.round(progress)}%)`);
|
||||
loadingIndicators.setProgress('mempool', progress);
|
||||
intervalTimer = Date.now()
|
||||
let loaded = false;
|
||||
if (config.MEMPOOL.BACKEND === 'esplora' && currentMempoolSize < transactions.length * 0.5 && transactions.length > 20_000) {
|
||||
this.inSync = false;
|
||||
logger.info(`Missing ${transactions.length - currentMempoolSize} mempool transactions, attempting to reload in bulk from esplora`);
|
||||
try {
|
||||
newTransactions = await this.$reloadMempool(transactions.length);
|
||||
if (config.REDIS.ENABLED) {
|
||||
for (const tx of newTransactions) {
|
||||
await redisCache.$addTransaction(tx);
|
||||
}
|
||||
}
|
||||
loaded = true;
|
||||
} catch (e) {
|
||||
logger.err('failed to load mempool in bulk from esplora, falling back to fetching individual transactions');
|
||||
}
|
||||
}
|
||||
|
||||
if (!loaded) {
|
||||
for (const txid of transactions) {
|
||||
if (!this.mempoolCache[txid]) {
|
||||
try {
|
||||
const transaction = await transactionUtils.$getMempoolTransactionExtended(txid, false, false, false);
|
||||
this.updateTimerProgress(timer, 'fetched new transaction');
|
||||
this.mempoolCache[txid] = transaction;
|
||||
if (this.inSync) {
|
||||
this.txPerSecondArray.push(new Date().getTime());
|
||||
this.vBytesPerSecondArray.push({
|
||||
unixTime: new Date().getTime(),
|
||||
vSize: transaction.vsize,
|
||||
});
|
||||
}
|
||||
hasChange = true;
|
||||
newTransactions.push(transaction);
|
||||
|
||||
if (config.REDIS.ENABLED) {
|
||||
await redisCache.$addTransaction(transaction);
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (config.MEMPOOL.BACKEND === 'esplora' && e.response?.status === 404) {
|
||||
this.missingTxCount++;
|
||||
}
|
||||
logger.debug(`Error finding transaction '${txid}' in the mempool: ` + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
if (Date.now() - intervalTimer > Math.max(pollRate * 2, 5_000)) {
|
||||
if (this.inSync) {
|
||||
// Break and restart mempool loop if we spend too much time processing
|
||||
// new transactions that may lead to falling behind on block height
|
||||
logger.debug('Breaking mempool loop because the 5s time limit exceeded.');
|
||||
break;
|
||||
} else {
|
||||
const progress = (currentMempoolSize + newTransactions.length) / transactions.length * 100;
|
||||
logger.debug(`Mempool is synchronizing. Processed ${newTransactions.length}/${diff} txs (${Math.round(progress)}%)`);
|
||||
if (Math.floor(progress) < 100) {
|
||||
loadingIndicators.setProgress('mempool', progress);
|
||||
}
|
||||
intervalTimer = Date.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -219,7 +294,7 @@ class Mempool {
|
||||
logger.warn(`Mempool clear protection triggered because transactions.length: ${transactions.length} and currentMempoolSize: ${currentMempoolSize}.`);
|
||||
setTimeout(() => {
|
||||
this.mempoolProtection = 2;
|
||||
logger.warn('Mempool clear protection resumed.');
|
||||
logger.warn('Mempool clear protection ended, normal operation resumed.');
|
||||
}, 1000 * 60 * config.MEMPOOL.CLEAR_PROTECTION_MINUTES);
|
||||
}
|
||||
|
||||
@@ -246,12 +321,6 @@ class Mempool {
|
||||
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
|
||||
this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6);
|
||||
|
||||
if (!this.inSync && transactions.length === newMempoolSize) {
|
||||
this.inSync = true;
|
||||
logger.notice('The mempool is now in sync!');
|
||||
loadingIndicators.setProgress('mempool', 100);
|
||||
}
|
||||
|
||||
this.mempoolCacheDelta = Math.abs(transactions.length - newMempoolSize);
|
||||
|
||||
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
||||
@@ -263,6 +332,19 @@ class Mempool {
|
||||
this.updateTimerProgress(timer, 'completed async mempool callback');
|
||||
}
|
||||
|
||||
if (!this.inSync && transactions.length === newMempoolSize) {
|
||||
this.inSync = true;
|
||||
logger.notice('The mempool is now in sync!');
|
||||
loadingIndicators.setProgress('mempool', 100);
|
||||
}
|
||||
|
||||
// Update Redis cache
|
||||
if (config.REDIS.ENABLED) {
|
||||
await redisCache.$flushTransactions();
|
||||
await redisCache.$removeTransactions(deletedTransactions.map(tx => tx.txid));
|
||||
await rbfCache.updateCache();
|
||||
}
|
||||
|
||||
const end = new Date().getTime();
|
||||
const time = end - start;
|
||||
logger.debug(`Mempool updated in ${time / 1000} seconds. New size: ${Object.keys(this.mempoolCache).length} (${diff > 0 ? '+' + diff : diff})`);
|
||||
|
||||
@@ -11,7 +11,7 @@ import DifficultyAdjustmentsRepository from '../../repositories/DifficultyAdjust
|
||||
import config from '../../config';
|
||||
import BlocksAuditsRepository from '../../repositories/BlocksAuditsRepository';
|
||||
import PricesRepository from '../../repositories/PricesRepository';
|
||||
import { bitcoinCoreApi } from '../bitcoin/bitcoin-api-factory';
|
||||
import bitcoinApi from '../bitcoin/bitcoin-api-factory';
|
||||
import { IEsploraApi } from '../bitcoin/esplora-api.interface';
|
||||
import database from '../../database';
|
||||
|
||||
@@ -201,7 +201,7 @@ class Mining {
|
||||
try {
|
||||
const oldestConsecutiveBlockTimestamp = 1000 * (await BlocksRepository.$getOldestConsecutiveBlock()).timestamp;
|
||||
|
||||
const genesisBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(await bitcoinClient.getBlockHash(0));
|
||||
const genesisBlock: IEsploraApi.Block = await bitcoinApi.$getBlock(await bitcoinApi.$getBlockHash(0));
|
||||
const genesisTimestamp = genesisBlock.timestamp * 1000;
|
||||
|
||||
const indexedTimestamp = await HashratesRepository.$getWeeklyHashrateTimestamps();
|
||||
@@ -312,7 +312,7 @@ class Mining {
|
||||
const oldestConsecutiveBlockTimestamp = 1000 * (await BlocksRepository.$getOldestConsecutiveBlock()).timestamp;
|
||||
|
||||
try {
|
||||
const genesisBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(await bitcoinClient.getBlockHash(0));
|
||||
const genesisBlock: IEsploraApi.Block = await bitcoinApi.$getBlock(await bitcoinApi.$getBlockHash(0));
|
||||
const genesisTimestamp = genesisBlock.timestamp * 1000;
|
||||
const indexedTimestamp = (await HashratesRepository.$getRawNetworkDailyHashrate(null)).map(hashrate => hashrate.timestamp);
|
||||
const lastMidnight = this.getDateMidnight(new Date());
|
||||
@@ -421,8 +421,9 @@ class Mining {
|
||||
}
|
||||
|
||||
const blocks: any = await BlocksRepository.$getBlocksDifficulty();
|
||||
const genesisBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(await bitcoinClient.getBlockHash(0));
|
||||
const genesisBlock: IEsploraApi.Block = await bitcoinApi.$getBlock(await bitcoinApi.$getBlockHash(0));
|
||||
let currentDifficulty = genesisBlock.difficulty;
|
||||
let currentBits = genesisBlock.bits;
|
||||
let totalIndexed = 0;
|
||||
|
||||
if (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT === -1 && indexedHeights[0] !== true) {
|
||||
@@ -436,17 +437,18 @@ class Mining {
|
||||
|
||||
const oldestConsecutiveBlock = await BlocksRepository.$getOldestConsecutiveBlock();
|
||||
if (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT !== -1) {
|
||||
currentDifficulty = oldestConsecutiveBlock.difficulty;
|
||||
currentBits = oldestConsecutiveBlock.bits;
|
||||
}
|
||||
|
||||
let totalBlockChecked = 0;
|
||||
let timer = new Date().getTime() / 1000;
|
||||
|
||||
for (const block of blocks) {
|
||||
if (block.difficulty !== currentDifficulty) {
|
||||
if (block.bits !== currentBits) {
|
||||
if (indexedHeights[block.height] === true) { // Already indexed
|
||||
if (block.height >= oldestConsecutiveBlock.height) {
|
||||
currentDifficulty = block.difficulty;
|
||||
currentBits = block.bits;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -464,6 +466,7 @@ class Mining {
|
||||
totalIndexed++;
|
||||
if (block.height >= oldestConsecutiveBlock.height) {
|
||||
currentDifficulty = block.difficulty;
|
||||
currentBits = block.bits;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import config from "../config";
|
||||
import logger from "../logger";
|
||||
import { MempoolTransactionExtended, TransactionStripped } from "../mempool.interfaces";
|
||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||
import { Common } from "./common";
|
||||
import redisCache from "./redis-cache";
|
||||
|
||||
interface RbfTransaction extends TransactionStripped {
|
||||
export interface RbfTransaction extends TransactionStripped {
|
||||
rbf?: boolean;
|
||||
mined?: boolean;
|
||||
fullRbf?: boolean;
|
||||
}
|
||||
|
||||
interface RbfTree {
|
||||
export interface RbfTree {
|
||||
tx: RbfTransaction;
|
||||
time: number;
|
||||
interval?: number;
|
||||
@@ -28,6 +30,19 @@ export interface ReplacementInfo {
|
||||
newVsize: number;
|
||||
}
|
||||
|
||||
enum CacheOp {
|
||||
Remove = 0,
|
||||
Add = 1,
|
||||
Change = 2,
|
||||
}
|
||||
|
||||
interface CacheEvent {
|
||||
op: CacheOp;
|
||||
type: 'tx' | 'tree' | 'exp';
|
||||
txid: string,
|
||||
value?: any,
|
||||
}
|
||||
|
||||
class RbfCache {
|
||||
private replacedBy: Map<string, string> = new Map();
|
||||
private replaces: Map<string, string[]> = new Map();
|
||||
@@ -36,11 +51,43 @@ class RbfCache {
|
||||
private treeMap: Map<string, string> = new Map(); // map of txids to sequence ids
|
||||
private txs: Map<string, MempoolTransactionExtended> = new Map();
|
||||
private expiring: Map<string, number> = new Map();
|
||||
private cacheQueue: CacheEvent[] = [];
|
||||
|
||||
constructor() {
|
||||
setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
|
||||
}
|
||||
|
||||
private addTx(txid: string, tx: MempoolTransactionExtended): void {
|
||||
this.txs.set(txid, tx);
|
||||
this.cacheQueue.push({ op: CacheOp.Add, type: 'tx', txid });
|
||||
}
|
||||
|
||||
private addTree(txid: string, tree: RbfTree): void {
|
||||
this.rbfTrees.set(txid, tree);
|
||||
this.dirtyTrees.add(txid);
|
||||
this.cacheQueue.push({ op: CacheOp.Add, type: 'tree', txid });
|
||||
}
|
||||
|
||||
private addExpiration(txid: string, expiry: number): void {
|
||||
this.expiring.set(txid, expiry);
|
||||
this.cacheQueue.push({ op: CacheOp.Add, type: 'exp', txid, value: expiry });
|
||||
}
|
||||
|
||||
private removeTx(txid: string): void {
|
||||
this.txs.delete(txid);
|
||||
this.cacheQueue.push({ op: CacheOp.Remove, type: 'tx', txid });
|
||||
}
|
||||
|
||||
private removeTree(txid: string): void {
|
||||
this.rbfTrees.delete(txid);
|
||||
this.cacheQueue.push({ op: CacheOp.Remove, type: 'tree', txid });
|
||||
}
|
||||
|
||||
private removeExpiration(txid: string): void {
|
||||
this.expiring.delete(txid);
|
||||
this.cacheQueue.push({ op: CacheOp.Remove, type: 'exp', txid });
|
||||
}
|
||||
|
||||
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
|
||||
if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
|
||||
return;
|
||||
@@ -49,7 +96,7 @@ class RbfCache {
|
||||
const newTx = Common.stripTransaction(newTxExtended) as RbfTransaction;
|
||||
const newTime = newTxExtended.firstSeen || (Date.now() / 1000);
|
||||
newTx.rbf = newTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
|
||||
this.txs.set(newTx.txid, newTxExtended);
|
||||
this.addTx(newTx.txid, newTxExtended);
|
||||
|
||||
// maintain rbf trees
|
||||
let txFullRbf = false;
|
||||
@@ -66,7 +113,7 @@ class RbfCache {
|
||||
const treeId = this.treeMap.get(replacedTx.txid);
|
||||
if (treeId) {
|
||||
const tree = this.rbfTrees.get(treeId);
|
||||
this.rbfTrees.delete(treeId);
|
||||
this.removeTree(treeId);
|
||||
if (tree) {
|
||||
tree.interval = newTime - tree?.time;
|
||||
replacedTrees.push(tree);
|
||||
@@ -83,7 +130,7 @@ class RbfCache {
|
||||
replaces: [],
|
||||
});
|
||||
treeFullRbf = treeFullRbf || !replacedTx.rbf;
|
||||
this.txs.set(replacedTx.txid, replacedTxExtended);
|
||||
this.addTx(replacedTx.txid, replacedTxExtended);
|
||||
}
|
||||
}
|
||||
newTx.fullRbf = txFullRbf;
|
||||
@@ -94,10 +141,27 @@ class RbfCache {
|
||||
fullRbf: treeFullRbf,
|
||||
replaces: replacedTrees
|
||||
};
|
||||
this.rbfTrees.set(treeId, newTree);
|
||||
this.addTree(treeId, newTree);
|
||||
this.updateTreeMap(treeId, newTree);
|
||||
this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid));
|
||||
this.dirtyTrees.add(treeId);
|
||||
}
|
||||
|
||||
public has(txId: string): boolean {
|
||||
return this.txs.has(txId);
|
||||
}
|
||||
|
||||
public anyInSameTree(txId: string, predicate: (tx: RbfTransaction) => boolean): boolean {
|
||||
const tree = this.getRbfTree(txId);
|
||||
if (!tree) {
|
||||
return false;
|
||||
}
|
||||
const txs = this.getTransactionsInTree(tree);
|
||||
for (const tx of txs) {
|
||||
if (predicate(tx)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public getReplacedBy(txId: string): string | undefined {
|
||||
@@ -173,6 +237,7 @@ class RbfCache {
|
||||
this.setTreeMined(tree, txid);
|
||||
tree.mined = true;
|
||||
this.dirtyTrees.add(treeId);
|
||||
this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId });
|
||||
}
|
||||
}
|
||||
this.evict(txid);
|
||||
@@ -181,7 +246,8 @@ class RbfCache {
|
||||
// flag a transaction as removed from the mempool
|
||||
public evict(txid: string, fast: boolean = false): void {
|
||||
if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) {
|
||||
this.expiring.set(txid, fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400)); // 24 hours
|
||||
const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours
|
||||
this.addExpiration(txid, expiryTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,11 +268,11 @@ class RbfCache {
|
||||
const now = Date.now();
|
||||
for (const txid of this.expiring.keys()) {
|
||||
if ((this.expiring.get(txid) || 0) < now) {
|
||||
this.expiring.delete(txid);
|
||||
this.removeExpiration(txid);
|
||||
this.remove(txid);
|
||||
}
|
||||
}
|
||||
logger.debug(`rbf cache contains ${this.txs.size} txs, ${this.expiring.size} due to expire`);
|
||||
logger.debug(`rbf cache contains ${this.txs.size} txs, ${this.rbfTrees.size} trees, ${this.expiring.size} due to expire`);
|
||||
}
|
||||
|
||||
// remove a transaction & all previous versions from the cache
|
||||
@@ -216,14 +282,14 @@ class RbfCache {
|
||||
const replaces = this.replaces.get(txid);
|
||||
this.replaces.delete(txid);
|
||||
this.treeMap.delete(txid);
|
||||
this.txs.delete(txid);
|
||||
this.expiring.delete(txid);
|
||||
this.removeTx(txid);
|
||||
this.removeExpiration(txid);
|
||||
for (const tx of (replaces || [])) {
|
||||
// recursively remove prior versions from the cache
|
||||
this.replacedBy.delete(tx);
|
||||
// if this is the id of a tree, remove that too
|
||||
if (this.treeMap.get(tx) === tx) {
|
||||
this.rbfTrees.delete(tx);
|
||||
this.removeTree(tx);
|
||||
}
|
||||
this.remove(tx);
|
||||
}
|
||||
@@ -255,6 +321,33 @@ class RbfCache {
|
||||
}
|
||||
}
|
||||
|
||||
public async updateCache(): Promise<void> {
|
||||
if (!config.REDIS.ENABLED) {
|
||||
return;
|
||||
}
|
||||
// Update the Redis cache by replaying queued events
|
||||
for (const e of this.cacheQueue) {
|
||||
if (e.op === CacheOp.Add || e.op === CacheOp.Change) {
|
||||
let value = e.value;
|
||||
switch(e.type) {
|
||||
case 'tx': {
|
||||
value = this.txs.get(e.txid);
|
||||
} break;
|
||||
case 'tree': {
|
||||
const tree = this.rbfTrees.get(e.txid);
|
||||
value = tree ? this.exportTree(tree) : null;
|
||||
} break;
|
||||
}
|
||||
if (value != null) {
|
||||
await redisCache.$setRbfEntry(e.type, e.txid, value);
|
||||
}
|
||||
} else if (e.op === CacheOp.Remove) {
|
||||
await redisCache.$removeRbfEntry(e.type, e.txid);
|
||||
}
|
||||
}
|
||||
this.cacheQueue = [];
|
||||
}
|
||||
|
||||
public dump(): any {
|
||||
const trees = Array.from(this.rbfTrees.values()).map((tree: RbfTree) => { return this.exportTree(tree); });
|
||||
|
||||
@@ -267,14 +360,14 @@ class RbfCache {
|
||||
|
||||
public async load({ txs, trees, expiring }): Promise<void> {
|
||||
txs.forEach(txEntry => {
|
||||
this.txs.set(txEntry[0], txEntry[1]);
|
||||
this.txs.set(txEntry.key, txEntry.value);
|
||||
});
|
||||
for (const deflatedTree of trees) {
|
||||
await this.importTree(deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
|
||||
}
|
||||
expiring.forEach(expiringEntry => {
|
||||
if (this.txs.has(expiringEntry[0])) {
|
||||
this.expiring.set(expiringEntry[0], new Date(expiringEntry[1]).getTime());
|
||||
if (this.txs.has(expiringEntry.key)) {
|
||||
this.expiring.set(expiringEntry.key, new Date(expiringEntry.value).getTime());
|
||||
}
|
||||
});
|
||||
this.cleanup();
|
||||
@@ -360,8 +453,7 @@ class RbfCache {
|
||||
};
|
||||
this.treeMap.set(txid, root);
|
||||
if (root === txid) {
|
||||
this.rbfTrees.set(root, tree);
|
||||
this.dirtyTrees.add(root);
|
||||
this.addTree(root, tree);
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
276
backend/src/api/redis-cache.ts
Normal file
276
backend/src/api/redis-cache.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
import { createClient } from 'redis';
|
||||
import memPool from './mempool';
|
||||
import blocks from './blocks';
|
||||
import logger from '../logger';
|
||||
import config from '../config';
|
||||
import { BlockExtended, BlockSummary, MempoolTransactionExtended } from '../mempool.interfaces';
|
||||
import rbfCache from './rbf-cache';
|
||||
import transactionUtils from './transaction-utils';
|
||||
|
||||
enum NetworkDB {
|
||||
mainnet = 0,
|
||||
testnet,
|
||||
signet,
|
||||
liquid,
|
||||
liquidtestnet,
|
||||
}
|
||||
|
||||
class RedisCache {
|
||||
private client;
|
||||
private connected = false;
|
||||
private schemaVersion = 1;
|
||||
|
||||
private cacheQueue: MempoolTransactionExtended[] = [];
|
||||
private txFlushLimit: number = 10000;
|
||||
|
||||
constructor() {
|
||||
if (config.REDIS.ENABLED) {
|
||||
const redisConfig = {
|
||||
socket: {
|
||||
path: config.REDIS.UNIX_SOCKET_PATH
|
||||
},
|
||||
database: NetworkDB[config.MEMPOOL.NETWORK],
|
||||
};
|
||||
this.client = createClient(redisConfig);
|
||||
this.client.on('error', (e) => {
|
||||
logger.err(`Error in Redis client: ${e instanceof Error ? e.message : e}`);
|
||||
});
|
||||
this.$ensureConnected();
|
||||
}
|
||||
}
|
||||
|
||||
private async $ensureConnected(): Promise<void> {
|
||||
if (!this.connected && config.REDIS.ENABLED) {
|
||||
return this.client.connect().then(async () => {
|
||||
this.connected = true;
|
||||
logger.info(`Redis client connected`);
|
||||
const version = await this.client.get('schema_version');
|
||||
if (version !== this.schemaVersion) {
|
||||
// schema changed
|
||||
// perform migrations or flush DB if necessary
|
||||
logger.info(`Redis schema version changed from ${version} to ${this.schemaVersion}`);
|
||||
await this.client.set('schema_version', this.schemaVersion);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async $updateBlocks(blocks: BlockExtended[]) {
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
await this.client.set('blocks', JSON.stringify(blocks));
|
||||
logger.debug(`Saved latest blocks to Redis cache`);
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to update blocks in Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
}
|
||||
|
||||
async $updateBlockSummaries(summaries: BlockSummary[]) {
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
await this.client.set('block-summaries', JSON.stringify(summaries));
|
||||
logger.debug(`Saved latest block summaries to Redis cache`);
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to update block summaries in Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
}
|
||||
|
||||
async $addTransaction(tx: MempoolTransactionExtended) {
|
||||
this.cacheQueue.push(tx);
|
||||
if (this.cacheQueue.length >= this.txFlushLimit) {
|
||||
await this.$flushTransactions();
|
||||
}
|
||||
}
|
||||
|
||||
async $flushTransactions() {
|
||||
const success = await this.$addTransactions(this.cacheQueue);
|
||||
if (success) {
|
||||
logger.debug(`Saved ${this.cacheQueue.length} transactions to Redis cache`);
|
||||
this.cacheQueue = [];
|
||||
} else {
|
||||
logger.err(`Failed to save ${this.cacheQueue.length} transactions to Redis cache`);
|
||||
}
|
||||
}
|
||||
|
||||
private async $addTransactions(newTransactions: MempoolTransactionExtended[]): Promise<boolean> {
|
||||
if (!newTransactions.length) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
const msetData = newTransactions.map(tx => {
|
||||
const minified: any = { ...tx };
|
||||
delete minified.hex;
|
||||
for (const vin of minified.vin) {
|
||||
delete vin.inner_redeemscript_asm;
|
||||
delete vin.inner_witnessscript_asm;
|
||||
delete vin.scriptsig_asm;
|
||||
}
|
||||
for (const vout of minified.vout) {
|
||||
delete vout.scriptpubkey_asm;
|
||||
}
|
||||
return [`mempool:tx:${tx.txid}`, JSON.stringify(minified)];
|
||||
});
|
||||
await this.client.MSET(msetData);
|
||||
return true;
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to add ${newTransactions.length} transactions to Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async $removeTransactions(transactions: string[]) {
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
for (let i = 0; i < Math.ceil(transactions.length / 10000); i++) {
|
||||
const slice = transactions.slice(i * 10000, (i + 1) * 10000);
|
||||
await this.client.unlink(slice.map(txid => `mempool:tx:${txid}`));
|
||||
logger.debug(`Deleted ${slice.length} transactions from the Redis cache`);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to remove ${transactions.length} transactions from Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
}
|
||||
|
||||
async $setRbfEntry(type: string, txid: string, value: any): Promise<void> {
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
await this.client.set(`rbf:${type}:${txid}`, JSON.stringify(value));
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to set RBF ${type} in Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
}
|
||||
|
||||
async $removeRbfEntry(type: string, txid: string): Promise<void> {
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
await this.client.unlink(`rbf:${type}:${txid}`);
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to remove RBF ${type} from Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
}
|
||||
|
||||
async $getBlocks(): Promise<BlockExtended[]> {
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
const json = await this.client.get('blocks');
|
||||
return JSON.parse(json);
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to retrieve blocks from Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async $getBlockSummaries(): Promise<BlockSummary[]> {
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
const json = await this.client.get('block-summaries');
|
||||
return JSON.parse(json);
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to retrieve blocks from Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async $getMempool(): Promise<{ [txid: string]: MempoolTransactionExtended }> {
|
||||
const start = Date.now();
|
||||
const mempool = {};
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
const mempoolList = await this.scanKeys<MempoolTransactionExtended>('mempool:tx:*');
|
||||
for (const tx of mempoolList) {
|
||||
mempool[tx.key] = tx.value;
|
||||
}
|
||||
logger.info(`Loaded mempool from Redis cache in ${Date.now() - start} ms`);
|
||||
return mempool || {};
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to retrieve mempool from Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
async $getRbfEntries(type: string): Promise<any[]> {
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
const rbfEntries = await this.scanKeys<MempoolTransactionExtended[]>(`rbf:${type}:*`);
|
||||
return rbfEntries;
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to retrieve Rbf ${type}s from Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async $loadCache() {
|
||||
logger.info('Restoring mempool and blocks data from Redis cache');
|
||||
// Load block data
|
||||
const loadedBlocks = await this.$getBlocks();
|
||||
const loadedBlockSummaries = await this.$getBlockSummaries();
|
||||
// Load mempool
|
||||
const loadedMempool = await this.$getMempool();
|
||||
this.inflateLoadedTxs(loadedMempool);
|
||||
// Load rbf data
|
||||
const rbfTxs = await this.$getRbfEntries('tx');
|
||||
const rbfTrees = await this.$getRbfEntries('tree');
|
||||
const rbfExpirations = await this.$getRbfEntries('exp');
|
||||
|
||||
// Set loaded data
|
||||
blocks.setBlocks(loadedBlocks || []);
|
||||
blocks.setBlockSummaries(loadedBlockSummaries || []);
|
||||
await memPool.$setMempool(loadedMempool);
|
||||
await rbfCache.load({
|
||||
txs: rbfTxs,
|
||||
trees: rbfTrees.map(loadedTree => loadedTree.value),
|
||||
expiring: rbfExpirations,
|
||||
});
|
||||
}
|
||||
|
||||
private inflateLoadedTxs(mempool: { [txid: string]: MempoolTransactionExtended }) {
|
||||
for (const tx of Object.values(mempool)) {
|
||||
for (const vin of tx.vin) {
|
||||
if (vin.scriptsig) {
|
||||
vin.scriptsig_asm = transactionUtils.convertScriptSigAsm(vin.scriptsig);
|
||||
transactionUtils.addInnerScriptsToVin(vin);
|
||||
}
|
||||
}
|
||||
for (const vout of tx.vout) {
|
||||
if (vout.scriptpubkey) {
|
||||
vout.scriptpubkey_asm = transactionUtils.convertScriptSigAsm(vout.scriptpubkey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async scanKeys<T>(pattern): Promise<{ key: string, value: T }[]> {
|
||||
logger.info(`loading Redis entries for ${pattern}`);
|
||||
let keys: string[] = [];
|
||||
const result: { key: string, value: T }[] = [];
|
||||
const patternLength = pattern.length - 1;
|
||||
let count = 0;
|
||||
const processValues = async (keys): Promise<void> => {
|
||||
const values = await this.client.MGET(keys);
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
if (values[i]) {
|
||||
result.push({ key: keys[i].slice(patternLength), value: JSON.parse(values[i]) });
|
||||
count++;
|
||||
}
|
||||
}
|
||||
logger.info(`loaded ${count} entries from Redis cache`);
|
||||
};
|
||||
for await (const key of this.client.scanIterator({
|
||||
MATCH: pattern,
|
||||
COUNT: 100
|
||||
})) {
|
||||
keys.push(key);
|
||||
if (keys.length >= 10000) {
|
||||
await processValues(keys);
|
||||
keys = [];
|
||||
}
|
||||
}
|
||||
if (keys.length) {
|
||||
await processValues(keys);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export default new RedisCache();
|
||||
@@ -3,6 +3,7 @@ import { IEsploraApi } from './bitcoin/esplora-api.interface';
|
||||
import { Common } from './common';
|
||||
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
|
||||
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||
import logger from '../logger';
|
||||
|
||||
class TransactionUtils {
|
||||
constructor() { }
|
||||
@@ -22,6 +23,23 @@ class TransactionUtils {
|
||||
};
|
||||
}
|
||||
|
||||
// Wrapper for $getTransactionExtended with an automatic retry direct to Core if the first API request fails.
|
||||
// Propagates any error from the retry request.
|
||||
public async $getTransactionExtendedRetry(txid: string, addPrevouts = false, lazyPrevouts = false, forceCore = false, addMempoolData = false): Promise<TransactionExtended> {
|
||||
try {
|
||||
const result = await this.$getTransactionExtended(txid, addPrevouts, lazyPrevouts, forceCore, addMempoolData);
|
||||
if (result) {
|
||||
return result;
|
||||
} else {
|
||||
logger.err(`Cannot fetch tx ${txid}. Reason: backend returned null data`);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.err(`Cannot fetch tx ${txid}. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
// retry direct from Core if first request failed
|
||||
return this.$getTransactionExtended(txid, addPrevouts, lazyPrevouts, true, addMempoolData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param txId
|
||||
* @param addPrevouts
|
||||
@@ -31,10 +49,17 @@ class TransactionUtils {
|
||||
public async $getTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false, forceCore = false, addMempoolData = false): Promise<TransactionExtended> {
|
||||
let transaction: IEsploraApi.Transaction;
|
||||
if (forceCore === true) {
|
||||
transaction = await bitcoinCoreApi.$getRawTransaction(txId, true);
|
||||
transaction = await bitcoinCoreApi.$getRawTransaction(txId, false, addPrevouts, lazyPrevouts);
|
||||
} else {
|
||||
transaction = await bitcoinApi.$getRawTransaction(txId, false, addPrevouts, lazyPrevouts);
|
||||
}
|
||||
|
||||
if (Common.isLiquid()) {
|
||||
if (!isFinite(Number(transaction.fee))) {
|
||||
transaction.fee = Object.values(transaction.fee || {}).reduce((total, output) => total + output, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (addMempoolData || !transaction?.status?.confirmed) {
|
||||
return this.extendMempoolTransaction(transaction);
|
||||
} else {
|
||||
@@ -46,14 +71,13 @@ class TransactionUtils {
|
||||
return (await this.$getTransactionExtended(txId, addPrevouts, lazyPrevouts, forceCore, true)) as MempoolTransactionExtended;
|
||||
}
|
||||
|
||||
private extendTransaction(transaction: IEsploraApi.Transaction): TransactionExtended {
|
||||
public extendTransaction(transaction: IEsploraApi.Transaction): TransactionExtended {
|
||||
// @ts-ignore
|
||||
if (transaction.vsize) {
|
||||
// @ts-ignore
|
||||
return transaction;
|
||||
}
|
||||
const feePerVbytes = Math.max(Common.isLiquid() ? 0.1 : 1,
|
||||
(transaction.fee || 0) / (transaction.weight / 4));
|
||||
const feePerVbytes = (transaction.fee || 0) / (transaction.weight / 4);
|
||||
const transactionExtended: TransactionExtended = Object.assign({
|
||||
vsize: Math.round(transaction.weight / 4),
|
||||
feePerVsize: feePerVbytes,
|
||||
@@ -68,13 +92,11 @@ class TransactionUtils {
|
||||
public extendMempoolTransaction(transaction: IEsploraApi.Transaction): MempoolTransactionExtended {
|
||||
const vsize = Math.ceil(transaction.weight / 4);
|
||||
const fractionalVsize = (transaction.weight / 4);
|
||||
const sigops = this.countSigops(transaction);
|
||||
const sigops = !Common.isLiquid() ? this.countSigops(transaction) : 0;
|
||||
// https://github.com/bitcoin/bitcoin/blob/e9262ea32a6e1d364fb7974844fadc36f931f8c6/src/policy/policy.cpp#L295-L298
|
||||
const adjustedVsize = Math.max(fractionalVsize, sigops * 5); // adjusted vsize = Max(weight, sigops * bytes_per_sigop) / witness_scale_factor
|
||||
const feePerVbytes = Math.max(Common.isLiquid() ? 0.1 : 1,
|
||||
(transaction.fee || 0) / fractionalVsize);
|
||||
const adjustedFeePerVsize = Math.max(Common.isLiquid() ? 0.1 : 1,
|
||||
(transaction.fee || 0) / adjustedVsize);
|
||||
const feePerVbytes = (transaction.fee || 0) / fractionalVsize;
|
||||
const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize;
|
||||
const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, {
|
||||
order: this.txidToOrdering(transaction.txid),
|
||||
vsize: Math.round(transaction.weight / 4),
|
||||
@@ -166,6 +188,122 @@ class TransactionUtils {
|
||||
16
|
||||
);
|
||||
}
|
||||
|
||||
public addInnerScriptsToVin(vin: IEsploraApi.Vin): void {
|
||||
if (!vin.prevout) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (vin.prevout.scriptpubkey_type === 'p2sh') {
|
||||
const redeemScript = vin.scriptsig_asm.split(' ').reverse()[0];
|
||||
vin.inner_redeemscript_asm = this.convertScriptSigAsm(redeemScript);
|
||||
if (vin.witness && vin.witness.length > 2) {
|
||||
const witnessScript = vin.witness[vin.witness.length - 1];
|
||||
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
|
||||
}
|
||||
}
|
||||
|
||||
if (vin.prevout.scriptpubkey_type === 'v0_p2wsh' && vin.witness) {
|
||||
const witnessScript = vin.witness[vin.witness.length - 1];
|
||||
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
|
||||
}
|
||||
|
||||
if (vin.prevout.scriptpubkey_type === 'v1_p2tr' && vin.witness) {
|
||||
const witnessScript = this.witnessToP2TRScript(vin.witness);
|
||||
if (witnessScript !== null) {
|
||||
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public convertScriptSigAsm(hex: string): string {
|
||||
const buf = Buffer.from(hex, 'hex');
|
||||
|
||||
const b: string[] = [];
|
||||
|
||||
let i = 0;
|
||||
while (i < buf.length) {
|
||||
const op = buf[i];
|
||||
if (op >= 0x01 && op <= 0x4e) {
|
||||
i++;
|
||||
let push: number;
|
||||
if (op === 0x4c) {
|
||||
push = buf.readUInt8(i);
|
||||
b.push('OP_PUSHDATA1');
|
||||
i += 1;
|
||||
} else if (op === 0x4d) {
|
||||
push = buf.readUInt16LE(i);
|
||||
b.push('OP_PUSHDATA2');
|
||||
i += 2;
|
||||
} else if (op === 0x4e) {
|
||||
push = buf.readUInt32LE(i);
|
||||
b.push('OP_PUSHDATA4');
|
||||
i += 4;
|
||||
} else {
|
||||
push = op;
|
||||
b.push('OP_PUSHBYTES_' + push);
|
||||
}
|
||||
|
||||
const data = buf.slice(i, i + push);
|
||||
if (data.length !== push) {
|
||||
break;
|
||||
}
|
||||
|
||||
b.push(data.toString('hex'));
|
||||
i += data.length;
|
||||
} else {
|
||||
if (op === 0x00) {
|
||||
b.push('OP_0');
|
||||
} else if (op === 0x4f) {
|
||||
b.push('OP_PUSHNUM_NEG1');
|
||||
} else if (op === 0xb1) {
|
||||
b.push('OP_CLTV');
|
||||
} else if (op === 0xb2) {
|
||||
b.push('OP_CSV');
|
||||
} else if (op === 0xba) {
|
||||
b.push('OP_CHECKSIGADD');
|
||||
} else {
|
||||
const opcode = bitcoinjs.script.toASM([ op ]);
|
||||
if (opcode && op < 0xfd) {
|
||||
if (/^OP_(\d+)$/.test(opcode)) {
|
||||
b.push(opcode.replace(/^OP_(\d+)$/, 'OP_PUSHNUM_$1'));
|
||||
} else {
|
||||
b.push(opcode);
|
||||
}
|
||||
} else {
|
||||
b.push('OP_RETURN_' + op);
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return b.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* This function must only be called when we know the witness we are parsing
|
||||
* is a taproot witness.
|
||||
* @param witness An array of hex strings that represents the witness stack of
|
||||
* the input.
|
||||
* @returns null if the witness is not a script spend, and the hex string of
|
||||
* the script item if it is a script spend.
|
||||
*/
|
||||
public witnessToP2TRScript(witness: string[]): string | null {
|
||||
if (witness.length < 2) return null;
|
||||
// Note: see BIP341 for parsing details of witness stack
|
||||
|
||||
// If there are at least two witness elements, and the first byte of the
|
||||
// last element is 0x50, this last element is called annex a and
|
||||
// is removed from the witness stack.
|
||||
const hasAnnex = witness[witness.length - 1].substring(0, 2) === '50';
|
||||
// If there are at least two witness elements left, script path spending is used.
|
||||
// Call the second-to-last stack element s, the script.
|
||||
// (Note: this phrasing from BIP341 assumes we've *removed* the annex from the stack)
|
||||
if (hasAnnex && witness.length < 3) return null;
|
||||
const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2;
|
||||
return witness[positionOfScript];
|
||||
}
|
||||
}
|
||||
|
||||
export default new TransactionUtils();
|
||||
|
||||
@@ -183,15 +183,25 @@ class WebsocketHandler {
|
||||
}
|
||||
|
||||
if (parsedMessage && parsedMessage['track-address']) {
|
||||
if (/^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100})$/
|
||||
if (/^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64})$/
|
||||
.test(parsedMessage['track-address'])) {
|
||||
let matchedAddress = parsedMessage['track-address'];
|
||||
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(parsedMessage['track-address'])) {
|
||||
matchedAddress = matchedAddress.toLowerCase();
|
||||
}
|
||||
client['track-address'] = matchedAddress;
|
||||
if (/^04[a-fA-F0-9]{128}$/.test(parsedMessage['track-address'])) {
|
||||
client['track-address'] = null;
|
||||
client['track-scriptpubkey'] = '41' + matchedAddress + 'ac';
|
||||
} else if (/^|(02|03)[a-fA-F0-9]{64}$/.test(parsedMessage['track-address'])) {
|
||||
client['track-address'] = null;
|
||||
client['track-scriptpubkey'] = '21' + matchedAddress + 'ac';
|
||||
} else {
|
||||
client['track-address'] = matchedAddress;
|
||||
client['track-scriptpubkey'] = null;
|
||||
}
|
||||
} else {
|
||||
client['track-address'] = null;
|
||||
client['track-scriptpubkey'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -546,6 +556,44 @@ class WebsocketHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (client['track-scriptpubkey']) {
|
||||
const foundTransactions: TransactionExtended[] = [];
|
||||
|
||||
for (const tx of newTransactions) {
|
||||
const someVin = tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk' && vin.prevout.scriptpubkey === client['track-scriptpubkey']);
|
||||
if (someVin) {
|
||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||
try {
|
||||
const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
|
||||
foundTransactions.push(fullTx);
|
||||
} catch (e) {
|
||||
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
} else {
|
||||
foundTransactions.push(tx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const someVout = tx.vout.some((vout) => vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey === client['track-scriptpubkey']);
|
||||
if (someVout) {
|
||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||
try {
|
||||
const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
|
||||
foundTransactions.push(fullTx);
|
||||
} catch (e) {
|
||||
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
} else {
|
||||
foundTransactions.push(tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (foundTransactions.length) {
|
||||
response['address-transactions'] = JSON.stringify(foundTransactions);
|
||||
}
|
||||
}
|
||||
|
||||
if (client['track-asset']) {
|
||||
const foundTransactions: TransactionExtended[] = [];
|
||||
|
||||
@@ -604,7 +652,7 @@ class WebsocketHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (client['track-mempool-block'] >= 0) {
|
||||
if (client['track-mempool-block'] >= 0 && memPool.isInSync()) {
|
||||
const index = client['track-mempool-block'];
|
||||
if (mBlockDeltas[index]) {
|
||||
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-${index}`, {
|
||||
@@ -644,7 +692,7 @@ class WebsocketHandler {
|
||||
memPool.handleMinedRbfTransactions(rbfTransactions);
|
||||
memPool.removeFromSpendMap(transactions);
|
||||
|
||||
if (config.MEMPOOL.AUDIT) {
|
||||
if (config.MEMPOOL.AUDIT && memPool.isInSync()) {
|
||||
let projectedBlocks;
|
||||
let auditMempool = _memPool;
|
||||
// template calculation functions have mempool side effects, so calculate audits using
|
||||
@@ -665,7 +713,7 @@ class WebsocketHandler {
|
||||
projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
|
||||
}
|
||||
|
||||
if (Common.indexingEnabled() && memPool.isInSync()) {
|
||||
if (Common.indexingEnabled()) {
|
||||
const { censored, added, fresh, sigop, fullrbf, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
|
||||
const matchRate = Math.round(score * 100 * 100) / 100;
|
||||
|
||||
@@ -821,6 +869,33 @@ class WebsocketHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (client['track-scriptpubkey']) {
|
||||
const foundTransactions: TransactionExtended[] = [];
|
||||
|
||||
transactions.forEach((tx) => {
|
||||
if (tx.vin && tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk' && vin.prevout.scriptpubkey === client['track-scriptpubkey'])) {
|
||||
foundTransactions.push(tx);
|
||||
return;
|
||||
}
|
||||
if (tx.vout && tx.vout.some((vout) => vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey === client['track-scriptpubkey'])) {
|
||||
foundTransactions.push(tx);
|
||||
}
|
||||
});
|
||||
|
||||
if (foundTransactions.length) {
|
||||
foundTransactions.forEach((tx) => {
|
||||
tx.status = {
|
||||
confirmed: true,
|
||||
block_height: block.height,
|
||||
block_hash: block.id,
|
||||
block_time: block.timestamp,
|
||||
};
|
||||
});
|
||||
|
||||
response['block-transactions'] = JSON.stringify(foundTransactions);
|
||||
}
|
||||
}
|
||||
|
||||
if (client['track-asset']) {
|
||||
const foundTransactions: TransactionExtended[] = [];
|
||||
|
||||
@@ -858,7 +933,7 @@ class WebsocketHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (client['track-mempool-block'] >= 0) {
|
||||
if (client['track-mempool-block'] >= 0 && memPool.isInSync()) {
|
||||
const index = client['track-mempool-block'];
|
||||
if (mBlockDeltas && mBlockDeltas[index]) {
|
||||
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-${index}`, {
|
||||
|
||||
@@ -12,6 +12,7 @@ interface IConfig {
|
||||
API_URL_PREFIX: string;
|
||||
POLL_RATE_MS: number;
|
||||
CACHE_DIR: string;
|
||||
CACHE_ENABLED: boolean;
|
||||
CLEAR_PROTECTION_MINUTES: number;
|
||||
RECOMMENDED_FEE_PERCENTILE: number;
|
||||
BLOCK_WEIGHT_UNITS: number;
|
||||
@@ -137,7 +138,11 @@ interface IConfig {
|
||||
AUDIT: boolean;
|
||||
AUDIT_START_HEIGHT: number;
|
||||
SERVERS: string[];
|
||||
}
|
||||
},
|
||||
REDIS: {
|
||||
ENABLED: boolean;
|
||||
UNIX_SOCKET_PATH: string;
|
||||
},
|
||||
}
|
||||
|
||||
const defaults: IConfig = {
|
||||
@@ -150,6 +155,7 @@ const defaults: IConfig = {
|
||||
'API_URL_PREFIX': '/api/v1/',
|
||||
'POLL_RATE_MS': 2000,
|
||||
'CACHE_DIR': './cache',
|
||||
'CACHE_ENABLED': true,
|
||||
'CLEAR_PROTECTION_MINUTES': 20,
|
||||
'RECOMMENDED_FEE_PERCENTILE': 50,
|
||||
'BLOCK_WEIGHT_UNITS': 4000000,
|
||||
@@ -275,7 +281,11 @@ const defaults: IConfig = {
|
||||
'AUDIT': false,
|
||||
'AUDIT_START_HEIGHT': 774000,
|
||||
'SERVERS': [],
|
||||
}
|
||||
},
|
||||
'REDIS': {
|
||||
'ENABLED': false,
|
||||
'UNIX_SOCKET_PATH': '',
|
||||
},
|
||||
};
|
||||
|
||||
class Config implements IConfig {
|
||||
@@ -296,6 +306,7 @@ class Config implements IConfig {
|
||||
EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER'];
|
||||
MAXMIND: IConfig['MAXMIND'];
|
||||
REPLICATION: IConfig['REPLICATION'];
|
||||
REDIS: IConfig['REDIS'];
|
||||
|
||||
constructor() {
|
||||
const configs = this.merge(configFromFile, defaults);
|
||||
@@ -316,6 +327,7 @@ class Config implements IConfig {
|
||||
this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER;
|
||||
this.MAXMIND = configs.MAXMIND;
|
||||
this.REPLICATION = configs.REPLICATION;
|
||||
this.REDIS = configs.REDIS;
|
||||
}
|
||||
|
||||
merge = (...objects: object[]): IConfig => {
|
||||
|
||||
@@ -41,6 +41,7 @@ import chainTips from './api/chain-tips';
|
||||
import { AxiosError } from 'axios';
|
||||
import v8 from 'v8';
|
||||
import { formatBytes, getBytesUnit } from './utils/format';
|
||||
import redisCache from './api/redis-cache';
|
||||
|
||||
class Server {
|
||||
private wss: WebSocket.Server | undefined;
|
||||
@@ -122,7 +123,11 @@ class Server {
|
||||
await poolsUpdater.updatePoolsJson(); // Needs to be done before loading the disk cache because we sometimes wipe it
|
||||
await syncAssets.syncAssets$();
|
||||
if (config.MEMPOOL.ENABLED) {
|
||||
await diskCache.$loadMempoolCache();
|
||||
if (config.MEMPOOL.CACHE_ENABLED) {
|
||||
await diskCache.$loadMempoolCache();
|
||||
} else if (config.REDIS.ENABLED) {
|
||||
await redisCache.$loadCache();
|
||||
}
|
||||
}
|
||||
|
||||
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && cluster.isPrimary) {
|
||||
@@ -183,14 +188,15 @@ class Server {
|
||||
}
|
||||
const newMempool = await bitcoinApi.$getRawMempool();
|
||||
const numHandledBlocks = await blocks.$updateBlocks();
|
||||
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerRunning ? 10 : 1);
|
||||
if (numHandledBlocks === 0) {
|
||||
await memPool.$updateMempool(newMempool);
|
||||
await memPool.$updateMempool(newMempool, pollRate);
|
||||
}
|
||||
indexer.$run();
|
||||
|
||||
// rerun immediately if we skipped the mempool update, otherwise wait POLL_RATE_MS
|
||||
const elapsed = Date.now() - start;
|
||||
const remainingTime = Math.max(0, config.MEMPOOL.POLL_RATE_MS - elapsed)
|
||||
const remainingTime = Math.max(0, pollRate - elapsed);
|
||||
setTimeout(this.runMainUpdateLoop.bind(this), numHandledBlocks > 0 ? 0 : remainingTime);
|
||||
this.backendRetryCount = 0;
|
||||
} catch (e: any) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
|
||||
import { BlockExtended, BlockExtension, BlockPrice, EffectiveFeeStats } from '../mempool.interfaces';
|
||||
import DB from '../database';
|
||||
import logger from '../logger';
|
||||
@@ -12,6 +13,7 @@ import config from '../config';
|
||||
import chainTips from '../api/chain-tips';
|
||||
import blocks from '../api/blocks';
|
||||
import BlocksAuditsRepository from './BlocksAuditsRepository';
|
||||
import transactionUtils from '../api/transaction-utils';
|
||||
|
||||
interface DatabaseBlock {
|
||||
id: string;
|
||||
@@ -539,7 +541,7 @@ class BlocksRepository {
|
||||
*/
|
||||
public async $getBlocksDifficulty(): Promise<object[]> {
|
||||
try {
|
||||
const [rows]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(blockTimestamp) as time, height, difficulty FROM blocks`);
|
||||
const [rows]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(blockTimestamp) as time, height, difficulty, bits FROM blocks`);
|
||||
return rows;
|
||||
} catch (e) {
|
||||
logger.err('Cannot get blocks difficulty list from the db. Reason: ' + (e instanceof Error ? e.message : e));
|
||||
@@ -848,7 +850,7 @@ class BlocksRepository {
|
||||
*/
|
||||
public async $getOldestConsecutiveBlock(): Promise<any> {
|
||||
try {
|
||||
const [rows]: any = await DB.query(`SELECT height, UNIX_TIMESTAMP(blockTimestamp) as timestamp, difficulty FROM blocks ORDER BY height DESC`);
|
||||
const [rows]: any = await DB.query(`SELECT height, UNIX_TIMESTAMP(blockTimestamp) as timestamp, difficulty, bits FROM blocks ORDER BY height DESC`);
|
||||
for (let i = 0; i < rows.length - 1; ++i) {
|
||||
if (rows[i].height - rows[i + 1].height > 1) {
|
||||
return rows[i];
|
||||
@@ -1036,8 +1038,17 @@ class BlocksRepository {
|
||||
{
|
||||
extras.feePercentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(dbBlk.id);
|
||||
if (extras.feePercentiles === null) {
|
||||
const block = await bitcoinClient.getBlock(dbBlk.id, 2);
|
||||
const summary = blocks.summarizeBlock(block);
|
||||
|
||||
let summary;
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
const txs = (await bitcoinApi.$getTxsForBlock(dbBlk.id)).map(tx => transactionUtils.extendTransaction(tx));
|
||||
summary = blocks.summarizeBlockTransactions(dbBlk.id, txs);
|
||||
} else {
|
||||
// Call Core RPC
|
||||
const block = await bitcoinClient.getBlock(dbBlk.id, 2);
|
||||
summary = blocks.summarizeBlock(block);
|
||||
}
|
||||
|
||||
await BlocksSummariesRepository.$saveTransactions(dbBlk.height, dbBlk.id, summary.transactions);
|
||||
extras.feePercentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(dbBlk.id);
|
||||
}
|
||||
|
||||
3
contributors/Czino.txt
Normal file
3
contributors/Czino.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of July 29, 2023.
|
||||
|
||||
Signed: Czino
|
||||
3
contributors/bguillaumat.txt
Normal file
3
contributors/bguillaumat.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 January 25, 2022.
|
||||
|
||||
Signed: bguillaumat
|
||||
3
contributors/devinbileck.txt
Normal file
3
contributors/devinbileck.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of July 21, 2023.
|
||||
|
||||
Signed: devinbileck
|
||||
5
contributors/fiatjaf.txt
Normal file
5
contributors/fiatjaf.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of January 25, 2022.
|
||||
I also regret having ever contributed to this repository since they keep asking me to sign this legalese timewaste things.
|
||||
And finally I don't care about licenses and won't sue anyone over intellectual property, which is a fake statist construct invented by evil lobby lawyers.
|
||||
|
||||
Signed: fiatjaf
|
||||
3
contributors/learntheropes.txt
Normal file
3
contributors/learntheropes.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of April 7, 203.
|
||||
|
||||
Signed: learntheropes
|
||||
3
contributors/nothing0012.txt
Normal file
3
contributors/nothing0012.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of April 8, 2023.
|
||||
|
||||
Signed: nothing0012
|
||||
3
contributors/pedromvpg.txt
Normal file
3
contributors/pedromvpg.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of July 20, 2023.
|
||||
|
||||
Signed: pedromvpg
|
||||
3
contributors/rishkwal.txt
Normal file
3
contributors/rishkwal.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of July 29, 2023.
|
||||
|
||||
Signed: rishkwal
|
||||
@@ -7,9 +7,10 @@ WORKDIR /build
|
||||
COPY . .
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y build-essential python3 pkg-config curl
|
||||
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
|
||||
ENV PATH="/root/.cargo/bin:$PATH"
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
|
||||
"POLL_RATE_MS": __MEMPOOL_POLL_RATE_MS__,
|
||||
"CACHE_DIR": "__MEMPOOL_CACHE_DIR__",
|
||||
"CACHE_ENABLED": __MEMPOOL_CACHE_ENABLED__,
|
||||
"CLEAR_PROTECTION_MINUTES": __MEMPOOL_CLEAR_PROTECTION_MINUTES__,
|
||||
"RECOMMENDED_FEE_PERCENTILE": __MEMPOOL_RECOMMENDED_FEE_PERCENTILE__,
|
||||
"BLOCK_WEIGHT_UNITS": __MEMPOOL_BLOCK_WEIGHT_UNITS__,
|
||||
@@ -133,5 +134,9 @@
|
||||
"AUDIT": __REPLICATION_AUDIT__,
|
||||
"AUDIT_START_HEIGHT": __REPLICATION_AUDIT_START_HEIGHT__,
|
||||
"SERVERS": __REPLICATION_SERVERS__
|
||||
},
|
||||
"REDIS": {
|
||||
"ENABLED": __REDIS_ENABLED__,
|
||||
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ __MEMPOOL_SPAWN_CLUSTER_PROCS__=${MEMPOOL_SPAWN_CLUSTER_PROCS:=0}
|
||||
__MEMPOOL_API_URL_PREFIX__=${MEMPOOL_API_URL_PREFIX:=/api/v1/}
|
||||
__MEMPOOL_POLL_RATE_MS__=${MEMPOOL_POLL_RATE_MS:=2000}
|
||||
__MEMPOOL_CACHE_DIR__=${MEMPOOL_CACHE_DIR:=./cache}
|
||||
__MEMPOOL_CACHE_ENABLED__=${MEMPOOL_CACHE_ENABLED:=true}
|
||||
__MEMPOOL_CLEAR_PROTECTION_MINUTES__=${MEMPOOL_CLEAR_PROTECTION_MINUTES:=20}
|
||||
__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__=${MEMPOOL_RECOMMENDED_FEE_PERCENTILE:=50}
|
||||
__MEMPOOL_BLOCK_WEIGHT_UNITS__=${MEMPOOL_BLOCK_WEIGHT_UNITS:=4000000}
|
||||
@@ -136,6 +137,9 @@ __REPLICATION_AUDIT__=${REPLICATION_AUDIT:=true}
|
||||
__REPLICATION_AUDIT_START_HEIGHT__=${REPLICATION_AUDIT_START_HEIGHT:=774000}
|
||||
__REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
|
||||
|
||||
# REDIS
|
||||
__REDIS_ENABLED__=${REDIS_ENABLED:=true}
|
||||
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=true}
|
||||
|
||||
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
|
||||
|
||||
@@ -147,6 +151,7 @@ sed -i "s!__MEMPOOL_SPAWN_CLUSTER_PROCS__!${__MEMPOOL_SPAWN_CLUSTER_PROCS__}!g"
|
||||
sed -i "s!__MEMPOOL_API_URL_PREFIX__!${__MEMPOOL_API_URL_PREFIX__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_POLL_RATE_MS__!${__MEMPOOL_POLL_RATE_MS__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_CACHE_DIR__!${__MEMPOOL_CACHE_DIR__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_CACHE_ENABLED__!${__MEMPOOL_CACHE_ENABLED__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_CLEAR_PROTECTION_MINUTES__!${__MEMPOOL_CLEAR_PROTECTION_MINUTES__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__!${__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_BLOCK_WEIGHT_UNITS__!${__MEMPOOL_BLOCK_WEIGHT_UNITS__}!g" mempool-config.json
|
||||
@@ -165,7 +170,7 @@ sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-co
|
||||
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_GBT__}!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_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
|
||||
@@ -262,4 +267,8 @@ sed -i "s!__REPLICATION_AUDIT__!${__REPLICATION_AUDIT__}!g" mempool-config.json
|
||||
sed -i "s!__REPLICATION_AUDIT_START_HEIGHT__!${__REPLICATION_AUDIT_START_HEIGHT__}!g" mempool-config.json
|
||||
sed -i "s!__REPLICATION_SERVERS__!${__REPLICATION_SERVERS__}!g" mempool-config.json
|
||||
|
||||
# REDIS
|
||||
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
|
||||
|
||||
node /backend/package/index.js
|
||||
|
||||
@@ -18,7 +18,7 @@ fi
|
||||
|
||||
__TESTNET_ENABLED__=${TESTNET_ENABLED:=false}
|
||||
__SIGNET_ENABLED__=${SIGNET_ENABLED:=false}
|
||||
__LIQUID_ENABLED__=${LIQUID_EANBLED:=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}
|
||||
@@ -39,7 +39,6 @@ __AUDIT__=${AUDIT:=false}
|
||||
__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}
|
||||
__FULL_RBF_ENABLED__=${FULL_RBF_ENABLED:=false}
|
||||
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
|
||||
|
||||
# Export as environment variables to be used by envsubst
|
||||
@@ -66,7 +65,6 @@ export __AUDIT__
|
||||
export __MAINNET_BLOCK_AUDIT_START_HEIGHT__
|
||||
export __TESTNET_BLOCK_AUDIT_START_HEIGHT__
|
||||
export __SIGNET_BLOCK_AUDIT_START_HEIGHT__
|
||||
export __FULL_RBF_ENABLED__
|
||||
export __HISTORICAL_PRICE__
|
||||
|
||||
folder=$(find /var/www/mempool -name "config.js" | xargs dirname)
|
||||
|
||||
@@ -2,11 +2,15 @@
|
||||
# For additional information regarding the format and rule options, please see:
|
||||
# https://github.com/browserslist/browserslist#queries
|
||||
|
||||
# For the full list of supported browsers by the Angular framework, please see:
|
||||
# https://angular.io/guide/browser-support
|
||||
|
||||
# You can see what browsers were selected by your queries by running:
|
||||
# npx browserslist
|
||||
|
||||
> 0.5%
|
||||
last 2 versions
|
||||
last 2 Chrome versions
|
||||
last 1 Firefox version
|
||||
last 2 Edge major versions
|
||||
last 2 Safari major versions
|
||||
last 2 iOS major versions
|
||||
Firefox ESR
|
||||
not dead
|
||||
not IE 9-11 # For IE 9-11 support, remove 'not'.
|
||||
@@ -22,6 +22,5 @@
|
||||
"TESTNET_BLOCK_AUDIT_START_HEIGHT": 0,
|
||||
"SIGNET_BLOCK_AUDIT_START_HEIGHT": 0,
|
||||
"LIGHTNING": false,
|
||||
"FULL_RBF_ENABLED": false,
|
||||
"HISTORICAL_PRICE": true
|
||||
}
|
||||
|
||||
15031
frontend/package-lock.json
generated
15031
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -61,60 +61,60 @@
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular-devkit/build-angular": "^14.2.10",
|
||||
"@angular/animations": "^14.2.12",
|
||||
"@angular/cli": "^14.2.10",
|
||||
"@angular/common": "^14.2.12",
|
||||
"@angular/compiler": "^14.2.12",
|
||||
"@angular/core": "^14.2.12",
|
||||
"@angular/forms": "^14.2.12",
|
||||
"@angular/localize": "^14.2.12",
|
||||
"@angular/platform-browser": "^14.2.12",
|
||||
"@angular/platform-browser-dynamic": "^14.2.12",
|
||||
"@angular/platform-server": "^14.2.12",
|
||||
"@angular/router": "^14.2.12",
|
||||
"@fortawesome/angular-fontawesome": "~0.11.1",
|
||||
"@fortawesome/fontawesome-common-types": "~6.2.1",
|
||||
"@fortawesome/fontawesome-svg-core": "~6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "~6.2.1",
|
||||
"@angular-devkit/build-angular": "^16.1.4",
|
||||
"@angular/animations": "^16.1.5",
|
||||
"@angular/cli": "^16.1.4",
|
||||
"@angular/common": "^16.1.5",
|
||||
"@angular/compiler": "^16.1.5",
|
||||
"@angular/core": "^16.1.5",
|
||||
"@angular/forms": "^16.1.5",
|
||||
"@angular/localize": "^16.1.5",
|
||||
"@angular/platform-browser": "^16.1.5",
|
||||
"@angular/platform-browser-dynamic": "^16.1.5",
|
||||
"@angular/platform-server": "^16.1.5",
|
||||
"@angular/router": "^16.1.5",
|
||||
"@fortawesome/angular-fontawesome": "~0.13.0",
|
||||
"@fortawesome/fontawesome-common-types": "~6.4.0",
|
||||
"@fortawesome/fontawesome-svg-core": "~6.4.0",
|
||||
"@fortawesome/free-solid-svg-icons": "~6.4.0",
|
||||
"@mempool/mempool.js": "2.3.0",
|
||||
"@ng-bootstrap/ng-bootstrap": "^13.1.1",
|
||||
"@ng-bootstrap/ng-bootstrap": "^15.1.0",
|
||||
"@types/qrcode": "~1.5.0",
|
||||
"bootstrap": "~4.6.1",
|
||||
"bootstrap": "~4.6.2",
|
||||
"browserify": "^17.0.0",
|
||||
"clipboard": "^2.0.11",
|
||||
"domino": "^2.1.6",
|
||||
"echarts": "~5.4.1",
|
||||
"echarts": "~5.4.3",
|
||||
"echarts-gl": "^2.0.9",
|
||||
"lightweight-charts": "~3.8.0",
|
||||
"ngx-echarts": "~14.0.0",
|
||||
"ngx-infinite-scroll": "^14.0.1",
|
||||
"ngx-echarts": "~16.0.0",
|
||||
"ngx-infinite-scroll": "^16.0.0",
|
||||
"qrcode": "1.5.1",
|
||||
"rxjs": "~7.8.0",
|
||||
"tinyify": "^3.1.0",
|
||||
"rxjs": "~7.8.1",
|
||||
"tinyify": "^4.0.0",
|
||||
"tlite": "^0.1.9",
|
||||
"tslib": "~2.4.1",
|
||||
"zone.js": "~0.12.0"
|
||||
"tslib": "~2.6.0",
|
||||
"zone.js": "~0.13.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/compiler-cli": "^14.2.12",
|
||||
"@angular/language-service": "^14.2.12",
|
||||
"@angular/compiler-cli": "^16.1.5",
|
||||
"@angular/language-service": "^16.1.5",
|
||||
"@types/node": "^18.11.9",
|
||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||
"@typescript-eslint/parser": "^5.48.1",
|
||||
"eslint": "^8.31.0",
|
||||
"http-proxy-middleware": "~2.0.6",
|
||||
"prettier": "^2.8.2",
|
||||
"prettier": "^3.0.0",
|
||||
"ts-node": "~10.9.1",
|
||||
"typescript": "~4.6.4"
|
||||
"typescript": "~4.9.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@cypress/schematic": "^2.4.0",
|
||||
"cypress": "^12.7.0",
|
||||
"cypress-fail-on-console-error": "~4.0.2",
|
||||
"@cypress/schematic": "^2.5.0",
|
||||
"cypress": "^12.17.1",
|
||||
"cypress-fail-on-console-error": "~4.0.3",
|
||||
"cypress-wait-until": "^1.7.2",
|
||||
"mock-socket": "~9.1.5",
|
||||
"start-server-and-test": "~1.14.0"
|
||||
"mock-socket": "~9.2.1",
|
||||
"start-server-and-test": "~2.0.0"
|
||||
},
|
||||
"scarfSettings": {
|
||||
"enabled": false
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser';
|
||||
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';
|
||||
@@ -48,8 +48,7 @@ const providers = [
|
||||
AppComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule.withServerTransition({ appId: 'serverApp' }),
|
||||
BrowserTransferStateModule,
|
||||
BrowserModule,
|
||||
AppRoutingModule,
|
||||
HttpClientModule,
|
||||
BrowserAnimationsModule,
|
||||
|
||||
@@ -281,3 +281,15 @@ export function isFeatureActive(network: string, height: number, feature: 'rbf'
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function calcScriptHash$(script: string): Promise<string> {
|
||||
if (!/^[0-9a-fA-F]*$/.test(script) || script.length % 2 !== 0) {
|
||||
throw new Error('script is not a valid hex string');
|
||||
}
|
||||
const buf = Uint8Array.from(script.match(/.{2}/g).map((byte) => parseInt(byte, 16)));
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', buf);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
return hashArray
|
||||
.map((bytes) => bytes.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
}
|
||||
@@ -411,7 +411,7 @@
|
||||
Trademark Notice<br>
|
||||
</div>
|
||||
<p>
|
||||
The Mempool Open Source Project™, mempool.space™, the mempool logo®, the mempool.space logos™, the mempool square logo®, and the mempool blocks logo™ are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
|
||||
The Mempool Open Source Project®, mempool.space™, the mempool logo®, the mempool.space logos™, the mempool square logo®, and the mempool blocks logo™ are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
|
||||
</p>
|
||||
<p>
|
||||
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on <https://mempool.space/trademark-policy>.
|
||||
|
||||
@@ -64,13 +64,15 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
|
||||
this.address = null;
|
||||
this.addressInfo = null;
|
||||
this.addressString = params.get('id') || '';
|
||||
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(this.addressString)) {
|
||||
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(this.addressString)) {
|
||||
this.addressString = this.addressString.toLowerCase();
|
||||
}
|
||||
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
|
||||
|
||||
return this.electrsApiService.getAddress$(this.addressString)
|
||||
.pipe(
|
||||
return (this.addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
|
||||
? this.electrsApiService.getPubKeyAddress$(this.addressString)
|
||||
: this.electrsApiService.getAddress$(this.addressString)
|
||||
).pipe(
|
||||
catchError((err) => {
|
||||
this.isLoadingAddress = false;
|
||||
this.error = err;
|
||||
|
||||
@@ -81,6 +81,7 @@ h1 {
|
||||
top: 11px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
max-width: calc(100% - 180px);
|
||||
top: 17px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { switchMap, filter, catchError, map, tap } from 'rxjs/operators';
|
||||
import { Address, Transaction } from '../../interfaces/electrs.interface';
|
||||
import { Address, ScriptHash, Transaction } from '../../interfaces/electrs.interface';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { AudioService } from '../../services/audio.service';
|
||||
@@ -72,7 +72,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
this.addressInfo = null;
|
||||
document.body.scrollTo(0, 0);
|
||||
this.addressString = params.get('id') || '';
|
||||
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(this.addressString)) {
|
||||
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(this.addressString)) {
|
||||
this.addressString = this.addressString.toLowerCase();
|
||||
}
|
||||
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
|
||||
@@ -83,8 +83,11 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
.pipe(filter((state) => state === 2 && this.transactions && this.transactions.length > 0))
|
||||
)
|
||||
.pipe(
|
||||
switchMap(() => this.electrsApiService.getAddress$(this.addressString)
|
||||
.pipe(
|
||||
switchMap(() => (
|
||||
this.addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
|
||||
? this.electrsApiService.getPubKeyAddress$(this.addressString)
|
||||
: this.electrsApiService.getAddress$(this.addressString)
|
||||
).pipe(
|
||||
catchError((err) => {
|
||||
this.isLoadingAddress = false;
|
||||
this.error = err;
|
||||
@@ -114,7 +117,9 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
this.updateChainStats();
|
||||
this.isLoadingAddress = false;
|
||||
this.isLoadingTransactions = true;
|
||||
return this.electrsApiService.getAddressTransactions$(address.address);
|
||||
return address.is_pubkey
|
||||
? this.electrsApiService.getScriptHashTransactions$((address.address.length === 66 ? '21' : '41') + address.address + 'ac')
|
||||
: this.electrsApiService.getAddressTransactions$(address.address);
|
||||
}),
|
||||
switchMap((transactions) => {
|
||||
this.tempTransactions = transactions;
|
||||
@@ -161,31 +166,8 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
this.stateService.mempoolTransactions$
|
||||
.subscribe((transaction) => {
|
||||
if (this.transactions.some((t) => t.txid === transaction.txid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.transactions.unshift(transaction);
|
||||
this.transactions = this.transactions.slice();
|
||||
this.txCount++;
|
||||
|
||||
if (transaction.vout.some((vout) => vout.scriptpubkey_address === this.address.address)) {
|
||||
this.audioService.playSound('cha-ching');
|
||||
} else {
|
||||
this.audioService.playSound('chime');
|
||||
}
|
||||
|
||||
transaction.vin.forEach((vin) => {
|
||||
if (vin.prevout.scriptpubkey_address === this.address.address) {
|
||||
this.sent += vin.prevout.value;
|
||||
}
|
||||
});
|
||||
transaction.vout.forEach((vout) => {
|
||||
if (vout.scriptpubkey_address === this.address.address) {
|
||||
this.received += vout.value;
|
||||
}
|
||||
});
|
||||
.subscribe(tx => {
|
||||
this.addTransaction(tx);
|
||||
});
|
||||
|
||||
this.stateService.blockTransactions$
|
||||
@@ -195,12 +177,47 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
tx.status = transaction.status;
|
||||
this.transactions = this.transactions.slice();
|
||||
this.audioService.playSound('magic');
|
||||
} else {
|
||||
if (this.addTransaction(transaction, false)) {
|
||||
this.audioService.playSound('magic');
|
||||
}
|
||||
}
|
||||
this.totalConfirmedTxCount++;
|
||||
this.loadedConfirmedTxCount++;
|
||||
});
|
||||
}
|
||||
|
||||
addTransaction(transaction: Transaction, playSound: boolean = true): boolean {
|
||||
if (this.transactions.some((t) => t.txid === transaction.txid)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.transactions.unshift(transaction);
|
||||
this.transactions = this.transactions.slice();
|
||||
this.txCount++;
|
||||
|
||||
if (playSound) {
|
||||
if (transaction.vout.some((vout) => vout?.scriptpubkey_address === this.address.address)) {
|
||||
this.audioService.playSound('cha-ching');
|
||||
} else {
|
||||
this.audioService.playSound('chime');
|
||||
}
|
||||
}
|
||||
|
||||
transaction.vin.forEach((vin) => {
|
||||
if (vin?.prevout?.scriptpubkey_address === this.address.address) {
|
||||
this.sent += vin.prevout.value;
|
||||
}
|
||||
});
|
||||
transaction.vout.forEach((vout) => {
|
||||
if (vout?.scriptpubkey_address === this.address.address) {
|
||||
this.received += vout.value;
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
if (this.isLoadingTransactions || !this.totalConfirmedTxCount || this.loadedConfirmedTxCount >= this.totalConfirmedTxCount) {
|
||||
return;
|
||||
|
||||
@@ -40,8 +40,8 @@
|
||||
</a>
|
||||
|
||||
<div ngbDropdown (window:resize)="onResize($event)" class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED || env.LIQUID_TESTNET_ENABLED">
|
||||
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split" aria-haspopup="true">
|
||||
<app-svg-images name="bisq" width="20" height="20" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images>
|
||||
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split d-flex justify-content-center align-items-center" aria-haspopup="true">
|
||||
<app-svg-images class="d-flex justify-content-center align-items-center current-network-svg" name="bisq" width="20" height="20" viewBox="0 0 80 80"></app-svg-images>
|
||||
</button>
|
||||
<div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['mainnet'] || '/')" ngbDropdownItem class="mainnet"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
|
||||
|
||||
@@ -147,3 +147,18 @@ nav {
|
||||
.navbar-brand {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.current-network-svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
:host-context(.rtl-layout) {
|
||||
.current-network-svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-left: 5px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,8 @@
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 250px);
|
||||
height: calc(100vh - 225px);
|
||||
min-height: 400px;
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 250px);
|
||||
height: calc(100vh - 225px);
|
||||
min-height: 400px;
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 250px);
|
||||
height: calc(100vh - 225px);
|
||||
min-height: 400px;
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export default class TxView implements TransactionStripped {
|
||||
value: number;
|
||||
feerate: number;
|
||||
rate?: number;
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'fullrbf';
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf';
|
||||
context?: 'projected' | 'actual';
|
||||
scene?: BlockScene;
|
||||
|
||||
@@ -207,7 +207,7 @@ export default class TxView implements TransactionStripped {
|
||||
return auditColors.censored;
|
||||
case 'missing':
|
||||
case 'sigop':
|
||||
case 'fullrbf':
|
||||
case 'rbf':
|
||||
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
|
||||
case 'fresh':
|
||||
case 'freshcpfp':
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<td *ngSwitchCase="'freshcpfp'"><span class="badge badge-warning" i18n="transaction.audit.recently-cpfped">Recently CPFP'd</span></td>
|
||||
<td *ngSwitchCase="'added'"><span class="badge badge-warning" i18n="transaction.audit.added">Added</span></td>
|
||||
<td *ngSwitchCase="'selected'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
|
||||
<td *ngSwitchCase="'fullrbf'"><span class="badge badge-warning" i18n="transaction.audit.fullrbf">Full RBF</span></td>
|
||||
<td *ngSwitchCase="'rbf'"><span class="badge badge-warning" i18n="transaction.audit.conflicting">Conflicting</span></td>
|
||||
</ng-container>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 250px);
|
||||
height: calc(100vh - 225px);
|
||||
min-height: 400px;
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 250px);
|
||||
height: calc(100vh - 225px);
|
||||
min-height: 400px;
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
@@ -144,10 +144,12 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
for (const block of blocks) {
|
||||
if (block.id === this.blockHash) {
|
||||
this.block = block;
|
||||
block.extras.minFee = this.getMinBlockFee(block);
|
||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||
if (block?.extras?.reward != undefined) {
|
||||
this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
|
||||
if (block.extras) {
|
||||
block.extras.minFee = this.getMinBlockFee(block);
|
||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||
if (block?.extras?.reward != undefined) {
|
||||
this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
|
||||
}
|
||||
}
|
||||
} else if (block.height === this.block?.height) {
|
||||
this.block.stale = true;
|
||||
@@ -246,8 +248,10 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
this.updateAuditAvailableFromBlockHeight(block.height);
|
||||
this.block = block;
|
||||
block.extras.minFee = this.getMinBlockFee(block);
|
||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||
if (block.extras) {
|
||||
block.extras.minFee = this.getMinBlockFee(block);
|
||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||
}
|
||||
this.blockHeight = block.height;
|
||||
this.lastBlockHeight = this.blockHeight;
|
||||
this.nextBlockHeight = block.height + 1;
|
||||
@@ -335,7 +339,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
const isSelected = {};
|
||||
const isFresh = {};
|
||||
const isSigop = {};
|
||||
const isFullRbf = {};
|
||||
const isRbf = {};
|
||||
this.numMissing = 0;
|
||||
this.numUnexpected = 0;
|
||||
|
||||
@@ -359,7 +363,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
isSigop[txid] = true;
|
||||
}
|
||||
for (const txid of blockAudit.fullrbfTxs || []) {
|
||||
isFullRbf[txid] = true;
|
||||
isRbf[txid] = true;
|
||||
}
|
||||
// set transaction statuses
|
||||
for (const tx of blockAudit.template) {
|
||||
@@ -377,8 +381,8 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
} else if (isSigop[tx.txid]) {
|
||||
tx.status = 'sigop';
|
||||
} else if (isFullRbf[tx.txid]) {
|
||||
tx.status = 'fullrbf';
|
||||
} else if (isRbf[tx.txid]) {
|
||||
tx.status = 'rbf';
|
||||
} else {
|
||||
tx.status = 'missing';
|
||||
}
|
||||
@@ -394,8 +398,8 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
tx.status = 'added';
|
||||
} else if (inTemplate[tx.txid]) {
|
||||
tx.status = 'found';
|
||||
} else if (isFullRbf[tx.txid]) {
|
||||
tx.status = 'fullrbf';
|
||||
} else if (isRbf[tx.txid]) {
|
||||
tx.status = 'rbf';
|
||||
} else {
|
||||
tx.status = 'selected';
|
||||
isSelected[tx.txid] = true;
|
||||
|
||||
@@ -113,8 +113,10 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
const animate = this.chainTip != null && latestHeight > this.chainTip;
|
||||
|
||||
for (const block of blocks) {
|
||||
block.extras.minFee = this.getMinBlockFee(block);
|
||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||
if (block?.extras) {
|
||||
block.extras.minFee = this.getMinBlockFee(block);
|
||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||
}
|
||||
}
|
||||
|
||||
this.blocks = blocks;
|
||||
@@ -251,7 +253,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
if (height >= 0) {
|
||||
this.cacheService.loadBlock(height);
|
||||
block = this.cacheService.getCachedBlock(height) || null;
|
||||
if (block) {
|
||||
if (block?.extras) {
|
||||
block.extras.minFee = this.getMinBlockFee(block);
|
||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||
}
|
||||
@@ -293,8 +295,10 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
onBlockLoaded(block: BlockExtended) {
|
||||
const blockIndex = this.height - block.height;
|
||||
if (blockIndex >= 0 && blockIndex < this.blocks.length) {
|
||||
block.extras.minFee = this.getMinBlockFee(block);
|
||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||
if (block?.extras) {
|
||||
block.extras.minFee = this.getMinBlockFee(block);
|
||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||
}
|
||||
this.blocks[blockIndex] = block;
|
||||
this.blockStyles[blockIndex] = this.getStyleForBlock(block, blockIndex);
|
||||
}
|
||||
|
||||
@@ -82,9 +82,7 @@ export class BlockchainComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
this.mempoolOffset = Math.max(0, width - this.dividerOffset);
|
||||
this.cd.markForCheck();
|
||||
setTimeout(() => {
|
||||
this.mempoolOffsetChange.emit(this.mempoolOffset);
|
||||
}, 0);
|
||||
this.mempoolOffsetChange.emit(this.mempoolOffset);
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
|
||||
@@ -68,7 +68,7 @@ export class BlocksList implements OnInit {
|
||||
for (const block of blocks) {
|
||||
// @ts-ignore: Need to add an extra field for the template
|
||||
block.extras.pool.logo = `/resources/mining-pools/` +
|
||||
block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
|
||||
block.extras.pool.slug + '.svg';
|
||||
}
|
||||
}
|
||||
if (this.widget) {
|
||||
@@ -84,10 +84,10 @@ export class BlocksList implements OnInit {
|
||||
.pipe(
|
||||
switchMap((blocks) => {
|
||||
if (blocks[0].height <= this.lastBlockHeight) {
|
||||
return [null]; // Return an empty stream so the last pipe is not executed
|
||||
return of([]); // Return an empty stream so the last pipe is not executed
|
||||
}
|
||||
this.lastBlockHeight = blocks[0].height;
|
||||
return blocks;
|
||||
return of(blocks);
|
||||
})
|
||||
)
|
||||
])
|
||||
@@ -102,7 +102,7 @@ export class BlocksList implements OnInit {
|
||||
if (this.stateService.env.MINING_DASHBOARD) {
|
||||
// @ts-ignore: Need to add an extra field for the template
|
||||
blocks[1][0].extras.pool.logo = `/resources/mining-pools/` +
|
||||
blocks[1][0].extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
|
||||
blocks[1][0].extras.pool.slug + '.svg';
|
||||
}
|
||||
acc.unshift(blocks[1][0]);
|
||||
acc = acc.slice(0, this.widget ? 6 : 15);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
overflow: hidden;
|
||||
|
||||
--chain-height: 60px;
|
||||
--clock-width: 300px;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChangeDetectionStrategy, Component, HostListener, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { combineLatest, Observable, timer } from 'rxjs';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
import { StateService } from '../..//services/state.service';
|
||||
@@ -61,6 +61,7 @@ export class DifficultyComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
private cd: ChangeDetectorRef,
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
) { }
|
||||
|
||||
@@ -189,9 +190,15 @@ export class DifficultyComponent implements OnInit {
|
||||
return shapes;
|
||||
}
|
||||
|
||||
@HostListener('pointerdown', ['$event'])
|
||||
onPointerDown(event) {
|
||||
this.onPointerMove(event);
|
||||
}
|
||||
|
||||
@HostListener('pointermove', ['$event'])
|
||||
onPointerMove(event) {
|
||||
this.tooltipPosition = { x: event.clientX, y: event.clientY };
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
onHover(event, rect): void {
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 250px);
|
||||
height: calc(100vh - 225px);
|
||||
min-height: 400px;
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 250px);
|
||||
height: calc(100vh - 225px);
|
||||
min-height: 400px;
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
@@ -45,8 +45,8 @@
|
||||
</a>
|
||||
|
||||
<div ngbDropdown (window:resize)="onResize()" class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED || env.LIQUID_TESTNET_ENABLED">
|
||||
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split" aria-haspopup="true">
|
||||
<app-svg-images [name]="network.val === '' ? 'liquid' : network.val" width="22" height="22" viewBox="0 0 125 125" style="width: 30px; height: 30px; margin-right: 5px;"></app-svg-images>
|
||||
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split d-flex justify-content-center align-items-center" aria-haspopup="true">
|
||||
<app-svg-images class="d-flex justify-content-center align-items-center current-network-svg" [name]="network.val === '' ? 'liquid' : network.val" width="20" height="20" viewBox="0 0 125 125"></app-svg-images>
|
||||
</button>
|
||||
<div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['mainnet'] || '')" ngbDropdownItem class="mainnet"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
|
||||
|
||||
@@ -136,4 +136,19 @@ nav {
|
||||
}
|
||||
.navbar-dark .navbar-nav .nav-link {
|
||||
color: #f1f1f1;
|
||||
}
|
||||
|
||||
.current-network-svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
:host-context(.rtl-layout) {
|
||||
.current-network-svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-left: 5px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,8 @@
|
||||
</a>
|
||||
|
||||
<div (window:resize)="onResize()" ngbDropdown class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED || env.LIQUID_TESTNET_ENABLED">
|
||||
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split" aria-haspopup="true">
|
||||
<app-svg-images [name]="network.val === '' ? 'bitcoin' : network.val" width="20" height="20" viewBox="0 0 65 65" style="width: 30px; height: 30px; margin-right: 5px;"></app-svg-images>
|
||||
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split d-flex justify-content-center align-items-center" aria-haspopup="true">
|
||||
<app-svg-images class="d-flex justify-content-center align-items-center current-network-svg" [name]="network.val === '' ? 'bitcoin' : network.val" width="20" height="20" viewBox="0 0 65 65"></app-svg-images>
|
||||
</button>
|
||||
<div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
|
||||
<a ngbDropdownItem class="mainnet" [routerLink]="networkPaths['mainnet'] || '/'"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
|
||||
|
||||
@@ -64,7 +64,9 @@ li.nav-item {
|
||||
|
||||
|
||||
.navbar-collapse {
|
||||
flex-basis: auto;
|
||||
@media (min-width: 564px) {
|
||||
flex-basis: auto;
|
||||
}
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@@ -193,3 +195,18 @@ nav {
|
||||
font-size: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
.current-network-svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
:host-context(.rtl-layout) {
|
||||
.current-network-svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-left: 5px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() getHref?: (index) => string = (index) => `/mempool-block/${index}`;
|
||||
@Input() allBlocks: boolean = false;
|
||||
|
||||
mempoolWidth: number = 0;
|
||||
@Output() widthChange: EventEmitter<number> = new EventEmitter();
|
||||
|
||||
specialBlocks = specialBlocks;
|
||||
@@ -49,6 +50,8 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
blockSubscription: Subscription;
|
||||
networkSubscription: Subscription;
|
||||
chainTipSubscription: Subscription;
|
||||
keySubscription: Subscription;
|
||||
isTabHiddenSubscription: Subscription;
|
||||
network = '';
|
||||
now = new Date().getTime();
|
||||
timeOffset = 0;
|
||||
@@ -115,8 +118,15 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.calculateTransactionPosition();
|
||||
});
|
||||
this.reduceMempoolBlocksToFitScreen(this.mempoolBlocks);
|
||||
this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden);
|
||||
this.loadingBlocks$ = this.stateService.isLoadingWebSocket$;
|
||||
this.isTabHiddenSubscription = this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden);
|
||||
this.loadingBlocks$ = combineLatest([
|
||||
this.stateService.isLoadingWebSocket$,
|
||||
this.stateService.isLoadingMempool$
|
||||
]).pipe(
|
||||
switchMap(([loadingBlocks, loadingMempool]) => {
|
||||
return of(loadingBlocks || loadingMempool);
|
||||
})
|
||||
);
|
||||
|
||||
this.mempoolBlocks$ = merge(
|
||||
of(true),
|
||||
@@ -155,7 +165,11 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}),
|
||||
tap(() => {
|
||||
this.cd.markForCheck();
|
||||
this.widthChange.emit(this.containerOffset + this.mempoolBlocks.length * this.blockOffset);
|
||||
const width = this.containerOffset + this.mempoolBlocks.length * this.blockOffset;
|
||||
if (this.mempoolWidth !== width) {
|
||||
this.mempoolWidth = width;
|
||||
this.widthChange.emit(this.mempoolWidth);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -212,7 +226,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.networkSubscription = this.stateService.networkChanged$
|
||||
.subscribe((network) => this.network = network);
|
||||
|
||||
this.stateService.keyNavigation$.subscribe((event) => {
|
||||
this.keySubscription = this.stateService.keyNavigation$.subscribe((event) => {
|
||||
if (this.markIndex === undefined) {
|
||||
return;
|
||||
}
|
||||
@@ -223,13 +237,12 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
if (this.mempoolBlocks[this.markIndex - 1]) {
|
||||
this.router.navigate([this.relativeUrlPipe.transform('mempool-block/'), this.markIndex - 1]);
|
||||
} else {
|
||||
this.stateService.blocks$
|
||||
.pipe(map((blocks) => blocks[0]))
|
||||
.subscribe((block) => {
|
||||
if (this.stateService.latestBlockHeight === block.height) {
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/block/'), block.id], { state: { data: { block } }});
|
||||
}
|
||||
});
|
||||
const blocks = this.stateService.blocksSubject$.getValue();
|
||||
for (const block of (blocks || [])) {
|
||||
if (this.stateService.latestBlockHeight === block.height) {
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/block/'), block.id], { state: { data: { block } }});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (event.key === nextKey) {
|
||||
if (this.mempoolBlocks[this.markIndex + 1]) {
|
||||
@@ -253,6 +266,8 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.networkSubscription.unsubscribe();
|
||||
this.timeLtrSubscription.unsubscribe();
|
||||
this.chainTipSubscription.unsubscribe();
|
||||
this.keySubscription.unsubscribe();
|
||||
this.isTabHiddenSubscription.unsubscribe();
|
||||
clearTimeout(this.resetTransitionTimeout);
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
<a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.latest-blocks">Latest blocks</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
|
||||
</a>
|
||||
<app-blocks-list [attr.data-cy]="'latest-blocks'" [widget]=true></app-blocks-list>
|
||||
</div>
|
||||
@@ -65,7 +65,7 @@
|
||||
<a class="title-link" href="" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.adjustments">Adjustments</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
|
||||
</a>
|
||||
<app-difficulty-adjustments-table [attr.data-cy]="'difficulty-adjustments-table'"></app-difficulty-adjustments-table>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { AfterViewInit, ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { EventType, NavigationStart, Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-mining-dashboard',
|
||||
@@ -8,10 +10,12 @@ import { WebsocketService } from '../../services/websocket.service';
|
||||
styleUrls: ['./mining-dashboard.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MiningDashboardComponent implements OnInit {
|
||||
export class MiningDashboardComponent implements OnInit, AfterViewInit {
|
||||
constructor(
|
||||
private seoService: SeoService,
|
||||
private websocketService: WebsocketService,
|
||||
private stateService: StateService,
|
||||
private router: Router
|
||||
) {
|
||||
this.seoService.setTitle($localize`:@@a681a4e2011bb28157689dbaa387de0dd0aa0c11:Mining Dashboard`);
|
||||
}
|
||||
@@ -19,4 +23,15 @@ export class MiningDashboardComponent implements OnInit {
|
||||
ngOnInit(): void {
|
||||
this.websocketService.want(['blocks', 'mempool-blocks', 'stats']);
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.stateService.focusSearchInputDesktop();
|
||||
this.router.events.subscribe((e: NavigationStart) => {
|
||||
if (e.type === EventType.NavigationStart) {
|
||||
if (e.url.indexOf('graphs') === -1) { // The mining dashboard and the graph component are part of the same module so we can't use ngAfterViewInit in graphs.component.ts to blur the input
|
||||
this.stateService.focusSearchInputDesktop();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,6 +139,8 @@
|
||||
<td class="" *ngIf="this.miningWindowPreference === '24h'"><b>{{ miningStats.lastEstimatedHashrate}} {{
|
||||
miningStats.miningUnits.hashrateUnit }}</b></td>
|
||||
<td class=""><b>{{ miningStats.blockCount }}</b></td>
|
||||
<td *ngIf="auditAvailable"></td>
|
||||
<td *ngIf="auditAvailable"></td>
|
||||
<td class="d-none d-md-table-cell"><b>{{ miningStats.totalEmptyBlock }} ({{ miningStats.totalEmptyBlockRatio
|
||||
}}%)</b></td>
|
||||
</tr>
|
||||
|
||||
@@ -89,7 +89,7 @@ export class PoolPreviewComponent implements OnInit {
|
||||
|
||||
this.openGraphService.waitOver('pool-stats-' + this.slug);
|
||||
|
||||
const logoSrc = `/resources/mining-pools/` + poolStats.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
|
||||
const logoSrc = `/resources/mining-pools/` + poolStats.pool.slug + '.svg';
|
||||
if (logoSrc === this.lastImgSrc) {
|
||||
this.openGraphService.waitOver('pool-img-' + this.slug);
|
||||
}
|
||||
|
||||
@@ -92,9 +92,9 @@
|
||||
<table class="table table-xs table-data">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.reward">Reward</th>
|
||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.hashrate">Hashrate (24h)</th>
|
||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="latest-blocks.avg_health" *ngIf="auditAvailable">Avg Health</th>
|
||||
<th scope="col" class="data-title clip text-center" style="width: 33%" i18n="mining.reward">Reward</th>
|
||||
<th scope="col" class="data-title clip text-center" style="width: 33%" i18n="mining.hashrate">Hashrate (24h)</th>
|
||||
<th scope="col" class="data-title clip text-center" style="width: 33%" i18n="latest-blocks.avg_health" *ngIf="auditAvailable">Avg Health</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -117,9 +117,9 @@
|
||||
<table class="table table-xs table-data">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.reward">Reward</th>
|
||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.hashrate">Hashrate (24h)</th>
|
||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="latest-blocks.avg_health" *ngIf="auditAvailable">Avg Health</th>
|
||||
<th scope="col" class="data-title clip text-center" style="width: 33%" i18n="mining.reward">Reward</th>
|
||||
<th scope="col" class="data-title clip text-center" style="width: 33%" i18n="mining.hashrate">Hashrate (24h)</th>
|
||||
<th scope="col" class="data-title clip text-center" style="width: 33%" i18n="latest-blocks.avg_health" *ngIf="auditAvailable">Avg Health</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -143,9 +143,9 @@
|
||||
<table class="table table-xs table-data">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
|
||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="1w">1w</th>
|
||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="all">All</th>
|
||||
<th scope="col" class="data-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
|
||||
<th scope="col" class="data-title text-center" style="width: 33%" i18n="1w">1w</th>
|
||||
<th scope="col" class="data-title text-center" style="width: 33%" i18n="all">All</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -165,9 +165,9 @@
|
||||
<table class="table table-xs table-data">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
|
||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="1w">1w</th>
|
||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="all">All</th>
|
||||
<th scope="col" class="data-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
|
||||
<th scope="col" class="data-title text-center" style="width: 33%" i18n="1w">1w</th>
|
||||
<th scope="col" class="data-title text-center" style="width: 33%" i18n="all">All</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -382,9 +382,9 @@
|
||||
<table class="table table-xs table-data text-center">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.total-reward">Reward</th>
|
||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.estimated">Hashrate (24h)</th>
|
||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.luck" *ngIf="auditAvailable">Avg Health</th>
|
||||
<th scope="col" class="data-title clip text-center" style="width: 33%" i18n="mining.total-reward">Reward</th>
|
||||
<th scope="col" class="data-title clip text-center" style="width: 33%" i18n="mining.estimated">Hashrate (24h)</th>
|
||||
<th scope="col" class="data-title clip text-center" style="width: 33%" i18n="mining.luck" *ngIf="auditAvailable">Avg Health</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -407,9 +407,9 @@
|
||||
<table class="table table-xs table-data text-center">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.total-reward">Reward</th>
|
||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.estimated">Hashrate (24h)</th>
|
||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.luck" *ngIf="auditAvailable">Avg Health</th>
|
||||
<th scope="col" class="data-title clip text-center" style="width: 33%" i18n="mining.total-reward">Reward</th>
|
||||
<th scope="col" class="data-title clip text-center" style="width: 33%" i18n="mining.estimated">Hashrate (24h)</th>
|
||||
<th scope="col" class="data-title clip text-center" style="width: 33%" i18n="mining.luck" *ngIf="auditAvailable">Avg Health</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -433,9 +433,9 @@
|
||||
<table class="table table-xs table-data text-center">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
|
||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="1w">1w</th>
|
||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="all">All</th>
|
||||
<th scope="col" class="data-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
|
||||
<th scope="col" class="data-title text-center" style="width: 33%" i18n="1w">1w</th>
|
||||
<th scope="col" class="data-title text-center" style="width: 33%" i18n="all">All</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -458,9 +458,9 @@
|
||||
<table class="table table-xs table-data text-center">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
|
||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="1w">1w</th>
|
||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="all">All</th>
|
||||
<th scope="col" class="data-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
|
||||
<th scope="col" class="data-title text-center" style="width: 33%" i18n="1w">1w</th>
|
||||
<th scope="col" class="data-title text-center" style="width: 33%" i18n="all">All</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -188,11 +188,19 @@ div.scrollable {
|
||||
}
|
||||
}
|
||||
|
||||
.block-count-title {
|
||||
.data-title {
|
||||
color: #4a68b9;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.clip {
|
||||
@media (max-width: 576px) {
|
||||
max-width: 85px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.table-data tr {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ export class PoolComponent implements OnInit {
|
||||
poolStats.pool.regexes = regexes.slice(0, -3);
|
||||
|
||||
return Object.assign({
|
||||
logo: `/resources/mining-pools/` + poolStats.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'
|
||||
logo: `/resources/mining-pools/` + poolStats.pool.slug + '.svg'
|
||||
}, poolStats);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
<h4>TRUST YOUR OWN SELF-HOSTED MEMPOOL EXPLORER</h4>
|
||||
|
||||
<p>For maximum privacy, we recommend that you use your own self-hosted instance of The Mempool Open Source Project™ on your own hardware. You can easily install your own self-hosted instance of this website on a Raspberry Pi using a one-click installation method maintained by various Bitcoin fullnode distributions such as Umbrel, RaspiBlitz, MyNode, and RoninDojo. See our project's GitHub page for more details about self-hosting this website.</p>
|
||||
<p>For maximum privacy, we recommend that you use your own self-hosted instance of The Mempool Open Source Project® on your own hardware. You can easily install your own self-hosted instance of this website on a Raspberry Pi using a one-click installation method maintained by various Bitcoin fullnode distributions such as Umbrel, RaspiBlitz, MyNode, and RoninDojo. See our project's GitHub page for more details about self-hosting this website.</p>
|
||||
|
||||
<br>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<h1 class="float-left" i18n="page.rbf-replacements">RBF Replacements</h1>
|
||||
<div *ngIf="isLoading" class="spinner-border ml-3" role="status"></div>
|
||||
|
||||
<div class="mode-toggle float-right" *ngIf="fullRbfEnabled">
|
||||
<div class="mode-toggle float-right">
|
||||
<form class="formRadioGroup">
|
||||
<div class="btn-group btn-group-toggle" name="radioBasic">
|
||||
<label class="btn btn-primary btn-sm" [class.active]="!fullRbf">
|
||||
@@ -22,12 +22,12 @@
|
||||
<div *ngFor="let tree of trees" class="tree">
|
||||
<p class="info">
|
||||
<span class="type">
|
||||
<span *ngIf="isMined(tree)" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
|
||||
<span *ngIf="isFullRbf(tree)" class="badge badge-info" i18n="transaction.full-rbf">Full RBF</span>
|
||||
<span *ngIf="tree.mined" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
|
||||
<span *ngIf="tree.fullRbf" class="badge badge-info" i18n="transaction.full-rbf">Full RBF</span>
|
||||
</span>
|
||||
<app-time kind="since" [time]="tree.time"></app-time>
|
||||
</p>
|
||||
<div class="timeline-wrapper" [class.mined]="isMined(tree)">
|
||||
<div class="timeline-wrapper" [class.mined]="tree.mined">
|
||||
<app-rbf-timeline [replacements]="tree"></app-rbf-timeline>
|
||||
</div>
|
||||
</div>
|
||||
@@ -36,11 +36,6 @@
|
||||
<p i18n="rbf.no-replacements-yet">there are no replacements in the mempool yet!</p>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<!-- <ngb-pagination class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''"
|
||||
[collectionSize]="blocksCount" [rotate]="true" [maxSize]="maxSize" [pageSize]="15" [(page)]="page"
|
||||
(pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false">
|
||||
</ngb-pagination> -->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,6 @@ export class RbfList implements OnInit, OnDestroy {
|
||||
rbfTrees$: Observable<RbfTree[]>;
|
||||
nextRbfSubject = new BehaviorSubject(null);
|
||||
urlFragmentSubscription: Subscription;
|
||||
fullRbfEnabled: boolean;
|
||||
fullRbf: boolean;
|
||||
isLoading = true;
|
||||
|
||||
@@ -27,9 +26,7 @@ export class RbfList implements OnInit, OnDestroy {
|
||||
private apiService: ApiService,
|
||||
public stateService: StateService,
|
||||
private websocketService: WebsocketService,
|
||||
) {
|
||||
this.fullRbfEnabled = stateService.env.FULL_RBF_ENABLED;
|
||||
}
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.urlFragmentSubscription = this.route.fragment.subscribe((fragment) => {
|
||||
@@ -56,25 +53,6 @@ export class RbfList implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
toggleFullRbf(event) {
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
fragment: this.fullRbf ? null : 'fullrbf'
|
||||
});
|
||||
}
|
||||
|
||||
isFullRbf(tree: RbfTree): boolean {
|
||||
return tree.fullRbf;
|
||||
}
|
||||
|
||||
isMined(tree: RbfTree): boolean {
|
||||
return tree.mined;
|
||||
}
|
||||
|
||||
// pageChange(page: number) {
|
||||
// this.fromTreeSubject.next(this.lastTreeId);
|
||||
// }
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.websocketService.stopTrackRbf();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<form [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate>
|
||||
<div class="d-flex">
|
||||
<div class="search-box-container mr-2">
|
||||
<input autofocus (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="Explore the full Bitcoin ecosystem">
|
||||
<input #searchInput (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="Explore the full Bitcoin ecosystem">
|
||||
<app-search-results #searchResults [hidden]="dropdownHidden" [results]="typeAhead$ | async" (selectedResult)="selectedResult($event)"></app-search-results>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -18,9 +18,10 @@
|
||||
|
||||
form {
|
||||
margin-top: 5px;
|
||||
@media (min-width: 576px) {
|
||||
@media (min-width: 564px) {
|
||||
margin-top: 0px;
|
||||
margin-left: 8px;
|
||||
margin-left: 5px;
|
||||
margin-right: -5px;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
width: 100%;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener, ElementRef } from '@angular/core';
|
||||
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { EventType, NavigationStart, Router } from '@angular/router';
|
||||
import { AssetsService } from '../../services/assets.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Observable, of, Subject, zip, BehaviorSubject, combineLatest } from 'rxjs';
|
||||
@@ -34,7 +34,7 @@ export class SearchFormComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59})$/;
|
||||
regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64})$/;
|
||||
regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/;
|
||||
regexTransaction = /^([a-fA-F0-9]{64})(:\d+)?$/;
|
||||
regexBlockheight = /^[0-9]{1,9}$/;
|
||||
@@ -47,6 +47,8 @@ export class SearchFormComponent implements OnInit {
|
||||
this.handleKeyDown($event);
|
||||
}
|
||||
|
||||
@ViewChild('searchInput') searchInput: ElementRef;
|
||||
|
||||
constructor(
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
private router: Router,
|
||||
@@ -55,11 +57,26 @@ export class SearchFormComponent implements OnInit {
|
||||
private electrsApiService: ElectrsApiService,
|
||||
private apiService: ApiService,
|
||||
private relativeUrlPipe: RelativeUrlPipe,
|
||||
private elementRef: ElementRef,
|
||||
) { }
|
||||
private elementRef: ElementRef
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||
|
||||
this.router.events.subscribe((e: NavigationStart) => { // Reset search focus when changing page
|
||||
if (this.searchInput && e.type === EventType.NavigationStart) {
|
||||
this.searchInput.nativeElement.blur();
|
||||
}
|
||||
});
|
||||
|
||||
this.stateService.searchFocus$.subscribe(() => {
|
||||
if (!this.searchInput) { // Try again a bit later once the view is properly initialized
|
||||
setTimeout(() => this.searchInput.nativeElement.focus(), 100);
|
||||
} else if (this.searchInput) {
|
||||
this.searchInput.nativeElement.focus();
|
||||
}
|
||||
});
|
||||
|
||||
this.searchForm = this.formBuilder.group({
|
||||
searchText: ['', Validators.required],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, ElementRef, HostListener, OnInit, OnDestroy, ViewChild, Input } from '@angular/core';
|
||||
import { Component, ElementRef, HostListener, OnInit, OnDestroy, ViewChild, Input, DoCheck } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { MarkBlockState, StateService } from '../../services/state.service';
|
||||
import { specialBlocks } from '../../app.constants';
|
||||
@@ -9,7 +9,7 @@ import { BlockExtended } from '../../interfaces/node-api.interface';
|
||||
templateUrl: './start.component.html',
|
||||
styleUrls: ['./start.component.scss'],
|
||||
})
|
||||
export class StartComponent implements OnInit, OnDestroy {
|
||||
export class StartComponent implements OnInit, OnDestroy, DoCheck {
|
||||
@Input() showLoadingIndicator = false;
|
||||
|
||||
interval = 60;
|
||||
@@ -43,6 +43,7 @@ export class StartComponent implements OnInit, OnDestroy {
|
||||
pageIndex: number = 0;
|
||||
pages: any[] = [];
|
||||
pendingMark: number | null = null;
|
||||
pendingOffset: number | null = null;
|
||||
lastUpdate: number = 0;
|
||||
lastMouseX: number;
|
||||
velocity: number = 0;
|
||||
@@ -54,6 +55,14 @@ export class StartComponent implements OnInit, OnDestroy {
|
||||
this.isiOS = ['iPhone','iPod','iPad'].includes((navigator as any)?.userAgentData?.platform || navigator.platform);
|
||||
}
|
||||
|
||||
ngDoCheck(): void {
|
||||
if (this.pendingOffset != null) {
|
||||
const offset = this.pendingOffset;
|
||||
this.pendingOffset = null;
|
||||
this.addConvertedScrollOffset(offset);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.firstPageWidth = 40 + (this.blockWidth * this.dynamicBlocksAmount);
|
||||
this.blockCounterSubscription = this.stateService.blocks$.subscribe((blocks) => {
|
||||
@@ -429,6 +438,7 @@ export class StartComponent implements OnInit, OnDestroy {
|
||||
|
||||
addConvertedScrollOffset(offset: number): void {
|
||||
if (!this.blockchainContainer?.nativeElement) {
|
||||
this.pendingOffset = offset;
|
||||
return;
|
||||
}
|
||||
if (this.timeLtr) {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
<div *ngIf="officialMempoolSpace">
|
||||
<h2>Trademark Policy and Guidelines</h2>
|
||||
<h5>The Mempool Open Source Project ™</h5>
|
||||
<h5>The Mempool Open Source Project ®</h5>
|
||||
<h6>Updated: July 19, 2021</h6>
|
||||
<br>
|
||||
|
||||
@@ -304,7 +304,7 @@
|
||||
|
||||
<p>Also, if you are using our Marks in a way described in the sections "Uses for Which We Are Granting a License," you must include the following trademark attribution at the foot of the webpage where you have used the Mark (or, if in a book, on the credits page), on any packaging or labeling, and on advertising or marketing materials:</p>
|
||||
|
||||
<p>“The Mempool Space K.K.™, The Mempool Open Source Project™, mempool.space™, the mempool logo®, the mempool.space logos™, the mempool square logo®, and the mempool blocks logo™ are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein.”</p>
|
||||
<p>“The Mempool Space K.K.™, The Mempool Open Source Project®, mempool.space™, the mempool logo®, the mempool.space logos™, the mempool square logo®, and the mempool blocks logo™ are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein.”</p>
|
||||
|
||||
<li>What to Do When You See Abuse</li>
|
||||
|
||||
|
||||
@@ -379,7 +379,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
ancestors: tx.ancestors,
|
||||
bestDescendant: tx.bestDescendant,
|
||||
};
|
||||
const hasRelatives = !!(tx.ancestors.length || tx.bestDescendant);
|
||||
const hasRelatives = !!(tx.ancestors?.length || tx.bestDescendant);
|
||||
this.hasEffectiveFeeRate = hasRelatives || (tx.effectiveFeePerVsize && (Math.abs(tx.effectiveFeePerVsize - tx.feePerVsize) > 0.01));
|
||||
} else {
|
||||
this.fetchCpfp$.next(this.tx.txid);
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<ng-template ngFor let-vin let-vindex="index" [ngForOf]="tx.vin.slice(0, getVinLimit(tx))" [ngForTrackBy]="trackByIndexFn">
|
||||
<tr [ngClass]="{
|
||||
'assetBox': (assetsMinimal && vin.prevout && assetsMinimal[vin.prevout.asset] && !vin.is_coinbase && vin.prevout.scriptpubkey_address && tx._unblinded) || inputIndex === vindex,
|
||||
'highlight': vin.prevout?.scriptpubkey_address === this.address && this.address !== ''
|
||||
'highlight': this.address !== '' && (vin.prevout?.scriptpubkey_address === this.address || (vin.prevout?.scriptpubkey_type === 'p2pk' && vin.prevout?.scriptpubkey.slice(2, -2) === this.address))
|
||||
}">
|
||||
<td class="arrow-td">
|
||||
<ng-template [ngIf]="vin.prevout === null && !vin.is_pegin" [ngIfElse]="hasPrevout">
|
||||
@@ -56,7 +56,9 @@
|
||||
<span i18n="transactions-list.peg-in">Peg-in</span>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk'">
|
||||
<span>P2PK</span>
|
||||
<span>P2PK <a class="address p2pk-address" [routerLink]="['/address/' | relativeUrl, vin.prevout.scriptpubkey.slice(2, -2)]" title="{{ vin.prevout.scriptpubkey.slice(2, -2) }}">
|
||||
<app-truncate [text]="vin.prevout.scriptpubkey.slice(2, -2)" [lastChars]="8"></app-truncate>
|
||||
</a></span>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchDefault>
|
||||
<ng-template [ngIf]="!vin.prevout" [ngIfElse]="defaultAddress">
|
||||
@@ -73,7 +75,7 @@
|
||||
{{ vin.prevout.scriptpubkey_type?.toUpperCase() }}
|
||||
</ng-template>
|
||||
<div>
|
||||
<app-address-labels [vin]="vin" [channel]="tx._channels && tx._channels.inputs[vindex] || null"></app-address-labels>
|
||||
<app-address-labels [vin]="vin" [channel]="tx._channels && tx._channels.inputs[vindex] ? tx._channels.inputs[vindex] : null"></app-address-labels>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
@@ -182,12 +184,19 @@
|
||||
<ng-template ngFor let-vout let-vindex="index" [ngForOf]="tx.vout.slice(0, getVoutLimit(tx))" [ngForTrackBy]="trackByIndexFn">
|
||||
<tr [ngClass]="{
|
||||
'assetBox': assetsMinimal && assetsMinimal[vout.asset] && vout.scriptpubkey_address && tx.vin && !tx.vin[0].is_coinbase && tx._unblinded || outputIndex === vindex,
|
||||
'highlight': vout.scriptpubkey_address === this.address && this.address !== ''
|
||||
'highlight': this.address !== '' && (vout.scriptpubkey_address === this.address || (vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey.slice(2, -2) === this.address))
|
||||
}">
|
||||
<td class="address-cell">
|
||||
<a class="address" *ngIf="vout.scriptpubkey_address; else scriptpubkey_type" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey_address]" title="{{ vout.scriptpubkey_address }}">
|
||||
<a class="address" *ngIf="vout.scriptpubkey_address; else pubkey_type" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey_address]" title="{{ vout.scriptpubkey_address }}">
|
||||
<app-truncate [text]="vout.scriptpubkey_address" [lastChars]="8"></app-truncate>
|
||||
</a>
|
||||
<ng-template #pubkey_type>
|
||||
<ng-container *ngIf="vout.scriptpubkey_type === 'p2pk'; else scriptpubkey_type">
|
||||
P2PK <a class="address p2pk-address" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey.slice(2, -2)]" title="{{ vout.scriptpubkey.slice(2, -2) }}">
|
||||
<app-truncate [text]="vout.scriptpubkey.slice(2, -2)" [lastChars]="8"></app-truncate>
|
||||
</a>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
<div>
|
||||
<app-address-labels [vout]="vout" [channel]="tx._channels && tx._channels.outputs[vindex] ? tx._channels.outputs[vindex] : null"></app-address-labels>
|
||||
</div>
|
||||
|
||||
@@ -140,6 +140,15 @@ h2 {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.p2pk-address {
|
||||
display: inline-block;
|
||||
margin-left: 1em;
|
||||
max-width: 100px;
|
||||
@media (min-width: 576px) {
|
||||
max-width: 200px
|
||||
}
|
||||
}
|
||||
|
||||
.grey-info-text {
|
||||
color:#6c757d;
|
||||
font-style: italic;
|
||||
|
||||
@@ -73,12 +73,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col" style="max-height: 410px">
|
||||
<div class="card">
|
||||
<div class="card" *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'; else latestBlocks">
|
||||
<div class="card-body">
|
||||
<a class="title-link" href="" [routerLink]="['/rbf' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.latest-rbf-replacements">Latest replacements</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
|
||||
</a>
|
||||
<table class="table lastest-replacements-table">
|
||||
<thead>
|
||||
@@ -106,6 +106,46 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #latestBlocks>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.latest-blocks">Latest blocks</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
|
||||
</a>
|
||||
<table class="table lastest-blocks-table">
|
||||
<thead>
|
||||
<th class="table-cell-height" i18n="dashboard.latest-blocks.height">Height</th>
|
||||
<th *ngIf="!stateService.env.MINING_DASHBOARD" class="table-cell-mined" i18n="dashboard.latest-blocks.mined">Mined</th>
|
||||
<th *ngIf="stateService.env.MINING_DASHBOARD" class="table-cell-mined pl-lg-4" i18n="mining.pool-name">Pool</th>
|
||||
<th class="table-cell-transaction-count" i18n="dashboard.latest-blocks.transaction-count">TXs</th>
|
||||
<th class="table-cell-size" i18n="dashboard.latest-blocks.size">Size</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let block of blocks$ | async; let i = index; trackBy: trackByBlock">
|
||||
<td class="table-cell-height" ><a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
|
||||
<td *ngIf="!stateService.env.MINING_DASHBOARD" class="table-cell-mined" ><app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time></td>
|
||||
<td *ngIf="stateService.env.MINING_DASHBOARD" class="table-cell-mined pl-lg-4">
|
||||
<a class="clear-link" [routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
|
||||
<img width="22" height="22" src="{{ block.extras.pool['logo'] }}"
|
||||
onError="this.src = '/resources/mining-pools/default.svg'">
|
||||
<span class="pool-name">{{ block.extras.pool.name }}</span>
|
||||
</a>
|
||||
</td>
|
||||
<td class="table-cell-transaction-count">{{ block.tx_count | number }}</td>
|
||||
<td class="table-cell-size">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-mempool {{ network$ | async }}" role="progressbar" [ngStyle]="{'width': (block.weight / stateService.env.BLOCK_WEIGHT_UNITS)*100 + '%' }"> </div>
|
||||
<div class="progress-text" [innerHTML]="block.size | bytes: 2"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="col" style="max-height: 410px">
|
||||
<div class="card">
|
||||
@@ -180,10 +220,10 @@
|
||||
<ng-template #mempoolTable let-mempoolInfoData>
|
||||
<div class="mempool-info-data">
|
||||
<div class="item">
|
||||
<h5 *ngIf="!mempoolInfoData.value || mempoolInfoData.value.memPoolInfo.mempoolminfee === 0.00001 || (stateService.env.BASE_MODULE === 'liquid' && mempoolInfoData.value.memPoolInfo.mempoolminfee === 0.000001) else purgingText" class="card-title" i18n="dashboard.minimum-fee|Minimum mempool fee">Minimum fee</h5>
|
||||
<h5 *ngIf="!mempoolInfoData.value || mempoolInfoData.value.memPoolInfo.mempoolminfee === mempoolInfoData.value.memPoolInfo.minrelaytxfee || (stateService.env.BASE_MODULE === 'liquid' && mempoolInfoData.value.memPoolInfo.mempoolminfee === 0.000001) else purgingText" class="card-title" i18n="dashboard.minimum-fee|Minimum mempool fee">Minimum fee</h5>
|
||||
<ng-template #purgingText><h5 class="card-title" i18n="dashboard.purging|Purgin below fee">Purging</h5></ng-template>
|
||||
<p class="card-text" *ngIf="(isLoadingWebSocket$ | async) === false && mempoolInfoData.value; else loading">
|
||||
<ng-template [ngIf]="mempoolInfoData.value.memPoolInfo.mempoolminfee > 0.00001">< </ng-template><app-fee-rate [fee]="mempoolInfoData.value.memPoolInfo.mempoolminfee * 100000"></app-fee-rate>
|
||||
<ng-template [ngIf]="mempoolInfoData.value.memPoolInfo.mempoolminfee !== mempoolInfoData.value.memPoolInfo.minrelaytxfee">< </ng-template><app-fee-rate [fee]="mempoolInfoData.value.memPoolInfo.mempoolminfee * 100000"></app-fee-rate>
|
||||
</p>
|
||||
</div>
|
||||
<div class="item">
|
||||
|
||||
@@ -175,6 +175,43 @@
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.lastest-blocks-table {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
tr, td, th {
|
||||
border: 0px;
|
||||
padding-top: 0.65rem !important;
|
||||
padding-bottom: 0.7rem !important;
|
||||
}
|
||||
.table-cell-height {
|
||||
width: 15%;
|
||||
}
|
||||
.table-cell-mined {
|
||||
width: 35%;
|
||||
text-align: left;
|
||||
}
|
||||
.table-cell-transaction-count {
|
||||
display: none;
|
||||
text-align: right;
|
||||
width: 20%;
|
||||
display: table-cell;
|
||||
}
|
||||
.table-cell-size {
|
||||
display: none;
|
||||
text-align: center;
|
||||
width: 30%;
|
||||
@media (min-width: 485px) {
|
||||
display: table-cell;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lastest-replacements-table {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { combineLatest, merge, Observable, of, Subscription } from 'rxjs';
|
||||
import { filter, map, scan, share, switchMap } from 'rxjs/operators';
|
||||
import { BlockExtended, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
|
||||
import { catchError, filter, map, scan, share, switchMap, tap } from 'rxjs/operators';
|
||||
import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface';
|
||||
import { MempoolInfo, TransactionStripped, ReplacementInfo } from '../interfaces/websocket.interface';
|
||||
import { ApiService } from '../services/api.service';
|
||||
import { StateService } from '../services/state.service';
|
||||
@@ -31,7 +31,7 @@ interface MempoolStatsData {
|
||||
styleUrls: ['./dashboard.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class DashboardComponent implements OnInit, OnDestroy {
|
||||
export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
featuredAssets$: Observable<any>;
|
||||
network$: Observable<string>;
|
||||
mempoolBlocksData$: Observable<MempoolBlocksData>;
|
||||
@@ -39,6 +39,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
mempoolLoadingStatus$: Observable<number>;
|
||||
vBytesPerSecondLimit = 1667;
|
||||
transactions$: Observable<TransactionStripped[]>;
|
||||
blocks$: Observable<BlockExtended[]>;
|
||||
replacements$: Observable<ReplacementInfo[]>;
|
||||
latestBlockHeight: number;
|
||||
mempoolTransactionsWeightPerSecondData: any;
|
||||
@@ -56,6 +57,10 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
private seoService: SeoService
|
||||
) { }
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.stateService.focusSearchInputDesktop();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.currencySubscription.unsubscribe();
|
||||
this.websocketService.stopTrackRbfSummary();
|
||||
@@ -144,12 +149,33 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
}, []),
|
||||
);
|
||||
|
||||
this.blocks$ = this.stateService.blocks$
|
||||
.pipe(
|
||||
tap((blocks) => {
|
||||
this.latestBlockHeight = blocks[0].height;
|
||||
}),
|
||||
switchMap((blocks) => {
|
||||
if (this.stateService.env.MINING_DASHBOARD === true) {
|
||||
for (const block of blocks) {
|
||||
// @ts-ignore: Need to add an extra field for the template
|
||||
block.extras.pool.logo = `/resources/mining-pools/` +
|
||||
block.extras.pool.slug + '.svg';
|
||||
}
|
||||
}
|
||||
return of(blocks.slice(0, 6));
|
||||
})
|
||||
);
|
||||
|
||||
this.replacements$ = this.stateService.rbfLatestSummary$;
|
||||
|
||||
this.mempoolStats$ = this.stateService.connectionState$
|
||||
.pipe(
|
||||
filter((state) => state === 2),
|
||||
switchMap(() => this.apiService.list2HStatistics$()),
|
||||
switchMap(() => this.apiService.list2HStatistics$().pipe(
|
||||
catchError((e) => {
|
||||
return of(null);
|
||||
})
|
||||
)),
|
||||
switchMap((mempoolStats) => {
|
||||
return merge(
|
||||
this.stateService.live2Chart$
|
||||
@@ -164,10 +190,14 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
}),
|
||||
map((mempoolStats) => {
|
||||
return {
|
||||
mempool: mempoolStats,
|
||||
weightPerSecond: this.handleNewMempoolData(mempoolStats.concat([])),
|
||||
};
|
||||
if (mempoolStats) {
|
||||
return {
|
||||
mempool: mempoolStats,
|
||||
weightPerSecond: this.handleNewMempoolData(mempoolStats.concat([])),
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
share(),
|
||||
);
|
||||
@@ -206,16 +236,4 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
trackByBlock(index: number, block: BlockExtended) {
|
||||
return block.height;
|
||||
}
|
||||
|
||||
checkFullRbf(tree: RbfTree): void {
|
||||
let fullRbf = false;
|
||||
for (const replaced of tree.replaces) {
|
||||
if (!replaced.tx.rbf) {
|
||||
fullRbf = true;
|
||||
}
|
||||
replaced.replacedBy = tree.tx;
|
||||
this.checkFullRbf(replaced);
|
||||
}
|
||||
tree.tx.fullRbf = fullRbf;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8936,8 +8936,8 @@ export const faqData = [
|
||||
type: "endpoint",
|
||||
category: "self-hosting",
|
||||
showConditions: bitcoinNetworks,
|
||||
fragment: "host-my-own-instance-linux-server",
|
||||
title: "How can I host my own instance on a Linux server?",
|
||||
fragment: "host-my-own-instance-server",
|
||||
title: "How can I host a Mempool instance on my own server?",
|
||||
},
|
||||
{
|
||||
type: "endpoint",
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<div class="doc-content">
|
||||
|
||||
<p class="doc-welcome-note">Below is a reference for the {{ network.val === '' ? 'Bitcoin' : network.val.charAt(0).toUpperCase() + network.val.slice(1) }} <ng-container i18n="api-docs.title">REST API service</ng-container>.</p>
|
||||
<p class="doc-welcome-note api-note" *ngIf="officialMempoolInstance">Note that we enforce rate limits. If you exceed these limits, you will get an HTTP 429 error. If you repeatedly exceed the limits, you may be banned from accessing the service altogether. Consider an <a [routerLink]="['/enterprise']">enterprise sponsorship</a> if you need higher API limits.</p>
|
||||
<p class="doc-welcome-note api-note" *ngIf="officialMempoolInstance">Note that we enforce rate limits. If you exceed these limits, you will get an HTTP 429 error. If you repeatedly exceed the limits, you may be banned from accessing the service altogether. Consider an <a href="/enterprise">enterprise sponsorship</a> if you need higher API limits.</p>
|
||||
|
||||
<div class="doc-item-container" *ngFor="let item of restDocs">
|
||||
<h3 *ngIf="( item.type === 'category' ) && ( item.showConditions.indexOf(network.val) > -1 )">{{ item.title }}</h3>
|
||||
@@ -279,8 +279,9 @@
|
||||
We support one-click installation on a number of Raspberry Pi full-node distros including Umbrel, RaspiBlitz, MyNode, RoninDojo, and Start9's Embassy.
|
||||
</ng-template>
|
||||
|
||||
<ng-template type="host-my-own-instance-linux-server">
|
||||
You can manually install Mempool on your own Linux server, but this requires advanced sysadmin skills since you will be manually configuring everything. We do not provide support for manual deployments.
|
||||
<ng-template type="host-my-own-instance-server">
|
||||
<p>You can manually install Mempool on your own server, but this requires advanced sysadmin skills since you will be manually configuring everything. You could also use our <a href="https://github.com/mempool/mempool/tree/master/docker" target="_blank">Docker images</a>.</p><p>In any case, we only provide support for manual deployments to <a href="/enterprise">enterprise sponsors</a>.</p>
|
||||
<p>For casual users, we strongly suggest installing Mempool using one of the <a href="https://github.com/mempool/mempool#one-click-installation" target="_blank">1-click install methods</a>.</p>
|
||||
</ng-template>
|
||||
|
||||
<ng-template type="install-mempool-with-docker">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user