Merge branch 'master' into nymkappa/tx-overflow
This commit is contained in:
commit
5959c426f3
15
.github/dependabot.yml
vendored
15
.github/dependabot.yml
vendored
@ -7,7 +7,8 @@ updates:
|
|||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
ignore:
|
ignore:
|
||||||
- dependency-name: "*"
|
- dependency-name: "*"
|
||||||
update-types: ["version-update:semver-major"]
|
update-types:
|
||||||
|
["version-update:semver-major", "version-update:semver-patch"]
|
||||||
allow:
|
allow:
|
||||||
- dependency-type: "production"
|
- dependency-type: "production"
|
||||||
|
|
||||||
@ -18,7 +19,8 @@ updates:
|
|||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
ignore:
|
ignore:
|
||||||
- dependency-name: "*"
|
- dependency-name: "*"
|
||||||
update-types: ["version-update:semver-major"]
|
update-types:
|
||||||
|
["version-update:semver-major", "version-update:semver-patch"]
|
||||||
allow:
|
allow:
|
||||||
- dependency-type: "production"
|
- dependency-type: "production"
|
||||||
|
|
||||||
@ -28,7 +30,8 @@ updates:
|
|||||||
interval: weekly
|
interval: weekly
|
||||||
ignore:
|
ignore:
|
||||||
- dependency-name: "*"
|
- dependency-name: "*"
|
||||||
update-types: ["version-update:semver-major"]
|
update-types:
|
||||||
|
["version-update:semver-major", "version-update:semver-patch"]
|
||||||
|
|
||||||
- package-ecosystem: docker
|
- package-ecosystem: docker
|
||||||
directory: "/docker/frontend"
|
directory: "/docker/frontend"
|
||||||
@ -36,7 +39,8 @@ updates:
|
|||||||
interval: weekly
|
interval: weekly
|
||||||
ignore:
|
ignore:
|
||||||
- dependency-name: "*"
|
- dependency-name: "*"
|
||||||
update-types: ["version-update:semver-major"]
|
update-types:
|
||||||
|
["version-update:semver-major", "version-update:semver-patch"]
|
||||||
|
|
||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
@ -44,4 +48,5 @@ updates:
|
|||||||
interval: weekly
|
interval: weekly
|
||||||
ignore:
|
ignore:
|
||||||
- dependency-name: "*"
|
- dependency-name: "*"
|
||||||
update-types: ["version-update:semver-major"]
|
update-types:
|
||||||
|
["version-update:semver-major", "version-update:semver-patch"]
|
||||||
|
|||||||
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
@ -9,7 +9,7 @@ jobs:
|
|||||||
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node: ["16", "17", "18"]
|
node: ["16", "17", "18", "20"]
|
||||||
flavor: ["dev", "prod"]
|
flavor: ["dev", "prod"]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
@ -28,9 +28,7 @@ jobs:
|
|||||||
registry-url: "https://registry.npmjs.org"
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|
||||||
- name: Install 1.70.x Rust toolchain
|
- name: Install 1.70.x Rust toolchain
|
||||||
uses: actions-rs/toolchain@v1
|
uses: dtolnay/rust-toolchain@1.70
|
||||||
with:
|
|
||||||
toolchain: 1.70
|
|
||||||
|
|
||||||
- name: Install
|
- name: Install
|
||||||
if: ${{ matrix.flavor == 'dev'}}
|
if: ${{ matrix.flavor == 'dev'}}
|
||||||
@ -60,7 +58,7 @@ jobs:
|
|||||||
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node: ["16", "17", "18"]
|
node: ["16", "17", "18", "20"]
|
||||||
flavor: ["dev", "prod"]
|
flavor: ["dev", "prod"]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
@ -99,3 +97,6 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/frontend
|
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/frontend
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@ -13,7 +13,7 @@ the terms of (at your option) either:
|
|||||||
proxy statement published on <https://mempool.space/about>.
|
proxy statement published on <https://mempool.space/about>.
|
||||||
|
|
||||||
However, this copyright license does not include an implied right or license to
|
However, this copyright license does not include an implied right or license to
|
||||||
use our trademarks: The Mempool Open Source Project™, mempool.space™, the
|
use our trademarks: The Mempool Open Source Project®, mempool.space™, the
|
||||||
mempool Logo™, the mempool.space Vertical Logo™, the mempool.space Horizontal
|
mempool Logo™, the mempool.space Vertical Logo™, the mempool.space Horizontal
|
||||||
Logo™, the mempool Square Logo™, and the mempool Blocks logo™ are registered
|
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,
|
trademarks or trademarks of Mempool Space K.K in Japan, the United States,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# The Mempool Open Source Project™ [](https://dashboard.cypress.io/projects/ry4br7/runs)
|
# The Mempool Open Source Project® [](https://dashboard.cypress.io/projects/ry4br7/runs)
|
||||||
|
|
||||||
https://user-images.githubusercontent.com/93150691/226236121-375ea64f-b4a1-4cc0-8fad-a6fb33226840.mp4
|
https://user-images.githubusercontent.com/93150691/226236121-375ea64f-b4a1-4cc0-8fad-a6fb33226840.mp4
|
||||||
|
|
||||||
|
|||||||
1
backend/.dockerignore
Normal file
1
backend/.dockerignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
Dockerfile
|
||||||
@ -8,6 +8,7 @@
|
|||||||
"API_URL_PREFIX": "/api/v1/",
|
"API_URL_PREFIX": "/api/v1/",
|
||||||
"POLL_RATE_MS": 2000,
|
"POLL_RATE_MS": 2000,
|
||||||
"CACHE_DIR": "./cache",
|
"CACHE_DIR": "./cache",
|
||||||
|
"CACHE_ENABLED": true,
|
||||||
"CLEAR_PROTECTION_MINUTES": 20,
|
"CLEAR_PROTECTION_MINUTES": 20,
|
||||||
"RECOMMENDED_FEE_PERCENTILE": 50,
|
"RECOMMENDED_FEE_PERCENTILE": 50,
|
||||||
"BLOCK_WEIGHT_UNITS": 4000000,
|
"BLOCK_WEIGHT_UNITS": 4000000,
|
||||||
|
|||||||
158
backend/package-lock.json
generated
158
backend/package-lock.json
generated
@ -19,6 +19,7 @@
|
|||||||
"maxmind": "~4.3.11",
|
"maxmind": "~4.3.11",
|
||||||
"mysql2": "~3.5.2",
|
"mysql2": "~3.5.2",
|
||||||
"rust-gbt": "file:./rust-gbt",
|
"rust-gbt": "file:./rust-gbt",
|
||||||
|
"redis": "^4.6.6",
|
||||||
"socks-proxy-agent": "~7.0.0",
|
"socks-proxy-agent": "~7.0.0",
|
||||||
"typescript": "~4.9.3",
|
"typescript": "~4.9.3",
|
||||||
"ws": "~8.13.0"
|
"ws": "~8.13.0"
|
||||||
@ -1555,6 +1556,64 @@
|
|||||||
"node": ">= 8"
|
"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": {
|
"node_modules/@sinclair/typebox": {
|
||||||
"version": "0.25.24",
|
"version": "0.25.24",
|
||||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz",
|
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz",
|
||||||
@ -2718,6 +2777,14 @@
|
|||||||
"node": ">=12"
|
"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": {
|
"node_modules/co": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||||
@ -3678,6 +3745,14 @@
|
|||||||
"is-property": "^1.0.2"
|
"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": {
|
"node_modules/gensync": {
|
||||||
"version": "1.0.0-beta.2",
|
"version": "1.0.0-beta.2",
|
||||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||||
@ -6577,6 +6652,19 @@
|
|||||||
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
|
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/require-directory": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
@ -8704,6 +8792,53 @@
|
|||||||
"fastq": "^1.6.0"
|
"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": {
|
"@sinclair/typebox": {
|
||||||
"version": "0.25.24",
|
"version": "0.25.24",
|
||||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz",
|
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz",
|
||||||
@ -9604,6 +9739,11 @@
|
|||||||
"wrap-ansi": "^7.0.0"
|
"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": {
|
"co": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||||
@ -10332,6 +10472,11 @@
|
|||||||
"is-property": "^1.0.2"
|
"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": {
|
"gensync": {
|
||||||
"version": "1.0.0-beta.2",
|
"version": "1.0.0-beta.2",
|
||||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||||
@ -12454,6 +12599,19 @@
|
|||||||
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
|
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
|
||||||
"dev": true
|
"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": {
|
"require-directory": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
|
|||||||
@ -47,13 +47,14 @@
|
|||||||
"maxmind": "~4.3.11",
|
"maxmind": "~4.3.11",
|
||||||
"mysql2": "~3.5.2",
|
"mysql2": "~3.5.2",
|
||||||
"rust-gbt": "file:./rust-gbt",
|
"rust-gbt": "file:./rust-gbt",
|
||||||
|
"redis": "^4.6.6",
|
||||||
"socks-proxy-agent": "~7.0.0",
|
"socks-proxy-agent": "~7.0.0",
|
||||||
"typescript": "~4.9.3",
|
"typescript": "~4.9.3",
|
||||||
"ws": "~8.13.0"
|
"ws": "~8.13.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.21.3",
|
|
||||||
"@babel/code-frame": "^7.18.6",
|
"@babel/code-frame": "^7.18.6",
|
||||||
|
"@babel/core": "^7.21.3",
|
||||||
"@types/compression": "^1.7.2",
|
"@types/compression": "^1.7.2",
|
||||||
"@types/crypto-js": "^4.1.1",
|
"@types/crypto-js": "^4.1.1",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
"AUTOMATIC_BLOCK_REINDEXING": false,
|
"AUTOMATIC_BLOCK_REINDEXING": false,
|
||||||
"POLL_RATE_MS": 3,
|
"POLL_RATE_MS": 3,
|
||||||
"CACHE_DIR": "__MEMPOOL_CACHE_DIR__",
|
"CACHE_DIR": "__MEMPOOL_CACHE_DIR__",
|
||||||
|
"CACHE_ENABLED": true,
|
||||||
"CLEAR_PROTECTION_MINUTES": 4,
|
"CLEAR_PROTECTION_MINUTES": 4,
|
||||||
"RECOMMENDED_FEE_PERCENTILE": 5,
|
"RECOMMENDED_FEE_PERCENTILE": 5,
|
||||||
"BLOCK_WEIGHT_UNITS": 6,
|
"BLOCK_WEIGHT_UNITS": 6,
|
||||||
@ -127,5 +128,9 @@
|
|||||||
"AUDIT": false,
|
"AUDIT": false,
|
||||||
"AUDIT_START_HEIGHT": 774000,
|
"AUDIT_START_HEIGHT": 774000,
|
||||||
"SERVERS": []
|
"SERVERS": []
|
||||||
|
},
|
||||||
|
"REDIS": {
|
||||||
|
"ENABLED": false,
|
||||||
|
"UNIX_SOCKET_PATH": "/tmp/redis.sock"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
import { calcDifficultyAdjustment, DifficultyAdjustment } from '../../api/difficulty-adjustment';
|
import {
|
||||||
|
calcBitsDifference,
|
||||||
|
calcDifficultyAdjustment,
|
||||||
|
DifficultyAdjustment,
|
||||||
|
} from '../../api/difficulty-adjustment';
|
||||||
|
|
||||||
describe('Mempool Difficulty Adjustment', () => {
|
describe('Mempool Difficulty Adjustment', () => {
|
||||||
test('should calculate Difficulty Adjustments properly', () => {
|
test('should calculate Difficulty Adjustments properly', () => {
|
||||||
@ -86,4 +90,46 @@ describe('Mempool Difficulty Adjustment', () => {
|
|||||||
expect(result).toStrictEqual(vector[1]);
|
expect(result).toStrictEqual(vector[1]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should calculate Difficulty change from bits fields of two blocks', () => {
|
||||||
|
// Check same exponent + check min max for output
|
||||||
|
expect(calcBitsDifference(0x1d000200, 0x1d000100)).toEqual(100);
|
||||||
|
expect(calcBitsDifference(0x1d000400, 0x1d000100)).toEqual(300);
|
||||||
|
expect(calcBitsDifference(0x1d000800, 0x1d000100)).toEqual(300); // Actually 700
|
||||||
|
expect(calcBitsDifference(0x1d000100, 0x1d000200)).toEqual(-50);
|
||||||
|
expect(calcBitsDifference(0x1d000100, 0x1d000400)).toEqual(-75);
|
||||||
|
expect(calcBitsDifference(0x1d000100, 0x1d000800)).toEqual(-75); // Actually -87.5
|
||||||
|
// Check new higher exponent
|
||||||
|
expect(calcBitsDifference(0x1c000200, 0x1d000001)).toEqual(100);
|
||||||
|
expect(calcBitsDifference(0x1c000400, 0x1d000001)).toEqual(300);
|
||||||
|
expect(calcBitsDifference(0x1c000800, 0x1d000001)).toEqual(300);
|
||||||
|
expect(calcBitsDifference(0x1c000100, 0x1d000002)).toEqual(-50);
|
||||||
|
expect(calcBitsDifference(0x1c000100, 0x1d000004)).toEqual(-75);
|
||||||
|
expect(calcBitsDifference(0x1c000100, 0x1d000008)).toEqual(-75);
|
||||||
|
// Check new lower exponent
|
||||||
|
expect(calcBitsDifference(0x1d000002, 0x1c000100)).toEqual(100);
|
||||||
|
expect(calcBitsDifference(0x1d000004, 0x1c000100)).toEqual(300);
|
||||||
|
expect(calcBitsDifference(0x1d000008, 0x1c000100)).toEqual(300);
|
||||||
|
expect(calcBitsDifference(0x1d000001, 0x1c000200)).toEqual(-50);
|
||||||
|
expect(calcBitsDifference(0x1d000001, 0x1c000400)).toEqual(-75);
|
||||||
|
expect(calcBitsDifference(0x1d000001, 0x1c000800)).toEqual(-75);
|
||||||
|
// Check error when exponents are too far apart
|
||||||
|
expect(() => calcBitsDifference(0x1d000001, 0x1a000800)).toThrow(
|
||||||
|
/Impossible exponent difference/
|
||||||
|
);
|
||||||
|
// Check invalid inputs
|
||||||
|
expect(() => calcBitsDifference(0x7f000001, 0x1a000800)).toThrow(
|
||||||
|
/Invalid bits/
|
||||||
|
);
|
||||||
|
expect(() => calcBitsDifference(0, 0x1a000800)).toThrow(/Invalid bits/);
|
||||||
|
expect(() => calcBitsDifference(100.2783, 0x1a000800)).toThrow(
|
||||||
|
/Invalid bits/
|
||||||
|
);
|
||||||
|
expect(() => calcBitsDifference(0x00800000, 0x1a000800)).toThrow(
|
||||||
|
/Invalid bits/
|
||||||
|
);
|
||||||
|
expect(() => calcBitsDifference(0x1c000000, 0x1a000800)).toThrow(
|
||||||
|
/Invalid bits/
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -23,6 +23,7 @@ describe('Mempool Backend Config', () => {
|
|||||||
AUTOMATIC_BLOCK_REINDEXING: false,
|
AUTOMATIC_BLOCK_REINDEXING: false,
|
||||||
POLL_RATE_MS: 2000,
|
POLL_RATE_MS: 2000,
|
||||||
CACHE_DIR: './cache',
|
CACHE_DIR: './cache',
|
||||||
|
CACHE_ENABLED: true,
|
||||||
CLEAR_PROTECTION_MINUTES: 20,
|
CLEAR_PROTECTION_MINUTES: 20,
|
||||||
RECOMMENDED_FEE_PERCENTILE: 50,
|
RECOMMENDED_FEE_PERCENTILE: 50,
|
||||||
BLOCK_WEIGHT_UNITS: 4000000,
|
BLOCK_WEIGHT_UNITS: 4000000,
|
||||||
@ -127,6 +128,11 @@ describe('Mempool Backend Config', () => {
|
|||||||
AUDIT_START_HEIGHT: 774000,
|
AUDIT_START_HEIGHT: 774000,
|
||||||
SERVERS: []
|
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.PRICE_DATA_SERVER).toStrictEqual(fixture.PRICE_DATA_SERVER);
|
||||||
|
|
||||||
expect(config.EXTERNAL_DATA_SERVER).toStrictEqual(fixture.EXTERNAL_DATA_SERVER);
|
expect(config.EXTERNAL_DATA_SERVER).toStrictEqual(fixture.EXTERNAL_DATA_SERVER);
|
||||||
|
|
||||||
|
expect(config.REDIS).toStrictEqual(fixture.REDIS);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -173,12 +181,12 @@ describe('Mempool Backend Config', () => {
|
|||||||
// We have a few cases where we can't follow the pattern
|
// We have a few cases where we can't follow the pattern
|
||||||
if (root === 'MEMPOOL' && key === 'HTTP_PORT') {
|
if (root === 'MEMPOOL' && key === 'HTTP_PORT') {
|
||||||
console.log('skipping check for MEMPOOL_HTTP_PORT');
|
console.log('skipping check for MEMPOOL_HTTP_PORT');
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
switch (typeof value) {
|
switch (typeof value) {
|
||||||
case 'object': {
|
case 'object': {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
return;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
parseJson(value, key);
|
parseJson(value, key);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@ class Audit {
|
|||||||
const matches: string[] = []; // present in both mined block and template
|
const matches: string[] = []; // present in both mined block and template
|
||||||
const added: string[] = []; // present in mined block, not in template
|
const added: string[] = []; // present in mined block, not in template
|
||||||
const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
|
const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
|
||||||
const fullrbf: string[] = []; // either missing or present, and part of a fullrbf replacement
|
const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
|
||||||
const isCensored = {}; // missing, without excuse
|
const isCensored = {}; // missing, without excuse
|
||||||
const isDisplaced = {};
|
const isDisplaced = {};
|
||||||
let displacedWeight = 0;
|
let displacedWeight = 0;
|
||||||
@ -36,8 +36,9 @@ class Audit {
|
|||||||
// look for transactions that were expected in the template, but missing from the mined block
|
// look for transactions that were expected in the template, but missing from the mined block
|
||||||
for (const txid of projectedBlocks[0].transactionIds) {
|
for (const txid of projectedBlocks[0].transactionIds) {
|
||||||
if (!inBlock[txid]) {
|
if (!inBlock[txid]) {
|
||||||
if (rbfCache.isFullRbf(txid)) {
|
// allow missing transactions which either belong to a full rbf tree, or conflict with any transaction in the mined block
|
||||||
fullrbf.push(txid);
|
if (rbfCache.has(txid) && (rbfCache.isFullRbf(txid) || rbfCache.anyInSameTree(txid, (tx) => inBlock[tx.txid]))) {
|
||||||
|
rbf.push(txid);
|
||||||
} else if (mempool[txid]?.firstSeen != null && (now - (mempool[txid]?.firstSeen || 0)) <= PROPAGATION_MARGIN) {
|
} else if (mempool[txid]?.firstSeen != null && (now - (mempool[txid]?.firstSeen || 0)) <= PROPAGATION_MARGIN) {
|
||||||
// tx is recent, may have reached the miner too late for inclusion
|
// tx is recent, may have reached the miner too late for inclusion
|
||||||
fresh.push(txid);
|
fresh.push(txid);
|
||||||
@ -98,8 +99,8 @@ class Audit {
|
|||||||
if (inTemplate[tx.txid]) {
|
if (inTemplate[tx.txid]) {
|
||||||
matches.push(tx.txid);
|
matches.push(tx.txid);
|
||||||
} else {
|
} else {
|
||||||
if (rbfCache.isFullRbf(tx.txid)) {
|
if (rbfCache.has(tx.txid)) {
|
||||||
fullrbf.push(tx.txid);
|
rbf.push(tx.txid);
|
||||||
} else if (!isDisplaced[tx.txid]) {
|
} else if (!isDisplaced[tx.txid]) {
|
||||||
added.push(tx.txid);
|
added.push(tx.txid);
|
||||||
}
|
}
|
||||||
@ -147,7 +148,7 @@ class Audit {
|
|||||||
added,
|
added,
|
||||||
fresh,
|
fresh,
|
||||||
sigop: [],
|
sigop: [],
|
||||||
fullrbf,
|
fullrbf: rbf,
|
||||||
score,
|
score,
|
||||||
similarity,
|
similarity,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,10 +3,12 @@ import { IEsploraApi } from './esplora-api.interface';
|
|||||||
export interface AbstractBitcoinApi {
|
export interface AbstractBitcoinApi {
|
||||||
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>;
|
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>;
|
||||||
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>;
|
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>;
|
||||||
|
$getMempoolTransactions(lastTxid: string);
|
||||||
$getTransactionHex(txId: string): Promise<string>;
|
$getTransactionHex(txId: string): Promise<string>;
|
||||||
$getBlockHeightTip(): Promise<number>;
|
$getBlockHeightTip(): Promise<number>;
|
||||||
$getBlockHashTip(): Promise<string>;
|
$getBlockHashTip(): Promise<string>;
|
||||||
$getTxIdsForBlock(hash: string): Promise<string[]>;
|
$getTxIdsForBlock(hash: string): Promise<string[]>;
|
||||||
|
$getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]>;
|
||||||
$getBlockHash(height: number): Promise<string>;
|
$getBlockHash(height: number): Promise<string>;
|
||||||
$getBlockHeader(hash: string): Promise<string>;
|
$getBlockHeader(hash: string): Promise<string>;
|
||||||
$getBlock(hash: string): Promise<IEsploraApi.Block>;
|
$getBlock(hash: string): Promise<IEsploraApi.Block>;
|
||||||
@ -14,6 +16,8 @@ export interface AbstractBitcoinApi {
|
|||||||
$getAddress(address: string): Promise<IEsploraApi.Address>;
|
$getAddress(address: string): Promise<IEsploraApi.Address>;
|
||||||
$getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
|
$getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
|
||||||
$getAddressPrefix(prefix: string): string[];
|
$getAddressPrefix(prefix: string): string[];
|
||||||
|
$getScriptHash(scripthash: string): Promise<IEsploraApi.ScriptHash>;
|
||||||
|
$getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
|
||||||
$sendRawTransaction(rawTransaction: string): Promise<string>;
|
$sendRawTransaction(rawTransaction: string): Promise<string>;
|
||||||
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
|
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
|
||||||
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
|
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { IEsploraApi } from './esplora-api.interface';
|
|||||||
import blocks from '../blocks';
|
import blocks from '../blocks';
|
||||||
import mempool from '../mempool';
|
import mempool from '../mempool';
|
||||||
import { TransactionExtended } from '../../mempool.interfaces';
|
import { TransactionExtended } from '../../mempool.interfaces';
|
||||||
|
import transactionUtils from '../transaction-utils';
|
||||||
|
|
||||||
class BitcoinApi implements AbstractBitcoinApi {
|
class BitcoinApi implements AbstractBitcoinApi {
|
||||||
private rawMempoolCache: IBitcoinApi.RawMempool | null = null;
|
private rawMempoolCache: IBitcoinApi.RawMempool | null = null;
|
||||||
@ -59,9 +60,20 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$getTransactionHex(txId: string): Promise<string> {
|
$getMempoolTransactions(lastTxid: string): Promise<IEsploraApi.Transaction[]> {
|
||||||
return this.$getRawTransaction(txId, true)
|
return Promise.resolve([]);
|
||||||
.then((tx) => tx.hex || '');
|
}
|
||||||
|
|
||||||
|
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> {
|
$getBlockHeightTip(): Promise<number> {
|
||||||
@ -77,6 +89,10 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
.then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx);
|
.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> {
|
$getRawBlock(hash: string): Promise<Buffer> {
|
||||||
return this.bitcoindClient.getBlock(hash, 0)
|
return this.bitcoindClient.getBlock(hash, 0)
|
||||||
.then((raw: string) => Buffer.from(raw, "hex"));
|
.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.');
|
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'][]> {
|
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> {
|
||||||
return this.bitcoindClient.getRawMemPool();
|
return this.bitcoindClient.getRawMemPool();
|
||||||
}
|
}
|
||||||
@ -193,7 +217,7 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
scriptpubkey: vout.scriptPubKey.hex,
|
scriptpubkey: vout.scriptPubKey.hex,
|
||||||
scriptpubkey_address: vout.scriptPubKey && vout.scriptPubKey.address ? vout.scriptPubKey.address
|
scriptpubkey_address: vout.scriptPubKey && vout.scriptPubKey.address ? vout.scriptPubKey.address
|
||||||
: vout.scriptPubKey.addresses ? vout.scriptPubKey.addresses[0] : '',
|
: 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),
|
scriptpubkey_type: this.translateScriptPubKeyType(vout.scriptPubKey.type),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -203,7 +227,7 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
is_coinbase: !!vin.coinbase,
|
is_coinbase: !!vin.coinbase,
|
||||||
prevout: null,
|
prevout: null,
|
||||||
scriptsig: vin.scriptSig && vin.scriptSig.hex || vin.coinbase || '',
|
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,
|
sequence: vin.sequence,
|
||||||
txid: vin.txid || '',
|
txid: vin.txid || '',
|
||||||
vout: vin.vout || 0,
|
vout: vin.vout || 0,
|
||||||
@ -275,7 +299,7 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
}
|
}
|
||||||
const innerTx = await this.$getRawTransaction(vin.txid, false, false);
|
const innerTx = await this.$getRawTransaction(vin.txid, false, false);
|
||||||
vin.prevout = innerTx.vout[vin.vout];
|
vin.prevout = innerTx.vout[vin.vout];
|
||||||
this.addInnerScriptsToVin(vin);
|
transactionUtils.addInnerScriptsToVin(vin);
|
||||||
}
|
}
|
||||||
return transaction;
|
return transaction;
|
||||||
}
|
}
|
||||||
@ -314,7 +338,7 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
}
|
}
|
||||||
const innerTx = await this.$getRawTransaction(transaction.vin[i].txid, false, false);
|
const innerTx = await this.$getRawTransaction(transaction.vin[i].txid, false, false);
|
||||||
transaction.vin[i].prevout = innerTx.vout[transaction.vin[i].vout];
|
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;
|
totalIn += innerTx.vout[transaction.vin[i].vout].value;
|
||||||
}
|
}
|
||||||
if (lazyPrevouts && transaction.vin.length > 12) {
|
if (lazyPrevouts && transaction.vin.length > 12) {
|
||||||
@ -326,122 +350,6 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
return transaction;
|
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;
|
export default BitcoinApi;
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import websocketHandler from '../websocket-handler';
|
|||||||
import mempool from '../mempool';
|
import mempool from '../mempool';
|
||||||
import feeApi from '../fee-api';
|
import feeApi from '../fee-api';
|
||||||
import mempoolBlocks from '../mempool-blocks';
|
import mempoolBlocks from '../mempool-blocks';
|
||||||
import bitcoinApi, { bitcoinCoreApi } from './bitcoin-api-factory';
|
import bitcoinApi from './bitcoin-api-factory';
|
||||||
import { Common } from '../common';
|
import { Common } from '../common';
|
||||||
import backendInfo from '../backend-info';
|
import backendInfo from '../backend-info';
|
||||||
import transactionUtils from '../transaction-utils';
|
import transactionUtils from '../transaction-utils';
|
||||||
@ -121,6 +121,8 @@ class BitcoinRoutes {
|
|||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block-height/:height', this.getBlockHeight)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block-height/:height', this.getBlockHeight)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address', this.getAddress)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address', this.getAddress)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs', this.getAddressTransactions)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs', 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)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'address-prefix/:prefix', this.getAddressPrefix)
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
@ -481,7 +483,7 @@ class BitcoinRoutes {
|
|||||||
returnBlocks.push(localBlock);
|
returnBlocks.push(localBlock);
|
||||||
nextHash = localBlock.previousblockhash;
|
nextHash = localBlock.previousblockhash;
|
||||||
} else {
|
} else {
|
||||||
const block = await bitcoinCoreApi.$getBlock(nextHash);
|
const block = await bitcoinApi.$getBlock(nextHash);
|
||||||
returnBlocks.push(block);
|
returnBlocks.push(block);
|
||||||
nextHash = block.previousblockhash;
|
nextHash = block.previousblockhash;
|
||||||
}
|
}
|
||||||
@ -567,6 +569,45 @@ class BitcoinRoutes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getScriptHash(req: Request, res: Response) {
|
||||||
|
if (config.MEMPOOL.BACKEND === 'none') {
|
||||||
|
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const addressData = await bitcoinApi.$getScriptHash(req.params.scripthash);
|
||||||
|
res.json(addressData);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
||||||
|
return res.status(413).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getScriptHashTransactions(req: Request, res: Response): Promise<void> {
|
||||||
|
if (config.MEMPOOL.BACKEND === 'none') {
|
||||||
|
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let lastTxId: string = '';
|
||||||
|
if (req.query.after_txid && typeof req.query.after_txid === 'string') {
|
||||||
|
lastTxId = req.query.after_txid;
|
||||||
|
}
|
||||||
|
const transactions = await bitcoinApi.$getScriptHashTransactions(req.params.scripthash, lastTxId);
|
||||||
|
res.json(transactions);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
||||||
|
res.status(413).send(e instanceof Error ? e.message : e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async getAddressPrefix(req: Request, res: Response) {
|
private async getAddressPrefix(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
|
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> {
|
private $getScriptHashBalance(scriptHash: string): Promise<IElectrumApi.ScriptHashBalance> {
|
||||||
return this.electrumClient.blockchainScripthash_getBalance(this.encodeScriptHash(scriptHash));
|
return this.electrumClient.blockchainScripthash_getBalance(this.encodeScriptHash(scriptHash));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -99,6 +99,13 @@ export namespace IEsploraApi {
|
|||||||
electrum?: boolean;
|
electrum?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ScriptHash {
|
||||||
|
scripthash: string;
|
||||||
|
chain_stats: ChainStats;
|
||||||
|
mempool_stats: MempoolStats;
|
||||||
|
electrum?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ChainStats {
|
export interface ChainStats {
|
||||||
funded_txo_count: number;
|
funded_txo_count: number;
|
||||||
funded_txo_sum: 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);
|
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> {
|
$getTransactionHex(txId: string): Promise<string> {
|
||||||
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/hex');
|
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');
|
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> {
|
$getBlockHash(height: number): Promise<string> {
|
||||||
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/block-height/' + height);
|
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.');
|
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[] {
|
$getAddressPrefix(prefix: string): string[] {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,12 +26,15 @@ import PricesRepository from '../repositories/PricesRepository';
|
|||||||
import priceUpdater from '../tasks/price-updater';
|
import priceUpdater from '../tasks/price-updater';
|
||||||
import chainTips from './chain-tips';
|
import chainTips from './chain-tips';
|
||||||
import websocketHandler from './websocket-handler';
|
import websocketHandler from './websocket-handler';
|
||||||
|
import redisCache from './redis-cache';
|
||||||
|
import rbfCache from './rbf-cache';
|
||||||
|
import { calcBitsDifference } from './difficulty-adjustment';
|
||||||
|
|
||||||
class Blocks {
|
class Blocks {
|
||||||
private blocks: BlockExtended[] = [];
|
private blocks: BlockExtended[] = [];
|
||||||
private blockSummaries: BlockSummary[] = [];
|
private blockSummaries: BlockSummary[] = [];
|
||||||
private currentBlockHeight = 0;
|
private currentBlockHeight = 0;
|
||||||
private currentDifficulty = 0;
|
private currentBits = 0;
|
||||||
private lastDifficultyAdjustmentTime = 0;
|
private lastDifficultyAdjustmentTime = 0;
|
||||||
private previousDifficultyRetarget = 0;
|
private previousDifficultyRetarget = 0;
|
||||||
private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
|
private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
|
||||||
@ -70,6 +73,9 @@ class Blocks {
|
|||||||
* @param blockHash
|
* @param blockHash
|
||||||
* @param blockHeight
|
* @param blockHeight
|
||||||
* @param onlyCoinbase - Set to true if you only need the coinbase transaction
|
* @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[]>
|
* @returns Promise<TransactionExtended[]>
|
||||||
*/
|
*/
|
||||||
private async $getTransactionsExtended(
|
private async $getTransactionsExtended(
|
||||||
@ -80,62 +86,98 @@ class Blocks {
|
|||||||
quiet: boolean = false,
|
quiet: boolean = false,
|
||||||
addMempoolData: boolean = false,
|
addMempoolData: boolean = false,
|
||||||
): Promise<TransactionExtended[]> {
|
): Promise<TransactionExtended[]> {
|
||||||
const transactions: TransactionExtended[] = [];
|
const isEsplora = config.MEMPOOL.BACKEND === 'esplora';
|
||||||
|
const transactionMap: { [txid: string]: TransactionExtended } = {};
|
||||||
|
|
||||||
if (!txIds) {
|
if (!txIds) {
|
||||||
txIds = await bitcoinApi.$getTxIdsForBlock(blockHash);
|
txIds = await bitcoinApi.$getTxIdsForBlock(blockHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mempool = memPool.getMempool();
|
const mempool = memPool.getMempool();
|
||||||
let transactionsFound = 0;
|
let foundInMempool = 0;
|
||||||
let transactionsFetched = 0;
|
let totalFound = 0;
|
||||||
|
|
||||||
for (let i = 0; i < txIds.length; i++) {
|
// Copy existing transactions from the mempool
|
||||||
if (mempool[txIds[i]]) {
|
if (!onlyCoinbase) {
|
||||||
// We update blocks before the mempool (index.ts), therefore we can
|
for (const txid of txIds) {
|
||||||
// optimize here by directly fetching txs in the "outdated" mempool
|
if (mempool[txid]) {
|
||||||
transactions.push(mempool[txIds[i]]);
|
transactionMap[txid] = mempool[txid];
|
||||||
transactionsFound++;
|
foundInMempool++;
|
||||||
} else if (config.MEMPOOL.BACKEND === 'esplora' || !memPool.hasPriority() || i === 0) {
|
totalFound++;
|
||||||
// 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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (onlyCoinbase === true) {
|
if (onlyCoinbase) {
|
||||||
break; // Fetch the first transaction and exit
|
try {
|
||||||
|
const coinbase = await transactionUtils.$getTransactionExtendedRetry(txIds[0], false, false, false, addMempoolData);
|
||||||
|
if (coinbase && coinbase.vin[0].is_coinbase) {
|
||||||
|
return [coinbase];
|
||||||
|
} else {
|
||||||
|
const msg = `Expected a coinbase tx, but the backend API returned something else`;
|
||||||
|
logger.err(msg);
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
const msg = `Cannot fetch coinbase tx ${txIds[0]}. Reason: ` + (e instanceof Error ? e.message : e);
|
||||||
|
logger.err(msg);
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch remaining txs in bulk
|
||||||
|
if (isEsplora && (txIds.length - totalFound > 500)) {
|
||||||
|
try {
|
||||||
|
const rawTransactions = await bitcoinApi.$getTxsForBlock(blockHash);
|
||||||
|
for (const tx of rawTransactions) {
|
||||||
|
if (!transactionMap[tx.txid]) {
|
||||||
|
transactionMap[tx.txid] = addMempoolData ? transactionUtils.extendMempoolTransaction(tx) : transactionUtils.extendTransaction(tx);
|
||||||
|
totalFound++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Cannot fetch bulk txs for block ${blockHash}. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch remaining txs individually
|
||||||
|
for (const txid of txIds.filter(txid => !transactionMap[txid])) {
|
||||||
|
if (!quiet && (totalFound % (Math.round((txIds.length) / 10)) === 0 || totalFound + 1 === txIds.length)) { // Avoid log spam
|
||||||
|
logger.debug(`Indexing tx ${totalFound + 1} of ${txIds.length} in block #${blockHeight}`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const tx = await transactionUtils.$getTransactionExtendedRetry(txid, false, false, false, addMempoolData);
|
||||||
|
transactionMap[txid] = tx;
|
||||||
|
totalFound++;
|
||||||
|
} catch (e) {
|
||||||
|
const msg = `Cannot fetch tx ${txid}. Reason: ` + (e instanceof Error ? e.message : e);
|
||||||
|
logger.err(msg);
|
||||||
|
throw new Error(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!quiet) {
|
if (!quiet) {
|
||||||
logger.debug(`${transactionsFound} of ${txIds.length} found in mempool. ${transactionsFetched} fetched through backend service.`);
|
logger.debug(`${foundInMempool} of ${txIds.length} found in mempool. ${totalFound - foundInMempool} fetched through backend service.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return transactions;
|
// Require the first transaction to be a coinbase
|
||||||
|
const coinbase = transactionMap[txIds[0]];
|
||||||
|
if (!coinbase || !coinbase.vin[0].is_coinbase) {
|
||||||
|
const msg = `Expected first tx in a block to be a coinbase, but found something else`;
|
||||||
|
logger.err(msg);
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Require all transactions to be present
|
||||||
|
// (we should have thrown an error already if a tx request failed)
|
||||||
|
if (txIds.some(txid => !transactionMap[txid])) {
|
||||||
|
const msg = `Failed to fetch ${txIds.length - totalFound} transactions from block`;
|
||||||
|
logger.err(msg);
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return list of transactions, preserving block order
|
||||||
|
return txIds.map(txid => transactionMap[txid]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -171,7 +213,9 @@ class Blocks {
|
|||||||
|
|
||||||
private convertLiquidFees(block: IBitcoinApi.VerboseBlock): IBitcoinApi.VerboseBlock {
|
private convertLiquidFees(block: IBitcoinApi.VerboseBlock): IBitcoinApi.VerboseBlock {
|
||||||
block.tx.forEach(tx => {
|
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;
|
return block;
|
||||||
}
|
}
|
||||||
@ -376,8 +420,8 @@ class Blocks {
|
|||||||
let newlyIndexed = 0;
|
let newlyIndexed = 0;
|
||||||
let totalIndexed = indexedBlockSummariesHashesArray.length;
|
let totalIndexed = indexedBlockSummariesHashesArray.length;
|
||||||
let indexedThisRun = 0;
|
let indexedThisRun = 0;
|
||||||
let timer = new Date().getTime() / 1000;
|
let timer = Date.now() / 1000;
|
||||||
const startedAt = new Date().getTime() / 1000;
|
const startedAt = Date.now() / 1000;
|
||||||
|
|
||||||
for (const block of indexedBlocks) {
|
for (const block of indexedBlocks) {
|
||||||
if (indexedBlockSummariesHashes[block.hash] === true) {
|
if (indexedBlockSummariesHashes[block.hash] === true) {
|
||||||
@ -385,17 +429,24 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
|
const elapsedSeconds = (Date.now() / 1000) - timer;
|
||||||
if (elapsedSeconds > 5) {
|
if (elapsedSeconds > 5) {
|
||||||
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
const runningFor = (Date.now() / 1000) - startedAt;
|
||||||
const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds);
|
const blockPerSeconds = indexedThisRun / elapsedSeconds;
|
||||||
const progress = Math.round(totalIndexed / indexedBlocks.length * 10000) / 100;
|
const progress = Math.round(totalIndexed / indexedBlocks.length * 10000) / 100;
|
||||||
logger.debug(`Indexing block summary for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexedBlocks.length} (${progress}%) | elapsed: ${runningFor} seconds`, logger.tags.mining);
|
logger.debug(`Indexing block summary for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexedBlocks.length} (${progress}%) | elapsed: ${runningFor.toFixed(2)} seconds`, logger.tags.mining);
|
||||||
timer = new Date().getTime() / 1000;
|
timer = Date.now() / 1000;
|
||||||
indexedThisRun = 0;
|
indexedThisRun = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.$getStrippedBlockTransactions(block.hash, true, true); // This will index the block summary
|
|
||||||
|
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||||
|
const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||||
|
const cpfpSummary = await this.$indexCPFP(block.hash, block.height, txs);
|
||||||
|
await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary
|
||||||
|
} else {
|
||||||
|
await this.$getStrippedBlockTransactions(block.hash, true, true); // This will index the block summary
|
||||||
|
}
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
indexedThisRun++;
|
indexedThisRun++;
|
||||||
@ -434,18 +485,18 @@ class Blocks {
|
|||||||
// Logging
|
// Logging
|
||||||
let count = 0;
|
let count = 0;
|
||||||
let countThisRun = 0;
|
let countThisRun = 0;
|
||||||
let timer = new Date().getTime() / 1000;
|
let timer = Date.now() / 1000;
|
||||||
const startedAt = new Date().getTime() / 1000;
|
const startedAt = Date.now() / 1000;
|
||||||
for (const height of unindexedBlockHeights) {
|
for (const height of unindexedBlockHeights) {
|
||||||
// Logging
|
// Logging
|
||||||
const hash = await bitcoinApi.$getBlockHash(height);
|
const hash = await bitcoinApi.$getBlockHash(height);
|
||||||
const elapsedSeconds = Math.max(1, new Date().getTime() / 1000 - timer);
|
const elapsedSeconds = (Date.now() / 1000) - timer;
|
||||||
if (elapsedSeconds > 5) {
|
if (elapsedSeconds > 5) {
|
||||||
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
const runningFor = (Date.now() / 1000) - startedAt;
|
||||||
const blockPerSeconds = (countThisRun / elapsedSeconds);
|
const blockPerSeconds = countThisRun / elapsedSeconds;
|
||||||
const progress = Math.round(count / unindexedBlockHeights.length * 10000) / 100;
|
const progress = Math.round(count / unindexedBlockHeights.length * 10000) / 100;
|
||||||
logger.debug(`Indexing cpfp clusters for #${height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${count}/${unindexedBlockHeights.length} (${progress}%) | elapsed: ${runningFor} seconds`);
|
logger.debug(`Indexing cpfp clusters for #${height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${count}/${unindexedBlockHeights.length} (${progress}%) | elapsed: ${runningFor.toFixed(2)} seconds`);
|
||||||
timer = new Date().getTime() / 1000;
|
timer = Date.now() / 1000;
|
||||||
countThisRun = 0;
|
countThisRun = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -524,8 +575,8 @@ class Blocks {
|
|||||||
let totalIndexed = await blocksRepository.$blockCountBetweenHeight(currentBlockHeight, lastBlockToIndex);
|
let totalIndexed = await blocksRepository.$blockCountBetweenHeight(currentBlockHeight, lastBlockToIndex);
|
||||||
let indexedThisRun = 0;
|
let indexedThisRun = 0;
|
||||||
let newlyIndexed = 0;
|
let newlyIndexed = 0;
|
||||||
const startedAt = new Date().getTime() / 1000;
|
const startedAt = Date.now() / 1000;
|
||||||
let timer = new Date().getTime() / 1000;
|
let timer = Date.now() / 1000;
|
||||||
|
|
||||||
while (currentBlockHeight >= lastBlockToIndex) {
|
while (currentBlockHeight >= lastBlockToIndex) {
|
||||||
const endBlock = Math.max(0, lastBlockToIndex, currentBlockHeight - chunkSize + 1);
|
const endBlock = Math.max(0, lastBlockToIndex, currentBlockHeight - chunkSize + 1);
|
||||||
@ -545,18 +596,18 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
++indexedThisRun;
|
++indexedThisRun;
|
||||||
++totalIndexed;
|
++totalIndexed;
|
||||||
const elapsedSeconds = Math.max(1, new Date().getTime() / 1000 - timer);
|
const elapsedSeconds = (Date.now() / 1000) - timer;
|
||||||
if (elapsedSeconds > 5 || blockHeight === lastBlockToIndex) {
|
if (elapsedSeconds > 5 || blockHeight === lastBlockToIndex) {
|
||||||
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
const runningFor = (Date.now() / 1000) - startedAt;
|
||||||
const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds);
|
const blockPerSeconds = indexedThisRun / elapsedSeconds;
|
||||||
const progress = Math.round(totalIndexed / indexingBlockAmount * 10000) / 100;
|
const progress = Math.round(totalIndexed / indexingBlockAmount * 10000) / 100;
|
||||||
logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress}%) | elapsed: ${runningFor} seconds`, logger.tags.mining);
|
logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress.toFixed(2)}%) | elapsed: ${runningFor.toFixed(2)} seconds`, logger.tags.mining);
|
||||||
timer = new Date().getTime() / 1000;
|
timer = Date.now() / 1000;
|
||||||
indexedThisRun = 0;
|
indexedThisRun = 0;
|
||||||
loadingIndicators.setProgress('block-indexing', progress, false);
|
loadingIndicators.setProgress('block-indexing', progress, false);
|
||||||
}
|
}
|
||||||
const blockHash = await bitcoinApi.$getBlockHash(blockHeight);
|
const blockHash = await bitcoinApi.$getBlockHash(blockHeight);
|
||||||
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(blockHash);
|
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(blockHash);
|
||||||
const transactions = await this.$getTransactionsExtended(blockHash, block.height, true, null, true);
|
const transactions = await this.$getTransactionsExtended(blockHash, block.height, true, null, true);
|
||||||
const blockExtended = await this.$getBlockExtended(block, transactions);
|
const blockExtended = await this.$getBlockExtended(block, transactions);
|
||||||
|
|
||||||
@ -613,17 +664,17 @@ class Blocks {
|
|||||||
const heightDiff = blockHeightTip % 2016;
|
const heightDiff = blockHeightTip % 2016;
|
||||||
const blockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff);
|
const blockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff);
|
||||||
this.updateTimerProgress(timer, 'got block hash for initial difficulty adjustment');
|
this.updateTimerProgress(timer, 'got block hash for initial difficulty adjustment');
|
||||||
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(blockHash);
|
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(blockHash);
|
||||||
this.updateTimerProgress(timer, 'got block for initial difficulty adjustment');
|
this.updateTimerProgress(timer, 'got block for initial difficulty adjustment');
|
||||||
this.lastDifficultyAdjustmentTime = block.timestamp;
|
this.lastDifficultyAdjustmentTime = block.timestamp;
|
||||||
this.currentDifficulty = block.difficulty;
|
this.currentBits = block.bits;
|
||||||
|
|
||||||
if (blockHeightTip >= 2016) {
|
if (blockHeightTip >= 2016) {
|
||||||
const previousPeriodBlockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff - 2016);
|
const previousPeriodBlockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff - 2016);
|
||||||
this.updateTimerProgress(timer, 'got previous block hash for initial difficulty adjustment');
|
this.updateTimerProgress(timer, 'got previous block hash for initial difficulty adjustment');
|
||||||
const previousPeriodBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(previousPeriodBlockHash);
|
const previousPeriodBlock: IEsploraApi.Block = await bitcoinApi.$getBlock(previousPeriodBlockHash);
|
||||||
this.updateTimerProgress(timer, 'got previous block for initial difficulty adjustment');
|
this.updateTimerProgress(timer, 'got previous block for initial difficulty adjustment');
|
||||||
this.previousDifficultyRetarget = (block.difficulty - previousPeriodBlock.difficulty) / previousPeriodBlock.difficulty * 100;
|
this.previousDifficultyRetarget = calcBitsDifference(previousPeriodBlock.bits, block.bits);
|
||||||
logger.debug(`Initial difficulty adjustment data set.`);
|
logger.debug(`Initial difficulty adjustment data set.`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -647,14 +698,14 @@ class Blocks {
|
|||||||
const block = BitcoinApi.convertBlock(verboseBlock);
|
const block = BitcoinApi.convertBlock(verboseBlock);
|
||||||
const txIds: string[] = verboseBlock.tx.map(tx => tx.txid);
|
const txIds: string[] = verboseBlock.tx.map(tx => tx.txid);
|
||||||
const transactions = await this.$getTransactionsExtended(blockHash, block.height, false, txIds, false, true) as MempoolTransactionExtended[];
|
const transactions = await this.$getTransactionsExtended(blockHash, block.height, false, txIds, false, true) as MempoolTransactionExtended[];
|
||||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
|
||||||
// fill in missing transaction fee data from verboseBlock
|
// fill in missing transaction fee data from verboseBlock
|
||||||
for (let i = 0; i < transactions.length; i++) {
|
for (let i = 0; i < transactions.length; i++) {
|
||||||
if (!transactions[i].fee && transactions[i].txid === verboseBlock.tx[i].txid) {
|
if (!transactions[i].fee && transactions[i].txid === verboseBlock.tx[i].txid) {
|
||||||
transactions[i].fee = verboseBlock.tx[i].fee * 100_000_000;
|
transactions[i].fee = (verboseBlock.tx[i].fee * 100_000_000) || 0;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cpfpSummary: CpfpSummary = Common.calculateCpfp(block.height, transactions);
|
const cpfpSummary: CpfpSummary = Common.calculateCpfp(block.height, transactions);
|
||||||
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
|
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
|
||||||
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions);
|
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions);
|
||||||
@ -736,14 +787,18 @@ class Blocks {
|
|||||||
time: block.timestamp,
|
time: block.timestamp,
|
||||||
height: block.height,
|
height: block.height,
|
||||||
difficulty: block.difficulty,
|
difficulty: block.difficulty,
|
||||||
adjustment: Math.round((block.difficulty / this.currentDifficulty) * 1000000) / 1000000, // Remove float point noise
|
adjustment: Math.round(
|
||||||
|
// calcBitsDifference returns +- percentage, +100 returns to positive, /100 returns to ratio.
|
||||||
|
// Instead of actually doing /100, just reduce the multiplier.
|
||||||
|
(calcBitsDifference(this.currentBits, block.bits) + 100) * 10000
|
||||||
|
) / 1000000, // Remove float point noise
|
||||||
});
|
});
|
||||||
this.updateTimerProgress(timer, `saved difficulty adjustment for ${this.currentBlockHeight}`);
|
this.updateTimerProgress(timer, `saved difficulty adjustment for ${this.currentBlockHeight}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.previousDifficultyRetarget = (block.difficulty - this.currentDifficulty) / this.currentDifficulty * 100;
|
this.previousDifficultyRetarget = calcBitsDifference(this.currentBits, block.bits);
|
||||||
this.lastDifficultyAdjustmentTime = block.timestamp;
|
this.lastDifficultyAdjustmentTime = block.timestamp;
|
||||||
this.currentDifficulty = block.difficulty;
|
this.currentBits = block.bits;
|
||||||
}
|
}
|
||||||
|
|
||||||
// wait for pending async callbacks to finish
|
// wait for pending async callbacks to finish
|
||||||
@ -763,10 +818,18 @@ class Blocks {
|
|||||||
if (this.newBlockCallbacks.length) {
|
if (this.newBlockCallbacks.length) {
|
||||||
this.newBlockCallbacks.forEach((cb) => cb(blockExtended, txIds, transactions));
|
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();
|
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++;
|
handledBlocks++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -811,7 +874,7 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const blockHash = await bitcoinApi.$getBlockHash(height);
|
const blockHash = await bitcoinApi.$getBlockHash(height);
|
||||||
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(blockHash);
|
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(blockHash);
|
||||||
const transactions = await this.$getTransactionsExtended(blockHash, block.height, true);
|
const transactions = await this.$getTransactionsExtended(blockHash, block.height, true);
|
||||||
const blockExtended = await this.$getBlockExtended(block, transactions);
|
const blockExtended = await this.$getBlockExtended(block, transactions);
|
||||||
|
|
||||||
@ -823,7 +886,7 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async $indexStaleBlock(hash: string): Promise<BlockExtended> {
|
public async $indexStaleBlock(hash: string): Promise<BlockExtended> {
|
||||||
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(hash);
|
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(hash);
|
||||||
const transactions = await this.$getTransactionsExtended(hash, block.height, true);
|
const transactions = await this.$getTransactionsExtended(hash, block.height, true);
|
||||||
const blockExtended = await this.$getBlockExtended(block, transactions);
|
const blockExtended = await this.$getBlockExtended(block, transactions);
|
||||||
|
|
||||||
@ -848,7 +911,7 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Bitcoin network, add our custom data on top
|
// Bitcoin network, add our custom data on top
|
||||||
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(hash);
|
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(hash);
|
||||||
if (block.stale) {
|
if (block.stale) {
|
||||||
return await this.$indexStaleBlock(hash);
|
return await this.$indexStaleBlock(hash);
|
||||||
} else {
|
} else {
|
||||||
@ -877,13 +940,13 @@ class Blocks {
|
|||||||
|
|
||||||
let height = blockHeight;
|
let height = blockHeight;
|
||||||
let summary: BlockSummary;
|
let summary: BlockSummary;
|
||||||
if (cpfpSummary) {
|
if (cpfpSummary && !Common.isLiquid()) {
|
||||||
summary = {
|
summary = {
|
||||||
id: hash,
|
id: hash,
|
||||||
transactions: cpfpSummary.transactions.map(tx => {
|
transactions: cpfpSummary.transactions.map(tx => {
|
||||||
return {
|
return {
|
||||||
txid: tx.txid,
|
txid: tx.txid,
|
||||||
fee: tx.fee,
|
fee: tx.fee || 0,
|
||||||
vsize: tx.vsize,
|
vsize: tx.vsize,
|
||||||
value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0)),
|
value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0)),
|
||||||
rate: tx.effectiveFeePerVsize
|
rate: tx.effectiveFeePerVsize
|
||||||
@ -891,10 +954,15 @@ class Blocks {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// Call Core RPC
|
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||||
const block = await bitcoinClient.getBlock(hash, 2);
|
const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||||
summary = this.summarizeBlock(block);
|
summary = this.summarizeBlockTransactions(hash, txs);
|
||||||
height = block.height;
|
} else {
|
||||||
|
// Call Core RPC
|
||||||
|
const block = await bitcoinClient.getBlock(hash, 2);
|
||||||
|
summary = this.summarizeBlock(block);
|
||||||
|
height = block.height;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (height == null) {
|
if (height == null) {
|
||||||
const block = await bitcoinApi.$getBlock(hash);
|
const block = await bitcoinApi.$getBlock(hash);
|
||||||
@ -1017,8 +1085,17 @@ class Blocks {
|
|||||||
if (Common.blocksSummariesIndexingEnabled() && cleanBlock.fee_amt_percentiles === null) {
|
if (Common.blocksSummariesIndexingEnabled() && cleanBlock.fee_amt_percentiles === null) {
|
||||||
cleanBlock.fee_amt_percentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(cleanBlock.hash);
|
cleanBlock.fee_amt_percentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(cleanBlock.hash);
|
||||||
if (cleanBlock.fee_amt_percentiles === null) {
|
if (cleanBlock.fee_amt_percentiles === null) {
|
||||||
const block = await bitcoinClient.getBlock(cleanBlock.hash, 2);
|
|
||||||
const summary = this.summarizeBlock(block);
|
let summary;
|
||||||
|
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||||
|
const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||||
|
summary = this.summarizeBlockTransactions(cleanBlock.hash, txs);
|
||||||
|
} else {
|
||||||
|
// Call Core RPC
|
||||||
|
const block = await bitcoinClient.getBlock(cleanBlock.hash, 2);
|
||||||
|
summary = this.summarizeBlock(block);
|
||||||
|
}
|
||||||
|
|
||||||
await BlocksSummariesRepository.$saveTransactions(cleanBlock.height, cleanBlock.hash, summary.transactions);
|
await BlocksSummariesRepository.$saveTransactions(cleanBlock.height, cleanBlock.hash, summary.transactions);
|
||||||
cleanBlock.fee_amt_percentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(cleanBlock.hash);
|
cleanBlock.fee_amt_percentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(cleanBlock.hash);
|
||||||
}
|
}
|
||||||
@ -1078,19 +1155,29 @@ class Blocks {
|
|||||||
return this.currentBlockHeight;
|
return this.currentBlockHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $indexCPFP(hash: string, height: number): Promise<void> {
|
public async $indexCPFP(hash: string, height: number, txs?: TransactionExtended[]): Promise<CpfpSummary> {
|
||||||
const block = await bitcoinClient.getBlock(hash, 2);
|
let transactions = txs;
|
||||||
const transactions = block.tx.map(tx => {
|
if (!transactions) {
|
||||||
tx.fee *= 100_000_000;
|
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||||
return tx;
|
transactions = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||||
});
|
}
|
||||||
|
if (!transactions) {
|
||||||
|
const block = await bitcoinClient.getBlock(hash, 2);
|
||||||
|
transactions = block.tx.map(tx => {
|
||||||
|
tx.fee *= 100_000_000;
|
||||||
|
return tx;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const summary = Common.calculateCpfp(height, transactions);
|
const summary = Common.calculateCpfp(height, transactions as TransactionExtended[]);
|
||||||
|
|
||||||
await this.$saveCpfp(hash, height, summary);
|
await this.$saveCpfp(hash, height, summary);
|
||||||
|
|
||||||
const effectiveFeeStats = Common.calcEffectiveFeeStatistics(summary.transactions);
|
const effectiveFeeStats = Common.calcEffectiveFeeStatistics(summary.transactions);
|
||||||
await blocksRepository.$saveEffectiveFeeStats(hash, effectiveFeeStats);
|
await blocksRepository.$saveEffectiveFeeStats(hash, effectiveFeeStats);
|
||||||
|
|
||||||
|
return summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $saveCpfp(hash: string, height: number, cpfpSummary: CpfpSummary): Promise<void> {
|
public async $saveCpfp(hash: string, height: number, cpfpSummary: CpfpSummary): Promise<void> {
|
||||||
|
|||||||
@ -108,7 +108,7 @@ export class Common {
|
|||||||
static stripTransaction(tx: TransactionExtended): TransactionStripped {
|
static stripTransaction(tx: TransactionExtended): TransactionStripped {
|
||||||
return {
|
return {
|
||||||
txid: tx.txid,
|
txid: tx.txid,
|
||||||
fee: tx.fee,
|
fee: tx.fee || 0,
|
||||||
vsize: tx.weight / 4,
|
vsize: tx.weight / 4,
|
||||||
value: tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0),
|
value: tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0),
|
||||||
rate: tx.effectiveFeePerVsize,
|
rate: tx.effectiveFeePerVsize,
|
||||||
|
|||||||
@ -16,6 +16,68 @@ export interface DifficultyAdjustment {
|
|||||||
expectedBlocks: number; // Block count
|
expectedBlocks: number; // Block count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the difficulty increase/decrease by using the `bits` integer contained in two
|
||||||
|
* block headers.
|
||||||
|
*
|
||||||
|
* Warning: Only compare `bits` from blocks in two adjacent difficulty periods. This code
|
||||||
|
* assumes the maximum difference is x4 or /4 (as per the protocol) and will throw an
|
||||||
|
* error if an exponent difference of 2 or more is seen.
|
||||||
|
*
|
||||||
|
* @param {number} oldBits The 32 bit `bits` integer from a block header.
|
||||||
|
* @param {number} newBits The 32 bit `bits` integer from a block header in the next difficulty period.
|
||||||
|
* @returns {number} A floating point decimal of the difficulty change from old to new.
|
||||||
|
* (ie. 21.3 means 21.3% increase in difficulty, -21.3 is a 21.3% decrease in difficulty)
|
||||||
|
*/
|
||||||
|
export function calcBitsDifference(oldBits: number, newBits: number): number {
|
||||||
|
// Must be
|
||||||
|
// - integer
|
||||||
|
// - highest exponent is 0x1f, so max value (as integer) is 0x1f0000ff
|
||||||
|
// - min value is 1 (exponent = 0)
|
||||||
|
// - highest bit of the number-part is +- sign, it must not be 1
|
||||||
|
const verifyBits = (bits: number): void => {
|
||||||
|
if (
|
||||||
|
Math.floor(bits) !== bits ||
|
||||||
|
bits > 0x1f0000ff ||
|
||||||
|
bits < 1 ||
|
||||||
|
(bits & 0x00800000) !== 0 ||
|
||||||
|
(bits & 0x007fffff) === 0
|
||||||
|
) {
|
||||||
|
throw new Error('Invalid bits');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
verifyBits(oldBits);
|
||||||
|
verifyBits(newBits);
|
||||||
|
|
||||||
|
// No need to mask exponents because we checked the bounds above
|
||||||
|
const oldExp = oldBits >> 24;
|
||||||
|
const newExp = newBits >> 24;
|
||||||
|
const oldNum = oldBits & 0x007fffff;
|
||||||
|
const newNum = newBits & 0x007fffff;
|
||||||
|
// The diff can only possibly be 1, 0, -1
|
||||||
|
// (because maximum difficulty change is x4 or /4 (2 bits up or down))
|
||||||
|
let result: number;
|
||||||
|
switch (newExp - oldExp) {
|
||||||
|
// New less than old, target lowered, difficulty increased
|
||||||
|
case -1:
|
||||||
|
result = ((oldNum << 8) * 100) / newNum - 100;
|
||||||
|
break;
|
||||||
|
// Same exponent, compare numbers as is.
|
||||||
|
case 0:
|
||||||
|
result = (oldNum * 100) / newNum - 100;
|
||||||
|
break;
|
||||||
|
// Old less than new, target raised, difficulty decreased
|
||||||
|
case 1:
|
||||||
|
result = (oldNum * 100) / (newNum << 8) - 100;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('Impossible exponent difference');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Min/Max values
|
||||||
|
return result > 300 ? 300 : result < -75 ? -75 : result;
|
||||||
|
}
|
||||||
|
|
||||||
export function calcDifficultyAdjustment(
|
export function calcDifficultyAdjustment(
|
||||||
DATime: number,
|
DATime: number,
|
||||||
nowSeconds: number,
|
nowSeconds: number,
|
||||||
|
|||||||
@ -29,7 +29,7 @@ class DiskCache {
|
|||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
if (!cluster.isPrimary) {
|
if (!cluster.isPrimary || !config.MEMPOOL.CACHE_ENABLED) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
process.on('SIGINT', (e) => {
|
process.on('SIGINT', (e) => {
|
||||||
@ -39,7 +39,7 @@ class DiskCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async $saveCacheToDisk(sync: boolean = false): Promise<void> {
|
async $saveCacheToDisk(sync: boolean = false): Promise<void> {
|
||||||
if (!cluster.isPrimary) {
|
if (!cluster.isPrimary || !config.MEMPOOL.CACHE_ENABLED) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.isWritingCache) {
|
if (this.isWritingCache) {
|
||||||
@ -175,10 +175,11 @@ class DiskCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async $loadMempoolCache(): Promise<void> {
|
async $loadMempoolCache(): Promise<void> {
|
||||||
if (!fs.existsSync(DiskCache.FILE_NAME)) {
|
if (!config.MEMPOOL.CACHE_ENABLED || !fs.existsSync(DiskCache.FILE_NAME)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
const start = Date.now();
|
||||||
let data: any = {};
|
let data: any = {};
|
||||||
const cacheData = fs.readFileSync(DiskCache.FILE_NAME, 'utf8');
|
const cacheData = fs.readFileSync(DiskCache.FILE_NAME, 'utf8');
|
||||||
if (cacheData) {
|
if (cacheData) {
|
||||||
@ -220,6 +221,8 @@ class DiskCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info(`Loaded mempool from disk cache in ${Date.now() - start} ms`);
|
||||||
|
|
||||||
await memPool.$setMempool(data.mempool);
|
await memPool.$setMempool(data.mempool);
|
||||||
if (!this.ignoreBlocksCache) {
|
if (!this.ignoreBlocksCache) {
|
||||||
blocks.setBlocks(data.blocks);
|
blocks.setBlocks(data.blocks);
|
||||||
|
|||||||
@ -80,7 +80,7 @@ class ChannelsApi {
|
|||||||
|
|
||||||
public async $searchChannelsById(search: string): Promise<any[]> {
|
public async $searchChannelsById(search: string): Promise<any[]> {
|
||||||
try {
|
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 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]);
|
const [rows]: any = await DB.query(query, [searchStripped, searchStripped]);
|
||||||
return rows;
|
return rows;
|
||||||
|
|||||||
@ -217,7 +217,7 @@ async function buildFullChannel(clChannelA: any, clChannelB: any): Promise<ILigh
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
channel_id: Common.channelShortIdToIntegerId(clChannelA.short_channel_id),
|
channel_id: Common.channelShortIdToIntegerId(clChannelA.short_channel_id),
|
||||||
capacity: clChannelA.satoshis,
|
capacity: (clChannelA.amount_msat / 1000).toString(),
|
||||||
last_update: lastUpdate,
|
last_update: lastUpdate,
|
||||||
node1_policy: convertPolicy(clChannelA),
|
node1_policy: convertPolicy(clChannelA),
|
||||||
node2_policy: convertPolicy(clChannelB),
|
node2_policy: convertPolicy(clChannelB),
|
||||||
@ -241,7 +241,7 @@ async function buildIncompleteChannel(clChannel: any): Promise<ILightningApi.Cha
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
channel_id: Common.channelShortIdToIntegerId(clChannel.short_channel_id),
|
channel_id: Common.channelShortIdToIntegerId(clChannel.short_channel_id),
|
||||||
capacity: clChannel.satoshis,
|
capacity: (clChannel.amount_msat / 1000).toString(),
|
||||||
last_update: clChannel.last_update ?? 0,
|
last_update: clChannel.last_update ?? 0,
|
||||||
node1_policy: convertPolicy(clChannel),
|
node1_policy: convertPolicy(clChannel),
|
||||||
node2_policy: null,
|
node2_policy: null,
|
||||||
@ -257,8 +257,8 @@ async function buildIncompleteChannel(clChannel: any): Promise<ILightningApi.Cha
|
|||||||
function convertPolicy(clChannel: any): ILightningApi.RoutingPolicy {
|
function convertPolicy(clChannel: any): ILightningApi.RoutingPolicy {
|
||||||
return {
|
return {
|
||||||
time_lock_delta: clChannel.delay,
|
time_lock_delta: clChannel.delay,
|
||||||
min_htlc: clChannel.htlc_minimum_msat.slice(0, -4),
|
min_htlc: clChannel.htlc_minimum_msat.toString(),
|
||||||
max_htlc_msat: clChannel.htlc_maximum_msat.slice(0, -4),
|
max_htlc_msat: clChannel.htlc_maximum_msat.toString(),
|
||||||
fee_base_msat: clChannel.base_fee_millisatoshi,
|
fee_base_msat: clChannel.base_fee_millisatoshi,
|
||||||
fee_rate_milli_msat: clChannel.fee_per_millionth,
|
fee_rate_milli_msat: clChannel.fee_per_millionth,
|
||||||
disabled: !clChannel.active,
|
disabled: !clChannel.active,
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import loadingIndicators from './loading-indicators';
|
|||||||
import bitcoinClient from './bitcoin/bitcoin-client';
|
import bitcoinClient from './bitcoin/bitcoin-client';
|
||||||
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
||||||
import rbfCache from './rbf-cache';
|
import rbfCache from './rbf-cache';
|
||||||
|
import redisCache from './redis-cache';
|
||||||
|
|
||||||
class Mempool {
|
class Mempool {
|
||||||
private inSync: boolean = false;
|
private inSync: boolean = false;
|
||||||
@ -85,14 +86,25 @@ class Mempool {
|
|||||||
public async $setMempool(mempoolData: { [txId: string]: MempoolTransactionExtended }) {
|
public async $setMempool(mempoolData: { [txId: string]: MempoolTransactionExtended }) {
|
||||||
this.mempoolCache = mempoolData;
|
this.mempoolCache = mempoolData;
|
||||||
let count = 0;
|
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)) {
|
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]);
|
this.mempoolCache[txid] = transactionUtils.extendMempoolTransaction(this.mempoolCache[txid]);
|
||||||
}
|
}
|
||||||
if (this.mempoolCache[txid].order == null) {
|
if (this.mempoolCache[txid].order == null) {
|
||||||
this.mempoolCache[txid].order = transactionUtils.txidToOrdering(txid);
|
this.mempoolCache[txid].order = transactionUtils.txidToOrdering(txid);
|
||||||
}
|
}
|
||||||
count++;
|
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) {
|
if (this.mempoolChangedCallback) {
|
||||||
this.mempoolChangedCallback(this.mempoolCache, [], []);
|
this.mempoolChangedCallback(this.mempoolCache, [], []);
|
||||||
@ -103,6 +115,44 @@ class Mempool {
|
|||||||
this.addToSpendMap(Object.values(this.mempoolCache));
|
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() {
|
public async $updateMemPoolInfo() {
|
||||||
this.mempoolInfo = await this.$getMempoolInfo();
|
this.mempoolInfo = await this.$getMempoolInfo();
|
||||||
}
|
}
|
||||||
@ -132,7 +182,7 @@ class Mempool {
|
|||||||
return txTimes;
|
return txTimes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $updateMempool(transactions: string[]): Promise<void> {
|
public async $updateMempool(transactions: string[], pollRate: number): Promise<void> {
|
||||||
logger.debug(`Updating mempool...`);
|
logger.debug(`Updating mempool...`);
|
||||||
|
|
||||||
// warn if this run stalls the main loop for more than 2 minutes
|
// warn if this run stalls the main loop for more than 2 minutes
|
||||||
@ -143,7 +193,7 @@ class Mempool {
|
|||||||
const currentMempoolSize = Object.keys(this.mempoolCache).length;
|
const currentMempoolSize = Object.keys(this.mempoolCache).length;
|
||||||
this.updateTimerProgress(timer, 'got raw mempool');
|
this.updateTimerProgress(timer, 'got raw mempool');
|
||||||
const diff = transactions.length - currentMempoolSize;
|
const diff = transactions.length - currentMempoolSize;
|
||||||
const newTransactions: MempoolTransactionExtended[] = [];
|
let newTransactions: MempoolTransactionExtended[] = [];
|
||||||
|
|
||||||
this.mempoolCacheDelta = Math.abs(diff);
|
this.mempoolCacheDelta = Math.abs(diff);
|
||||||
|
|
||||||
@ -162,41 +212,66 @@ class Mempool {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let intervalTimer = Date.now();
|
let intervalTimer = Date.now();
|
||||||
for (const txid of transactions) {
|
|
||||||
if (!this.mempoolCache[txid]) {
|
let loaded = false;
|
||||||
try {
|
if (config.MEMPOOL.BACKEND === 'esplora' && currentMempoolSize < transactions.length * 0.5 && transactions.length > 20_000) {
|
||||||
const transaction = await transactionUtils.$getMempoolTransactionExtended(txid, false, false, false);
|
this.inSync = false;
|
||||||
this.updateTimerProgress(timer, 'fetched new transaction');
|
logger.info(`Missing ${transactions.length - currentMempoolSize} mempool transactions, attempting to reload in bulk from esplora`);
|
||||||
this.mempoolCache[txid] = transaction;
|
try {
|
||||||
if (this.inSync) {
|
newTransactions = await this.$reloadMempool(transactions.length);
|
||||||
this.txPerSecondArray.push(new Date().getTime());
|
if (config.REDIS.ENABLED) {
|
||||||
this.vBytesPerSecondArray.push({
|
for (const tx of newTransactions) {
|
||||||
unixTime: new Date().getTime(),
|
await redisCache.$addTransaction(tx);
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
loaded = true;
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('failed to load mempool in bulk from esplora, falling back to fetching individual transactions');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (Date.now() - intervalTimer > 5_000) {
|
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 (this.inSync) {
|
if (config.REDIS.ENABLED) {
|
||||||
// Break and restart mempool loop if we spend too much time processing
|
await redisCache.$addTransaction(transaction);
|
||||||
// new transactions that may lead to falling behind on block height
|
}
|
||||||
logger.debug('Breaking mempool loop because the 5s time limit exceeded.');
|
} catch (e: any) {
|
||||||
break;
|
if (config.MEMPOOL.BACKEND === 'esplora' && e.response?.status === 404) {
|
||||||
} else {
|
this.missingTxCount++;
|
||||||
const progress = (currentMempoolSize + newTransactions.length) / transactions.length * 100;
|
}
|
||||||
logger.debug(`Mempool is synchronizing. Processed ${newTransactions.length}/${diff} txs (${Math.round(progress)}%)`);
|
logger.debug(`Error finding transaction '${txid}' in the mempool: ` + (e instanceof Error ? e.message : e));
|
||||||
loadingIndicators.setProgress('mempool', progress);
|
}
|
||||||
intervalTimer = Date.now()
|
}
|
||||||
|
|
||||||
|
if (Date.now() - intervalTimer > Math.max(pollRate * 2, 5_000)) {
|
||||||
|
if (this.inSync) {
|
||||||
|
// Break and restart mempool loop if we spend too much time processing
|
||||||
|
// new transactions that may lead to falling behind on block height
|
||||||
|
logger.debug('Breaking mempool loop because the 5s time limit exceeded.');
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
const progress = (currentMempoolSize + newTransactions.length) / transactions.length * 100;
|
||||||
|
logger.debug(`Mempool is synchronizing. Processed ${newTransactions.length}/${diff} txs (${Math.round(progress)}%)`);
|
||||||
|
if (Math.floor(progress) < 100) {
|
||||||
|
loadingIndicators.setProgress('mempool', progress);
|
||||||
|
}
|
||||||
|
intervalTimer = Date.now();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -219,7 +294,7 @@ class Mempool {
|
|||||||
logger.warn(`Mempool clear protection triggered because transactions.length: ${transactions.length} and currentMempoolSize: ${currentMempoolSize}.`);
|
logger.warn(`Mempool clear protection triggered because transactions.length: ${transactions.length} and currentMempoolSize: ${currentMempoolSize}.`);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.mempoolProtection = 2;
|
this.mempoolProtection = 2;
|
||||||
logger.warn('Mempool clear protection resumed.');
|
logger.warn('Mempool clear protection ended, normal operation resumed.');
|
||||||
}, 1000 * 60 * config.MEMPOOL.CLEAR_PROTECTION_MINUTES);
|
}, 1000 * 60 * config.MEMPOOL.CLEAR_PROTECTION_MINUTES);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -246,12 +321,6 @@ class Mempool {
|
|||||||
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
|
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
|
||||||
this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6);
|
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);
|
this.mempoolCacheDelta = Math.abs(transactions.length - newMempoolSize);
|
||||||
|
|
||||||
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
||||||
@ -263,6 +332,19 @@ class Mempool {
|
|||||||
this.updateTimerProgress(timer, 'completed async mempool callback');
|
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 end = new Date().getTime();
|
||||||
const time = end - start;
|
const time = end - start;
|
||||||
logger.debug(`Mempool updated in ${time / 1000} seconds. New size: ${Object.keys(this.mempoolCache).length} (${diff > 0 ? '+' + diff : diff})`);
|
logger.debug(`Mempool updated in ${time / 1000} seconds. New size: ${Object.keys(this.mempoolCache).length} (${diff > 0 ? '+' + diff : diff})`);
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import DifficultyAdjustmentsRepository from '../../repositories/DifficultyAdjust
|
|||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import BlocksAuditsRepository from '../../repositories/BlocksAuditsRepository';
|
import BlocksAuditsRepository from '../../repositories/BlocksAuditsRepository';
|
||||||
import PricesRepository from '../../repositories/PricesRepository';
|
import PricesRepository from '../../repositories/PricesRepository';
|
||||||
import { bitcoinCoreApi } from '../bitcoin/bitcoin-api-factory';
|
import bitcoinApi from '../bitcoin/bitcoin-api-factory';
|
||||||
import { IEsploraApi } from '../bitcoin/esplora-api.interface';
|
import { IEsploraApi } from '../bitcoin/esplora-api.interface';
|
||||||
import database from '../../database';
|
import database from '../../database';
|
||||||
|
|
||||||
@ -201,7 +201,7 @@ class Mining {
|
|||||||
try {
|
try {
|
||||||
const oldestConsecutiveBlockTimestamp = 1000 * (await BlocksRepository.$getOldestConsecutiveBlock()).timestamp;
|
const oldestConsecutiveBlockTimestamp = 1000 * (await BlocksRepository.$getOldestConsecutiveBlock()).timestamp;
|
||||||
|
|
||||||
const genesisBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(await bitcoinClient.getBlockHash(0));
|
const genesisBlock: IEsploraApi.Block = await bitcoinApi.$getBlock(await bitcoinApi.$getBlockHash(0));
|
||||||
const genesisTimestamp = genesisBlock.timestamp * 1000;
|
const genesisTimestamp = genesisBlock.timestamp * 1000;
|
||||||
|
|
||||||
const indexedTimestamp = await HashratesRepository.$getWeeklyHashrateTimestamps();
|
const indexedTimestamp = await HashratesRepository.$getWeeklyHashrateTimestamps();
|
||||||
@ -312,7 +312,7 @@ class Mining {
|
|||||||
const oldestConsecutiveBlockTimestamp = 1000 * (await BlocksRepository.$getOldestConsecutiveBlock()).timestamp;
|
const oldestConsecutiveBlockTimestamp = 1000 * (await BlocksRepository.$getOldestConsecutiveBlock()).timestamp;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const genesisBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(await bitcoinClient.getBlockHash(0));
|
const genesisBlock: IEsploraApi.Block = await bitcoinApi.$getBlock(await bitcoinApi.$getBlockHash(0));
|
||||||
const genesisTimestamp = genesisBlock.timestamp * 1000;
|
const genesisTimestamp = genesisBlock.timestamp * 1000;
|
||||||
const indexedTimestamp = (await HashratesRepository.$getRawNetworkDailyHashrate(null)).map(hashrate => hashrate.timestamp);
|
const indexedTimestamp = (await HashratesRepository.$getRawNetworkDailyHashrate(null)).map(hashrate => hashrate.timestamp);
|
||||||
const lastMidnight = this.getDateMidnight(new Date());
|
const lastMidnight = this.getDateMidnight(new Date());
|
||||||
@ -421,8 +421,9 @@ class Mining {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const blocks: any = await BlocksRepository.$getBlocksDifficulty();
|
const blocks: any = await BlocksRepository.$getBlocksDifficulty();
|
||||||
const genesisBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(await bitcoinClient.getBlockHash(0));
|
const genesisBlock: IEsploraApi.Block = await bitcoinApi.$getBlock(await bitcoinApi.$getBlockHash(0));
|
||||||
let currentDifficulty = genesisBlock.difficulty;
|
let currentDifficulty = genesisBlock.difficulty;
|
||||||
|
let currentBits = genesisBlock.bits;
|
||||||
let totalIndexed = 0;
|
let totalIndexed = 0;
|
||||||
|
|
||||||
if (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT === -1 && indexedHeights[0] !== true) {
|
if (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT === -1 && indexedHeights[0] !== true) {
|
||||||
@ -436,6 +437,7 @@ class Mining {
|
|||||||
|
|
||||||
const oldestConsecutiveBlock = await BlocksRepository.$getOldestConsecutiveBlock();
|
const oldestConsecutiveBlock = await BlocksRepository.$getOldestConsecutiveBlock();
|
||||||
if (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT !== -1) {
|
if (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT !== -1) {
|
||||||
|
currentBits = oldestConsecutiveBlock.bits;
|
||||||
currentDifficulty = oldestConsecutiveBlock.difficulty;
|
currentDifficulty = oldestConsecutiveBlock.difficulty;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -443,10 +445,11 @@ class Mining {
|
|||||||
let timer = new Date().getTime() / 1000;
|
let timer = new Date().getTime() / 1000;
|
||||||
|
|
||||||
for (const block of blocks) {
|
for (const block of blocks) {
|
||||||
if (block.difficulty !== currentDifficulty) {
|
if (block.bits !== currentBits) {
|
||||||
if (indexedHeights[block.height] === true) { // Already indexed
|
if (indexedHeights[block.height] === true) { // Already indexed
|
||||||
if (block.height >= oldestConsecutiveBlock.height) {
|
if (block.height >= oldestConsecutiveBlock.height) {
|
||||||
currentDifficulty = block.difficulty;
|
currentDifficulty = block.difficulty;
|
||||||
|
currentBits = block.bits;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -464,6 +467,7 @@ class Mining {
|
|||||||
totalIndexed++;
|
totalIndexed++;
|
||||||
if (block.height >= oldestConsecutiveBlock.height) {
|
if (block.height >= oldestConsecutiveBlock.height) {
|
||||||
currentDifficulty = block.difficulty;
|
currentDifficulty = block.difficulty;
|
||||||
|
currentBits = block.bits;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +1,17 @@
|
|||||||
|
import config from "../config";
|
||||||
import logger from "../logger";
|
import logger from "../logger";
|
||||||
import { MempoolTransactionExtended, TransactionStripped } from "../mempool.interfaces";
|
import { MempoolTransactionExtended, TransactionStripped } from "../mempool.interfaces";
|
||||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||||
import { Common } from "./common";
|
import { Common } from "./common";
|
||||||
|
import redisCache from "./redis-cache";
|
||||||
|
|
||||||
interface RbfTransaction extends TransactionStripped {
|
export interface RbfTransaction extends TransactionStripped {
|
||||||
rbf?: boolean;
|
rbf?: boolean;
|
||||||
mined?: boolean;
|
mined?: boolean;
|
||||||
fullRbf?: boolean;
|
fullRbf?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RbfTree {
|
export interface RbfTree {
|
||||||
tx: RbfTransaction;
|
tx: RbfTransaction;
|
||||||
time: number;
|
time: number;
|
||||||
interval?: number;
|
interval?: number;
|
||||||
@ -28,6 +30,19 @@ export interface ReplacementInfo {
|
|||||||
newVsize: number;
|
newVsize: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum CacheOp {
|
||||||
|
Remove = 0,
|
||||||
|
Add = 1,
|
||||||
|
Change = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CacheEvent {
|
||||||
|
op: CacheOp;
|
||||||
|
type: 'tx' | 'tree' | 'exp';
|
||||||
|
txid: string,
|
||||||
|
value?: any,
|
||||||
|
}
|
||||||
|
|
||||||
class RbfCache {
|
class RbfCache {
|
||||||
private replacedBy: Map<string, string> = new Map();
|
private replacedBy: Map<string, string> = new Map();
|
||||||
private replaces: 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 treeMap: Map<string, string> = new Map(); // map of txids to sequence ids
|
||||||
private txs: Map<string, MempoolTransactionExtended> = new Map();
|
private txs: Map<string, MempoolTransactionExtended> = new Map();
|
||||||
private expiring: Map<string, number> = new Map();
|
private expiring: Map<string, number> = new Map();
|
||||||
|
private cacheQueue: CacheEvent[] = [];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
|
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 {
|
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
|
||||||
if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
|
if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
|
||||||
return;
|
return;
|
||||||
@ -49,7 +96,7 @@ class RbfCache {
|
|||||||
const newTx = Common.stripTransaction(newTxExtended) as RbfTransaction;
|
const newTx = Common.stripTransaction(newTxExtended) as RbfTransaction;
|
||||||
const newTime = newTxExtended.firstSeen || (Date.now() / 1000);
|
const newTime = newTxExtended.firstSeen || (Date.now() / 1000);
|
||||||
newTx.rbf = newTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
|
newTx.rbf = newTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
|
||||||
this.txs.set(newTx.txid, newTxExtended);
|
this.addTx(newTx.txid, newTxExtended);
|
||||||
|
|
||||||
// maintain rbf trees
|
// maintain rbf trees
|
||||||
let txFullRbf = false;
|
let txFullRbf = false;
|
||||||
@ -66,7 +113,7 @@ class RbfCache {
|
|||||||
const treeId = this.treeMap.get(replacedTx.txid);
|
const treeId = this.treeMap.get(replacedTx.txid);
|
||||||
if (treeId) {
|
if (treeId) {
|
||||||
const tree = this.rbfTrees.get(treeId);
|
const tree = this.rbfTrees.get(treeId);
|
||||||
this.rbfTrees.delete(treeId);
|
this.removeTree(treeId);
|
||||||
if (tree) {
|
if (tree) {
|
||||||
tree.interval = newTime - tree?.time;
|
tree.interval = newTime - tree?.time;
|
||||||
replacedTrees.push(tree);
|
replacedTrees.push(tree);
|
||||||
@ -83,7 +130,7 @@ class RbfCache {
|
|||||||
replaces: [],
|
replaces: [],
|
||||||
});
|
});
|
||||||
treeFullRbf = treeFullRbf || !replacedTx.rbf;
|
treeFullRbf = treeFullRbf || !replacedTx.rbf;
|
||||||
this.txs.set(replacedTx.txid, replacedTxExtended);
|
this.addTx(replacedTx.txid, replacedTxExtended);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
newTx.fullRbf = txFullRbf;
|
newTx.fullRbf = txFullRbf;
|
||||||
@ -94,10 +141,27 @@ class RbfCache {
|
|||||||
fullRbf: treeFullRbf,
|
fullRbf: treeFullRbf,
|
||||||
replaces: replacedTrees
|
replaces: replacedTrees
|
||||||
};
|
};
|
||||||
this.rbfTrees.set(treeId, newTree);
|
this.addTree(treeId, newTree);
|
||||||
this.updateTreeMap(treeId, newTree);
|
this.updateTreeMap(treeId, newTree);
|
||||||
this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid));
|
this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid));
|
||||||
this.dirtyTrees.add(treeId);
|
}
|
||||||
|
|
||||||
|
public has(txId: string): boolean {
|
||||||
|
return this.txs.has(txId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public anyInSameTree(txId: string, predicate: (tx: RbfTransaction) => boolean): boolean {
|
||||||
|
const tree = this.getRbfTree(txId);
|
||||||
|
if (!tree) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const txs = this.getTransactionsInTree(tree);
|
||||||
|
for (const tx of txs) {
|
||||||
|
if (predicate(tx)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getReplacedBy(txId: string): string | undefined {
|
public getReplacedBy(txId: string): string | undefined {
|
||||||
@ -173,6 +237,7 @@ class RbfCache {
|
|||||||
this.setTreeMined(tree, txid);
|
this.setTreeMined(tree, txid);
|
||||||
tree.mined = true;
|
tree.mined = true;
|
||||||
this.dirtyTrees.add(treeId);
|
this.dirtyTrees.add(treeId);
|
||||||
|
this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.evict(txid);
|
this.evict(txid);
|
||||||
@ -181,7 +246,8 @@ class RbfCache {
|
|||||||
// flag a transaction as removed from the mempool
|
// flag a transaction as removed from the mempool
|
||||||
public evict(txid: string, fast: boolean = false): void {
|
public evict(txid: string, fast: boolean = false): void {
|
||||||
if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) {
|
if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) {
|
||||||
this.expiring.set(txid, fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400)); // 24 hours
|
const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours
|
||||||
|
this.addExpiration(txid, expiryTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,11 +268,11 @@ class RbfCache {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
for (const txid of this.expiring.keys()) {
|
for (const txid of this.expiring.keys()) {
|
||||||
if ((this.expiring.get(txid) || 0) < now) {
|
if ((this.expiring.get(txid) || 0) < now) {
|
||||||
this.expiring.delete(txid);
|
this.removeExpiration(txid);
|
||||||
this.remove(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
|
// remove a transaction & all previous versions from the cache
|
||||||
@ -216,14 +282,14 @@ class RbfCache {
|
|||||||
const replaces = this.replaces.get(txid);
|
const replaces = this.replaces.get(txid);
|
||||||
this.replaces.delete(txid);
|
this.replaces.delete(txid);
|
||||||
this.treeMap.delete(txid);
|
this.treeMap.delete(txid);
|
||||||
this.txs.delete(txid);
|
this.removeTx(txid);
|
||||||
this.expiring.delete(txid);
|
this.removeExpiration(txid);
|
||||||
for (const tx of (replaces || [])) {
|
for (const tx of (replaces || [])) {
|
||||||
// recursively remove prior versions from the cache
|
// recursively remove prior versions from the cache
|
||||||
this.replacedBy.delete(tx);
|
this.replacedBy.delete(tx);
|
||||||
// if this is the id of a tree, remove that too
|
// if this is the id of a tree, remove that too
|
||||||
if (this.treeMap.get(tx) === tx) {
|
if (this.treeMap.get(tx) === tx) {
|
||||||
this.rbfTrees.delete(tx);
|
this.removeTree(tx);
|
||||||
}
|
}
|
||||||
this.remove(tx);
|
this.remove(tx);
|
||||||
}
|
}
|
||||||
@ -255,6 +321,33 @@ class RbfCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async updateCache(): Promise<void> {
|
||||||
|
if (!config.REDIS.ENABLED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Update the Redis cache by replaying queued events
|
||||||
|
for (const e of this.cacheQueue) {
|
||||||
|
if (e.op === CacheOp.Add || e.op === CacheOp.Change) {
|
||||||
|
let value = e.value;
|
||||||
|
switch(e.type) {
|
||||||
|
case 'tx': {
|
||||||
|
value = this.txs.get(e.txid);
|
||||||
|
} break;
|
||||||
|
case 'tree': {
|
||||||
|
const tree = this.rbfTrees.get(e.txid);
|
||||||
|
value = tree ? this.exportTree(tree) : null;
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
if (value != null) {
|
||||||
|
await redisCache.$setRbfEntry(e.type, e.txid, value);
|
||||||
|
}
|
||||||
|
} else if (e.op === CacheOp.Remove) {
|
||||||
|
await redisCache.$removeRbfEntry(e.type, e.txid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.cacheQueue = [];
|
||||||
|
}
|
||||||
|
|
||||||
public dump(): any {
|
public dump(): any {
|
||||||
const trees = Array.from(this.rbfTrees.values()).map((tree: RbfTree) => { return this.exportTree(tree); });
|
const trees = Array.from(this.rbfTrees.values()).map((tree: RbfTree) => { return this.exportTree(tree); });
|
||||||
|
|
||||||
@ -267,14 +360,14 @@ class RbfCache {
|
|||||||
|
|
||||||
public async load({ txs, trees, expiring }): Promise<void> {
|
public async load({ txs, trees, expiring }): Promise<void> {
|
||||||
txs.forEach(txEntry => {
|
txs.forEach(txEntry => {
|
||||||
this.txs.set(txEntry[0], txEntry[1]);
|
this.txs.set(txEntry.key, txEntry.value);
|
||||||
});
|
});
|
||||||
for (const deflatedTree of trees) {
|
for (const deflatedTree of trees) {
|
||||||
await this.importTree(deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
|
await this.importTree(deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
|
||||||
}
|
}
|
||||||
expiring.forEach(expiringEntry => {
|
expiring.forEach(expiringEntry => {
|
||||||
if (this.txs.has(expiringEntry[0])) {
|
if (this.txs.has(expiringEntry.key)) {
|
||||||
this.expiring.set(expiringEntry[0], new Date(expiringEntry[1]).getTime());
|
this.expiring.set(expiringEntry.key, new Date(expiringEntry.value).getTime());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
@ -360,8 +453,7 @@ class RbfCache {
|
|||||||
};
|
};
|
||||||
this.treeMap.set(txid, root);
|
this.treeMap.set(txid, root);
|
||||||
if (root === txid) {
|
if (root === txid) {
|
||||||
this.rbfTrees.set(root, tree);
|
this.addTree(root, tree);
|
||||||
this.dirtyTrees.add(root);
|
|
||||||
}
|
}
|
||||||
return tree;
|
return tree;
|
||||||
}
|
}
|
||||||
|
|||||||
276
backend/src/api/redis-cache.ts
Normal file
276
backend/src/api/redis-cache.ts
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
import { createClient } from 'redis';
|
||||||
|
import memPool from './mempool';
|
||||||
|
import blocks from './blocks';
|
||||||
|
import logger from '../logger';
|
||||||
|
import config from '../config';
|
||||||
|
import { BlockExtended, BlockSummary, MempoolTransactionExtended } from '../mempool.interfaces';
|
||||||
|
import rbfCache from './rbf-cache';
|
||||||
|
import transactionUtils from './transaction-utils';
|
||||||
|
|
||||||
|
enum NetworkDB {
|
||||||
|
mainnet = 0,
|
||||||
|
testnet,
|
||||||
|
signet,
|
||||||
|
liquid,
|
||||||
|
liquidtestnet,
|
||||||
|
}
|
||||||
|
|
||||||
|
class RedisCache {
|
||||||
|
private client;
|
||||||
|
private connected = false;
|
||||||
|
private schemaVersion = 1;
|
||||||
|
|
||||||
|
private cacheQueue: MempoolTransactionExtended[] = [];
|
||||||
|
private txFlushLimit: number = 10000;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
if (config.REDIS.ENABLED) {
|
||||||
|
const redisConfig = {
|
||||||
|
socket: {
|
||||||
|
path: config.REDIS.UNIX_SOCKET_PATH
|
||||||
|
},
|
||||||
|
database: NetworkDB[config.MEMPOOL.NETWORK],
|
||||||
|
};
|
||||||
|
this.client = createClient(redisConfig);
|
||||||
|
this.client.on('error', (e) => {
|
||||||
|
logger.err(`Error in Redis client: ${e instanceof Error ? e.message : e}`);
|
||||||
|
});
|
||||||
|
this.$ensureConnected();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $ensureConnected(): Promise<void> {
|
||||||
|
if (!this.connected && config.REDIS.ENABLED) {
|
||||||
|
return this.client.connect().then(async () => {
|
||||||
|
this.connected = true;
|
||||||
|
logger.info(`Redis client connected`);
|
||||||
|
const version = await this.client.get('schema_version');
|
||||||
|
if (version !== this.schemaVersion) {
|
||||||
|
// schema changed
|
||||||
|
// perform migrations or flush DB if necessary
|
||||||
|
logger.info(`Redis schema version changed from ${version} to ${this.schemaVersion}`);
|
||||||
|
await this.client.set('schema_version', this.schemaVersion);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async $updateBlocks(blocks: BlockExtended[]) {
|
||||||
|
try {
|
||||||
|
await this.$ensureConnected();
|
||||||
|
await this.client.set('blocks', JSON.stringify(blocks));
|
||||||
|
logger.debug(`Saved latest blocks to Redis cache`);
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`Failed to update blocks in Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async $updateBlockSummaries(summaries: BlockSummary[]) {
|
||||||
|
try {
|
||||||
|
await this.$ensureConnected();
|
||||||
|
await this.client.set('block-summaries', JSON.stringify(summaries));
|
||||||
|
logger.debug(`Saved latest block summaries to Redis cache`);
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`Failed to update block summaries in Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async $addTransaction(tx: MempoolTransactionExtended) {
|
||||||
|
this.cacheQueue.push(tx);
|
||||||
|
if (this.cacheQueue.length >= this.txFlushLimit) {
|
||||||
|
await this.$flushTransactions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async $flushTransactions() {
|
||||||
|
const success = await this.$addTransactions(this.cacheQueue);
|
||||||
|
if (success) {
|
||||||
|
logger.debug(`Saved ${this.cacheQueue.length} transactions to Redis cache`);
|
||||||
|
this.cacheQueue = [];
|
||||||
|
} else {
|
||||||
|
logger.err(`Failed to save ${this.cacheQueue.length} transactions to Redis cache`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $addTransactions(newTransactions: MempoolTransactionExtended[]): Promise<boolean> {
|
||||||
|
if (!newTransactions.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this.$ensureConnected();
|
||||||
|
const msetData = newTransactions.map(tx => {
|
||||||
|
const minified: any = { ...tx };
|
||||||
|
delete minified.hex;
|
||||||
|
for (const vin of minified.vin) {
|
||||||
|
delete vin.inner_redeemscript_asm;
|
||||||
|
delete vin.inner_witnessscript_asm;
|
||||||
|
delete vin.scriptsig_asm;
|
||||||
|
}
|
||||||
|
for (const vout of minified.vout) {
|
||||||
|
delete vout.scriptpubkey_asm;
|
||||||
|
}
|
||||||
|
return [`mempool:tx:${tx.txid}`, JSON.stringify(minified)];
|
||||||
|
});
|
||||||
|
await this.client.MSET(msetData);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`Failed to add ${newTransactions.length} transactions to Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async $removeTransactions(transactions: string[]) {
|
||||||
|
try {
|
||||||
|
await this.$ensureConnected();
|
||||||
|
for (let i = 0; i < Math.ceil(transactions.length / 10000); i++) {
|
||||||
|
const slice = transactions.slice(i * 10000, (i + 1) * 10000);
|
||||||
|
await this.client.unlink(slice.map(txid => `mempool:tx:${txid}`));
|
||||||
|
logger.debug(`Deleted ${slice.length} transactions from the Redis cache`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`Failed to remove ${transactions.length} transactions from Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async $setRbfEntry(type: string, txid: string, value: any): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.$ensureConnected();
|
||||||
|
await this.client.set(`rbf:${type}:${txid}`, JSON.stringify(value));
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`Failed to set RBF ${type} in Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async $removeRbfEntry(type: string, txid: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.$ensureConnected();
|
||||||
|
await this.client.unlink(`rbf:${type}:${txid}`);
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`Failed to remove RBF ${type} from Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async $getBlocks(): Promise<BlockExtended[]> {
|
||||||
|
try {
|
||||||
|
await this.$ensureConnected();
|
||||||
|
const json = await this.client.get('blocks');
|
||||||
|
return JSON.parse(json);
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`Failed to retrieve blocks from Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async $getBlockSummaries(): Promise<BlockSummary[]> {
|
||||||
|
try {
|
||||||
|
await this.$ensureConnected();
|
||||||
|
const json = await this.client.get('block-summaries');
|
||||||
|
return JSON.parse(json);
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`Failed to retrieve blocks from Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async $getMempool(): Promise<{ [txid: string]: MempoolTransactionExtended }> {
|
||||||
|
const start = Date.now();
|
||||||
|
const mempool = {};
|
||||||
|
try {
|
||||||
|
await this.$ensureConnected();
|
||||||
|
const mempoolList = await this.scanKeys<MempoolTransactionExtended>('mempool:tx:*');
|
||||||
|
for (const tx of mempoolList) {
|
||||||
|
mempool[tx.key] = tx.value;
|
||||||
|
}
|
||||||
|
logger.info(`Loaded mempool from Redis cache in ${Date.now() - start} ms`);
|
||||||
|
return mempool || {};
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`Failed to retrieve mempool from Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async $getRbfEntries(type: string): Promise<any[]> {
|
||||||
|
try {
|
||||||
|
await this.$ensureConnected();
|
||||||
|
const rbfEntries = await this.scanKeys<MempoolTransactionExtended[]>(`rbf:${type}:*`);
|
||||||
|
return rbfEntries;
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`Failed to retrieve Rbf ${type}s from Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async $loadCache() {
|
||||||
|
logger.info('Restoring mempool and blocks data from Redis cache');
|
||||||
|
// Load block data
|
||||||
|
const loadedBlocks = await this.$getBlocks();
|
||||||
|
const loadedBlockSummaries = await this.$getBlockSummaries();
|
||||||
|
// Load mempool
|
||||||
|
const loadedMempool = await this.$getMempool();
|
||||||
|
this.inflateLoadedTxs(loadedMempool);
|
||||||
|
// Load rbf data
|
||||||
|
const rbfTxs = await this.$getRbfEntries('tx');
|
||||||
|
const rbfTrees = await this.$getRbfEntries('tree');
|
||||||
|
const rbfExpirations = await this.$getRbfEntries('exp');
|
||||||
|
|
||||||
|
// Set loaded data
|
||||||
|
blocks.setBlocks(loadedBlocks || []);
|
||||||
|
blocks.setBlockSummaries(loadedBlockSummaries || []);
|
||||||
|
await memPool.$setMempool(loadedMempool);
|
||||||
|
await rbfCache.load({
|
||||||
|
txs: rbfTxs,
|
||||||
|
trees: rbfTrees.map(loadedTree => loadedTree.value),
|
||||||
|
expiring: rbfExpirations,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private inflateLoadedTxs(mempool: { [txid: string]: MempoolTransactionExtended }) {
|
||||||
|
for (const tx of Object.values(mempool)) {
|
||||||
|
for (const vin of tx.vin) {
|
||||||
|
if (vin.scriptsig) {
|
||||||
|
vin.scriptsig_asm = transactionUtils.convertScriptSigAsm(vin.scriptsig);
|
||||||
|
transactionUtils.addInnerScriptsToVin(vin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const vout of tx.vout) {
|
||||||
|
if (vout.scriptpubkey) {
|
||||||
|
vout.scriptpubkey_asm = transactionUtils.convertScriptSigAsm(vout.scriptpubkey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async scanKeys<T>(pattern): Promise<{ key: string, value: T }[]> {
|
||||||
|
logger.info(`loading Redis entries for ${pattern}`);
|
||||||
|
let keys: string[] = [];
|
||||||
|
const result: { key: string, value: T }[] = [];
|
||||||
|
const patternLength = pattern.length - 1;
|
||||||
|
let count = 0;
|
||||||
|
const processValues = async (keys): Promise<void> => {
|
||||||
|
const values = await this.client.MGET(keys);
|
||||||
|
for (let i = 0; i < values.length; i++) {
|
||||||
|
if (values[i]) {
|
||||||
|
result.push({ key: keys[i].slice(patternLength), value: JSON.parse(values[i]) });
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.info(`loaded ${count} entries from Redis cache`);
|
||||||
|
};
|
||||||
|
for await (const key of this.client.scanIterator({
|
||||||
|
MATCH: pattern,
|
||||||
|
COUNT: 100
|
||||||
|
})) {
|
||||||
|
keys.push(key);
|
||||||
|
if (keys.length >= 10000) {
|
||||||
|
await processValues(keys);
|
||||||
|
keys = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (keys.length) {
|
||||||
|
await processValues(keys);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new RedisCache();
|
||||||
@ -3,6 +3,7 @@ import { IEsploraApi } from './bitcoin/esplora-api.interface';
|
|||||||
import { Common } from './common';
|
import { Common } from './common';
|
||||||
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
|
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
|
||||||
import * as bitcoinjs from 'bitcoinjs-lib';
|
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||||
|
import logger from '../logger';
|
||||||
|
|
||||||
class TransactionUtils {
|
class TransactionUtils {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
@ -22,6 +23,23 @@ class TransactionUtils {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wrapper for $getTransactionExtended with an automatic retry direct to Core if the first API request fails.
|
||||||
|
// Propagates any error from the retry request.
|
||||||
|
public async $getTransactionExtendedRetry(txid: string, addPrevouts = false, lazyPrevouts = false, forceCore = false, addMempoolData = false): Promise<TransactionExtended> {
|
||||||
|
try {
|
||||||
|
const result = await this.$getTransactionExtended(txid, addPrevouts, lazyPrevouts, forceCore, addMempoolData);
|
||||||
|
if (result) {
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
logger.err(`Cannot fetch tx ${txid}. Reason: backend returned null data`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Cannot fetch tx ${txid}. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
}
|
||||||
|
// retry direct from Core if first request failed
|
||||||
|
return this.$getTransactionExtended(txid, addPrevouts, lazyPrevouts, true, addMempoolData);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param txId
|
* @param txId
|
||||||
* @param addPrevouts
|
* @param addPrevouts
|
||||||
@ -31,10 +49,17 @@ class TransactionUtils {
|
|||||||
public async $getTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false, forceCore = false, addMempoolData = false): Promise<TransactionExtended> {
|
public async $getTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false, forceCore = false, addMempoolData = false): Promise<TransactionExtended> {
|
||||||
let transaction: IEsploraApi.Transaction;
|
let transaction: IEsploraApi.Transaction;
|
||||||
if (forceCore === true) {
|
if (forceCore === true) {
|
||||||
transaction = await bitcoinCoreApi.$getRawTransaction(txId, true);
|
transaction = await bitcoinCoreApi.$getRawTransaction(txId, false, addPrevouts, lazyPrevouts);
|
||||||
} else {
|
} else {
|
||||||
transaction = await bitcoinApi.$getRawTransaction(txId, false, addPrevouts, lazyPrevouts);
|
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) {
|
if (addMempoolData || !transaction?.status?.confirmed) {
|
||||||
return this.extendMempoolTransaction(transaction);
|
return this.extendMempoolTransaction(transaction);
|
||||||
} else {
|
} else {
|
||||||
@ -46,14 +71,13 @@ class TransactionUtils {
|
|||||||
return (await this.$getTransactionExtended(txId, addPrevouts, lazyPrevouts, forceCore, true)) as MempoolTransactionExtended;
|
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
|
// @ts-ignore
|
||||||
if (transaction.vsize) {
|
if (transaction.vsize) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return transaction;
|
return transaction;
|
||||||
}
|
}
|
||||||
const feePerVbytes = Math.max(Common.isLiquid() ? 0.1 : 1,
|
const feePerVbytes = (transaction.fee || 0) / (transaction.weight / 4);
|
||||||
(transaction.fee || 0) / (transaction.weight / 4));
|
|
||||||
const transactionExtended: TransactionExtended = Object.assign({
|
const transactionExtended: TransactionExtended = Object.assign({
|
||||||
vsize: Math.round(transaction.weight / 4),
|
vsize: Math.round(transaction.weight / 4),
|
||||||
feePerVsize: feePerVbytes,
|
feePerVsize: feePerVbytes,
|
||||||
@ -68,13 +92,11 @@ class TransactionUtils {
|
|||||||
public extendMempoolTransaction(transaction: IEsploraApi.Transaction): MempoolTransactionExtended {
|
public extendMempoolTransaction(transaction: IEsploraApi.Transaction): MempoolTransactionExtended {
|
||||||
const vsize = Math.ceil(transaction.weight / 4);
|
const vsize = Math.ceil(transaction.weight / 4);
|
||||||
const fractionalVsize = (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
|
// 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 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,
|
const feePerVbytes = (transaction.fee || 0) / fractionalVsize;
|
||||||
(transaction.fee || 0) / fractionalVsize);
|
const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize;
|
||||||
const adjustedFeePerVsize = Math.max(Common.isLiquid() ? 0.1 : 1,
|
|
||||||
(transaction.fee || 0) / adjustedVsize);
|
|
||||||
const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, {
|
const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, {
|
||||||
order: this.txidToOrdering(transaction.txid),
|
order: this.txidToOrdering(transaction.txid),
|
||||||
vsize: Math.round(transaction.weight / 4),
|
vsize: Math.round(transaction.weight / 4),
|
||||||
@ -166,6 +188,122 @@ class TransactionUtils {
|
|||||||
16
|
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();
|
export default new TransactionUtils();
|
||||||
|
|||||||
@ -183,15 +183,25 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (parsedMessage && parsedMessage['track-address']) {
|
if (parsedMessage && parsedMessage['track-address']) {
|
||||||
if (/^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100})$/
|
if (/^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64})$/
|
||||||
.test(parsedMessage['track-address'])) {
|
.test(parsedMessage['track-address'])) {
|
||||||
let matchedAddress = parsedMessage['track-address'];
|
let matchedAddress = parsedMessage['track-address'];
|
||||||
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(parsedMessage['track-address'])) {
|
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(parsedMessage['track-address'])) {
|
||||||
matchedAddress = matchedAddress.toLowerCase();
|
matchedAddress = matchedAddress.toLowerCase();
|
||||||
}
|
}
|
||||||
client['track-address'] = matchedAddress;
|
if (/^04[a-fA-F0-9]{128}$/.test(parsedMessage['track-address'])) {
|
||||||
|
client['track-address'] = null;
|
||||||
|
client['track-scriptpubkey'] = '41' + matchedAddress + 'ac';
|
||||||
|
} else if (/^|(02|03)[a-fA-F0-9]{64}$/.test(parsedMessage['track-address'])) {
|
||||||
|
client['track-address'] = null;
|
||||||
|
client['track-scriptpubkey'] = '21' + matchedAddress + 'ac';
|
||||||
|
} else {
|
||||||
|
client['track-address'] = matchedAddress;
|
||||||
|
client['track-scriptpubkey'] = null;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
client['track-address'] = null;
|
client['track-address'] = null;
|
||||||
|
client['track-scriptpubkey'] = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -546,6 +556,44 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (client['track-scriptpubkey']) {
|
||||||
|
const foundTransactions: TransactionExtended[] = [];
|
||||||
|
|
||||||
|
for (const tx of newTransactions) {
|
||||||
|
const someVin = tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk' && vin.prevout.scriptpubkey === client['track-scriptpubkey']);
|
||||||
|
if (someVin) {
|
||||||
|
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||||
|
try {
|
||||||
|
const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
|
||||||
|
foundTransactions.push(fullTx);
|
||||||
|
} catch (e) {
|
||||||
|
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
foundTransactions.push(tx);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const someVout = tx.vout.some((vout) => vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey === client['track-scriptpubkey']);
|
||||||
|
if (someVout) {
|
||||||
|
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||||
|
try {
|
||||||
|
const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
|
||||||
|
foundTransactions.push(fullTx);
|
||||||
|
} catch (e) {
|
||||||
|
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
foundTransactions.push(tx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundTransactions.length) {
|
||||||
|
response['address-transactions'] = JSON.stringify(foundTransactions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (client['track-asset']) {
|
if (client['track-asset']) {
|
||||||
const foundTransactions: TransactionExtended[] = [];
|
const foundTransactions: TransactionExtended[] = [];
|
||||||
|
|
||||||
@ -604,7 +652,7 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (client['track-mempool-block'] >= 0) {
|
if (client['track-mempool-block'] >= 0 && memPool.isInSync()) {
|
||||||
const index = client['track-mempool-block'];
|
const index = client['track-mempool-block'];
|
||||||
if (mBlockDeltas[index]) {
|
if (mBlockDeltas[index]) {
|
||||||
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-${index}`, {
|
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-${index}`, {
|
||||||
@ -644,7 +692,7 @@ class WebsocketHandler {
|
|||||||
memPool.handleMinedRbfTransactions(rbfTransactions);
|
memPool.handleMinedRbfTransactions(rbfTransactions);
|
||||||
memPool.removeFromSpendMap(transactions);
|
memPool.removeFromSpendMap(transactions);
|
||||||
|
|
||||||
if (config.MEMPOOL.AUDIT) {
|
if (config.MEMPOOL.AUDIT && memPool.isInSync()) {
|
||||||
let projectedBlocks;
|
let projectedBlocks;
|
||||||
let auditMempool = _memPool;
|
let auditMempool = _memPool;
|
||||||
// template calculation functions have mempool side effects, so calculate audits using
|
// template calculation functions have mempool side effects, so calculate audits using
|
||||||
@ -665,7 +713,7 @@ class WebsocketHandler {
|
|||||||
projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
|
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 { censored, added, fresh, sigop, fullrbf, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
|
||||||
const matchRate = Math.round(score * 100 * 100) / 100;
|
const matchRate = Math.round(score * 100 * 100) / 100;
|
||||||
|
|
||||||
@ -821,6 +869,33 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (client['track-scriptpubkey']) {
|
||||||
|
const foundTransactions: TransactionExtended[] = [];
|
||||||
|
|
||||||
|
transactions.forEach((tx) => {
|
||||||
|
if (tx.vin && tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk' && vin.prevout.scriptpubkey === client['track-scriptpubkey'])) {
|
||||||
|
foundTransactions.push(tx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (tx.vout && tx.vout.some((vout) => vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey === client['track-scriptpubkey'])) {
|
||||||
|
foundTransactions.push(tx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (foundTransactions.length) {
|
||||||
|
foundTransactions.forEach((tx) => {
|
||||||
|
tx.status = {
|
||||||
|
confirmed: true,
|
||||||
|
block_height: block.height,
|
||||||
|
block_hash: block.id,
|
||||||
|
block_time: block.timestamp,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
response['block-transactions'] = JSON.stringify(foundTransactions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (client['track-asset']) {
|
if (client['track-asset']) {
|
||||||
const foundTransactions: TransactionExtended[] = [];
|
const foundTransactions: TransactionExtended[] = [];
|
||||||
|
|
||||||
@ -858,7 +933,7 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (client['track-mempool-block'] >= 0) {
|
if (client['track-mempool-block'] >= 0 && memPool.isInSync()) {
|
||||||
const index = client['track-mempool-block'];
|
const index = client['track-mempool-block'];
|
||||||
if (mBlockDeltas && mBlockDeltas[index]) {
|
if (mBlockDeltas && mBlockDeltas[index]) {
|
||||||
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-${index}`, {
|
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-${index}`, {
|
||||||
|
|||||||
@ -12,6 +12,7 @@ interface IConfig {
|
|||||||
API_URL_PREFIX: string;
|
API_URL_PREFIX: string;
|
||||||
POLL_RATE_MS: number;
|
POLL_RATE_MS: number;
|
||||||
CACHE_DIR: string;
|
CACHE_DIR: string;
|
||||||
|
CACHE_ENABLED: boolean;
|
||||||
CLEAR_PROTECTION_MINUTES: number;
|
CLEAR_PROTECTION_MINUTES: number;
|
||||||
RECOMMENDED_FEE_PERCENTILE: number;
|
RECOMMENDED_FEE_PERCENTILE: number;
|
||||||
BLOCK_WEIGHT_UNITS: number;
|
BLOCK_WEIGHT_UNITS: number;
|
||||||
@ -137,7 +138,11 @@ interface IConfig {
|
|||||||
AUDIT: boolean;
|
AUDIT: boolean;
|
||||||
AUDIT_START_HEIGHT: number;
|
AUDIT_START_HEIGHT: number;
|
||||||
SERVERS: string[];
|
SERVERS: string[];
|
||||||
}
|
},
|
||||||
|
REDIS: {
|
||||||
|
ENABLED: boolean;
|
||||||
|
UNIX_SOCKET_PATH: string;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaults: IConfig = {
|
const defaults: IConfig = {
|
||||||
@ -150,6 +155,7 @@ const defaults: IConfig = {
|
|||||||
'API_URL_PREFIX': '/api/v1/',
|
'API_URL_PREFIX': '/api/v1/',
|
||||||
'POLL_RATE_MS': 2000,
|
'POLL_RATE_MS': 2000,
|
||||||
'CACHE_DIR': './cache',
|
'CACHE_DIR': './cache',
|
||||||
|
'CACHE_ENABLED': true,
|
||||||
'CLEAR_PROTECTION_MINUTES': 20,
|
'CLEAR_PROTECTION_MINUTES': 20,
|
||||||
'RECOMMENDED_FEE_PERCENTILE': 50,
|
'RECOMMENDED_FEE_PERCENTILE': 50,
|
||||||
'BLOCK_WEIGHT_UNITS': 4000000,
|
'BLOCK_WEIGHT_UNITS': 4000000,
|
||||||
@ -275,7 +281,11 @@ const defaults: IConfig = {
|
|||||||
'AUDIT': false,
|
'AUDIT': false,
|
||||||
'AUDIT_START_HEIGHT': 774000,
|
'AUDIT_START_HEIGHT': 774000,
|
||||||
'SERVERS': [],
|
'SERVERS': [],
|
||||||
}
|
},
|
||||||
|
'REDIS': {
|
||||||
|
'ENABLED': false,
|
||||||
|
'UNIX_SOCKET_PATH': '',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
class Config implements IConfig {
|
class Config implements IConfig {
|
||||||
@ -296,6 +306,7 @@ class Config implements IConfig {
|
|||||||
EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER'];
|
EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER'];
|
||||||
MAXMIND: IConfig['MAXMIND'];
|
MAXMIND: IConfig['MAXMIND'];
|
||||||
REPLICATION: IConfig['REPLICATION'];
|
REPLICATION: IConfig['REPLICATION'];
|
||||||
|
REDIS: IConfig['REDIS'];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const configs = this.merge(configFromFile, defaults);
|
const configs = this.merge(configFromFile, defaults);
|
||||||
@ -316,6 +327,7 @@ class Config implements IConfig {
|
|||||||
this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER;
|
this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER;
|
||||||
this.MAXMIND = configs.MAXMIND;
|
this.MAXMIND = configs.MAXMIND;
|
||||||
this.REPLICATION = configs.REPLICATION;
|
this.REPLICATION = configs.REPLICATION;
|
||||||
|
this.REDIS = configs.REDIS;
|
||||||
}
|
}
|
||||||
|
|
||||||
merge = (...objects: object[]): IConfig => {
|
merge = (...objects: object[]): IConfig => {
|
||||||
|
|||||||
@ -41,6 +41,7 @@ import chainTips from './api/chain-tips';
|
|||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import v8 from 'v8';
|
import v8 from 'v8';
|
||||||
import { formatBytes, getBytesUnit } from './utils/format';
|
import { formatBytes, getBytesUnit } from './utils/format';
|
||||||
|
import redisCache from './api/redis-cache';
|
||||||
|
|
||||||
class Server {
|
class Server {
|
||||||
private wss: WebSocket.Server | undefined;
|
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 poolsUpdater.updatePoolsJson(); // Needs to be done before loading the disk cache because we sometimes wipe it
|
||||||
await syncAssets.syncAssets$();
|
await syncAssets.syncAssets$();
|
||||||
if (config.MEMPOOL.ENABLED) {
|
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) {
|
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && cluster.isPrimary) {
|
||||||
@ -183,14 +188,15 @@ class Server {
|
|||||||
}
|
}
|
||||||
const newMempool = await bitcoinApi.$getRawMempool();
|
const newMempool = await bitcoinApi.$getRawMempool();
|
||||||
const numHandledBlocks = await blocks.$updateBlocks();
|
const numHandledBlocks = await blocks.$updateBlocks();
|
||||||
|
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerRunning ? 10 : 1);
|
||||||
if (numHandledBlocks === 0) {
|
if (numHandledBlocks === 0) {
|
||||||
await memPool.$updateMempool(newMempool);
|
await memPool.$updateMempool(newMempool, pollRate);
|
||||||
}
|
}
|
||||||
indexer.$run();
|
indexer.$run();
|
||||||
|
|
||||||
// rerun immediately if we skipped the mempool update, otherwise wait POLL_RATE_MS
|
// rerun immediately if we skipped the mempool update, otherwise wait POLL_RATE_MS
|
||||||
const elapsed = Date.now() - start;
|
const elapsed = Date.now() - start;
|
||||||
const remainingTime = Math.max(0, config.MEMPOOL.POLL_RATE_MS - elapsed)
|
const remainingTime = Math.max(0, pollRate - elapsed);
|
||||||
setTimeout(this.runMainUpdateLoop.bind(this), numHandledBlocks > 0 ? 0 : remainingTime);
|
setTimeout(this.runMainUpdateLoop.bind(this), numHandledBlocks > 0 ? 0 : remainingTime);
|
||||||
this.backendRetryCount = 0;
|
this.backendRetryCount = 0;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
|
||||||
import { BlockExtended, BlockExtension, BlockPrice, EffectiveFeeStats } from '../mempool.interfaces';
|
import { BlockExtended, BlockExtension, BlockPrice, EffectiveFeeStats } from '../mempool.interfaces';
|
||||||
import DB from '../database';
|
import DB from '../database';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
@ -12,6 +13,7 @@ import config from '../config';
|
|||||||
import chainTips from '../api/chain-tips';
|
import chainTips from '../api/chain-tips';
|
||||||
import blocks from '../api/blocks';
|
import blocks from '../api/blocks';
|
||||||
import BlocksAuditsRepository from './BlocksAuditsRepository';
|
import BlocksAuditsRepository from './BlocksAuditsRepository';
|
||||||
|
import transactionUtils from '../api/transaction-utils';
|
||||||
|
|
||||||
interface DatabaseBlock {
|
interface DatabaseBlock {
|
||||||
id: string;
|
id: string;
|
||||||
@ -539,7 +541,7 @@ class BlocksRepository {
|
|||||||
*/
|
*/
|
||||||
public async $getBlocksDifficulty(): Promise<object[]> {
|
public async $getBlocksDifficulty(): Promise<object[]> {
|
||||||
try {
|
try {
|
||||||
const [rows]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(blockTimestamp) as time, height, difficulty FROM blocks`);
|
const [rows]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(blockTimestamp) as time, height, difficulty, bits FROM blocks`);
|
||||||
return rows;
|
return rows;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('Cannot get blocks difficulty list from the db. Reason: ' + (e instanceof Error ? e.message : e));
|
logger.err('Cannot get blocks difficulty list from the db. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
@ -848,7 +850,7 @@ class BlocksRepository {
|
|||||||
*/
|
*/
|
||||||
public async $getOldestConsecutiveBlock(): Promise<any> {
|
public async $getOldestConsecutiveBlock(): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const [rows]: any = await DB.query(`SELECT height, UNIX_TIMESTAMP(blockTimestamp) as timestamp, difficulty FROM blocks ORDER BY height DESC`);
|
const [rows]: any = await DB.query(`SELECT height, UNIX_TIMESTAMP(blockTimestamp) as timestamp, difficulty, bits FROM blocks ORDER BY height DESC`);
|
||||||
for (let i = 0; i < rows.length - 1; ++i) {
|
for (let i = 0; i < rows.length - 1; ++i) {
|
||||||
if (rows[i].height - rows[i + 1].height > 1) {
|
if (rows[i].height - rows[i + 1].height > 1) {
|
||||||
return rows[i];
|
return rows[i];
|
||||||
@ -1036,8 +1038,17 @@ class BlocksRepository {
|
|||||||
{
|
{
|
||||||
extras.feePercentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(dbBlk.id);
|
extras.feePercentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(dbBlk.id);
|
||||||
if (extras.feePercentiles === null) {
|
if (extras.feePercentiles === null) {
|
||||||
const block = await bitcoinClient.getBlock(dbBlk.id, 2);
|
|
||||||
const summary = blocks.summarizeBlock(block);
|
let summary;
|
||||||
|
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||||
|
const txs = (await bitcoinApi.$getTxsForBlock(dbBlk.id)).map(tx => transactionUtils.extendTransaction(tx));
|
||||||
|
summary = blocks.summarizeBlockTransactions(dbBlk.id, txs);
|
||||||
|
} else {
|
||||||
|
// Call Core RPC
|
||||||
|
const block = await bitcoinClient.getBlock(dbBlk.id, 2);
|
||||||
|
summary = blocks.summarizeBlock(block);
|
||||||
|
}
|
||||||
|
|
||||||
await BlocksSummariesRepository.$saveTransactions(dbBlk.height, dbBlk.id, summary.transactions);
|
await BlocksSummariesRepository.$saveTransactions(dbBlk.height, dbBlk.id, summary.transactions);
|
||||||
extras.feePercentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(dbBlk.id);
|
extras.feePercentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(dbBlk.id);
|
||||||
}
|
}
|
||||||
|
|||||||
3
contributors/Czino.txt
Normal file
3
contributors/Czino.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of July 29, 2023.
|
||||||
|
|
||||||
|
Signed: Czino
|
||||||
3
contributors/andrewtoth.txt
Normal file
3
contributors/andrewtoth.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 August 2, 2023.
|
||||||
|
|
||||||
|
Signed: andrewtoth
|
||||||
3
contributors/bguillaumat.txt
Normal file
3
contributors/bguillaumat.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of January 25, 2022.
|
||||||
|
|
||||||
|
Signed: bguillaumat
|
||||||
3
contributors/devinbileck.txt
Normal file
3
contributors/devinbileck.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of July 21, 2023.
|
||||||
|
|
||||||
|
Signed: devinbileck
|
||||||
5
contributors/fiatjaf.txt
Normal file
5
contributors/fiatjaf.txt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of January 25, 2022.
|
||||||
|
I also regret having ever contributed to this repository since they keep asking me to sign this legalese timewaste things.
|
||||||
|
And finally I don't care about licenses and won't sue anyone over intellectual property, which is a fake statist construct invented by evil lobby lawyers.
|
||||||
|
|
||||||
|
Signed: fiatjaf
|
||||||
3
contributors/pedromvpg.txt
Normal file
3
contributors/pedromvpg.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of July 20, 2023.
|
||||||
|
|
||||||
|
Signed: pedromvpg
|
||||||
3
contributors/rishkwal.txt
Normal file
3
contributors/rishkwal.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of July 29, 2023.
|
||||||
|
|
||||||
|
Signed: rishkwal
|
||||||
@ -7,9 +7,10 @@ WORKDIR /build
|
|||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN apt-get update
|
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
|
# Install Rust via rustup
|
||||||
|
RUN CPU_ARCH=$(uname -m); if [ "$CPU_ARCH" = "armv7l" ]; then c_rehash; fi
|
||||||
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable
|
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable
|
||||||
ENV PATH="/root/.cargo/bin:$PATH"
|
ENV PATH="/root/.cargo/bin:$PATH"
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
"API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
|
"API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
|
||||||
"POLL_RATE_MS": __MEMPOOL_POLL_RATE_MS__,
|
"POLL_RATE_MS": __MEMPOOL_POLL_RATE_MS__,
|
||||||
"CACHE_DIR": "__MEMPOOL_CACHE_DIR__",
|
"CACHE_DIR": "__MEMPOOL_CACHE_DIR__",
|
||||||
|
"CACHE_ENABLED": __MEMPOOL_CACHE_ENABLED__,
|
||||||
"CLEAR_PROTECTION_MINUTES": __MEMPOOL_CLEAR_PROTECTION_MINUTES__,
|
"CLEAR_PROTECTION_MINUTES": __MEMPOOL_CLEAR_PROTECTION_MINUTES__,
|
||||||
"RECOMMENDED_FEE_PERCENTILE": __MEMPOOL_RECOMMENDED_FEE_PERCENTILE__,
|
"RECOMMENDED_FEE_PERCENTILE": __MEMPOOL_RECOMMENDED_FEE_PERCENTILE__,
|
||||||
"BLOCK_WEIGHT_UNITS": __MEMPOOL_BLOCK_WEIGHT_UNITS__,
|
"BLOCK_WEIGHT_UNITS": __MEMPOOL_BLOCK_WEIGHT_UNITS__,
|
||||||
@ -133,5 +134,9 @@
|
|||||||
"AUDIT": __REPLICATION_AUDIT__,
|
"AUDIT": __REPLICATION_AUDIT__,
|
||||||
"AUDIT_START_HEIGHT": __REPLICATION_AUDIT_START_HEIGHT__,
|
"AUDIT_START_HEIGHT": __REPLICATION_AUDIT_START_HEIGHT__,
|
||||||
"SERVERS": __REPLICATION_SERVERS__
|
"SERVERS": __REPLICATION_SERVERS__
|
||||||
|
},
|
||||||
|
"REDIS": {
|
||||||
|
"ENABLED": __REDIS_ENABLED__,
|
||||||
|
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ __MEMPOOL_SPAWN_CLUSTER_PROCS__=${MEMPOOL_SPAWN_CLUSTER_PROCS:=0}
|
|||||||
__MEMPOOL_API_URL_PREFIX__=${MEMPOOL_API_URL_PREFIX:=/api/v1/}
|
__MEMPOOL_API_URL_PREFIX__=${MEMPOOL_API_URL_PREFIX:=/api/v1/}
|
||||||
__MEMPOOL_POLL_RATE_MS__=${MEMPOOL_POLL_RATE_MS:=2000}
|
__MEMPOOL_POLL_RATE_MS__=${MEMPOOL_POLL_RATE_MS:=2000}
|
||||||
__MEMPOOL_CACHE_DIR__=${MEMPOOL_CACHE_DIR:=./cache}
|
__MEMPOOL_CACHE_DIR__=${MEMPOOL_CACHE_DIR:=./cache}
|
||||||
|
__MEMPOOL_CACHE_ENABLED__=${MEMPOOL_CACHE_ENABLED:=true}
|
||||||
__MEMPOOL_CLEAR_PROTECTION_MINUTES__=${MEMPOOL_CLEAR_PROTECTION_MINUTES:=20}
|
__MEMPOOL_CLEAR_PROTECTION_MINUTES__=${MEMPOOL_CLEAR_PROTECTION_MINUTES:=20}
|
||||||
__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__=${MEMPOOL_RECOMMENDED_FEE_PERCENTILE:=50}
|
__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__=${MEMPOOL_RECOMMENDED_FEE_PERCENTILE:=50}
|
||||||
__MEMPOOL_BLOCK_WEIGHT_UNITS__=${MEMPOOL_BLOCK_WEIGHT_UNITS:=4000000}
|
__MEMPOOL_BLOCK_WEIGHT_UNITS__=${MEMPOOL_BLOCK_WEIGHT_UNITS:=4000000}
|
||||||
@ -136,6 +137,9 @@ __REPLICATION_AUDIT__=${REPLICATION_AUDIT:=true}
|
|||||||
__REPLICATION_AUDIT_START_HEIGHT__=${REPLICATION_AUDIT_START_HEIGHT:=774000}
|
__REPLICATION_AUDIT_START_HEIGHT__=${REPLICATION_AUDIT_START_HEIGHT:=774000}
|
||||||
__REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
|
__REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
|
||||||
|
|
||||||
|
# REDIS
|
||||||
|
__REDIS_ENABLED__=${REDIS_ENABLED:=true}
|
||||||
|
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=true}
|
||||||
|
|
||||||
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
|
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
|
||||||
|
|
||||||
@ -147,6 +151,7 @@ sed -i "s!__MEMPOOL_SPAWN_CLUSTER_PROCS__!${__MEMPOOL_SPAWN_CLUSTER_PROCS__}!g"
|
|||||||
sed -i "s!__MEMPOOL_API_URL_PREFIX__!${__MEMPOOL_API_URL_PREFIX__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_API_URL_PREFIX__!${__MEMPOOL_API_URL_PREFIX__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_POLL_RATE_MS__!${__MEMPOOL_POLL_RATE_MS__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_POLL_RATE_MS__!${__MEMPOOL_POLL_RATE_MS__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_CACHE_DIR__!${__MEMPOOL_CACHE_DIR__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_CACHE_DIR__!${__MEMPOOL_CACHE_DIR__}!g" mempool-config.json
|
||||||
|
sed -i "s!__MEMPOOL_CACHE_ENABLED__!${__MEMPOOL_CACHE_ENABLED__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_CLEAR_PROTECTION_MINUTES__!${__MEMPOOL_CLEAR_PROTECTION_MINUTES__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_CLEAR_PROTECTION_MINUTES__!${__MEMPOOL_CLEAR_PROTECTION_MINUTES__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__!${__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__!${__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_BLOCK_WEIGHT_UNITS__!${__MEMPOOL_BLOCK_WEIGHT_UNITS__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_BLOCK_WEIGHT_UNITS__!${__MEMPOOL_BLOCK_WEIGHT_UNITS__}!g" mempool-config.json
|
||||||
@ -165,7 +170,7 @@ sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-co
|
|||||||
sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_AUDIT__!${__MEMPOOL_AUDIT__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_AUDIT__!${__MEMPOOL_AUDIT__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_ADVANCED_GBT_MEMPOOL__!${__MEMPOOL_ADVANCED_GBT_MEMPOOL__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_ADVANCED_GBT_MEMPOOL__!${__MEMPOOL_ADVANCED_GBT_MEMPOOL__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_RUST_GBT__!${__MEMPOOL_GBT__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_RUST_GBT__!${__MEMPOOL_RUST_GBT__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_ADVANCED_GBT_AUDIT__!${__MEMPOOL_ADVANCED_GBT_AUDIT__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_ADVANCED_GBT_AUDIT__!${__MEMPOOL_ADVANCED_GBT_AUDIT__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_CPFP_INDEXING__!${__MEMPOOL_CPFP_INDEXING__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_CPFP_INDEXING__!${__MEMPOOL_CPFP_INDEXING__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_MAX_BLOCKS_BULK_QUERY__!${__MEMPOOL_MAX_BLOCKS_BULK_QUERY__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_MAX_BLOCKS_BULK_QUERY__!${__MEMPOOL_MAX_BLOCKS_BULK_QUERY__}!g" mempool-config.json
|
||||||
@ -262,4 +267,8 @@ sed -i "s!__REPLICATION_AUDIT__!${__REPLICATION_AUDIT__}!g" mempool-config.json
|
|||||||
sed -i "s!__REPLICATION_AUDIT_START_HEIGHT__!${__REPLICATION_AUDIT_START_HEIGHT__}!g" mempool-config.json
|
sed -i "s!__REPLICATION_AUDIT_START_HEIGHT__!${__REPLICATION_AUDIT_START_HEIGHT__}!g" mempool-config.json
|
||||||
sed -i "s!__REPLICATION_SERVERS__!${__REPLICATION_SERVERS__}!g" mempool-config.json
|
sed -i "s!__REPLICATION_SERVERS__!${__REPLICATION_SERVERS__}!g" mempool-config.json
|
||||||
|
|
||||||
|
# REDIS
|
||||||
|
sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json
|
||||||
|
sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json
|
||||||
|
|
||||||
node /backend/package/index.js
|
node /backend/package/index.js
|
||||||
|
|||||||
@ -18,7 +18,7 @@ fi
|
|||||||
|
|
||||||
__TESTNET_ENABLED__=${TESTNET_ENABLED:=false}
|
__TESTNET_ENABLED__=${TESTNET_ENABLED:=false}
|
||||||
__SIGNET_ENABLED__=${SIGNET_ENABLED:=false}
|
__SIGNET_ENABLED__=${SIGNET_ENABLED:=false}
|
||||||
__LIQUID_ENABLED__=${LIQUID_EANBLED:=false}
|
__LIQUID_ENABLED__=${LIQUID_ENABLED:=false}
|
||||||
__LIQUID_TESTNET_ENABLED__=${LIQUID_TESTNET_ENABLED:=false}
|
__LIQUID_TESTNET_ENABLED__=${LIQUID_TESTNET_ENABLED:=false}
|
||||||
__BISQ_ENABLED__=${BISQ_ENABLED:=false}
|
__BISQ_ENABLED__=${BISQ_ENABLED:=false}
|
||||||
__BISQ_SEPARATE_BACKEND__=${BISQ_SEPARATE_BACKEND:=false}
|
__BISQ_SEPARATE_BACKEND__=${BISQ_SEPARATE_BACKEND:=false}
|
||||||
|
|||||||
@ -2,11 +2,15 @@
|
|||||||
# For additional information regarding the format and rule options, please see:
|
# For additional information regarding the format and rule options, please see:
|
||||||
# https://github.com/browserslist/browserslist#queries
|
# 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:
|
# You can see what browsers were selected by your queries by running:
|
||||||
# npx browserslist
|
# npx browserslist
|
||||||
|
|
||||||
> 0.5%
|
last 2 Chrome versions
|
||||||
last 2 versions
|
last 1 Firefox version
|
||||||
|
last 2 Edge major versions
|
||||||
|
last 2 Safari major versions
|
||||||
|
last 2 iOS major versions
|
||||||
Firefox ESR
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function calcScriptHash$(script: string): Promise<string> {
|
||||||
|
if (!/^[0-9a-fA-F]*$/.test(script) || script.length % 2 !== 0) {
|
||||||
|
throw new Error('script is not a valid hex string');
|
||||||
|
}
|
||||||
|
const buf = Uint8Array.from(script.match(/.{2}/g).map((byte) => parseInt(byte, 16)));
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-256', buf);
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
return hashArray
|
||||||
|
.map((bytes) => bytes.toString(16).padStart(2, '0'))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
@ -411,7 +411,7 @@
|
|||||||
Trademark Notice<br>
|
Trademark Notice<br>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
The Mempool Open Source Project™, mempool.space™, the mempool logo®, the mempool.space logos™, the mempool square logo®, and the mempool blocks logo™ are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
|
The Mempool Open Source Project®, mempool.space™, the mempool logo®, the mempool.space logos™, the mempool square logo®, and the mempool blocks logo™ are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on <https://mempool.space/trademark-policy>.
|
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on <https://mempool.space/trademark-policy>.
|
||||||
|
|||||||
@ -64,13 +64,15 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
|
|||||||
this.address = null;
|
this.address = null;
|
||||||
this.addressInfo = null;
|
this.addressInfo = null;
|
||||||
this.addressString = params.get('id') || '';
|
this.addressString = params.get('id') || '';
|
||||||
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(this.addressString)) {
|
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(this.addressString)) {
|
||||||
this.addressString = this.addressString.toLowerCase();
|
this.addressString = this.addressString.toLowerCase();
|
||||||
}
|
}
|
||||||
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
|
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
|
||||||
|
|
||||||
return this.electrsApiService.getAddress$(this.addressString)
|
return (this.addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
|
||||||
.pipe(
|
? this.electrsApiService.getPubKeyAddress$(this.addressString)
|
||||||
|
: this.electrsApiService.getAddress$(this.addressString)
|
||||||
|
).pipe(
|
||||||
catchError((err) => {
|
catchError((err) => {
|
||||||
this.isLoadingAddress = false;
|
this.isLoadingAddress = false;
|
||||||
this.error = err;
|
this.error = err;
|
||||||
|
|||||||
@ -81,6 +81,7 @@ h1 {
|
|||||||
top: 11px;
|
top: 11px;
|
||||||
}
|
}
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
|
max-width: calc(100% - 180px);
|
||||||
top: 17px;
|
top: 17px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Component, OnInit, OnDestroy } from '@angular/core';
|
|||||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||||
import { switchMap, filter, catchError, map, tap } from 'rxjs/operators';
|
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 { WebsocketService } from '../../services/websocket.service';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { AudioService } from '../../services/audio.service';
|
import { AudioService } from '../../services/audio.service';
|
||||||
@ -72,7 +72,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
this.addressInfo = null;
|
this.addressInfo = null;
|
||||||
document.body.scrollTo(0, 0);
|
document.body.scrollTo(0, 0);
|
||||||
this.addressString = params.get('id') || '';
|
this.addressString = params.get('id') || '';
|
||||||
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(this.addressString)) {
|
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(this.addressString)) {
|
||||||
this.addressString = this.addressString.toLowerCase();
|
this.addressString = this.addressString.toLowerCase();
|
||||||
}
|
}
|
||||||
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
|
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(filter((state) => state === 2 && this.transactions && this.transactions.length > 0))
|
||||||
)
|
)
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap(() => this.electrsApiService.getAddress$(this.addressString)
|
switchMap(() => (
|
||||||
.pipe(
|
this.addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
|
||||||
|
? this.electrsApiService.getPubKeyAddress$(this.addressString)
|
||||||
|
: this.electrsApiService.getAddress$(this.addressString)
|
||||||
|
).pipe(
|
||||||
catchError((err) => {
|
catchError((err) => {
|
||||||
this.isLoadingAddress = false;
|
this.isLoadingAddress = false;
|
||||||
this.error = err;
|
this.error = err;
|
||||||
@ -114,7 +117,9 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
this.updateChainStats();
|
this.updateChainStats();
|
||||||
this.isLoadingAddress = false;
|
this.isLoadingAddress = false;
|
||||||
this.isLoadingTransactions = true;
|
this.isLoadingTransactions = true;
|
||||||
return this.electrsApiService.getAddressTransactions$(address.address);
|
return address.is_pubkey
|
||||||
|
? this.electrsApiService.getScriptHashTransactions$((address.address.length === 66 ? '21' : '41') + address.address + 'ac')
|
||||||
|
: this.electrsApiService.getAddressTransactions$(address.address);
|
||||||
}),
|
}),
|
||||||
switchMap((transactions) => {
|
switchMap((transactions) => {
|
||||||
this.tempTransactions = transactions;
|
this.tempTransactions = transactions;
|
||||||
@ -161,31 +166,8 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.stateService.mempoolTransactions$
|
this.stateService.mempoolTransactions$
|
||||||
.subscribe((transaction) => {
|
.subscribe(tx => {
|
||||||
if (this.transactions.some((t) => t.txid === transaction.txid)) {
|
this.addTransaction(tx);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.transactions.unshift(transaction);
|
|
||||||
this.transactions = this.transactions.slice();
|
|
||||||
this.txCount++;
|
|
||||||
|
|
||||||
if (transaction.vout.some((vout) => vout.scriptpubkey_address === this.address.address)) {
|
|
||||||
this.audioService.playSound('cha-ching');
|
|
||||||
} else {
|
|
||||||
this.audioService.playSound('chime');
|
|
||||||
}
|
|
||||||
|
|
||||||
transaction.vin.forEach((vin) => {
|
|
||||||
if (vin.prevout.scriptpubkey_address === this.address.address) {
|
|
||||||
this.sent += vin.prevout.value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
transaction.vout.forEach((vout) => {
|
|
||||||
if (vout.scriptpubkey_address === this.address.address) {
|
|
||||||
this.received += vout.value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.stateService.blockTransactions$
|
this.stateService.blockTransactions$
|
||||||
@ -195,12 +177,47 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
tx.status = transaction.status;
|
tx.status = transaction.status;
|
||||||
this.transactions = this.transactions.slice();
|
this.transactions = this.transactions.slice();
|
||||||
this.audioService.playSound('magic');
|
this.audioService.playSound('magic');
|
||||||
|
} else {
|
||||||
|
if (this.addTransaction(transaction, false)) {
|
||||||
|
this.audioService.playSound('magic');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.totalConfirmedTxCount++;
|
this.totalConfirmedTxCount++;
|
||||||
this.loadedConfirmedTxCount++;
|
this.loadedConfirmedTxCount++;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addTransaction(transaction: Transaction, playSound: boolean = true): boolean {
|
||||||
|
if (this.transactions.some((t) => t.txid === transaction.txid)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.transactions.unshift(transaction);
|
||||||
|
this.transactions = this.transactions.slice();
|
||||||
|
this.txCount++;
|
||||||
|
|
||||||
|
if (playSound) {
|
||||||
|
if (transaction.vout.some((vout) => vout?.scriptpubkey_address === this.address.address)) {
|
||||||
|
this.audioService.playSound('cha-ching');
|
||||||
|
} else {
|
||||||
|
this.audioService.playSound('chime');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.vin.forEach((vin) => {
|
||||||
|
if (vin?.prevout?.scriptpubkey_address === this.address.address) {
|
||||||
|
this.sent += vin.prevout.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
transaction.vout.forEach((vout) => {
|
||||||
|
if (vout?.scriptpubkey_address === this.address.address) {
|
||||||
|
this.received += vout.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
loadMore() {
|
loadMore() {
|
||||||
if (this.isLoadingTransactions || !this.totalConfirmedTxCount || this.loadedConfirmedTxCount >= this.totalConfirmedTxCount) {
|
if (this.isLoadingTransactions || !this.totalConfirmedTxCount || this.loadedConfirmedTxCount >= this.totalConfirmedTxCount) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -25,7 +25,8 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0px 15px;
|
padding: 0px 15px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100vh - 250px);
|
height: calc(100vh - 225px);
|
||||||
|
min-height: 400px;
|
||||||
@media (min-width: 992px) {
|
@media (min-width: 992px) {
|
||||||
height: calc(100vh - 150px);
|
height: calc(100vh - 150px);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,7 +25,8 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0px 15px;
|
padding: 0px 15px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100vh - 250px);
|
height: calc(100vh - 225px);
|
||||||
|
min-height: 400px;
|
||||||
@media (min-width: 992px) {
|
@media (min-width: 992px) {
|
||||||
height: calc(100vh - 150px);
|
height: calc(100vh - 150px);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,7 +25,8 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0px 15px;
|
padding: 0px 15px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100vh - 250px);
|
height: calc(100vh - 225px);
|
||||||
|
min-height: 400px;
|
||||||
@media (min-width: 992px) {
|
@media (min-width: 992px) {
|
||||||
height: calc(100vh - 150px);
|
height: calc(100vh - 150px);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export default class TxView implements TransactionStripped {
|
|||||||
value: number;
|
value: number;
|
||||||
feerate: number;
|
feerate: number;
|
||||||
rate?: number;
|
rate?: number;
|
||||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'fullrbf';
|
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf';
|
||||||
context?: 'projected' | 'actual';
|
context?: 'projected' | 'actual';
|
||||||
scene?: BlockScene;
|
scene?: BlockScene;
|
||||||
|
|
||||||
@ -207,7 +207,7 @@ export default class TxView implements TransactionStripped {
|
|||||||
return auditColors.censored;
|
return auditColors.censored;
|
||||||
case 'missing':
|
case 'missing':
|
||||||
case 'sigop':
|
case 'sigop':
|
||||||
case 'fullrbf':
|
case 'rbf':
|
||||||
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
|
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
|
||||||
case 'fresh':
|
case 'fresh':
|
||||||
case 'freshcpfp':
|
case 'freshcpfp':
|
||||||
|
|||||||
@ -53,7 +53,7 @@
|
|||||||
<td *ngSwitchCase="'freshcpfp'"><span class="badge badge-warning" i18n="transaction.audit.recently-cpfped">Recently CPFP'd</span></td>
|
<td *ngSwitchCase="'freshcpfp'"><span class="badge badge-warning" i18n="transaction.audit.recently-cpfped">Recently CPFP'd</span></td>
|
||||||
<td *ngSwitchCase="'added'"><span class="badge badge-warning" i18n="transaction.audit.added">Added</span></td>
|
<td *ngSwitchCase="'added'"><span class="badge badge-warning" i18n="transaction.audit.added">Added</span></td>
|
||||||
<td *ngSwitchCase="'selected'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
|
<td *ngSwitchCase="'selected'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
|
||||||
<td *ngSwitchCase="'fullrbf'"><span class="badge badge-warning" i18n="transaction.audit.fullrbf">Full RBF</span></td>
|
<td *ngSwitchCase="'rbf'"><span class="badge badge-warning" i18n="transaction.audit.conflicting">Conflicting</span></td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@ -25,7 +25,8 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0px 15px;
|
padding: 0px 15px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100vh - 250px);
|
height: calc(100vh - 225px);
|
||||||
|
min-height: 400px;
|
||||||
@media (min-width: 992px) {
|
@media (min-width: 992px) {
|
||||||
height: calc(100vh - 150px);
|
height: calc(100vh - 150px);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,7 +25,8 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0px 15px;
|
padding: 0px 15px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100vh - 250px);
|
height: calc(100vh - 225px);
|
||||||
|
min-height: 400px;
|
||||||
@media (min-width: 992px) {
|
@media (min-width: 992px) {
|
||||||
height: calc(100vh - 150px);
|
height: calc(100vh - 150px);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -144,10 +144,12 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
for (const block of blocks) {
|
for (const block of blocks) {
|
||||||
if (block.id === this.blockHash) {
|
if (block.id === this.blockHash) {
|
||||||
this.block = block;
|
this.block = block;
|
||||||
block.extras.minFee = this.getMinBlockFee(block);
|
if (block.extras) {
|
||||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
block.extras.minFee = this.getMinBlockFee(block);
|
||||||
if (block?.extras?.reward != undefined) {
|
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||||
this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
|
if (block?.extras?.reward != undefined) {
|
||||||
|
this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (block.height === this.block?.height) {
|
} else if (block.height === this.block?.height) {
|
||||||
this.block.stale = true;
|
this.block.stale = true;
|
||||||
@ -246,8 +248,10 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
this.updateAuditAvailableFromBlockHeight(block.height);
|
this.updateAuditAvailableFromBlockHeight(block.height);
|
||||||
this.block = block;
|
this.block = block;
|
||||||
block.extras.minFee = this.getMinBlockFee(block);
|
if (block.extras) {
|
||||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
block.extras.minFee = this.getMinBlockFee(block);
|
||||||
|
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||||
|
}
|
||||||
this.blockHeight = block.height;
|
this.blockHeight = block.height;
|
||||||
this.lastBlockHeight = this.blockHeight;
|
this.lastBlockHeight = this.blockHeight;
|
||||||
this.nextBlockHeight = block.height + 1;
|
this.nextBlockHeight = block.height + 1;
|
||||||
@ -335,7 +339,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
const isSelected = {};
|
const isSelected = {};
|
||||||
const isFresh = {};
|
const isFresh = {};
|
||||||
const isSigop = {};
|
const isSigop = {};
|
||||||
const isFullRbf = {};
|
const isRbf = {};
|
||||||
this.numMissing = 0;
|
this.numMissing = 0;
|
||||||
this.numUnexpected = 0;
|
this.numUnexpected = 0;
|
||||||
|
|
||||||
@ -359,7 +363,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
isSigop[txid] = true;
|
isSigop[txid] = true;
|
||||||
}
|
}
|
||||||
for (const txid of blockAudit.fullrbfTxs || []) {
|
for (const txid of blockAudit.fullrbfTxs || []) {
|
||||||
isFullRbf[txid] = true;
|
isRbf[txid] = true;
|
||||||
}
|
}
|
||||||
// set transaction statuses
|
// set transaction statuses
|
||||||
for (const tx of blockAudit.template) {
|
for (const tx of blockAudit.template) {
|
||||||
@ -377,8 +381,8 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
} else if (isSigop[tx.txid]) {
|
} else if (isSigop[tx.txid]) {
|
||||||
tx.status = 'sigop';
|
tx.status = 'sigop';
|
||||||
} else if (isFullRbf[tx.txid]) {
|
} else if (isRbf[tx.txid]) {
|
||||||
tx.status = 'fullrbf';
|
tx.status = 'rbf';
|
||||||
} else {
|
} else {
|
||||||
tx.status = 'missing';
|
tx.status = 'missing';
|
||||||
}
|
}
|
||||||
@ -394,8 +398,8 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
tx.status = 'added';
|
tx.status = 'added';
|
||||||
} else if (inTemplate[tx.txid]) {
|
} else if (inTemplate[tx.txid]) {
|
||||||
tx.status = 'found';
|
tx.status = 'found';
|
||||||
} else if (isFullRbf[tx.txid]) {
|
} else if (isRbf[tx.txid]) {
|
||||||
tx.status = 'fullrbf';
|
tx.status = 'rbf';
|
||||||
} else {
|
} else {
|
||||||
tx.status = 'selected';
|
tx.status = 'selected';
|
||||||
isSelected[tx.txid] = true;
|
isSelected[tx.txid] = true;
|
||||||
|
|||||||
@ -113,8 +113,10 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
const animate = this.chainTip != null && latestHeight > this.chainTip;
|
const animate = this.chainTip != null && latestHeight > this.chainTip;
|
||||||
|
|
||||||
for (const block of blocks) {
|
for (const block of blocks) {
|
||||||
block.extras.minFee = this.getMinBlockFee(block);
|
if (block?.extras) {
|
||||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
block.extras.minFee = this.getMinBlockFee(block);
|
||||||
|
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.blocks = blocks;
|
this.blocks = blocks;
|
||||||
@ -251,7 +253,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
if (height >= 0) {
|
if (height >= 0) {
|
||||||
this.cacheService.loadBlock(height);
|
this.cacheService.loadBlock(height);
|
||||||
block = this.cacheService.getCachedBlock(height) || null;
|
block = this.cacheService.getCachedBlock(height) || null;
|
||||||
if (block) {
|
if (block?.extras) {
|
||||||
block.extras.minFee = this.getMinBlockFee(block);
|
block.extras.minFee = this.getMinBlockFee(block);
|
||||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||||
}
|
}
|
||||||
@ -293,8 +295,10 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
onBlockLoaded(block: BlockExtended) {
|
onBlockLoaded(block: BlockExtended) {
|
||||||
const blockIndex = this.height - block.height;
|
const blockIndex = this.height - block.height;
|
||||||
if (blockIndex >= 0 && blockIndex < this.blocks.length) {
|
if (blockIndex >= 0 && blockIndex < this.blocks.length) {
|
||||||
block.extras.minFee = this.getMinBlockFee(block);
|
if (block?.extras) {
|
||||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
block.extras.minFee = this.getMinBlockFee(block);
|
||||||
|
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||||
|
}
|
||||||
this.blocks[blockIndex] = block;
|
this.blocks[blockIndex] = block;
|
||||||
this.blockStyles[blockIndex] = this.getStyleForBlock(block, blockIndex);
|
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.mempoolOffset = Math.max(0, width - this.dividerOffset);
|
||||||
this.cd.markForCheck();
|
this.cd.markForCheck();
|
||||||
setTimeout(() => {
|
this.mempoolOffsetChange.emit(this.mempoolOffset);
|
||||||
this.mempoolOffsetChange.emit(this.mempoolOffset);
|
|
||||||
}, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('window:resize', ['$event'])
|
@HostListener('window:resize', ['$event'])
|
||||||
|
|||||||
@ -68,7 +68,7 @@ export class BlocksList implements OnInit {
|
|||||||
for (const block of blocks) {
|
for (const block of blocks) {
|
||||||
// @ts-ignore: Need to add an extra field for the template
|
// @ts-ignore: Need to add an extra field for the template
|
||||||
block.extras.pool.logo = `/resources/mining-pools/` +
|
block.extras.pool.logo = `/resources/mining-pools/` +
|
||||||
block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
|
block.extras.pool.slug + '.svg';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.widget) {
|
if (this.widget) {
|
||||||
@ -84,10 +84,10 @@ export class BlocksList implements OnInit {
|
|||||||
.pipe(
|
.pipe(
|
||||||
switchMap((blocks) => {
|
switchMap((blocks) => {
|
||||||
if (blocks[0].height <= this.lastBlockHeight) {
|
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;
|
this.lastBlockHeight = blocks[0].height;
|
||||||
return blocks;
|
return of(blocks);
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
])
|
])
|
||||||
@ -102,7 +102,7 @@ export class BlocksList implements OnInit {
|
|||||||
if (this.stateService.env.MINING_DASHBOARD) {
|
if (this.stateService.env.MINING_DASHBOARD) {
|
||||||
// @ts-ignore: Need to add an extra field for the template
|
// @ts-ignore: Need to add an extra field for the template
|
||||||
blocks[1][0].extras.pool.logo = `/resources/mining-pools/` +
|
blocks[1][0].extras.pool.logo = `/resources/mining-pools/` +
|
||||||
blocks[1][0].extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
|
blocks[1][0].extras.pool.slug + '.svg';
|
||||||
}
|
}
|
||||||
acc.unshift(blocks[1][0]);
|
acc.unshift(blocks[1][0]);
|
||||||
acc = acc.slice(0, this.widget ? 6 : 15);
|
acc = acc.slice(0, this.widget ? 6 : 15);
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { ChangeDetectionStrategy, Component, HostListener, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||||
import { combineLatest, Observable, timer } from 'rxjs';
|
import { combineLatest, Observable, timer } from 'rxjs';
|
||||||
import { map, switchMap } from 'rxjs/operators';
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
import { StateService } from '../..//services/state.service';
|
import { StateService } from '../..//services/state.service';
|
||||||
@ -61,6 +61,7 @@ export class DifficultyComponent implements OnInit {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public stateService: StateService,
|
public stateService: StateService,
|
||||||
|
private cd: ChangeDetectorRef,
|
||||||
@Inject(LOCALE_ID) private locale: string,
|
@Inject(LOCALE_ID) private locale: string,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
@ -189,9 +190,15 @@ export class DifficultyComponent implements OnInit {
|
|||||||
return shapes;
|
return shapes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@HostListener('pointerdown', ['$event'])
|
||||||
|
onPointerDown(event) {
|
||||||
|
this.onPointerMove(event);
|
||||||
|
}
|
||||||
|
|
||||||
@HostListener('pointermove', ['$event'])
|
@HostListener('pointermove', ['$event'])
|
||||||
onPointerMove(event) {
|
onPointerMove(event) {
|
||||||
this.tooltipPosition = { x: event.clientX, y: event.clientY };
|
this.tooltipPosition = { x: event.clientX, y: event.clientY };
|
||||||
|
this.cd.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
onHover(event, rect): void {
|
onHover(event, rect): void {
|
||||||
|
|||||||
@ -74,14 +74,14 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr
|
|||||||
this.labelInterval = this.numSamples / this.numLabels;
|
this.labelInterval = this.numSamples / this.numLabels;
|
||||||
while (nextSample <= maxBlockVSize) {
|
while (nextSample <= maxBlockVSize) {
|
||||||
if (txIndex >= txs.length) {
|
if (txIndex >= txs.length) {
|
||||||
samples.push([(1 - (sampleIndex / this.numSamples)) * 100, 0]);
|
samples.push([(1 - (sampleIndex / this.numSamples)) * 100, 0.000001]);
|
||||||
nextSample += sampleInterval;
|
nextSample += sampleInterval;
|
||||||
sampleIndex++;
|
sampleIndex++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (txs[txIndex] && nextSample < cumVSize + txs[txIndex].vsize) {
|
while (txs[txIndex] && nextSample < cumVSize + txs[txIndex].vsize) {
|
||||||
samples.push([(1 - (sampleIndex / this.numSamples)) * 100, txs[txIndex].rate]);
|
samples.push([(1 - (sampleIndex / this.numSamples)) * 100, txs[txIndex].rate || 0.000001]);
|
||||||
nextSample += sampleInterval;
|
nextSample += sampleInterval;
|
||||||
sampleIndex++;
|
sampleIndex++;
|
||||||
}
|
}
|
||||||
@ -118,7 +118,9 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: 'value',
|
type: 'log',
|
||||||
|
min: 1,
|
||||||
|
max: this.data.reduce((min, val) => Math.max(min, val[1]), 1),
|
||||||
// name: 'Effective Fee Rate s/vb',
|
// name: 'Effective Fee Rate s/vb',
|
||||||
// nameLocation: 'middle',
|
// nameLocation: 'middle',
|
||||||
splitLine: {
|
splitLine: {
|
||||||
@ -129,12 +131,16 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
|
show: true,
|
||||||
formatter: (value: number): string => {
|
formatter: (value: number): string => {
|
||||||
const unitValue = this.weightMode ? value / 4 : value;
|
const unitValue = this.weightMode ? value / 4 : value;
|
||||||
const selectedPowerOfTen = selectPowerOfTen(unitValue);
|
const selectedPowerOfTen = selectPowerOfTen(unitValue);
|
||||||
const newVal = Math.round(unitValue / selectedPowerOfTen.divider);
|
const newVal = Math.round(unitValue / selectedPowerOfTen.divider);
|
||||||
return `${newVal}${selectedPowerOfTen.unit}`;
|
return `${newVal}${selectedPowerOfTen.unit}`;
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
series: [{
|
series: [{
|
||||||
|
|||||||
@ -25,7 +25,8 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0px 15px;
|
padding: 0px 15px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100vh - 250px);
|
height: calc(100vh - 225px);
|
||||||
|
min-height: 400px;
|
||||||
@media (min-width: 992px) {
|
@media (min-width: 992px) {
|
||||||
height: calc(100vh - 150px);
|
height: calc(100vh - 150px);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,7 +25,8 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0px 15px;
|
padding: 0px 15px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100vh - 250px);
|
height: calc(100vh - 225px);
|
||||||
|
min-height: 400px;
|
||||||
@media (min-width: 992px) {
|
@media (min-width: 992px) {
|
||||||
height: calc(100vh - 150px);
|
height: calc(100vh - 150px);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -64,7 +64,9 @@ li.nav-item {
|
|||||||
|
|
||||||
|
|
||||||
.navbar-collapse {
|
.navbar-collapse {
|
||||||
flex-basis: auto;
|
@media (min-width: 564px) {
|
||||||
|
flex-basis: auto;
|
||||||
|
}
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -31,6 +31,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
@Input() getHref?: (index) => string = (index) => `/mempool-block/${index}`;
|
@Input() getHref?: (index) => string = (index) => `/mempool-block/${index}`;
|
||||||
@Input() allBlocks: boolean = false;
|
@Input() allBlocks: boolean = false;
|
||||||
|
|
||||||
|
mempoolWidth: number = 0;
|
||||||
@Output() widthChange: EventEmitter<number> = new EventEmitter();
|
@Output() widthChange: EventEmitter<number> = new EventEmitter();
|
||||||
|
|
||||||
specialBlocks = specialBlocks;
|
specialBlocks = specialBlocks;
|
||||||
@ -49,6 +50,8 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
blockSubscription: Subscription;
|
blockSubscription: Subscription;
|
||||||
networkSubscription: Subscription;
|
networkSubscription: Subscription;
|
||||||
chainTipSubscription: Subscription;
|
chainTipSubscription: Subscription;
|
||||||
|
keySubscription: Subscription;
|
||||||
|
isTabHiddenSubscription: Subscription;
|
||||||
network = '';
|
network = '';
|
||||||
now = new Date().getTime();
|
now = new Date().getTime();
|
||||||
timeOffset = 0;
|
timeOffset = 0;
|
||||||
@ -115,8 +118,15 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.calculateTransactionPosition();
|
this.calculateTransactionPosition();
|
||||||
});
|
});
|
||||||
this.reduceMempoolBlocksToFitScreen(this.mempoolBlocks);
|
this.reduceMempoolBlocksToFitScreen(this.mempoolBlocks);
|
||||||
this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden);
|
this.isTabHiddenSubscription = 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(
|
this.mempoolBlocks$ = merge(
|
||||||
of(true),
|
of(true),
|
||||||
@ -155,7 +165,11 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
}),
|
}),
|
||||||
tap(() => {
|
tap(() => {
|
||||||
this.cd.markForCheck();
|
this.cd.markForCheck();
|
||||||
this.widthChange.emit(this.containerOffset + this.mempoolBlocks.length * this.blockOffset);
|
const width = this.containerOffset + this.mempoolBlocks.length * this.blockOffset;
|
||||||
|
if (this.mempoolWidth !== width) {
|
||||||
|
this.mempoolWidth = width;
|
||||||
|
this.widthChange.emit(this.mempoolWidth);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -212,7 +226,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.networkSubscription = this.stateService.networkChanged$
|
this.networkSubscription = this.stateService.networkChanged$
|
||||||
.subscribe((network) => this.network = network);
|
.subscribe((network) => this.network = network);
|
||||||
|
|
||||||
this.stateService.keyNavigation$.subscribe((event) => {
|
this.keySubscription = this.stateService.keyNavigation$.subscribe((event) => {
|
||||||
if (this.markIndex === undefined) {
|
if (this.markIndex === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -223,13 +237,12 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
if (this.mempoolBlocks[this.markIndex - 1]) {
|
if (this.mempoolBlocks[this.markIndex - 1]) {
|
||||||
this.router.navigate([this.relativeUrlPipe.transform('mempool-block/'), this.markIndex - 1]);
|
this.router.navigate([this.relativeUrlPipe.transform('mempool-block/'), this.markIndex - 1]);
|
||||||
} else {
|
} else {
|
||||||
this.stateService.blocks$
|
const blocks = this.stateService.blocksSubject$.getValue();
|
||||||
.pipe(map((blocks) => blocks[0]))
|
for (const block of (blocks || [])) {
|
||||||
.subscribe((block) => {
|
if (this.stateService.latestBlockHeight === block.height) {
|
||||||
if (this.stateService.latestBlockHeight === block.height) {
|
this.router.navigate([this.relativeUrlPipe.transform('/block/'), block.id], { state: { data: { block } }});
|
||||||
this.router.navigate([this.relativeUrlPipe.transform('/block/'), block.id], { state: { data: { block } }});
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else if (event.key === nextKey) {
|
} else if (event.key === nextKey) {
|
||||||
if (this.mempoolBlocks[this.markIndex + 1]) {
|
if (this.mempoolBlocks[this.markIndex + 1]) {
|
||||||
@ -253,6 +266,8 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.networkSubscription.unsubscribe();
|
this.networkSubscription.unsubscribe();
|
||||||
this.timeLtrSubscription.unsubscribe();
|
this.timeLtrSubscription.unsubscribe();
|
||||||
this.chainTipSubscription.unsubscribe();
|
this.chainTipSubscription.unsubscribe();
|
||||||
|
this.keySubscription.unsubscribe();
|
||||||
|
this.isTabHiddenSubscription.unsubscribe();
|
||||||
clearTimeout(this.resetTransitionTimeout);
|
clearTimeout(this.resetTransitionTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -51,7 +51,7 @@
|
|||||||
<a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]">
|
<a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]">
|
||||||
<h5 class="card-title d-inline" i18n="dashboard.latest-blocks">Latest blocks</h5>
|
<h5 class="card-title d-inline" i18n="dashboard.latest-blocks">Latest blocks</h5>
|
||||||
<span> </span>
|
<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>
|
</a>
|
||||||
<app-blocks-list [attr.data-cy]="'latest-blocks'" [widget]=true></app-blocks-list>
|
<app-blocks-list [attr.data-cy]="'latest-blocks'" [widget]=true></app-blocks-list>
|
||||||
</div>
|
</div>
|
||||||
@ -65,7 +65,7 @@
|
|||||||
<a class="title-link" href="" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]">
|
<a class="title-link" href="" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]">
|
||||||
<h5 class="card-title d-inline" i18n="dashboard.adjustments">Adjustments</h5>
|
<h5 class="card-title d-inline" i18n="dashboard.adjustments">Adjustments</h5>
|
||||||
<span> </span>
|
<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>
|
</a>
|
||||||
<app-difficulty-adjustments-table [attr.data-cy]="'difficulty-adjustments-table'"></app-difficulty-adjustments-table>
|
<app-difficulty-adjustments-table [attr.data-cy]="'difficulty-adjustments-table'"></app-difficulty-adjustments-table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
import { AfterViewInit, ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '../../services/seo.service';
|
||||||
import { WebsocketService } from '../../services/websocket.service';
|
import { WebsocketService } from '../../services/websocket.service';
|
||||||
|
import { StateService } from '../../services/state.service';
|
||||||
|
import { EventType, NavigationStart, Router } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-mining-dashboard',
|
selector: 'app-mining-dashboard',
|
||||||
@ -8,10 +10,12 @@ import { WebsocketService } from '../../services/websocket.service';
|
|||||||
styleUrls: ['./mining-dashboard.component.scss'],
|
styleUrls: ['./mining-dashboard.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class MiningDashboardComponent implements OnInit {
|
export class MiningDashboardComponent implements OnInit, AfterViewInit {
|
||||||
constructor(
|
constructor(
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
private websocketService: WebsocketService,
|
private websocketService: WebsocketService,
|
||||||
|
private stateService: StateService,
|
||||||
|
private router: Router
|
||||||
) {
|
) {
|
||||||
this.seoService.setTitle($localize`:@@a681a4e2011bb28157689dbaa387de0dd0aa0c11:Mining Dashboard`);
|
this.seoService.setTitle($localize`:@@a681a4e2011bb28157689dbaa387de0dd0aa0c11:Mining Dashboard`);
|
||||||
}
|
}
|
||||||
@ -19,4 +23,15 @@ export class MiningDashboardComponent implements OnInit {
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.websocketService.want(['blocks', 'mempool-blocks', 'stats']);
|
this.websocketService.want(['blocks', 'mempool-blocks', 'stats']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
this.stateService.focusSearchInputDesktop();
|
||||||
|
this.router.events.subscribe((e: NavigationStart) => {
|
||||||
|
if (e.type === EventType.NavigationStart) {
|
||||||
|
if (e.url.indexOf('graphs') === -1) { // The mining dashboard and the graph component are part of the same module so we can't use ngAfterViewInit in graphs.component.ts to blur the input
|
||||||
|
this.stateService.focusSearchInputDesktop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -139,6 +139,8 @@
|
|||||||
<td class="" *ngIf="this.miningWindowPreference === '24h'"><b>{{ miningStats.lastEstimatedHashrate}} {{
|
<td class="" *ngIf="this.miningWindowPreference === '24h'"><b>{{ miningStats.lastEstimatedHashrate}} {{
|
||||||
miningStats.miningUnits.hashrateUnit }}</b></td>
|
miningStats.miningUnits.hashrateUnit }}</b></td>
|
||||||
<td class=""><b>{{ miningStats.blockCount }}</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
|
<td class="d-none d-md-table-cell"><b>{{ miningStats.totalEmptyBlock }} ({{ miningStats.totalEmptyBlockRatio
|
||||||
}}%)</b></td>
|
}}%)</b></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@ -89,7 +89,7 @@ export class PoolPreviewComponent implements OnInit {
|
|||||||
|
|
||||||
this.openGraphService.waitOver('pool-stats-' + this.slug);
|
this.openGraphService.waitOver('pool-stats-' + this.slug);
|
||||||
|
|
||||||
const logoSrc = `/resources/mining-pools/` + poolStats.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
|
const logoSrc = `/resources/mining-pools/` + poolStats.pool.slug + '.svg';
|
||||||
if (logoSrc === this.lastImgSrc) {
|
if (logoSrc === this.lastImgSrc) {
|
||||||
this.openGraphService.waitOver('pool-img-' + this.slug);
|
this.openGraphService.waitOver('pool-img-' + this.slug);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -79,7 +79,7 @@ export class PoolComponent implements OnInit {
|
|||||||
poolStats.pool.regexes = regexes.slice(0, -3);
|
poolStats.pool.regexes = regexes.slice(0, -3);
|
||||||
|
|
||||||
return Object.assign({
|
return Object.assign({
|
||||||
logo: `/resources/mining-pools/` + poolStats.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'
|
logo: `/resources/mining-pools/` + poolStats.pool.slug + '.svg'
|
||||||
}, poolStats);
|
}, poolStats);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@ -43,7 +43,7 @@
|
|||||||
|
|
||||||
<h4>TRUST YOUR OWN SELF-HOSTED MEMPOOL EXPLORER</h4>
|
<h4>TRUST YOUR OWN SELF-HOSTED MEMPOOL EXPLORER</h4>
|
||||||
|
|
||||||
<p>For maximum privacy, we recommend that you use your own self-hosted instance of The Mempool Open Source Project™ on your own hardware. You can easily install your own self-hosted instance of this website on a Raspberry Pi using a one-click installation method maintained by various Bitcoin fullnode distributions such as Umbrel, RaspiBlitz, MyNode, and RoninDojo. See our project's GitHub page for more details about self-hosting this website.</p>
|
<p>For maximum privacy, we recommend that you use your own self-hosted instance of The Mempool Open Source Project® on your own hardware. You can easily install your own self-hosted instance of this website on a Raspberry Pi using a one-click installation method maintained by various Bitcoin fullnode distributions such as Umbrel, RaspiBlitz, MyNode, and RoninDojo. See our project's GitHub page for more details about self-hosting this website.</p>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<form [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate>
|
<form [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate>
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<div class="search-box-container mr-2">
|
<div class="search-box-container mr-2">
|
||||||
<input autofocus (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="Explore the full Bitcoin ecosystem">
|
<input #searchInput (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="Explore the full Bitcoin ecosystem">
|
||||||
<app-search-results #searchResults [hidden]="dropdownHidden" [results]="typeAhead$ | async" (selectedResult)="selectedResult($event)"></app-search-results>
|
<app-search-results #searchResults [hidden]="dropdownHidden" [results]="typeAhead$ | async" (selectedResult)="selectedResult($event)"></app-search-results>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -18,9 +18,10 @@
|
|||||||
|
|
||||||
form {
|
form {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
@media (min-width: 576px) {
|
@media (min-width: 564px) {
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
margin-left: 8px;
|
margin-left: 5px;
|
||||||
|
margin-right: -5px;
|
||||||
}
|
}
|
||||||
@media (min-width: 992px) {
|
@media (min-width: 992px) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener, ElementRef } from '@angular/core';
|
import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener, ElementRef } from '@angular/core';
|
||||||
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
|
||||||
import { Router } from '@angular/router';
|
import { EventType, NavigationStart, Router } from '@angular/router';
|
||||||
import { AssetsService } from '../../services/assets.service';
|
import { AssetsService } from '../../services/assets.service';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { Observable, of, Subject, zip, BehaviorSubject, combineLatest } from 'rxjs';
|
import { Observable, of, Subject, zip, BehaviorSubject, combineLatest } from 'rxjs';
|
||||||
@ -34,7 +34,7 @@ export class SearchFormComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59})$/;
|
regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64})$/;
|
||||||
regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/;
|
regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/;
|
||||||
regexTransaction = /^([a-fA-F0-9]{64})(:\d+)?$/;
|
regexTransaction = /^([a-fA-F0-9]{64})(:\d+)?$/;
|
||||||
regexBlockheight = /^[0-9]{1,9}$/;
|
regexBlockheight = /^[0-9]{1,9}$/;
|
||||||
@ -47,6 +47,8 @@ export class SearchFormComponent implements OnInit {
|
|||||||
this.handleKeyDown($event);
|
this.handleKeyDown($event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewChild('searchInput') searchInput: ElementRef;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private formBuilder: UntypedFormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
@ -55,12 +57,27 @@ export class SearchFormComponent implements OnInit {
|
|||||||
private electrsApiService: ElectrsApiService,
|
private electrsApiService: ElectrsApiService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private relativeUrlPipe: RelativeUrlPipe,
|
private relativeUrlPipe: RelativeUrlPipe,
|
||||||
private elementRef: ElementRef,
|
private elementRef: ElementRef
|
||||||
) { }
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||||
|
|
||||||
|
this.router.events.subscribe((e: NavigationStart) => { // Reset search focus when changing page
|
||||||
|
if (this.searchInput && e.type === EventType.NavigationStart) {
|
||||||
|
this.searchInput.nativeElement.blur();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.stateService.searchFocus$.subscribe(() => {
|
||||||
|
if (!this.searchInput) { // Try again a bit later once the view is properly initialized
|
||||||
|
setTimeout(() => this.searchInput.nativeElement.focus(), 100);
|
||||||
|
} else if (this.searchInput) {
|
||||||
|
this.searchInput.nativeElement.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.searchForm = this.formBuilder.group({
|
this.searchForm = this.formBuilder.group({
|
||||||
searchText: ['', Validators.required],
|
searchText: ['', Validators.required],
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Component, ElementRef, HostListener, OnInit, OnDestroy, ViewChild, Input } from '@angular/core';
|
import { Component, ElementRef, HostListener, OnInit, OnDestroy, ViewChild, Input, DoCheck } from '@angular/core';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { MarkBlockState, StateService } from '../../services/state.service';
|
import { MarkBlockState, StateService } from '../../services/state.service';
|
||||||
import { specialBlocks } from '../../app.constants';
|
import { specialBlocks } from '../../app.constants';
|
||||||
@ -9,7 +9,7 @@ import { BlockExtended } from '../../interfaces/node-api.interface';
|
|||||||
templateUrl: './start.component.html',
|
templateUrl: './start.component.html',
|
||||||
styleUrls: ['./start.component.scss'],
|
styleUrls: ['./start.component.scss'],
|
||||||
})
|
})
|
||||||
export class StartComponent implements OnInit, OnDestroy {
|
export class StartComponent implements OnInit, OnDestroy, DoCheck {
|
||||||
@Input() showLoadingIndicator = false;
|
@Input() showLoadingIndicator = false;
|
||||||
|
|
||||||
interval = 60;
|
interval = 60;
|
||||||
@ -43,6 +43,7 @@ export class StartComponent implements OnInit, OnDestroy {
|
|||||||
pageIndex: number = 0;
|
pageIndex: number = 0;
|
||||||
pages: any[] = [];
|
pages: any[] = [];
|
||||||
pendingMark: number | null = null;
|
pendingMark: number | null = null;
|
||||||
|
pendingOffset: number | null = null;
|
||||||
lastUpdate: number = 0;
|
lastUpdate: number = 0;
|
||||||
lastMouseX: number;
|
lastMouseX: number;
|
||||||
velocity: number = 0;
|
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);
|
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() {
|
ngOnInit() {
|
||||||
this.firstPageWidth = 40 + (this.blockWidth * this.dynamicBlocksAmount);
|
this.firstPageWidth = 40 + (this.blockWidth * this.dynamicBlocksAmount);
|
||||||
this.blockCounterSubscription = this.stateService.blocks$.subscribe((blocks) => {
|
this.blockCounterSubscription = this.stateService.blocks$.subscribe((blocks) => {
|
||||||
@ -429,6 +438,7 @@ export class StartComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
addConvertedScrollOffset(offset: number): void {
|
addConvertedScrollOffset(offset: number): void {
|
||||||
if (!this.blockchainContainer?.nativeElement) {
|
if (!this.blockchainContainer?.nativeElement) {
|
||||||
|
this.pendingOffset = offset;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.timeLtr) {
|
if (this.timeLtr) {
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<div *ngIf="officialMempoolSpace">
|
<div *ngIf="officialMempoolSpace">
|
||||||
<h2>Trademark Policy and Guidelines</h2>
|
<h2>Trademark Policy and Guidelines</h2>
|
||||||
<h5>The Mempool Open Source Project ™</h5>
|
<h5>The Mempool Open Source Project ®</h5>
|
||||||
<h6>Updated: July 19, 2021</h6>
|
<h6>Updated: July 19, 2021</h6>
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
@ -304,7 +304,7 @@
|
|||||||
|
|
||||||
<p>Also, if you are using our Marks in a way described in the sections "Uses for Which We Are Granting a License," you must include the following trademark attribution at the foot of the webpage where you have used the Mark (or, if in a book, on the credits page), on any packaging or labeling, and on advertising or marketing materials:</p>
|
<p>Also, if you are using our Marks in a way described in the sections "Uses for Which We Are Granting a License," you must include the following trademark attribution at the foot of the webpage where you have used the Mark (or, if in a book, on the credits page), on any packaging or labeling, and on advertising or marketing materials:</p>
|
||||||
|
|
||||||
<p>“The Mempool Space K.K.™, The Mempool Open Source Project™, mempool.space™, the mempool logo®, the mempool.space logos™, the mempool square logo®, and the mempool blocks logo™ are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein.”</p>
|
<p>“The Mempool Space K.K.™, The Mempool Open Source Project®, mempool.space™, the mempool logo®, the mempool.space logos™, the mempool square logo®, and the mempool blocks logo™ are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein.”</p>
|
||||||
|
|
||||||
<li>What to Do When You See Abuse</li>
|
<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">
|
<ng-template ngFor let-vin let-vindex="index" [ngForOf]="tx.vin.slice(0, getVinLimit(tx))" [ngForTrackBy]="trackByIndexFn">
|
||||||
<tr [ngClass]="{
|
<tr [ngClass]="{
|
||||||
'assetBox': (assetsMinimal && vin.prevout && assetsMinimal[vin.prevout.asset] && !vin.is_coinbase && vin.prevout.scriptpubkey_address && tx._unblinded) || inputIndex === vindex,
|
'assetBox': (assetsMinimal && vin.prevout && assetsMinimal[vin.prevout.asset] && !vin.is_coinbase && vin.prevout.scriptpubkey_address && tx._unblinded) || inputIndex === vindex,
|
||||||
'highlight': vin.prevout?.scriptpubkey_address === this.address && this.address !== ''
|
'highlight': this.address !== '' && (vin.prevout?.scriptpubkey_address === this.address || (vin.prevout?.scriptpubkey_type === 'p2pk' && vin.prevout?.scriptpubkey.slice(2, -2) === this.address))
|
||||||
}">
|
}">
|
||||||
<td class="arrow-td">
|
<td class="arrow-td">
|
||||||
<ng-template [ngIf]="vin.prevout === null && !vin.is_pegin" [ngIfElse]="hasPrevout">
|
<ng-template [ngIf]="vin.prevout === null && !vin.is_pegin" [ngIfElse]="hasPrevout">
|
||||||
@ -56,7 +56,9 @@
|
|||||||
<span i18n="transactions-list.peg-in">Peg-in</span>
|
<span i18n="transactions-list.peg-in">Peg-in</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngSwitchCase="vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk'">
|
<ng-container *ngSwitchCase="vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk'">
|
||||||
<span>P2PK</span>
|
<span>P2PK <a class="address p2pk-address" [routerLink]="['/address/' | relativeUrl, vin.prevout.scriptpubkey.slice(2, -2)]" title="{{ vin.prevout.scriptpubkey.slice(2, -2) }}">
|
||||||
|
<app-truncate [text]="vin.prevout.scriptpubkey.slice(2, -2)" [lastChars]="8"></app-truncate>
|
||||||
|
</a></span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngSwitchDefault>
|
<ng-container *ngSwitchDefault>
|
||||||
<ng-template [ngIf]="!vin.prevout" [ngIfElse]="defaultAddress">
|
<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">
|
<ng-template ngFor let-vout let-vindex="index" [ngForOf]="tx.vout.slice(0, getVoutLimit(tx))" [ngForTrackBy]="trackByIndexFn">
|
||||||
<tr [ngClass]="{
|
<tr [ngClass]="{
|
||||||
'assetBox': assetsMinimal && assetsMinimal[vout.asset] && vout.scriptpubkey_address && tx.vin && !tx.vin[0].is_coinbase && tx._unblinded || outputIndex === vindex,
|
'assetBox': assetsMinimal && assetsMinimal[vout.asset] && vout.scriptpubkey_address && tx.vin && !tx.vin[0].is_coinbase && tx._unblinded || outputIndex === vindex,
|
||||||
'highlight': vout.scriptpubkey_address === this.address && this.address !== ''
|
'highlight': this.address !== '' && (vout.scriptpubkey_address === this.address || (vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey.slice(2, -2) === this.address))
|
||||||
}">
|
}">
|
||||||
<td class="address-cell">
|
<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>
|
<app-truncate [text]="vout.scriptpubkey_address" [lastChars]="8"></app-truncate>
|
||||||
</a>
|
</a>
|
||||||
|
<ng-template #pubkey_type>
|
||||||
|
<ng-container *ngIf="vout.scriptpubkey_type === 'p2pk'; else scriptpubkey_type">
|
||||||
|
P2PK <a class="address p2pk-address" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey.slice(2, -2)]" title="{{ vout.scriptpubkey.slice(2, -2) }}">
|
||||||
|
<app-truncate [text]="vout.scriptpubkey.slice(2, -2)" [lastChars]="8"></app-truncate>
|
||||||
|
</a>
|
||||||
|
</ng-container>
|
||||||
|
</ng-template>
|
||||||
<div>
|
<div>
|
||||||
<app-address-labels [vout]="vout" [channel]="tx._channels && tx._channels.outputs[vindex] ? tx._channels.outputs[vindex] : null"></app-address-labels>
|
<app-address-labels [vout]="vout" [channel]="tx._channels && tx._channels.outputs[vindex] ? tx._channels.outputs[vindex] : null"></app-address-labels>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -149,6 +149,15 @@ h2 {
|
|||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.p2pk-address {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 1em;
|
||||||
|
max-width: 100px;
|
||||||
|
@media (min-width: 576px) {
|
||||||
|
max-width: 200px
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.grey-info-text {
|
.grey-info-text {
|
||||||
color:#6c757d;
|
color:#6c757d;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
|||||||
@ -78,7 +78,7 @@
|
|||||||
<a class="title-link" href="" [routerLink]="['/rbf' | relativeUrl]">
|
<a class="title-link" href="" [routerLink]="['/rbf' | relativeUrl]">
|
||||||
<h5 class="card-title d-inline" i18n="dashboard.latest-rbf-replacements">Latest replacements</h5>
|
<h5 class="card-title d-inline" i18n="dashboard.latest-rbf-replacements">Latest replacements</h5>
|
||||||
<span> </span>
|
<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>
|
</a>
|
||||||
<table class="table lastest-replacements-table">
|
<table class="table lastest-replacements-table">
|
||||||
<thead>
|
<thead>
|
||||||
@ -112,7 +112,7 @@
|
|||||||
<a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]">
|
<a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]">
|
||||||
<h5 class="card-title d-inline" i18n="dashboard.latest-blocks">Latest blocks</h5>
|
<h5 class="card-title d-inline" i18n="dashboard.latest-blocks">Latest blocks</h5>
|
||||||
<span> </span>
|
<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>
|
</a>
|
||||||
<table class="table lastest-blocks-table">
|
<table class="table lastest-blocks-table">
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { combineLatest, merge, Observable, of, Subscription } from 'rxjs';
|
import { combineLatest, merge, Observable, of, Subscription } from 'rxjs';
|
||||||
import { filter, map, scan, share, switchMap, tap } from 'rxjs/operators';
|
import { catchError, filter, map, scan, share, switchMap, tap } from 'rxjs/operators';
|
||||||
import { BlockExtended, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
|
import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface';
|
||||||
import { MempoolInfo, TransactionStripped, ReplacementInfo } from '../interfaces/websocket.interface';
|
import { MempoolInfo, TransactionStripped, ReplacementInfo } from '../interfaces/websocket.interface';
|
||||||
import { ApiService } from '../services/api.service';
|
import { ApiService } from '../services/api.service';
|
||||||
import { StateService } from '../services/state.service';
|
import { StateService } from '../services/state.service';
|
||||||
@ -31,7 +31,7 @@ interface MempoolStatsData {
|
|||||||
styleUrls: ['./dashboard.component.scss'],
|
styleUrls: ['./dashboard.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class DashboardComponent implements OnInit, OnDestroy {
|
export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||||
featuredAssets$: Observable<any>;
|
featuredAssets$: Observable<any>;
|
||||||
network$: Observable<string>;
|
network$: Observable<string>;
|
||||||
mempoolBlocksData$: Observable<MempoolBlocksData>;
|
mempoolBlocksData$: Observable<MempoolBlocksData>;
|
||||||
@ -57,6 +57,10 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
|||||||
private seoService: SeoService
|
private seoService: SeoService
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
this.stateService.focusSearchInputDesktop();
|
||||||
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.currencySubscription.unsubscribe();
|
this.currencySubscription.unsubscribe();
|
||||||
this.websocketService.stopTrackRbfSummary();
|
this.websocketService.stopTrackRbfSummary();
|
||||||
@ -155,7 +159,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
|||||||
for (const block of blocks) {
|
for (const block of blocks) {
|
||||||
// @ts-ignore: Need to add an extra field for the template
|
// @ts-ignore: Need to add an extra field for the template
|
||||||
block.extras.pool.logo = `/resources/mining-pools/` +
|
block.extras.pool.logo = `/resources/mining-pools/` +
|
||||||
block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
|
block.extras.pool.slug + '.svg';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return of(blocks.slice(0, 6));
|
return of(blocks.slice(0, 6));
|
||||||
@ -167,7 +171,11 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
|||||||
this.mempoolStats$ = this.stateService.connectionState$
|
this.mempoolStats$ = this.stateService.connectionState$
|
||||||
.pipe(
|
.pipe(
|
||||||
filter((state) => state === 2),
|
filter((state) => state === 2),
|
||||||
switchMap(() => this.apiService.list2HStatistics$()),
|
switchMap(() => this.apiService.list2HStatistics$().pipe(
|
||||||
|
catchError((e) => {
|
||||||
|
return of(null);
|
||||||
|
})
|
||||||
|
)),
|
||||||
switchMap((mempoolStats) => {
|
switchMap((mempoolStats) => {
|
||||||
return merge(
|
return merge(
|
||||||
this.stateService.live2Chart$
|
this.stateService.live2Chart$
|
||||||
@ -182,10 +190,14 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
map((mempoolStats) => {
|
map((mempoolStats) => {
|
||||||
return {
|
if (mempoolStats) {
|
||||||
mempool: mempoolStats,
|
return {
|
||||||
weightPerSecond: this.handleNewMempoolData(mempoolStats.concat([])),
|
mempool: mempoolStats,
|
||||||
};
|
weightPerSecond: this.handleNewMempoolData(mempoolStats.concat([])),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
share(),
|
share(),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -10,8 +10,8 @@
|
|||||||
<div class="doc-content">
|
<div class="doc-content">
|
||||||
|
|
||||||
<div id="disclaimer">
|
<div id="disclaimer">
|
||||||
<table *ngIf="!mobileViewport"><tr><td><app-svg-images name="warning" class="disclaimer-warning" viewBox="0 0 304 304" fill="#ffc107" width="50" height="50"></app-svg-images></td><td><p i18n="faq.big-disclaimer"><b>mempool.space merely provides data about the Bitcoin network.</b> It cannot help you with retrieving funds, confirming your transaction quicker, etc.</p><p>For any such requests, you need to get in touch with the entity that helped make the transaction (wallet software, exchange company, etc).</p></td></tr></table>
|
<table *ngIf="!mobileViewport"><tr><td><app-svg-images name="warning" class="disclaimer-warning" viewBox="0 0 304 304" fill="#ffc107" width="50" height="50"></app-svg-images></td><td><p i18n="faq.big-disclaimer"><b>mempool.space merely provides data about the Bitcoin network.</b> It cannot help you with retrieving funds, wallet issues, etc.</p><p>For any such requests, you need to get in touch with the entity that helped make the transaction (wallet software, exchange company, etc).</p></td></tr></table>
|
||||||
<div *ngIf="mobileViewport"><app-svg-images name="warning" class="disclaimer-warning" viewBox="0 0 304 304" fill="#ffc107" width="50" height="50"></app-svg-images><p i18n="faq.big-disclaimer"><b>mempool.space merely provides data about the Bitcoin network.</b> It cannot help you with retrieving funds, confirming your transaction quicker, etc.</p><p>For any such requests, you need to get in touch with the entity that helped make the transaction (wallet software, exchange company, etc).</p></div>
|
<div *ngIf="mobileViewport"><app-svg-images name="warning" class="disclaimer-warning" viewBox="0 0 304 304" fill="#ffc107" width="50" height="50"></app-svg-images><p i18n="faq.big-disclaimer"><b>mempool.space merely provides data about the Bitcoin network.</b> It cannot help you with retrieving funds, wallet issues, etc.</p><p>For any such requests, you need to get in touch with the entity that helped make the transaction (wallet software, exchange company, etc).</p></div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -129,6 +129,22 @@ export interface Address {
|
|||||||
address: string;
|
address: string;
|
||||||
chain_stats: ChainStats;
|
chain_stats: ChainStats;
|
||||||
mempool_stats: MempoolStats;
|
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 {
|
export interface ChainStats {
|
||||||
|
|||||||
@ -110,6 +110,7 @@ export interface PoolInfo {
|
|||||||
regexes: string; // JSON array
|
regexes: string; // JSON array
|
||||||
addresses: string; // JSON array
|
addresses: string; // JSON array
|
||||||
emptyBlocks: number;
|
emptyBlocks: number;
|
||||||
|
slug: string;
|
||||||
}
|
}
|
||||||
export interface PoolStat {
|
export interface PoolStat {
|
||||||
pool: PoolInfo;
|
pool: PoolInfo;
|
||||||
@ -174,7 +175,7 @@ export interface TransactionStripped {
|
|||||||
vsize: number;
|
vsize: number;
|
||||||
value: number;
|
value: number;
|
||||||
rate?: number; // effective fee rate
|
rate?: number; // effective fee rate
|
||||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'fullrbf';
|
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf';
|
||||||
context?: 'projected' | 'actual';
|
context?: 'projected' | 'actual';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -89,7 +89,7 @@ export interface TransactionStripped {
|
|||||||
vsize: number;
|
vsize: number;
|
||||||
value: number;
|
value: number;
|
||||||
rate?: number; // effective fee rate
|
rate?: number; // effective fee rate
|
||||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'fullrbf';
|
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf';
|
||||||
context?: 'projected' | 'actual';
|
context?: 'projected' | 'actual';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,19 +1,43 @@
|
|||||||
<div class="box">
|
<div class="box">
|
||||||
<table class="table table-borderless table-striped">
|
<div class="starting-balance" *ngIf="showStartingBalance">
|
||||||
<tbody>
|
<h5 i18n="lightning.starting-balance|Channel starting balance">Starting balance</h5>
|
||||||
<tr></tr>
|
<div class="nodes">
|
||||||
<tr>
|
<h5 class="alias">{{ left.alias }}</h5>
|
||||||
<td i18n="lightning.starting-balance|Channel starting balance">Starting balance</td>
|
<h5 class="alias">{{ right.alias }}</h5>
|
||||||
<td *ngIf="showStartingBalance && minStartingBalance === maxStartingBalance"><app-sats [satoshis]="minStartingBalance"></app-sats></td>
|
</div>
|
||||||
<td *ngIf="showStartingBalance && minStartingBalance !== maxStartingBalance">{{ minStartingBalance | number : '1.0-0' }} - {{ maxStartingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></td>
|
<div class="balances">
|
||||||
<td *ngIf="!showStartingBalance">?</td>
|
<div class="balance left">
|
||||||
</tr>
|
<span class="value" *ngIf="minStartingBalance !== maxStartingBalance">{{ minStartingBalance | number : '1.0-0' }} - {{ maxStartingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></span>
|
||||||
<tr *ngIf="channel.status === 2">
|
<span class="value" *ngIf="minStartingBalance === maxStartingBalance">{{ minStartingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></span>
|
||||||
<td i18n="lightning.closing-balance|Channel closing balance">Closing balance</td>
|
</div>
|
||||||
<td *ngIf="showClosingBalance && minClosingBalance === maxClosingBalance"><app-sats [satoshis]="minClosingBalance"></app-sats></td>
|
<div class="balance right">
|
||||||
<td *ngIf="showClosingBalance && minClosingBalance !== maxClosingBalance">{{ minClosingBalance | number : '1.0-0' }} - {{ maxClosingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></td>
|
<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>
|
||||||
<td *ngIf="!showClosingBalance">?</td>
|
<span class="value" *ngIf="minStartingBalance === maxStartingBalance">{{ channel.capacity - maxStartingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></span>
|
||||||
</tr>
|
</div>
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
<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>
|
</div>
|
||||||
@ -7,3 +7,97 @@
|
|||||||
margin-bottom: 20px;
|
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 {
|
export class ChannelCloseBoxComponent implements OnChanges {
|
||||||
@Input() channel: any;
|
@Input() channel: any;
|
||||||
@Input() local: any;
|
@Input() left: any;
|
||||||
@Input() remote: any;
|
@Input() right: any;
|
||||||
|
|
||||||
showStartingBalance: boolean = false;
|
showStartingBalance: boolean = false;
|
||||||
showClosingBalance: boolean = false;
|
showClosingBalance: boolean = false;
|
||||||
@ -18,29 +18,55 @@ export class ChannelCloseBoxComponent implements OnChanges {
|
|||||||
minClosingBalance: number;
|
minClosingBalance: number;
|
||||||
maxClosingBalance: 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() { }
|
constructor() { }
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
if (this.channel && this.local && this.remote) {
|
let closingCapacity;
|
||||||
this.showStartingBalance = (this.local.funding_balance || this.remote.funding_balance) && this.channel.funding_ratio;
|
if (this.channel && this.left && this.right) {
|
||||||
this.showClosingBalance = this.local.closing_balance || this.remote.closing_balance;
|
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.channel.single_funded) {
|
||||||
if (this.local.funding_balance) {
|
if (this.left.funding_balance) {
|
||||||
this.minStartingBalance = this.channel.capacity;
|
this.minStartingBalance = this.channel.capacity;
|
||||||
this.maxStartingBalance = this.channel.capacity;
|
this.maxStartingBalance = this.channel.capacity;
|
||||||
} else if (this.remote.funding_balance) {
|
} else if (this.right.funding_balance) {
|
||||||
this.minStartingBalance = 0;
|
this.minStartingBalance = 0;
|
||||||
this.maxStartingBalance = 0;
|
this.maxStartingBalance = 0;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.minStartingBalance = clampRound(0, this.channel.capacity, this.local.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.remote.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;
|
closingCapacity = this.channel.capacity - this.channel.closing_fee;
|
||||||
this.minClosingBalance = clampRound(0, closingCapacity, this.local.closing_balance);
|
this.minClosingBalance = clampRound(0, closingCapacity, this.left.closing_balance);
|
||||||
this.maxClosingBalance = clampRound(0, closingCapacity, closingCapacity - this.remote.closing_balance);
|
this.maxClosingBalance = clampRound(0, closingCapacity, closingCapacity - this.right.closing_balance);
|
||||||
|
|
||||||
// margin of error to account for 2 x 330 sat anchor outputs
|
// margin of error to account for 2 x 330 sat anchor outputs
|
||||||
if (Math.abs(this.minClosingBalance - this.maxClosingBalance) <= 660) {
|
if (Math.abs(this.minClosingBalance - this.maxClosingBalance) <= 660) {
|
||||||
@ -50,6 +76,26 @@ export class ChannelCloseBoxComponent implements OnChanges {
|
|||||||
this.showStartingBalance = false;
|
this.showStartingBalance = false;
|
||||||
this.showClosingBalance = 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="row row-cols-1 row-cols-md-2" *ngIf="!error">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<app-channel-box [channel]="channel.node_left"></app-channel-box>
|
<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>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<app-channel-box [channel]="channel.node_right"></app-channel-box>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<app-channel-close-box *ngIf="showCloseBoxes(channel)" [channel]="channel" [left]="channel.node_left" [right]="channel.node_right"></app-channel-close-box>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<ng-container *ngIf="transactions$ | async as transactions">
|
<ng-container *ngIf="transactions$ | async as transactions">
|
||||||
|
|||||||
@ -61,7 +61,7 @@
|
|||||||
<a class="title-link" href="" [routerLink]="['/lightning/nodes/rankings/liquidity' | relativeUrl]">
|
<a class="title-link" href="" [routerLink]="['/lightning/nodes/rankings/liquidity' | relativeUrl]">
|
||||||
<h5 class="card-title d-inline" i18n="lightning.liquidity-ranking">Liquidity Ranking</h5>
|
<h5 class="card-title d-inline" i18n="lightning.liquidity-ranking">Liquidity Ranking</h5>
|
||||||
<span> </span>
|
<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>
|
</a>
|
||||||
<app-top-nodes-per-capacity [nodes$]="nodesRanking$" [widget]="true"></app-top-nodes-per-capacity>
|
<app-top-nodes-per-capacity [nodes$]="nodesRanking$" [widget]="true"></app-top-nodes-per-capacity>
|
||||||
</div>
|
</div>
|
||||||
@ -75,7 +75,7 @@
|
|||||||
<a class="title-link" href="" [routerLink]="['/lightning/nodes/rankings/connectivity' | relativeUrl]">
|
<a class="title-link" href="" [routerLink]="['/lightning/nodes/rankings/connectivity' | relativeUrl]">
|
||||||
<h5 class="card-title d-inline" i18n="lightning.connectivity-ranking">Connectivity Ranking</h5>
|
<h5 class="card-title d-inline" i18n="lightning.connectivity-ranking">Connectivity Ranking</h5>
|
||||||
<span> </span>
|
<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>
|
</a>
|
||||||
<app-top-nodes-per-channels [nodes$]="nodesRanking$" [widget]="true"></app-top-nodes-per-channels>
|
<app-top-nodes-per-channels [nodes$]="nodesRanking$" [widget]="true"></app-top-nodes-per-channels>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
import { AfterViewInit, ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { share } from 'rxjs/operators';
|
import { share } from 'rxjs/operators';
|
||||||
import { INodesRanking } from '../../interfaces/node-api.interface';
|
import { INodesRanking } from '../../interfaces/node-api.interface';
|
||||||
@ -12,7 +12,7 @@ import { LightningApiService } from '../lightning-api.service';
|
|||||||
styleUrls: ['./lightning-dashboard.component.scss'],
|
styleUrls: ['./lightning-dashboard.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class LightningDashboardComponent implements OnInit {
|
export class LightningDashboardComponent implements OnInit, AfterViewInit {
|
||||||
statistics$: Observable<any>;
|
statistics$: Observable<any>;
|
||||||
nodesRanking$: Observable<INodesRanking>;
|
nodesRanking$: Observable<INodesRanking>;
|
||||||
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
|
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
|
||||||
@ -30,4 +30,7 @@ export class LightningDashboardComponent implements OnInit {
|
|||||||
this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share());
|
this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
this.stateService.focusSearchInputDesktop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,8 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0px 15px;
|
padding: 0px 15px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100vh - 250px);
|
height: calc(100vh - 225px);
|
||||||
|
min-height: 400px;
|
||||||
@media (min-width: 992px) {
|
@media (min-width: 992px) {
|
||||||
height: calc(100vh - 150px);
|
height: calc(100vh - 150px);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,7 +25,8 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0px 15px;
|
padding: 0px 15px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100vh - 250px);
|
height: calc(100vh - 225px);
|
||||||
|
min-height: 400px;
|
||||||
@media (min-width: 992px) {
|
@media (min-width: 992px) {
|
||||||
height: calc(100vh - 150px);
|
height: calc(100vh - 150px);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
<h5 class="card-title d-inline" i18n="lightning.liquidity-ranking">Liquidity Ranking</h5>
|
<h5 class="card-title d-inline" i18n="lightning.liquidity-ranking">Liquidity Ranking</h5>
|
||||||
<span> </span>
|
<span> </span>
|
||||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true"
|
<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>
|
</a>
|
||||||
<app-top-nodes-per-capacity [nodes$]="nodesRanking$" [widget]="true"></app-top-nodes-per-capacity>
|
<app-top-nodes-per-capacity [nodes$]="nodesRanking$" [widget]="true"></app-top-nodes-per-capacity>
|
||||||
</div>
|
</div>
|
||||||
@ -22,7 +22,7 @@
|
|||||||
<h5 class="card-title d-inline" i18n="lightning.connectivity-ranking">Connectivity Ranking</h5>
|
<h5 class="card-title d-inline" i18n="lightning.connectivity-ranking">Connectivity Ranking</h5>
|
||||||
<span> </span>
|
<span> </span>
|
||||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true"
|
<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>
|
</a>
|
||||||
<app-top-nodes-per-channels [nodes$]="nodesRanking$" [widget]="true"></app-top-nodes-per-channels>
|
<app-top-nodes-per-channels [nodes$]="nodesRanking$" [widget]="true"></app-top-nodes-per-channels>
|
||||||
</div>
|
</div>
|
||||||
@ -36,7 +36,7 @@
|
|||||||
<h5 class="card-title d-inline" i18n="lightning.top-channels-age">Oldest nodes</h5>
|
<h5 class="card-title d-inline" i18n="lightning.top-channels-age">Oldest nodes</h5>
|
||||||
<span> </span>
|
<span> </span>
|
||||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true"
|
<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>
|
</a>
|
||||||
<app-oldest-nodes [widget]="true"></app-oldest-nodes>
|
<app-oldest-nodes [widget]="true"></app-oldest-nodes>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -25,7 +25,8 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0px 15px;
|
padding: 0px 15px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100vh - 250px);
|
height: calc(100vh - 225px);
|
||||||
|
min-height: 400px;
|
||||||
@media (min-width: 992px) {
|
@media (min-width: 992px) {
|
||||||
height: calc(100vh - 150px);
|
height: calc(100vh - 150px);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, from, of, switchMap } from 'rxjs';
|
||||||
import { Transaction, Address, Outspend, Recent, Asset } from '../interfaces/electrs.interface';
|
import { Transaction, Address, Outspend, Recent, Asset, ScriptHash } from '../interfaces/electrs.interface';
|
||||||
import { StateService } from './state.service';
|
import { StateService } from './state.service';
|
||||||
import { BlockExtended } from '../interfaces/node-api.interface';
|
import { BlockExtended } from '../interfaces/node-api.interface';
|
||||||
|
import { calcScriptHash$ } from '../bitcoin.utils';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@ -65,6 +66,25 @@ export class ElectrsApiService {
|
|||||||
return this.httpClient.get<Address>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address);
|
return this.httpClient.get<Address>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPubKeyAddress$(pubkey: string): Observable<Address> {
|
||||||
|
const scriptpubkey = (pubkey.length === 130 ? '41' : '21') + pubkey + 'ac';
|
||||||
|
return this.getScriptHash$(scriptpubkey).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[]> {
|
getAddressTransactions$(address: string, txid?: string): Observable<Transaction[]> {
|
||||||
let params = new HttpParams();
|
let params = new HttpParams();
|
||||||
if (txid) {
|
if (txid) {
|
||||||
@ -73,6 +93,16 @@ export class ElectrsApiService {
|
|||||||
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs', { params });
|
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> {
|
getAsset$(assetId: string): Observable<Asset> {
|
||||||
return this.httpClient.get<Asset>(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId);
|
return this.httpClient.get<Asset>(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -96,7 +96,7 @@ export class MiningService {
|
|||||||
share: parseFloat((poolStat.blockCount / stats.blockCount * 100).toFixed(2)),
|
share: parseFloat((poolStat.blockCount / stats.blockCount * 100).toFixed(2)),
|
||||||
lastEstimatedHashrate: (poolStat.blockCount / stats.blockCount * stats.lastEstimatedHashrate / hashrateDivider).toFixed(2),
|
lastEstimatedHashrate: (poolStat.blockCount / stats.blockCount * stats.lastEstimatedHashrate / hashrateDivider).toFixed(2),
|
||||||
emptyBlockRatio: (poolStat.emptyBlocks / poolStat.blockCount * 100).toFixed(2),
|
emptyBlockRatio: (poolStat.emptyBlocks / poolStat.blockCount * 100).toFixed(2),
|
||||||
logo: `/resources/mining-pools/` + poolStat.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg',
|
logo: `/resources/mining-pools/` + poolStat.slug + '.svg',
|
||||||
...poolStat
|
...poolStat
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { Router, NavigationStart } from '@angular/router';
|
|||||||
import { isPlatformBrowser } from '@angular/common';
|
import { isPlatformBrowser } from '@angular/common';
|
||||||
import { filter, map, scan, shareReplay } from 'rxjs/operators';
|
import { filter, map, scan, shareReplay } from 'rxjs/operators';
|
||||||
import { StorageService } from './storage.service';
|
import { StorageService } from './storage.service';
|
||||||
|
import { hasTouchScreen } from '../shared/pipes/bytes-pipe/utils';
|
||||||
|
|
||||||
export interface MarkBlockState {
|
export interface MarkBlockState {
|
||||||
blockHeight?: number;
|
blockHeight?: number;
|
||||||
@ -113,6 +114,7 @@ export class StateService {
|
|||||||
mempoolTxPosition$ = new Subject<{ txid: string, position: MempoolPosition}>();
|
mempoolTxPosition$ = new Subject<{ txid: string, position: MempoolPosition}>();
|
||||||
blockTransactions$ = new Subject<Transaction>();
|
blockTransactions$ = new Subject<Transaction>();
|
||||||
isLoadingWebSocket$ = new ReplaySubject<boolean>(1);
|
isLoadingWebSocket$ = new ReplaySubject<boolean>(1);
|
||||||
|
isLoadingMempool$ = new BehaviorSubject<boolean>(true);
|
||||||
vbytesPerSecond$ = new ReplaySubject<number>(1);
|
vbytesPerSecond$ = new ReplaySubject<number>(1);
|
||||||
previousRetarget$ = new ReplaySubject<number>(1);
|
previousRetarget$ = new ReplaySubject<number>(1);
|
||||||
backendInfo$ = new ReplaySubject<IBackendInfo>(1);
|
backendInfo$ = new ReplaySubject<IBackendInfo>(1);
|
||||||
@ -138,6 +140,8 @@ export class StateService {
|
|||||||
fiatCurrency$: BehaviorSubject<string>;
|
fiatCurrency$: BehaviorSubject<string>;
|
||||||
rateUnits$: BehaviorSubject<string>;
|
rateUnits$: BehaviorSubject<string>;
|
||||||
|
|
||||||
|
searchFocus$: Subject<boolean> = new Subject<boolean>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(PLATFORM_ID) private platformId: any,
|
@Inject(PLATFORM_ID) private platformId: any,
|
||||||
@Inject(LOCALE_ID) private locale: string,
|
@Inject(LOCALE_ID) private locale: string,
|
||||||
@ -355,4 +359,10 @@ export class StateService {
|
|||||||
this.blocks = this.blocks.slice(0, this.env.KEEP_BLOCKS_AMOUNT);
|
this.blocks = this.blocks.slice(0, this.env.KEEP_BLOCKS_AMOUNT);
|
||||||
this.blocksSubject$.next(this.blocks);
|
this.blocksSubject$.next(this.blocks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
focusSearchInputDesktop() {
|
||||||
|
if (!hasTouchScreen()) {
|
||||||
|
this.searchFocus$.next(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -113,7 +113,7 @@ export class WebsocketService {
|
|||||||
this.stateService.connectionState$.next(2);
|
this.stateService.connectionState$.next(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.stateService.connectionState$.value === 1) {
|
if (this.stateService.connectionState$.value !== 2) {
|
||||||
this.stateService.connectionState$.next(2);
|
this.stateService.connectionState$.next(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -368,6 +368,11 @@ export class WebsocketService {
|
|||||||
|
|
||||||
if (response.loadingIndicators) {
|
if (response.loadingIndicators) {
|
||||||
this.stateService.loadingIndicators$.next(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) {
|
if (response.mempoolInfo) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user