Compare commits
70 Commits
hunicus/tr
...
mononaut/r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1037b50851 | ||
|
|
a3c82e44d9 | ||
|
|
78a0059d01 | ||
|
|
172d4f9dfc | ||
|
|
97ecc7b90f | ||
|
|
381d2f0b1f | ||
|
|
38a79249a3 | ||
|
|
b58abe4779 | ||
|
|
25925751eb | ||
|
|
0ebfd6f017 | ||
|
|
81d1c0a4d5 | ||
|
|
36fe5627c7 | ||
|
|
2d463326e0 | ||
|
|
a6edfcc272 | ||
|
|
de4265a6d1 | ||
|
|
dc43a81899 | ||
|
|
e59c961f25 | ||
|
|
db715a1dba | ||
|
|
202d4122b4 | ||
|
|
e2fdacfddd | ||
|
|
c84a444f79 | ||
|
|
ee2d8f8c5a | ||
|
|
44f2217a68 | ||
|
|
caa8cfbc0e | ||
|
|
02f361af73 | ||
|
|
a1e05c0c37 | ||
|
|
5e91af168b | ||
|
|
ae183210e0 | ||
|
|
56127dce6a | ||
|
|
0376467e6c | ||
|
|
48b55eed46 | ||
|
|
0ce043cca9 | ||
|
|
65dbafd2ec | ||
|
|
3f36a30d1d | ||
|
|
b021746e9e | ||
|
|
975ec772fa | ||
|
|
442a4ff6e0 | ||
|
|
cea218b81a | ||
|
|
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 | ||
|
|
709783280a | ||
|
|
7c29e51bbb | ||
|
|
548611f13a | ||
|
|
29c53a7852 | ||
|
|
ec918d57b2 |
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"]
|
||||
|
||||
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@@ -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'}}
|
||||
@@ -99,3 +97,6 @@ jobs:
|
||||
- name: Build
|
||||
run: npm run build
|
||||
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/frontend
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
21
LICENSE
21
LICENSE
@@ -1,4 +1,4 @@
|
||||
The Mempool Open Source Project™
|
||||
The Mempool Open Source Project
|
||||
Copyright (c) 2019-2023 The Mempool Open Source Project Developers
|
||||
|
||||
This program is free software; you can redistribute it and/or modify it under
|
||||
@@ -12,18 +12,13 @@ the terms of (at your option) either:
|
||||
Foundation, either version 3 of the License or any later version approved by a
|
||||
proxy statement published on <https://mempool.space/about>.
|
||||
|
||||
However, the above copyright licenses do not include an implied right or license
|
||||
to use any trademarks, service marks, logos, or trade names of Mempool Space K.K.
|
||||
or any other contributor to The Mempool Open Source Project.
|
||||
|
||||
The Mempool Open Source Project™, Mempool Accelerator™, Mempool Enterprise®,
|
||||
Mempool Liquidity™, mempool.space™, the mempool Logo, the mempool Square logo,
|
||||
the mempool Blocks logo, the mempool.space Vertical Logo, and the mempool.space
|
||||
Horizontal logo are registered trademarks or trademarks of Mempool Space K.K in
|
||||
Japan, the United States, and/or other countries.
|
||||
|
||||
See our full Trademark Policy and Guidelines for more details, published on
|
||||
<https://mempool.space/trademark-policy>.
|
||||
However, this copyright license does not include an implied right or license to
|
||||
use our trademarks: The Mempool Open Source Project™, mempool.space™, the
|
||||
mempool Logo™, the mempool.space Vertical Logo™, the mempool.space Horizontal
|
||||
Logo™, the mempool Square Logo™, and the mempool Blocks logo™ are registered
|
||||
trademarks or trademarks of Mempool Space K.K in Japan, the United States,
|
||||
and/or other countries. See our full Trademark Policy and Guidelines for more
|
||||
details, published on <https://mempool.space/trademark-policy>.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
|
||||
1
backend/.dockerignore
Normal file
1
backend/.dockerignore
Normal file
@@ -0,0 +1 @@
|
||||
Dockerfile
|
||||
158
backend/package-lock.json
generated
158
backend/package-lock.json
generated
@@ -19,6 +19,7 @@
|
||||
"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.9.3",
|
||||
"ws": "~8.13.0"
|
||||
@@ -1555,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",
|
||||
@@ -2718,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",
|
||||
@@ -3678,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",
|
||||
@@ -6577,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",
|
||||
@@ -8704,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",
|
||||
@@ -9604,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",
|
||||
@@ -10332,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",
|
||||
@@ -12454,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",
|
||||
|
||||
@@ -47,13 +47,14 @@
|
||||
"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.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.17",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
;
|
||||
}
|
||||
@@ -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.address);
|
||||
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.address, 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,77 @@ 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
|
||||
// Skip expensive lookups while mempool has priority
|
||||
if (onlyCoinbase) {
|
||||
try {
|
||||
const coinbase = await transactionUtils.$getTransactionExtended(txIds[0], false, false, false, addMempoolData);
|
||||
return [coinbase];
|
||||
} 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 (!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.$getTransactionExtended(txid, false, false, false, addMempoolData);
|
||||
transactionMap[txid] = tx;
|
||||
totalFound++;
|
||||
} catch (e) {
|
||||
logger.err(`Cannot fetch tx ${txid}. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
// Return list of transactions, preserving block order
|
||||
return txIds.map(txid => transactionMap[txid]).filter(tx => tx != null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -171,7 +191,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;
|
||||
}
|
||||
@@ -763,10 +785,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++;
|
||||
}
|
||||
|
||||
@@ -877,7 +907,7 @@ class Blocks {
|
||||
|
||||
let height = blockHeight;
|
||||
let summary: BlockSummary;
|
||||
if (cpfpSummary) {
|
||||
if (cpfpSummary && !Common.isLiquid()) {
|
||||
summary = {
|
||||
id: hash,
|
||||
transactions: cpfpSummary.transactions.map(tx => {
|
||||
|
||||
@@ -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,7 +175,7 @@ class DiskCache {
|
||||
}
|
||||
|
||||
async $loadMempoolCache(): Promise<void> {
|
||||
if (!fs.existsSync(DiskCache.FILE_NAME)) {
|
||||
if (!config.MEMPOOL.CACHE_ENABLED || !fs.existsSync(DiskCache.FILE_NAME)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import config from '../config';
|
||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
|
||||
import { MempoolTransactionExtended, TransactionExtended, VbytesPerSecond } from '../mempool.interfaces';
|
||||
import logger from '../logger';
|
||||
import { Common } from './common';
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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 > 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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})`);
|
||||
|
||||
@@ -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,9 @@ 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 getReplacedBy(txId: string): string | undefined {
|
||||
@@ -173,6 +219,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 +228,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 +250,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 +264,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 +303,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); });
|
||||
|
||||
@@ -360,8 +435,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;
|
||||
}
|
||||
|
||||
253
backend/src/api/redis-cache.ts
Normal file
253
backend/src/api/redis-cache.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
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();
|
||||
this.client.exists('mempool:0').then((mempoolExists) => {
|
||||
if (!mempoolExists) {
|
||||
for (let i = 0; i < 16; i++) {
|
||||
this.client.json.set(`mempool:${i.toString(16)}`, '$', {});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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.json.set('blocks', '$', blocks);
|
||||
} 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.json.set('block-summaries', '$', summaries);
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to update blocks 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.info(`Flushed ${this.cacheQueue.length} transactions to Redis cache`);
|
||||
this.cacheQueue = [];
|
||||
} else {
|
||||
logger.err(`Failed to flush ${this.cacheQueue.length} transactions to Redis cache`);
|
||||
}
|
||||
}
|
||||
|
||||
private async $addTransactions(newTransactions: MempoolTransactionExtended[]): Promise<boolean> {
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
await Promise.all(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 this.client.json.set(`mempool:${tx.txid.slice(0,1)}`, tx.txid, minified);
|
||||
}));
|
||||
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();
|
||||
await Promise.all(transactions.map(txid => {
|
||||
return this.client.json.del(`mempool:${txid.slice(0,1)}`, txid);
|
||||
}));
|
||||
} 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.json.set(`rbf:${type}:${txid}`, '$', 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.del(`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();
|
||||
return this.client.json.get('blocks');
|
||||
} 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();
|
||||
return this.client.json.get('block-summaries');
|
||||
} 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();
|
||||
let mempool = {};
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
for (let i = 0; i < 16; i++) {
|
||||
const shard = await this.client.json.get(`mempool:${i.toString(16)}`);
|
||||
logger.info(`Loaded ${Object.keys(shard).length} transactions from redis cache ${i.toString(16)}`);
|
||||
mempool = Object.assign(mempool, shard);
|
||||
}
|
||||
logger.info(`Total ${Object.keys(mempool).length} transactions loaded from redis cache `);
|
||||
logger.info(`Loaded 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 keys = await this.client.keys(`rbf:${type}:*`);
|
||||
const promises: Promise<MempoolTransactionExtended[]>[] = [];
|
||||
for (let i = 0; i < keys.length; i += 10000) {
|
||||
const keySlice = keys.slice(i, i + 10000);
|
||||
if (!keySlice.length) {
|
||||
continue;
|
||||
}
|
||||
promises.push(this.client.json.mGet(keySlice, '$').then(chunk => chunk?.length ? chunk.flat().map((v, i) => [keySlice[i].slice(`rbf:${type}:`.length), v]) : [] ));
|
||||
}
|
||||
const entries = await Promise.all(promises);
|
||||
return entries.flat();
|
||||
} 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[1]),
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default new RedisCache();
|
||||
@@ -35,6 +35,13 @@ class TransactionUtils {
|
||||
} 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 +53,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 +74,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 +170,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();
|
||||
|
||||
@@ -604,7 +604,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 +644,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 +665,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;
|
||||
|
||||
@@ -858,7 +858,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) {
|
||||
|
||||
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
|
||||
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
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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'.
|
||||
@@ -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('');
|
||||
}
|
||||
@@ -394,14 +394,34 @@
|
||||
The Mempool Open Source Project
|
||||
</div>
|
||||
<p>
|
||||
The Mempool Open Source Project™, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space™, the mempool Logo, the mempool Square logo, the mempool Blocks logo, the mempool.space Vertical Logo, and the mempool.space Horizontal logo are registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
|
||||
<a href="https://github.com/mempool/mempool">The Mempool Open Source Project</a> is free software; you can redistribute it and/or modify it under the terms of (at your option) either:<br>
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
1) the <a href="https://www.gnu.org/licenses/agpl-3.0-standalone.html">GNU Affero General Public License</a> as published by the Free Software Foundation, either version 3 of the License or any later version approved by a proxy statement published on <https://mempool.space/about>; or<br>
|
||||
</li>
|
||||
<li>
|
||||
2) the <a href="https://www.gnu.org/licenses/gpl-3.0-standalone.html">GNU General Public License</a> as published by the Free Software Foundation, either version 3 of the License or any later version approved by a proxy statement published on <https://mempool.space/about>.<br>
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the full license terms for more details.<br>
|
||||
</p>
|
||||
<div class="title">
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on <https://mempool.space/trademark-policy>.
|
||||
</p>
|
||||
<p>
|
||||
This program incorporates software and other components licensed from third parties. See the full list of <a href="/3rdpartylicenses.txt">Third-Party Licenses</a> for legal notices from those projects.
|
||||
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on <https://mempool.space/trademark-policy>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="footer-links">
|
||||
<a href="/3rdpartylicenses.txt">Third-party Licenses</a>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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}|[A-F0-9]{130}$/.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(/[a-f0-9]{130}/)
|
||||
? 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}|[A-F0-9]{130}$/.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(/[a-f0-9]{130}/)
|
||||
? 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$('41' + address.address + 'ac')
|
||||
: this.electrsApiService.getAddressTransactions$(address.address);
|
||||
}),
|
||||
switchMap((transactions) => {
|
||||
this.tempTransactions = transactions;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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);
|
||||
})
|
||||
)
|
||||
])
|
||||
|
||||
@@ -64,7 +64,9 @@ li.nav-item {
|
||||
|
||||
|
||||
.navbar-collapse {
|
||||
flex-basis: auto;
|
||||
@media (min-width: 564px) {
|
||||
flex-basis: auto;
|
||||
}
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -116,7 +117,14 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
});
|
||||
this.reduceMempoolBlocksToFitScreen(this.mempoolBlocks);
|
||||
this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden);
|
||||
this.loadingBlocks$ = this.stateService.isLoadingWebSocket$;
|
||||
this.loadingBlocks$ = combineLatest([
|
||||
this.stateService.isLoadingWebSocket$,
|
||||
this.stateService.isLoadingMempool$
|
||||
]).pipe(
|
||||
switchMap(([loadingBlocks, loadingMempool]) => {
|
||||
return of(loadingBlocks || loadingMempool);
|
||||
})
|
||||
);
|
||||
|
||||
this.mempoolBlocks$ = merge(
|
||||
of(true),
|
||||
@@ -155,7 +163,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);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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}|[0-9a-fA-F]{130})$/;
|
||||
regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/;
|
||||
regexTransaction = /^([a-fA-F0-9]{64})(:\d+)?$/;
|
||||
regexBlockheight = /^[0-9]{1,9}$/;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div *ngIf="officialMempoolSpace">
|
||||
<h2>Trademark Policy and Guidelines</h2>
|
||||
<h5>The Mempool Open Source Project ™</h5>
|
||||
<h6>Updated: July 20, 2023</h6>
|
||||
<h6>Updated: July 19, 2021</h6>
|
||||
<br>
|
||||
|
||||
<div class="text-left">
|
||||
@@ -56,9 +56,6 @@
|
||||
<tbody>
|
||||
<tr><td>Mempool Space K.K.</td></tr>
|
||||
<tr><td>The Mempool Open Source Project</td></tr>
|
||||
<tr><td>Mempool Accelerator</td></tr>
|
||||
<tr><td>Mempool Enterprise</td></tr>
|
||||
<tr><td>Mempool Liquidity</td></tr>
|
||||
<tr><td>mempool.space</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -307,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 Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space™, the mempool Logo, the mempool Square logo, the mempool Blocks logo, the mempool.space Vertical Logo, and the mempool.space Horizontal 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>
|
||||
|
||||
|
||||
@@ -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, 132) === 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, 132)]" title="{{ vin.prevout.scriptpubkey.slice(2, 132) }}">
|
||||
<app-truncate [text]="vin.prevout.scriptpubkey.slice(2, 132)" [lastChars]="8"></app-truncate>
|
||||
</a></span>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchDefault>
|
||||
<ng-template [ngIf]="!vin.prevout" [ngIfElse]="defaultAddress">
|
||||
@@ -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, 132) === 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, 132)]" title="{{ vout.scriptpubkey.slice(2, 132) }}">
|
||||
<app-truncate [text]="vout.scriptpubkey.slice(2, 132)" [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;
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
<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>
|
||||
@@ -112,7 +112,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>
|
||||
<table class="table lastest-blocks-table">
|
||||
<thead>
|
||||
|
||||
@@ -129,6 +129,22 @@ export interface Address {
|
||||
address: string;
|
||||
chain_stats: ChainStats;
|
||||
mempool_stats: MempoolStats;
|
||||
is_pubkey?: boolean;
|
||||
}
|
||||
|
||||
export interface ScriptHash {
|
||||
electrum?: boolean;
|
||||
scripthash: string;
|
||||
chain_stats: ChainStats;
|
||||
mempool_stats: MempoolStats;
|
||||
}
|
||||
|
||||
export interface AddressOrScriptHash {
|
||||
electrum?: boolean;
|
||||
address?: string;
|
||||
scripthash?: string;
|
||||
chain_stats: ChainStats;
|
||||
mempool_stats: MempoolStats;
|
||||
}
|
||||
|
||||
export interface ChainStats {
|
||||
|
||||
@@ -1,19 +1,43 @@
|
||||
<div class="box">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr></tr>
|
||||
<tr>
|
||||
<td i18n="lightning.starting-balance|Channel starting balance">Starting balance</td>
|
||||
<td *ngIf="showStartingBalance && minStartingBalance === maxStartingBalance"><app-sats [satoshis]="minStartingBalance"></app-sats></td>
|
||||
<td *ngIf="showStartingBalance && minStartingBalance !== maxStartingBalance">{{ minStartingBalance | number : '1.0-0' }} - {{ maxStartingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></td>
|
||||
<td *ngIf="!showStartingBalance">?</td>
|
||||
</tr>
|
||||
<tr *ngIf="channel.status === 2">
|
||||
<td i18n="lightning.closing-balance|Channel closing balance">Closing balance</td>
|
||||
<td *ngIf="showClosingBalance && minClosingBalance === maxClosingBalance"><app-sats [satoshis]="minClosingBalance"></app-sats></td>
|
||||
<td *ngIf="showClosingBalance && minClosingBalance !== maxClosingBalance">{{ minClosingBalance | number : '1.0-0' }} - {{ maxClosingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></td>
|
||||
<td *ngIf="!showClosingBalance">?</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="starting-balance" *ngIf="showStartingBalance">
|
||||
<h5 i18n="lightning.starting-balance|Channel starting balance">Starting balance</h5>
|
||||
<div class="nodes">
|
||||
<h5 class="alias">{{ left.alias }}</h5>
|
||||
<h5 class="alias">{{ right.alias }}</h5>
|
||||
</div>
|
||||
<div class="balances">
|
||||
<div class="balance left">
|
||||
<span class="value" *ngIf="minStartingBalance !== maxStartingBalance">{{ minStartingBalance | number : '1.0-0' }} - {{ maxStartingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></span>
|
||||
<span class="value" *ngIf="minStartingBalance === maxStartingBalance">{{ minStartingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></span>
|
||||
</div>
|
||||
<div class="balance right">
|
||||
<span class="value" *ngIf="minStartingBalance !== maxStartingBalance">{{ channel.capacity - maxStartingBalance | number : '1.0-0' }} - {{ channel.capacity - minStartingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></span>
|
||||
<span class="value" *ngIf="minStartingBalance === maxStartingBalance">{{ channel.capacity - maxStartingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="balance-bar">
|
||||
<div class="bar left" [class.hide-value]="hideStartingLeft" [style]="startingBalanceStyle.left"></div>
|
||||
<div class="bar center" [style]="startingBalanceStyle.center"></div>
|
||||
<div class="bar right" [class.hide-value]="hideStartingRight" [style]="startingBalanceStyle.right"></div>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="closing-balance" *ngIf="showClosingBalance">
|
||||
<h5 i18n="lightning.closing-balance|Channel closing balance">Closing balance</h5>
|
||||
<div class="balances">
|
||||
<div class="balance left">
|
||||
<span class="value" *ngIf="minClosingBalance !== maxClosingBalance">{{ minClosingBalance | number : '1.0-0' }} - {{ maxClosingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></span>
|
||||
<span class="value" *ngIf="minClosingBalance === maxClosingBalance">{{ minClosingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></span>
|
||||
</div>
|
||||
<div class="balance right">
|
||||
<span class="value" *ngIf="minClosingBalance !== maxClosingBalance">{{ channel.capacity - maxClosingBalance | number : '1.0-0' }} - {{ channel.capacity - minClosingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></span>
|
||||
<span class="value" *ngIf="minClosingBalance === maxClosingBalance">{{ channel.capacity - maxClosingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="balance-bar">
|
||||
<div class="bar left" [class.hide-value]="hideClosingLeft" [style]="closingBalanceStyle.left"></div>
|
||||
<div class="bar center" [style]="closingBalanceStyle.center"></div>
|
||||
<div class="bar right" [class.hide-value]="hideClosingRight" [style]="closingBalanceStyle.right"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -6,4 +6,98 @@
|
||||
.box {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.starting-balance, .closing-balance {
|
||||
width: 100%;
|
||||
|
||||
h5 {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.nodes {
|
||||
display: none;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.balances {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.balance {
|
||||
&.left {
|
||||
text-align: start;
|
||||
}
|
||||
&.right {
|
||||
text-align: end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.balance-bar {
|
||||
width: 100%;
|
||||
height: 2em;
|
||||
position: relative;
|
||||
|
||||
.bar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.left {
|
||||
background: #105fb0;
|
||||
}
|
||||
&.center {
|
||||
background: repeating-linear-gradient(
|
||||
60deg,
|
||||
#105fb0 0,
|
||||
#105fb0 12px,
|
||||
#1a9436 12px,
|
||||
#1a9436 24px
|
||||
);
|
||||
}
|
||||
&.right {
|
||||
background: #1a9436;
|
||||
}
|
||||
|
||||
.value {
|
||||
flex: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&.hide-value {
|
||||
.value {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
height: 1em;
|
||||
|
||||
.bar.center {
|
||||
background: repeating-linear-gradient(
|
||||
60deg,
|
||||
#105fb0 0,
|
||||
#105fb0 8px,
|
||||
#1a9436 8px,
|
||||
#1a9436 16px
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,8 @@ import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } f
|
||||
})
|
||||
export class ChannelCloseBoxComponent implements OnChanges {
|
||||
@Input() channel: any;
|
||||
@Input() local: any;
|
||||
@Input() remote: any;
|
||||
@Input() left: any;
|
||||
@Input() right: any;
|
||||
|
||||
showStartingBalance: boolean = false;
|
||||
showClosingBalance: boolean = false;
|
||||
@@ -18,29 +18,55 @@ export class ChannelCloseBoxComponent implements OnChanges {
|
||||
minClosingBalance: number;
|
||||
maxClosingBalance: number;
|
||||
|
||||
startingBalanceStyle: {
|
||||
left: string,
|
||||
center: string,
|
||||
right: string,
|
||||
} = {
|
||||
left: '',
|
||||
center: '',
|
||||
right: '',
|
||||
};
|
||||
|
||||
closingBalanceStyle: {
|
||||
left: string,
|
||||
center: string,
|
||||
right: string,
|
||||
} = {
|
||||
left: '',
|
||||
center: '',
|
||||
right: '',
|
||||
};
|
||||
|
||||
hideStartingLeft: boolean = false;
|
||||
hideStartingRight: boolean = false;
|
||||
hideClosingLeft: boolean = false;
|
||||
hideClosingRight: boolean = false;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (this.channel && this.local && this.remote) {
|
||||
this.showStartingBalance = (this.local.funding_balance || this.remote.funding_balance) && this.channel.funding_ratio;
|
||||
this.showClosingBalance = this.local.closing_balance || this.remote.closing_balance;
|
||||
let closingCapacity;
|
||||
if (this.channel && this.left && this.right) {
|
||||
this.showStartingBalance = (this.left.funding_balance || this.right.funding_balance) && this.channel.funding_ratio;
|
||||
this.showClosingBalance = this.left.closing_balance || this.right.closing_balance;
|
||||
|
||||
if (this.channel.single_funded) {
|
||||
if (this.local.funding_balance) {
|
||||
if (this.left.funding_balance) {
|
||||
this.minStartingBalance = this.channel.capacity;
|
||||
this.maxStartingBalance = this.channel.capacity;
|
||||
} else if (this.remote.funding_balance) {
|
||||
} else if (this.right.funding_balance) {
|
||||
this.minStartingBalance = 0;
|
||||
this.maxStartingBalance = 0;
|
||||
}
|
||||
} else {
|
||||
this.minStartingBalance = clampRound(0, this.channel.capacity, this.local.funding_balance * this.channel.funding_ratio);
|
||||
this.maxStartingBalance = clampRound(0, this.channel.capacity, this.channel.capacity - (this.remote.funding_balance * this.channel.funding_ratio));
|
||||
this.minStartingBalance = clampRound(0, this.channel.capacity, this.left.funding_balance * this.channel.funding_ratio);
|
||||
this.maxStartingBalance = clampRound(0, this.channel.capacity, this.channel.capacity - (this.right.funding_balance * this.channel.funding_ratio));
|
||||
}
|
||||
|
||||
const closingCapacity = this.channel.capacity - this.channel.closing_fee;
|
||||
this.minClosingBalance = clampRound(0, closingCapacity, this.local.closing_balance);
|
||||
this.maxClosingBalance = clampRound(0, closingCapacity, closingCapacity - this.remote.closing_balance);
|
||||
closingCapacity = this.channel.capacity - this.channel.closing_fee;
|
||||
this.minClosingBalance = clampRound(0, closingCapacity, this.left.closing_balance);
|
||||
this.maxClosingBalance = clampRound(0, closingCapacity, closingCapacity - this.right.closing_balance);
|
||||
|
||||
// margin of error to account for 2 x 330 sat anchor outputs
|
||||
if (Math.abs(this.minClosingBalance - this.maxClosingBalance) <= 660) {
|
||||
@@ -50,6 +76,26 @@ export class ChannelCloseBoxComponent implements OnChanges {
|
||||
this.showStartingBalance = false;
|
||||
this.showClosingBalance = false;
|
||||
}
|
||||
|
||||
const startingMinPc = (this.minStartingBalance / this.channel.capacity) * 100;
|
||||
const startingMaxPc = (this.maxStartingBalance / this.channel.capacity) * 100;
|
||||
this.startingBalanceStyle = {
|
||||
left: `left: 0%; right: ${100 - startingMinPc}%;`,
|
||||
center: `left: ${startingMinPc}%; right: ${100 -startingMaxPc}%;`,
|
||||
right: `left: ${startingMaxPc}%; right: 0%;`,
|
||||
};
|
||||
this.hideStartingLeft = startingMinPc < 15;
|
||||
this.hideStartingRight = startingMaxPc > 85;
|
||||
|
||||
const closingMinPc = (this.minClosingBalance / closingCapacity) * 100;
|
||||
const closingMaxPc = (this.maxClosingBalance / closingCapacity) * 100;
|
||||
this.closingBalanceStyle = {
|
||||
left: `left: 0%; right: ${100 - closingMinPc}%;`,
|
||||
center: `left: ${closingMinPc}%; right: ${100 - closingMaxPc}%;`,
|
||||
right: `left: ${closingMaxPc}%; right: 0%;`,
|
||||
};
|
||||
this.hideClosingLeft = closingMinPc < 15;
|
||||
this.hideClosingRight = closingMaxPc > 85;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,14 +75,14 @@
|
||||
<div class="row row-cols-1 row-cols-md-2" *ngIf="!error">
|
||||
<div class="col">
|
||||
<app-channel-box [channel]="channel.node_left"></app-channel-box>
|
||||
<app-channel-close-box *ngIf="showCloseBoxes(channel)" [channel]="channel" [local]="channel.node_left" [remote]="channel.node_right"></app-channel-close-box>
|
||||
</div>
|
||||
<div class="col">
|
||||
<app-channel-box [channel]="channel.node_right"></app-channel-box>
|
||||
<app-channel-close-box *ngIf="showCloseBoxes(channel)" [channel]="channel" [local]="channel.node_right" [remote]="channel.node_left"></app-channel-close-box>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-channel-close-box *ngIf="showCloseBoxes(channel)" [channel]="channel" [left]="channel.node_left" [right]="channel.node_right"></app-channel-close-box>
|
||||
|
||||
<br>
|
||||
|
||||
<ng-container *ngIf="transactions$ | async as transactions">
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
<a class="title-link" href="" [routerLink]="['/lightning/nodes/rankings/liquidity' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="lightning.liquidity-ranking">Liquidity Ranking</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-top-nodes-per-capacity [nodes$]="nodesRanking$" [widget]="true"></app-top-nodes-per-capacity>
|
||||
</div>
|
||||
@@ -75,7 +75,7 @@
|
||||
<a class="title-link" href="" [routerLink]="['/lightning/nodes/rankings/connectivity' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="lightning.connectivity-ranking">Connectivity Ranking</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-top-nodes-per-channels [nodes$]="nodesRanking$" [widget]="true"></app-top-nodes-per-channels>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<h5 class="card-title d-inline" i18n="lightning.liquidity-ranking">Liquidity Ranking</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true"
|
||||
style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
|
||||
style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
|
||||
</a>
|
||||
<app-top-nodes-per-capacity [nodes$]="nodesRanking$" [widget]="true"></app-top-nodes-per-capacity>
|
||||
</div>
|
||||
@@ -22,7 +22,7 @@
|
||||
<h5 class="card-title d-inline" i18n="lightning.connectivity-ranking">Connectivity Ranking</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true"
|
||||
style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
|
||||
style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
|
||||
</a>
|
||||
<app-top-nodes-per-channels [nodes$]="nodesRanking$" [widget]="true"></app-top-nodes-per-channels>
|
||||
</div>
|
||||
@@ -36,7 +36,7 @@
|
||||
<h5 class="card-title d-inline" i18n="lightning.top-channels-age">Oldest nodes</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true"
|
||||
style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
|
||||
style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
|
||||
</a>
|
||||
<app-oldest-nodes [widget]="true"></app-oldest-nodes>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Transaction, Address, Outspend, Recent, Asset } from '../interfaces/electrs.interface';
|
||||
import { Observable, from, of, switchMap } from 'rxjs';
|
||||
import { Transaction, Address, Outspend, Recent, Asset, ScriptHash } from '../interfaces/electrs.interface';
|
||||
import { StateService } from './state.service';
|
||||
import { BlockExtended } from '../interfaces/node-api.interface';
|
||||
import { calcScriptHash$ } from '../bitcoin.utils';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -65,6 +66,24 @@ export class ElectrsApiService {
|
||||
return this.httpClient.get<Address>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address);
|
||||
}
|
||||
|
||||
getPubKeyAddress$(pubkey: string): Observable<Address> {
|
||||
return this.getScriptHash$('41' + pubkey + 'ac').pipe(
|
||||
switchMap((scripthash: ScriptHash) => {
|
||||
return of({
|
||||
...scripthash,
|
||||
address: pubkey,
|
||||
is_pubkey: true,
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getScriptHash$(script: string): Observable<ScriptHash> {
|
||||
return from(calcScriptHash$(script)).pipe(
|
||||
switchMap(scriptHash => this.httpClient.get<ScriptHash>(this.apiBaseUrl + this.apiBasePath + '/api/scripthash/' + scriptHash))
|
||||
);
|
||||
}
|
||||
|
||||
getAddressTransactions$(address: string, txid?: string): Observable<Transaction[]> {
|
||||
let params = new HttpParams();
|
||||
if (txid) {
|
||||
@@ -73,6 +92,16 @@ export class ElectrsApiService {
|
||||
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs', { params });
|
||||
}
|
||||
|
||||
getScriptHashTransactions$(script: string, txid?: string): Observable<Transaction[]> {
|
||||
let params = new HttpParams();
|
||||
if (txid) {
|
||||
params = params.append('after_txid', txid);
|
||||
}
|
||||
return from(calcScriptHash$(script)).pipe(
|
||||
switchMap(scriptHash => this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/scripthash/' + scriptHash + '/txs', { params })),
|
||||
);
|
||||
}
|
||||
|
||||
getAsset$(assetId: string): Observable<Asset> {
|
||||
return this.httpClient.get<Asset>(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId);
|
||||
}
|
||||
|
||||
@@ -113,6 +113,7 @@ export class StateService {
|
||||
mempoolTxPosition$ = new Subject<{ txid: string, position: MempoolPosition}>();
|
||||
blockTransactions$ = new Subject<Transaction>();
|
||||
isLoadingWebSocket$ = new ReplaySubject<boolean>(1);
|
||||
isLoadingMempool$ = new BehaviorSubject<boolean>(true);
|
||||
vbytesPerSecond$ = new ReplaySubject<number>(1);
|
||||
previousRetarget$ = new ReplaySubject<number>(1);
|
||||
backendInfo$ = new ReplaySubject<IBackendInfo>(1);
|
||||
|
||||
@@ -368,6 +368,11 @@ export class WebsocketService {
|
||||
|
||||
if (response.loadingIndicators) {
|
||||
this.stateService.loadingIndicators$.next(response.loadingIndicators);
|
||||
if (response.loadingIndicators.mempool != null && response.loadingIndicators.mempool < 100) {
|
||||
this.stateService.isLoadingMempool$.next(true);
|
||||
} else {
|
||||
this.stateService.isLoadingMempool$.next(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (response.mempoolInfo) {
|
||||
|
||||
@@ -26,6 +26,8 @@ try {
|
||||
}
|
||||
}
|
||||
|
||||
const githubSecret = process.env.GITHUB_TOKEN;
|
||||
|
||||
function download(filename, url) {
|
||||
https.get(url, (response) => {
|
||||
if (response.statusCode < 200 || response.statusCode > 299) {
|
||||
@@ -55,6 +57,12 @@ function downloadMiningPoolLogos$() {
|
||||
headers: {'user-agent': 'node.js'}
|
||||
};
|
||||
|
||||
if (githubSecret) {
|
||||
console.log('Downloading the mining pool logos with authentication');
|
||||
options.headers['authorization'] = `Bearer ${githubSecret}`;
|
||||
options.headers['X-GitHub-Api-Version'] = '2022-11-28';
|
||||
}
|
||||
|
||||
https.get(options, (response) => {
|
||||
const chunks_of_data = [];
|
||||
|
||||
@@ -109,6 +117,13 @@ function downloadPromoVideoSubtiles$() {
|
||||
headers: {'user-agent': 'node.js'}
|
||||
};
|
||||
|
||||
if (githubSecret) {
|
||||
console.log('Downloading the promo video subtitles with authentication');
|
||||
options.headers['authorization'] = `Bearer ${githubSecret}`;
|
||||
options.headers['X-GitHub-Api-Version'] = '2022-11-28';
|
||||
}
|
||||
|
||||
|
||||
https.get(options, (response) => {
|
||||
const chunks_of_data = [];
|
||||
|
||||
@@ -163,6 +178,12 @@ function downloadPromoVideo$() {
|
||||
headers: {'user-agent': 'node.js'}
|
||||
};
|
||||
|
||||
if (githubSecret) {
|
||||
console.log('Downloading the promo videos with authentication');
|
||||
options.headers['authorization'] = `Bearer ${githubSecret}`;
|
||||
options.headers['X-GitHub-Api-Version'] = '2022-11-28';
|
||||
}
|
||||
|
||||
https.get(options, (response) => {
|
||||
const chunks_of_data = [];
|
||||
|
||||
|
||||
@@ -7,15 +7,15 @@
|
||||
"declaration": false,
|
||||
"downlevelIteration": true,
|
||||
"experimentalDecorators": true,
|
||||
"module": "es2020",
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"target": "es2020",
|
||||
"target": "ES2022",
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
],
|
||||
"lib": [
|
||||
"es2018",
|
||||
"ES2018",
|
||||
"dom",
|
||||
"dom.iterable"
|
||||
]
|
||||
@@ -24,5 +24,6 @@
|
||||
"fullTemplateTypeCheck": true,
|
||||
"strictInjectionParameters": true,
|
||||
"strictTemplates": true,
|
||||
"useDefineForClassFields": false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user