Compare commits

..

3 Commits

Author SHA1 Message Date
natsoni
428cc9eacd Analytic form for ETA algorithm 2024-09-09 16:03:40 +02:00
natsoni
638acffbad Consider incoming transactions flow into ETA calculation 2024-09-07 23:07:16 +02:00
natsoni
ae9125d316 statistics: class incoming transactions by fee rate 2024-09-07 17:49:21 +02:00
431 changed files with 5035 additions and 13933 deletions

View File

@@ -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: ["20", "22"] node: ["20", "21"]
flavor: ["dev", "prod"] flavor: ["dev", "prod"]
fail-fast: false fail-fast: false
runs-on: "ubuntu-latest" runs-on: "ubuntu-latest"
@@ -160,7 +160,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: ["20", "22"] node: ["20", "21"]
flavor: ["dev", "prod"] flavor: ["dev", "prod"]
fail-fast: false fail-fast: false
runs-on: "ubuntu-latest" runs-on: "ubuntu-latest"
@@ -251,7 +251,17 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
module: ["mempool", "liquid", "testnet4"] module: ["mempool", "liquid"]
include:
- module: "mempool"
spec: |
cypress/e2e/mainnet/*.spec.ts
cypress/e2e/signet/*.spec.ts
cypress/e2e/testnet4/*.spec.ts
- module: "liquid"
spec: |
cypress/e2e/liquid/liquid.spec.ts
cypress/e2e/liquidtestnet/liquidtestnet.spec.ts
name: E2E tests for ${{ matrix.module }} name: E2E tests for ${{ matrix.module }}
steps: steps:
@@ -263,7 +273,7 @@ jobs:
- name: Setup node - name: Setup node
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 22 node-version: 20
cache: "npm" cache: "npm"
cache-dependency-path: ${{ matrix.module }}/frontend/package-lock.json cache-dependency-path: ${{ matrix.module }}/frontend/package-lock.json
@@ -300,10 +310,8 @@ jobs:
- name: Unzip assets before building (src/resources) - name: Unzip assets before building (src/resources)
run: unzip -o promo-video-assets.zip -d ${{ matrix.module }}/frontend/src/resources/promo-video run: unzip -o promo-video-assets.zip -d ${{ matrix.module }}/frontend/src/resources/promo-video
# mempool
- name: Chrome browser tests (${{ matrix.module }}) - name: Chrome browser tests (${{ matrix.module }})
if: ${{ matrix.module == 'mempool' }}
uses: cypress-io/github-action@v5 uses: cypress-io/github-action@v5
with: with:
tag: ${{ github.event_name }} tag: ${{ github.event_name }}
@@ -314,9 +322,7 @@ jobs:
wait-on-timeout: 120 wait-on-timeout: 120
record: true record: true
parallel: true parallel: true
spec: | spec: ${{ matrix.spec }}
cypress/e2e/mainnet/*.spec.ts
cypress/e2e/signet/*.spec.ts
group: Tests on Chrome (${{ matrix.module }}) group: Tests on Chrome (${{ matrix.module }})
browser: "chrome" browser: "chrome"
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}" ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
@@ -326,56 +332,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }} CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
# liquid
- name: Chrome browser tests (${{ matrix.module }})
if: ${{ matrix.module == 'liquid' }}
uses: cypress-io/github-action@v5
with:
tag: ${{ github.event_name }}
working-directory: ${{ matrix.module }}/frontend
build: npm run config:defaults:${{ matrix.module }}
start: npm run start:local-staging
wait-on: "http://localhost:4200"
wait-on-timeout: 120
record: true
parallel: true
spec: |
cypress/e2e/liquid/liquid.spec.ts
cypress/e2e/liquidtestnet/liquidtestnet.spec.ts
group: Tests on Chrome (${{ matrix.module }})
browser: "chrome"
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
env:
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
# testnet
- name: Chrome browser tests (${{ matrix.module }})
if: ${{ matrix.module == 'testnet4' }}
uses: cypress-io/github-action@v5
with:
tag: ${{ github.event_name }}
working-directory: ${{ matrix.module }}/frontend
build: npm run config:defaults:mempool
start: npm run start:local-staging
wait-on: "http://localhost:4200"
wait-on-timeout: 120
record: true
parallel: true
spec: |
cypress/e2e/testnet4/*.spec.ts
group: Tests on Chrome (${{ matrix.module }})
browser: "chrome"
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
env:
CYPRESS_REROUTE_TESTNET: true
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
validate_docker_json: validate_docker_json:
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/')"
runs-on: "ubuntu-latest" runs-on: "ubuntu-latest"
@@ -403,4 +359,4 @@ jobs:
- name: Validate JSON syntax - name: Validate JSON syntax
run: | run: |
cat mempool-config.json | jq cat mempool-config.json | jq
working-directory: docker/docker/backend working-directory: docker/docker/backend

2
.nvmrc
View File

@@ -1 +1 @@
v22.13.0 v20.8.0

View File

@@ -27,9 +27,8 @@
"AUTOMATIC_POOLS_UPDATE": false, "AUTOMATIC_POOLS_UPDATE": false,
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json", "POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json",
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master", "POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
"POOLS_UPDATE_DELAY": 604800,
"AUDIT": false, "AUDIT": false,
"RUST_GBT": true, "RUST_GBT": false,
"LIMIT_GBT": false, "LIMIT_GBT": false,
"CPFP_INDEXING": false, "CPFP_INDEXING": false,
"DISK_CACHE_BLOCK_INTERVAL": 6, "DISK_CACHE_BLOCK_INTERVAL": 6,
@@ -46,8 +45,7 @@
"PASSWORD": "mempool", "PASSWORD": "mempool",
"TIMEOUT": 60000, "TIMEOUT": 60000,
"COOKIE": false, "COOKIE": false,
"COOKIE_PATH": "/path/to/bitcoin/.cookie", "COOKIE_PATH": "/path/to/bitcoin/.cookie"
"DEBUG_LOG_PATH": "/path/to/bitcoin/debug.log"
}, },
"ELECTRUM": { "ELECTRUM": {
"HOST": "127.0.0.1", "HOST": "127.0.0.1",
@@ -155,10 +153,6 @@
"API": "https://mempool.space/api/v1/services", "API": "https://mempool.space/api/v1/services",
"ACCELERATIONS": false "ACCELERATIONS": false
}, },
"STRATUM": {
"ENABLED": false,
"API": "http://localhost:1234"
},
"FIAT_PRICE": { "FIAT_PRICE": {
"ENABLED": true, "ENABLED": true,
"PAID": false, "PAID": false,

View File

@@ -13,10 +13,10 @@
"@babel/core": "^7.25.2", "@babel/core": "^7.25.2",
"@mempool/electrum-client": "1.1.9", "@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3", "@types/node": "^18.15.3",
"axios": "1.7.2", "axios": "~1.7.4",
"bitcoinjs-lib": "~6.1.3", "bitcoinjs-lib": "~6.1.3",
"crypto-js": "~4.2.0", "crypto-js": "~4.2.0",
"express": "~4.21.1", "express": "~4.19.2",
"maxmind": "~4.3.11", "maxmind": "~4.3.11",
"mysql2": "~3.11.0", "mysql2": "~3.11.0",
"redis": "^4.7.0", "redis": "^4.7.0",
@@ -2278,10 +2278,9 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.7.2", "version": "1.7.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
"license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",
"form-data": "^4.0.0", "form-data": "^4.0.0",
@@ -2490,9 +2489,9 @@
} }
}, },
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "1.20.3", "version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
"dependencies": { "dependencies": {
"bytes": "3.1.2", "bytes": "3.1.2",
"content-type": "~1.0.5", "content-type": "~1.0.5",
@@ -2502,7 +2501,7 @@
"http-errors": "2.0.0", "http-errors": "2.0.0",
"iconv-lite": "0.4.24", "iconv-lite": "0.4.24",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"qs": "6.13.0", "qs": "6.11.0",
"raw-body": "2.5.2", "raw-body": "2.5.2",
"type-is": "~1.6.18", "type-is": "~1.6.18",
"unpipe": "1.0.0" "unpipe": "1.0.0"
@@ -2827,9 +2826,9 @@
"dev": true "dev": true
}, },
"node_modules/cookie": { "node_modules/cookie": {
"version": "0.7.1", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
@@ -3031,9 +3030,9 @@
"dev": true "dev": true
}, },
"node_modules/encodeurl": { "node_modules/encodeurl": {
"version": "2.0.0", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
} }
@@ -3461,36 +3460,36 @@
} }
}, },
"node_modules/express": { "node_modules/express": {
"version": "4.21.1", "version": "4.19.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
"dependencies": { "dependencies": {
"accepts": "~1.3.8", "accepts": "~1.3.8",
"array-flatten": "1.1.1", "array-flatten": "1.1.1",
"body-parser": "1.20.3", "body-parser": "1.20.2",
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"content-type": "~1.0.4", "content-type": "~1.0.4",
"cookie": "0.7.1", "cookie": "0.6.0",
"cookie-signature": "1.0.6", "cookie-signature": "1.0.6",
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
"encodeurl": "~2.0.0", "encodeurl": "~1.0.2",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"etag": "~1.8.1", "etag": "~1.8.1",
"finalhandler": "1.3.1", "finalhandler": "1.2.0",
"fresh": "0.5.2", "fresh": "0.5.2",
"http-errors": "2.0.0", "http-errors": "2.0.0",
"merge-descriptors": "1.0.3", "merge-descriptors": "1.0.1",
"methods": "~1.1.2", "methods": "~1.1.2",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"path-to-regexp": "0.1.10", "path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.7", "proxy-addr": "~2.0.7",
"qs": "6.13.0", "qs": "6.11.0",
"range-parser": "~1.2.1", "range-parser": "~1.2.1",
"safe-buffer": "5.2.1", "safe-buffer": "5.2.1",
"send": "0.19.0", "send": "0.18.0",
"serve-static": "1.16.2", "serve-static": "1.15.0",
"setprototypeof": "1.2.0", "setprototypeof": "1.2.0",
"statuses": "2.0.1", "statuses": "2.0.1",
"type-is": "~1.6.18", "type-is": "~1.6.18",
@@ -3603,12 +3602,12 @@
} }
}, },
"node_modules/finalhandler": { "node_modules/finalhandler": {
"version": "1.3.1", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
"dependencies": { "dependencies": {
"debug": "2.6.9", "debug": "2.6.9",
"encodeurl": "~2.0.0", "encodeurl": "~1.0.2",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
@@ -6052,12 +6051,9 @@
} }
}, },
"node_modules/merge-descriptors": { "node_modules/merge-descriptors": {
"version": "1.0.3", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
}, },
"node_modules/merge-stream": { "node_modules/merge-stream": {
"version": "2.0.0", "version": "2.0.0",
@@ -6271,12 +6267,9 @@
} }
}, },
"node_modules/object-inspect": { "node_modules/object-inspect": {
"version": "1.13.2", "version": "1.13.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
"integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
"engines": {
"node": ">= 0.4"
},
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
@@ -6444,9 +6437,9 @@
"dev": true "dev": true
}, },
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "0.1.10", "version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
}, },
"node_modules/path-type": { "node_modules/path-type": {
"version": "4.0.0", "version": "4.0.0",
@@ -6654,11 +6647,11 @@
] ]
}, },
"node_modules/qs": { "node_modules/qs": {
"version": "6.13.0", "version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"dependencies": { "dependencies": {
"side-channel": "^1.0.6" "side-channel": "^1.0.4"
}, },
"engines": { "engines": {
"node": ">=0.6" "node": ">=0.6"
@@ -6879,9 +6872,9 @@
} }
}, },
"node_modules/send": { "node_modules/send": {
"version": "0.19.0", "version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
"dependencies": { "dependencies": {
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
@@ -6914,14 +6907,6 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
}, },
"node_modules/send/node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/send/node_modules/ms": { "node_modules/send/node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -6933,14 +6918,14 @@
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
}, },
"node_modules/serve-static": { "node_modules/serve-static": {
"version": "1.16.2", "version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
"dependencies": { "dependencies": {
"encodeurl": "~2.0.0", "encodeurl": "~1.0.2",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"send": "0.19.0" "send": "0.18.0"
}, },
"engines": { "engines": {
"node": ">= 0.8.0" "node": ">= 0.8.0"
@@ -9454,9 +9439,9 @@
"integrity": "sha512-+H+kuK34PfMaI9PNU/NSjBKL5hh/KDM9J72kwYeYEm0A8B1AC4fuCy3qsjnA7lxklgyXsB68yn8Z2xoZEjgwCQ==" "integrity": "sha512-+H+kuK34PfMaI9PNU/NSjBKL5hh/KDM9J72kwYeYEm0A8B1AC4fuCy3qsjnA7lxklgyXsB68yn8Z2xoZEjgwCQ=="
}, },
"axios": { "axios": {
"version": "1.7.2", "version": "1.7.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
"requires": { "requires": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",
"form-data": "^4.0.0", "form-data": "^4.0.0",
@@ -9619,9 +9604,9 @@
} }
}, },
"body-parser": { "body-parser": {
"version": "1.20.3", "version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
"requires": { "requires": {
"bytes": "3.1.2", "bytes": "3.1.2",
"content-type": "~1.0.5", "content-type": "~1.0.5",
@@ -9631,7 +9616,7 @@
"http-errors": "2.0.0", "http-errors": "2.0.0",
"iconv-lite": "0.4.24", "iconv-lite": "0.4.24",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"qs": "6.13.0", "qs": "6.11.0",
"raw-body": "2.5.2", "raw-body": "2.5.2",
"type-is": "~1.6.18", "type-is": "~1.6.18",
"unpipe": "1.0.0" "unpipe": "1.0.0"
@@ -9865,9 +9850,9 @@
"dev": true "dev": true
}, },
"cookie": { "cookie": {
"version": "0.7.1", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==" "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="
}, },
"cookie-signature": { "cookie-signature": {
"version": "1.0.6", "version": "1.0.6",
@@ -10012,9 +9997,9 @@
"dev": true "dev": true
}, },
"encodeurl": { "encodeurl": {
"version": "2.0.0", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
}, },
"error-ex": { "error-ex": {
"version": "1.3.2", "version": "1.3.2",
@@ -10319,36 +10304,36 @@
} }
}, },
"express": { "express": {
"version": "4.21.1", "version": "4.19.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
"requires": { "requires": {
"accepts": "~1.3.8", "accepts": "~1.3.8",
"array-flatten": "1.1.1", "array-flatten": "1.1.1",
"body-parser": "1.20.3", "body-parser": "1.20.2",
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"content-type": "~1.0.4", "content-type": "~1.0.4",
"cookie": "0.7.1", "cookie": "0.6.0",
"cookie-signature": "1.0.6", "cookie-signature": "1.0.6",
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
"encodeurl": "~2.0.0", "encodeurl": "~1.0.2",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"etag": "~1.8.1", "etag": "~1.8.1",
"finalhandler": "1.3.1", "finalhandler": "1.2.0",
"fresh": "0.5.2", "fresh": "0.5.2",
"http-errors": "2.0.0", "http-errors": "2.0.0",
"merge-descriptors": "1.0.3", "merge-descriptors": "1.0.1",
"methods": "~1.1.2", "methods": "~1.1.2",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"path-to-regexp": "0.1.10", "path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.7", "proxy-addr": "~2.0.7",
"qs": "6.13.0", "qs": "6.11.0",
"range-parser": "~1.2.1", "range-parser": "~1.2.1",
"safe-buffer": "5.2.1", "safe-buffer": "5.2.1",
"send": "0.19.0", "send": "0.18.0",
"serve-static": "1.16.2", "serve-static": "1.15.0",
"setprototypeof": "1.2.0", "setprototypeof": "1.2.0",
"statuses": "2.0.1", "statuses": "2.0.1",
"type-is": "~1.6.18", "type-is": "~1.6.18",
@@ -10450,12 +10435,12 @@
} }
}, },
"finalhandler": { "finalhandler": {
"version": "1.3.1", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
"requires": { "requires": {
"debug": "2.6.9", "debug": "2.6.9",
"encodeurl": "~2.0.0", "encodeurl": "~1.0.2",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
@@ -12252,9 +12237,9 @@
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="
}, },
"merge-descriptors": { "merge-descriptors": {
"version": "1.0.3", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==" "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
}, },
"merge-stream": { "merge-stream": {
"version": "2.0.0", "version": "2.0.0",
@@ -12417,9 +12402,9 @@
} }
}, },
"object-inspect": { "object-inspect": {
"version": "1.13.2", "version": "1.13.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
"integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==" "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ=="
}, },
"on-finished": { "on-finished": {
"version": "2.4.1", "version": "2.4.1",
@@ -12536,9 +12521,9 @@
"dev": true "dev": true
}, },
"path-to-regexp": { "path-to-regexp": {
"version": "0.1.10", "version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
}, },
"path-type": { "path-type": {
"version": "4.0.0", "version": "4.0.0",
@@ -12680,11 +12665,11 @@
"dev": true "dev": true
}, },
"qs": { "qs": {
"version": "6.13.0", "version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"requires": { "requires": {
"side-channel": "^1.0.6" "side-channel": "^1.0.4"
} }
}, },
"queue-microtask": { "queue-microtask": {
@@ -12818,9 +12803,9 @@
"dev": true "dev": true
}, },
"send": { "send": {
"version": "0.19.0", "version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
"requires": { "requires": {
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
@@ -12852,11 +12837,6 @@
} }
} }
}, },
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
},
"ms": { "ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -12870,14 +12850,14 @@
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
}, },
"serve-static": { "serve-static": {
"version": "1.16.2", "version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
"requires": { "requires": {
"encodeurl": "~2.0.0", "encodeurl": "~1.0.2",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"send": "0.19.0" "send": "0.18.0"
} }
}, },
"set-function-length": { "set-function-length": {

View File

@@ -42,10 +42,10 @@
"@babel/core": "^7.25.2", "@babel/core": "^7.25.2",
"@mempool/electrum-client": "1.1.9", "@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3", "@types/node": "^18.15.3",
"axios": "1.7.2", "axios": "~1.7.4",
"bitcoinjs-lib": "~6.1.3", "bitcoinjs-lib": "~6.1.3",
"crypto-js": "~4.2.0", "crypto-js": "~4.2.0",
"express": "~4.21.1", "express": "~4.19.2",
"maxmind": "~4.3.11", "maxmind": "~4.3.11",
"mysql2": "~3.11.0", "mysql2": "~3.11.0",
"rust-gbt": "file:./rust-gbt", "rust-gbt": "file:./rust-gbt",

View File

@@ -28,7 +28,6 @@
"INDEXING_BLOCKS_AMOUNT": 14, "INDEXING_BLOCKS_AMOUNT": 14,
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__", "POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__", "POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
"POOLS_UPDATE_DELAY": 604800,
"AUDIT": true, "AUDIT": true,
"RUST_GBT": false, "RUST_GBT": false,
"LIMIT_GBT": false, "LIMIT_GBT": false,
@@ -47,8 +46,7 @@
"PASSWORD": "__CORE_RPC_PASSWORD__", "PASSWORD": "__CORE_RPC_PASSWORD__",
"TIMEOUT": 1000, "TIMEOUT": 1000,
"COOKIE": false, "COOKIE": false,
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__", "COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__"
"DEBUG_LOG_PATH": "__CORE_RPC_DEBUG_LOG_PATH__"
}, },
"ELECTRUM": { "ELECTRUM": {
"HOST": "__ELECTRUM_HOST__", "HOST": "__ELECTRUM_HOST__",
@@ -151,9 +149,5 @@
"ENABLED": true, "ENABLED": true,
"PAID": false, "PAID": false,
"API_KEY": "__MEMPOOL_CURRENCY_API_KEY__" "API_KEY": "__MEMPOOL_CURRENCY_API_KEY__"
},
"STRATUM": {
"ENABLED": false,
"API": "http://localhost:1234"
} }
} }

View File

@@ -1,5 +1,5 @@
import { Common } from '../../api/common'; import { Common } from '../../api/common';
import { MempoolTransactionExtended, TransactionExtended } from '../../mempool.interfaces'; import { MempoolTransactionExtended } from '../../mempool.interfaces';
const randomTransactions = require('./test-data/transactions-random.json'); const randomTransactions = require('./test-data/transactions-random.json');
const replacedTransactions = require('./test-data/transactions-replaced.json'); const replacedTransactions = require('./test-data/transactions-replaced.json');
@@ -10,14 +10,14 @@ describe('Common', () => {
describe('RBF', () => { describe('RBF', () => {
const newTransactions = rbfTransactions.concat(randomTransactions); const newTransactions = rbfTransactions.concat(randomTransactions);
test('should detect RBF transactions with fast method', () => { test('should detect RBF transactions with fast method', () => {
const result: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = Common.findRbfTransactions(newTransactions, replacedTransactions); const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions);
expect(Object.values(result).length).toEqual(2); expect(Object.values(result).length).toEqual(2);
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6'); expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875'); expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
}); });
test('should detect RBF transactions with scalable method', () => { test('should detect RBF transactions with scalable method', () => {
const result: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = Common.findRbfTransactions(newTransactions, replacedTransactions, true); const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions, true);
expect(Object.values(result).length).toEqual(2); expect(Object.values(result).length).toEqual(2);
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6'); expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875'); expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');

View File

@@ -41,9 +41,8 @@ describe('Mempool Backend Config', () => {
STDOUT_LOG_MIN_PRIORITY: 'debug', STDOUT_LOG_MIN_PRIORITY: 'debug',
POOLS_JSON_TREE_URL: 'https://api.github.com/repos/mempool/mining-pools/git/trees/master', POOLS_JSON_TREE_URL: 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json', POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json',
POOLS_UPDATE_DELAY: 604800,
AUDIT: false, AUDIT: false,
RUST_GBT: true, RUST_GBT: false,
LIMIT_GBT: false, LIMIT_GBT: false,
CPFP_INDEXING: false, CPFP_INDEXING: false,
MAX_BLOCKS_BULK_QUERY: 0, MAX_BLOCKS_BULK_QUERY: 0,
@@ -74,8 +73,7 @@ describe('Mempool Backend Config', () => {
PASSWORD: 'mempool', PASSWORD: 'mempool',
TIMEOUT: 60000, TIMEOUT: 60000,
COOKIE: false, COOKIE: false,
COOKIE_PATH: '/bitcoin/.cookie', COOKIE_PATH: '/bitcoin/.cookie'
DEBUG_LOG_PATH: '',
}); });
expect(config.SECOND_CORE_RPC).toStrictEqual({ expect(config.SECOND_CORE_RPC).toStrictEqual({
@@ -159,11 +157,6 @@ describe('Mempool Backend Config', () => {
PAID: false, PAID: false,
API_KEY: '', API_KEY: '',
}); });
expect(config.STRATUM).toStrictEqual({
ENABLED: false,
API: 'http://localhost:1234',
});
}); });
}); });

View File

@@ -1,4 +1,4 @@
import { IBitcoinApi, SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface'; import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface';
import { IEsploraApi } from './esplora-api.interface'; import { IEsploraApi } from './esplora-api.interface';
export interface AbstractBitcoinApi { export interface AbstractBitcoinApi {
@@ -23,14 +23,12 @@ export interface AbstractBitcoinApi {
$getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>; $getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
$sendRawTransaction(rawTransaction: string): Promise<string>; $sendRawTransaction(rawTransaction: string): Promise<string>;
$testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]>; $testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]>;
$submitPackage(rawTransactions: string[], maxfeerate?: number, maxburnamount?: number): Promise<SubmitPackageResult>;
$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[]>;
$getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>; $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
$getBatchedOutspendsInternal(txId: string[]): Promise<IEsploraApi.Outspend[][]>; $getBatchedOutspendsInternal(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
$getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]>; $getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]>;
$getCoinbaseTx(blockhash: string): Promise<IEsploraApi.Transaction>; $getCoinbaseTx(blockhash: string): Promise<IEsploraApi.Transaction>;
$getAddressTransactionSummary(address: string): Promise<IEsploraApi.AddressTxSummary[]>;
startHealthChecks(): void; startHealthChecks(): void;
getHealthStatus(): HealthCheckHost[]; getHealthStatus(): HealthCheckHost[];

View File

@@ -218,21 +218,3 @@ export interface TestMempoolAcceptResult {
}, },
['reject-reason']?: string, ['reject-reason']?: string,
} }
export interface SubmitPackageResult {
package_msg: string;
"tx-results": { [wtxid: string]: TxResult };
"replaced-transactions"?: string[];
}
export interface TxResult {
txid: string;
"other-wtxid"?: string;
vsize?: number;
fees?: {
base: number;
"effective-feerate"?: number;
"effective-includes"?: string[];
};
error?: string;
}

View File

@@ -1,6 +1,6 @@
import * as bitcoinjs from 'bitcoinjs-lib'; import * as bitcoinjs from 'bitcoinjs-lib';
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory'; import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
import { IBitcoinApi, SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface'; import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface';
import { IEsploraApi } from './esplora-api.interface'; import { IEsploraApi } from './esplora-api.interface';
import blocks from '../blocks'; import blocks from '../blocks';
import mempool from '../mempool'; import mempool from '../mempool';
@@ -196,10 +196,6 @@ class BitcoinApi implements AbstractBitcoinApi {
} }
} }
$submitPackage(rawTransactions: string[], maxfeerate?: number, maxburnamount?: number): Promise<SubmitPackageResult> {
return this.bitcoindClient.submitPackage(rawTransactions, maxfeerate ?? undefined, maxburnamount ?? undefined);
}
async $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> { async $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
const txOut = await this.bitcoindClient.getTxOut(txId, vout, false); const txOut = await this.bitcoindClient.getTxOut(txId, vout, false);
return { return {
@@ -255,10 +251,6 @@ class BitcoinApi implements AbstractBitcoinApi {
return this.$getRawTransaction(txids[0]); return this.$getRawTransaction(txids[0]);
} }
async $getAddressTransactionSummary(address: string): Promise<IEsploraApi.AddressTxSummary[]> {
throw new Error('Method getAddressTransactionSummary not supported by the Bitcoin RPC API.');
}
$getEstimatedHashrate(blockHeight: number): Promise<number> { $getEstimatedHashrate(blockHeight: number): Promise<number> {
// 120 is the default block span in Core // 120 is the default block span in Core
return this.bitcoindClient.getNetworkHashPs(120, blockHeight); return this.bitcoindClient.getNetworkHashPs(120, blockHeight);

View File

@@ -1,11 +1,6 @@
import { Application, NextFunction, Request, Response } from 'express'; import { Application, NextFunction, Request, Response } from 'express';
import logger from '../../logger'; import logger from '../../logger';
import bitcoinClient from './bitcoin-client'; import bitcoinClient from './bitcoin-client';
import config from '../../config';
const BLOCKHASH_REGEX = /^[a-f0-9]{64}$/i;
const TXID_REGEX = /^[a-f0-9]{64}$/i;
const RAW_TX_REGEX = /^[a-f0-9]{2,}$/i;
/** /**
* Define a set of routes used by the accelerator server * Define a set of routes used by the accelerator server
@@ -14,26 +9,26 @@ const RAW_TX_REGEX = /^[a-f0-9]{2,}$/i;
class BitcoinBackendRoutes { class BitcoinBackendRoutes {
private static tag = 'BitcoinBackendRoutes'; private static tag = 'BitcoinBackendRoutes';
public initRoutes(app: Application): void { public initRoutes(app: Application) {
app app
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-mempool-entry', this.disableCache, this.$getMempoolEntry) .get('/api/internal/bitcoin-core/' + 'get-mempool-entry', this.disableCache, this.$getMempoolEntry)
.post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'decode-raw-transaction', this.disableCache, this.$decodeRawTransaction) .post('/api/internal/bitcoin-core/' + 'decode-raw-transaction', this.disableCache, this.$decodeRawTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-raw-transaction', this.disableCache, this.$getRawTransaction) .get('/api/internal/bitcoin-core/' + 'get-raw-transaction', this.disableCache, this.$getRawTransaction)
.post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'send-raw-transaction', this.disableCache, this.$sendRawTransaction) .post('/api/internal/bitcoin-core/' + 'send-raw-transaction', this.disableCache, this.$sendRawTransaction)
.post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'test-mempool-accept', this.disableCache, this.$testMempoolAccept) .post('/api/internal/bitcoin-core/' + 'test-mempool-accept', this.disableCache, this.$testMempoolAccept)
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-mempool-ancestors', this.disableCache, this.$getMempoolAncestors) .get('/api/internal/bitcoin-core/' + 'get-mempool-ancestors', this.disableCache, this.$getMempoolAncestors)
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block', this.disableCache, this.$getBlock) .get('/api/internal/bitcoin-core/' + 'get-block', this.disableCache, this.$getBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block-hash', this.disableCache, this.$getBlockHash) .get('/api/internal/bitcoin-core/' + 'get-block-hash', this.disableCache, this.$getBlockHash)
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block-count', this.disableCache, this.$getBlockCount) .get('/api/internal/bitcoin-core/' + 'get-block-count', this.disableCache, this.$getBlockCount)
; ;
} }
/** /**
* Disable caching for bitcoin core routes * Disable caching for bitcoin core routes
* *
* @param req * @param req
* @param res * @param res
* @param next * @param next
*/ */
private disableCache(req: Request, res: Response, next: NextFunction): void { private disableCache(req: Request, res: Response, next: NextFunction): void {
res.setHeader('Pragma', 'no-cache'); res.setHeader('Pragma', 'no-cache');
@@ -44,16 +39,16 @@ class BitcoinBackendRoutes {
/** /**
* Exeption handler to return proper details to the accelerator server * Exeption handler to return proper details to the accelerator server
* *
* @param e * @param e
* @param fnName * @param fnName
* @param res * @param res
*/ */
private static handleException(e: any, fnName: string, res: Response): void { private static handleException(e: any, fnName: string, res: Response): void {
if (typeof(e.code) === 'number') { if (typeof(e.code) === 'number') {
res.status(400).send(JSON.stringify(e, ['code'])); res.status(400).send(JSON.stringify(e, ['code', 'message']));
} else { } else {
const err = `unknown exception in ${fnName}`; const err = `exception in ${fnName}. ${e}. Details: ${JSON.stringify(e, ['code', 'message'])}`;
logger.err(err, BitcoinBackendRoutes.tag); logger.err(err, BitcoinBackendRoutes.tag);
res.status(500).send(err); res.status(500).send(err);
} }
@@ -62,13 +57,13 @@ class BitcoinBackendRoutes {
private async $getMempoolEntry(req: Request, res: Response): Promise<void> { private async $getMempoolEntry(req: Request, res: Response): Promise<void> {
const txid = req.query.txid; const txid = req.query.txid;
try { try {
if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) { if (typeof(txid) !== 'string' || txid.length !== 64) {
res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`); res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`);
return; return;
} }
const mempoolEntry = await bitcoinClient.getMempoolEntry(txid); const mempoolEntry = await bitcoinClient.getMempoolEntry(txid);
if (!mempoolEntry) { if (!mempoolEntry) {
res.status(404).send(); res.status(404).send(`no mempool entry found for txid ${txid}`);
return; return;
} }
res.status(200).send(mempoolEntry); res.status(200).send(mempoolEntry);
@@ -80,13 +75,13 @@ class BitcoinBackendRoutes {
private async $decodeRawTransaction(req: Request, res: Response): Promise<void> { private async $decodeRawTransaction(req: Request, res: Response): Promise<void> {
const rawTx = req.body.rawTx; const rawTx = req.body.rawTx;
try { try {
if (typeof(rawTx) !== 'string' || !RAW_TX_REGEX.test(rawTx)) { if (typeof(rawTx) !== 'string') {
res.status(400).send(`invalid param rawTx. must be a string of hexadecimal characters`); res.status(400).send(`invalid param rawTx ${rawTx}. must be a string`);
return; return;
} }
const decodedTx = await bitcoinClient.decodeRawTransaction(rawTx); const decodedTx = await bitcoinClient.decodeRawTransaction(rawTx);
if (!decodedTx) { if (!decodedTx) {
res.status(400).send(`unable to decode rawTx`); res.status(400).send(`unable to decode rawTx ${rawTx}`);
return; return;
} }
res.status(200).send(decodedTx); res.status(200).send(decodedTx);
@@ -99,23 +94,23 @@ class BitcoinBackendRoutes {
const txid = req.query.txid; const txid = req.query.txid;
const verbose = req.query.verbose; const verbose = req.query.verbose;
try { try {
if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) { if (typeof(txid) !== 'string' || txid.length !== 64) {
res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`); res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`);
return; return;
} }
if (typeof(verbose) !== 'string') { if (typeof(verbose) !== 'string') {
res.status(400).send(`invalid param verbose. must be a string representing an integer`); res.status(400).send(`invalid param verbose ${verbose}. must be a string representing an integer`);
return; return;
} }
const verboseNumber = parseInt(verbose, 10); const verboseNumber = parseInt(verbose, 10);
if (typeof(verboseNumber) !== 'number') { if (typeof(verboseNumber) !== 'number') {
res.status(400).send(`invalid param verbose. must be a valid integer`); res.status(400).send(`invalid param verbose ${verbose}. must be a valid integer`);
return; return;
} }
const decodedTx = await bitcoinClient.getRawTransaction(txid, verboseNumber); const decodedTx = await bitcoinClient.getRawTransaction(txid, verboseNumber);
if (!decodedTx) { if (!decodedTx) {
res.status(400).send(`unable to get raw transaction`); res.status(400).send(`unable to get raw transaction for txid ${txid}`);
return; return;
} }
res.status(200).send(decodedTx); res.status(200).send(decodedTx);
@@ -127,13 +122,13 @@ class BitcoinBackendRoutes {
private async $sendRawTransaction(req: Request, res: Response): Promise<void> { private async $sendRawTransaction(req: Request, res: Response): Promise<void> {
const rawTx = req.body.rawTx; const rawTx = req.body.rawTx;
try { try {
if (typeof(rawTx) !== 'string' || !RAW_TX_REGEX.test(rawTx)) { if (typeof(rawTx) !== 'string') {
res.status(400).send(`invalid param rawTx. must be a string of hexadecimal characters`); res.status(400).send(`invalid param rawTx ${rawTx}. must be a string`);
return; return;
} }
const txHex = await bitcoinClient.sendRawTransaction(rawTx); const txHex = await bitcoinClient.sendRawTransaction(rawTx);
if (!txHex) { if (!txHex) {
res.status(400).send(`unable to send rawTx`); res.status(400).send(`unable to send rawTx ${rawTx}`);
return; return;
} }
res.status(200).send(txHex); res.status(200).send(txHex);
@@ -145,13 +140,13 @@ class BitcoinBackendRoutes {
private async $testMempoolAccept(req: Request, res: Response): Promise<void> { private async $testMempoolAccept(req: Request, res: Response): Promise<void> {
const rawTxs = req.body.rawTxs; const rawTxs = req.body.rawTxs;
try { try {
if (typeof(rawTxs) !== 'object' || !Array.isArray(rawTxs) || rawTxs.some((tx) => typeof(tx) !== 'string' || !RAW_TX_REGEX.test(tx))) { if (typeof(rawTxs) !== 'object') {
res.status(400).send(`invalid param rawTxs. must be an array of strings of hexadecimal characters`); res.status(400).send(`invalid param rawTxs ${JSON.stringify(rawTxs)}. must be an array of string`);
return; return;
} }
const txHex = await bitcoinClient.testMempoolAccept(rawTxs); const txHex = await bitcoinClient.testMempoolAccept(rawTxs);
if (typeof(txHex) !== 'object' || txHex.length === 0) { if (typeof(txHex) !== 'object' || txHex.length === 0) {
res.status(400).send(`testmempoolaccept failed for raw txs, got an empty result`); res.status(400).send(`testmempoolaccept failed for raw txs ${JSON.stringify(rawTxs)}, got an empty result`);
return; return;
} }
res.status(200).send(txHex); res.status(200).send(txHex);
@@ -164,18 +159,18 @@ class BitcoinBackendRoutes {
const txid = req.query.txid; const txid = req.query.txid;
const verbose = req.query.verbose; const verbose = req.query.verbose;
try { try {
if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) { if (typeof(txid) !== 'string' || txid.length !== 64) {
res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`); res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`);
return; return;
} }
if (typeof(verbose) !== 'string' || (verbose !== 'true' && verbose !== 'false')) { if (typeof(verbose) !== 'string' || (verbose !== 'true' && verbose !== 'false')) {
res.status(400).send(`invalid param verbose. must be a string ('true' | 'false')`); res.status(400).send(`invalid param verbose ${verbose}. must be a string ('true' | 'false')`);
return; return;
} }
const ancestors = await bitcoinClient.getMempoolAncestors(txid, verbose === 'true' ? true : false); const ancestors = await bitcoinClient.getMempoolAncestors(txid, verbose === 'true' ? true : false);
if (!ancestors) { if (!ancestors) {
res.status(400).send(`unable to get mempool ancestors`); res.status(400).send(`unable to get mempool ancestors for txid ${txid}`);
return; return;
} }
res.status(200).send(ancestors); res.status(200).send(ancestors);
@@ -188,23 +183,23 @@ class BitcoinBackendRoutes {
const blockHash = req.query.hash; const blockHash = req.query.hash;
const verbosity = req.query.verbosity; const verbosity = req.query.verbosity;
try { try {
if (typeof(blockHash) !== 'string' || blockHash.length !== 64 || !BLOCKHASH_REGEX.test(blockHash)) { if (typeof(blockHash) !== 'string' || blockHash.length !== 64) {
res.status(400).send(`invalid param blockHash. must be 64 hexadecimal characters`); res.status(400).send(`invalid param blockHash ${blockHash}. must be a string of 64 char`);
return; return;
} }
if (typeof(verbosity) !== 'string') { if (typeof(verbosity) !== 'string') {
res.status(400).send(`invalid param verbosity. must be a string representing an integer`); res.status(400).send(`invalid param verbosity ${verbosity}. must be a string representing an integer`);
return; return;
} }
const verbosityNumber = parseInt(verbosity, 10); const verbosityNumber = parseInt(verbosity, 10);
if (typeof(verbosityNumber) !== 'number') { if (typeof(verbosityNumber) !== 'number') {
res.status(400).send(`invalid param verbosity. must be a valid integer`); res.status(400).send(`invalid param verbosity ${verbosity}. must be a valid integer`);
return; return;
} }
const block = await bitcoinClient.getBlock(blockHash, verbosityNumber); const block = await bitcoinClient.getBlock(blockHash, verbosityNumber);
if (!block) { if (!block) {
res.status(400).send(`unable to get block`); res.status(400).send(`unable to get block for block hash ${blockHash}`);
return; return;
} }
res.status(200).send(block); res.status(200).send(block);
@@ -217,18 +212,18 @@ class BitcoinBackendRoutes {
const blockHeight = req.query.height; const blockHeight = req.query.height;
try { try {
if (typeof(blockHeight) !== 'string') { if (typeof(blockHeight) !== 'string') {
res.status(400).send(`invalid param blockHeight, must be a string representing an integer`); res.status(400).send(`invalid param blockHeight ${blockHeight}, must be a string representing an integer`);
return; return;
} }
const blockHeightNumber = parseInt(blockHeight, 10); const blockHeightNumber = parseInt(blockHeight, 10);
if (typeof(blockHeightNumber) !== 'number') { if (typeof(blockHeightNumber) !== 'number') {
res.status(400).send(`invalid param blockHeight. must be a valid integer`); res.status(400).send(`invalid param blockHeight ${blockHeight}. must be a valid integer`);
return; return;
} }
const block = await bitcoinClient.getBlockHash(blockHeightNumber); const block = await bitcoinClient.getBlockHash(blockHeightNumber);
if (!block) { if (!block) {
res.status(400).send(`unable to get block hash`); res.status(400).send(`unable to get block hash for block height ${blockHeightNumber}`);
return; return;
} }
res.status(200).send(block); res.status(200).send(block);
@@ -251,4 +246,4 @@ class BitcoinBackendRoutes {
} }
} }
export default new BitcoinBackendRoutes; export default new BitcoinBackendRoutes

View File

@@ -20,12 +20,6 @@ import difficultyAdjustment from '../difficulty-adjustment';
import transactionRepository from '../../repositories/TransactionRepository'; import transactionRepository from '../../repositories/TransactionRepository';
import rbfCache from '../rbf-cache'; import rbfCache from '../rbf-cache';
import { calculateMempoolTxCpfp } from '../cpfp'; import { calculateMempoolTxCpfp } from '../cpfp';
import { handleError } from '../../utils/api';
const TXID_REGEX = /^[a-f0-9]{64}$/i;
const BLOCK_HASH_REGEX = /^[a-f0-9]{64}$/i;
const ADDRESS_REGEX = /^[a-z0-9]{2,120}$/i;
const SCRIPT_HASH_REGEX = /^([a-f0-9]{2})+$/i;
class BitcoinRoutes { class BitcoinRoutes {
public initRoutes(app: Application) { public initRoutes(app: Application) {
@@ -47,15 +41,12 @@ class BitcoinRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/summary', this.getStrippedBlockTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
.post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion) .post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', this.getBlocksByBulk.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', this.getBlocksByBulk.bind(this))
// Temporarily add txs/package endpoint for all backends until esplora supports it
.post(config.MEMPOOL.API_URL_PREFIX + 'txs/package', this.$submitPackage)
; ;
if (config.MEMPOOL.BACKEND !== 'esplora') { if (config.MEMPOOL.BACKEND !== 'esplora') {
@@ -95,7 +86,7 @@ class BitcoinRoutes {
res.set('Content-Type', 'application/json'); res.set('Content-Type', 'application/json');
res.send(result); res.send(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get init data'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -114,22 +105,19 @@ class BitcoinRoutes {
const result = mempoolBlocks.getMempoolBlocks(); const result = mempoolBlocks.getMempoolBlocks();
res.json(result); res.json(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get mempool blocks'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
private getTransactionTimes(req: Request, res: Response) { private getTransactionTimes(req: Request, res: Response) {
if (!Array.isArray(req.query.txId)) { if (!Array.isArray(req.query.txId)) {
handleError(req, res, 500, 'Not an array'); res.status(500).send('Not an array');
return; return;
} }
const txIds: string[] = []; const txIds: string[] = [];
for (const _txId in req.query.txId) { for (const _txId in req.query.txId) {
if (typeof req.query.txId[_txId] === 'string') { if (typeof req.query.txId[_txId] === 'string') {
const txid = req.query.txId[_txId].toString(); txIds.push(req.query.txId[_txId].toString());
if (TXID_REGEX.test(txid)) {
txIds.push(txid);
}
} }
} }
@@ -140,16 +128,12 @@ class BitcoinRoutes {
private async $getBatchedOutspends(req: Request, res: Response): Promise<IEsploraApi.Outspend[][] | void> { private async $getBatchedOutspends(req: Request, res: Response): Promise<IEsploraApi.Outspend[][] | void> {
const txids_csv = req.query.txids; const txids_csv = req.query.txids;
if (!txids_csv || typeof txids_csv !== 'string') { if (!txids_csv || typeof txids_csv !== 'string') {
handleError(req, res, 500, 'Invalid txids format'); res.status(500).send('Invalid txids format');
return; return;
} }
const txids = txids_csv.split(','); const txids = txids_csv.split(',');
if (txids.length > 50) { if (txids.length > 50) {
handleError(req, res, 400, 'Too many txids requested'); res.status(400).send('Too many txids requested');
return;
}
if (txids.some((txid) => !TXID_REGEX.test(txid))) {
handleError(req, res, 400, 'Invalid txids format');
return; return;
} }
@@ -157,13 +141,13 @@ class BitcoinRoutes {
const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids); const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids);
res.json(batchedOutspends); res.json(batchedOutspends);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get batched outspends'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
private async $getCpfpInfo(req: Request, res: Response) { private async $getCpfpInfo(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) { if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`); res.status(501).send(`Invalid transaction ID.`);
return; return;
} }
@@ -196,7 +180,7 @@ class BitcoinRoutes {
try { try {
cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId); cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get CPFP info'); res.status(500).send('failed to get CPFP info');
return; return;
} }
} }
@@ -217,10 +201,6 @@ class BitcoinRoutes {
} }
private async getTransaction(req: Request, res: Response) { private async getTransaction(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try { try {
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true, false, false, true); const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true, false, false, true);
res.json(transaction); res.json(transaction);
@@ -228,18 +208,12 @@ class BitcoinRoutes {
let statusCode = 500; let statusCode = 500;
if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404; statusCode = 404;
handleError(req, res, statusCode, 'No such mempool or blockchain transaction');
return;
} }
handleError(req, res, statusCode, 'Failed to get transaction'); res.status(statusCode).send(e instanceof Error ? e.message : e);
} }
} }
private async getRawTransaction(req: Request, res: Response) { private async getRawTransaction(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try { try {
const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(req.params.txId, true); const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(req.params.txId, true);
res.setHeader('content-type', 'text/plain'); res.setHeader('content-type', 'text/plain');
@@ -248,10 +222,8 @@ class BitcoinRoutes {
let statusCode = 500; let statusCode = 500;
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404; statusCode = 404;
handleError(req, res, statusCode, 'No such mempool or blockchain transaction');
return;
} }
handleError(req, res, statusCode, 'Failed to get raw transaction'); res.status(statusCode).send(e instanceof Error ? e.message : e);
} }
} }
@@ -312,22 +284,18 @@ class BitcoinRoutes {
// Not modified // Not modified
// 422 Unprocessable Entity // 422 Unprocessable Entity
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422 // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422
handleError(req, res, 422, `Psbt had no missing nonWitnessUtxos.`); res.status(422).send(`Psbt had no missing nonWitnessUtxos.`);
} }
} catch (e: any) { } catch (e: any) {
if (e instanceof Error && new RegExp(notFoundError).test(e.message)) { if (e instanceof Error && new RegExp(notFoundError).test(e.message)) {
handleError(req, res, 404, notFoundError); res.status(404).send(e.message);
} else { } else {
handleError(req, res, 500, 'Failed to process PSBT'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
} }
private async getTransactionStatus(req: Request, res: Response) { private async getTransactionStatus(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try { try {
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true); const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true);
res.json(transaction.status); res.json(transaction.status);
@@ -335,54 +303,22 @@ class BitcoinRoutes {
let statusCode = 500; let statusCode = 500;
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404; statusCode = 404;
handleError(req, res, statusCode, 'No such mempool or blockchain transaction');
return;
} }
handleError(req, res, statusCode, 'Failed to get transaction status'); res.status(statusCode).send(e instanceof Error ? e.message : e);
} }
} }
private async getStrippedBlockTransactions(req: Request, res: Response) { private async getStrippedBlockTransactions(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try { try {
const transactions = await blocks.$getStrippedBlockTransactions(req.params.hash); const transactions = await blocks.$getStrippedBlockTransactions(req.params.hash);
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(transactions); res.json(transactions);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get block summary'); res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getStrippedBlockTransaction(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
if (!TXID_REGEX.test(req.params.txid)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const transaction = await blocks.$getSingleTxFromSummary(req.params.hash, req.params.txid);
if (!transaction) {
handleError(req, res, 404, `Transaction not found in summary`);
return;
}
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(transaction);
} catch (e) {
handleError(req, res, 500, 'Failed to get transaction from summary');
} }
} }
private async getBlock(req: Request, res: Response) { private async getBlock(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try { try {
const block = await blocks.$getBlock(req.params.hash); const block = await blocks.$getBlock(req.params.hash);
@@ -394,69 +330,51 @@ class BitcoinRoutes {
} else if (blockAge > 30 * day) { } else if (blockAge > 30 * day) {
cacheDuration = 10 * day; cacheDuration = 10 * day;
} else { } else {
cacheDuration = 600; cacheDuration = 600
} }
res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString());
res.json(block); res.json(block);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get block'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
private async getBlockHeader(req: Request, res: Response) { private async getBlockHeader(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try { try {
const blockHeader = await bitcoinApi.$getBlockHeader(req.params.hash); const blockHeader = await bitcoinApi.$getBlockHeader(req.params.hash);
res.setHeader('content-type', 'text/plain'); res.setHeader('content-type', 'text/plain');
res.send(blockHeader); res.send(blockHeader);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get block header'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
private async getBlockAuditSummary(req: Request, res: Response) { private async getBlockAuditSummary(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try { try {
const auditSummary = await blocks.$getBlockAuditSummary(req.params.hash); const auditSummary = await blocks.$getBlockAuditSummary(req.params.hash);
if (auditSummary) { if (auditSummary) {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(auditSummary); res.json(auditSummary);
} else { } else {
handleError(req, res, 404, `Audit not available`); return res.status(404).send(`audit not available`);
return;
} }
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get block audit summary'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
private async $getBlockTxAuditSummary(req: Request, res: Response) { private async $getBlockTxAuditSummary(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
if (!TXID_REGEX.test(req.params.txid)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try { try {
const auditSummary = await blocks.$getBlockTxAuditSummary(req.params.hash, req.params.txid); const auditSummary = await blocks.$getBlockTxAuditSummary(req.params.hash, req.params.txid);
if (auditSummary) { if (auditSummary) {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(auditSummary); res.json(auditSummary);
} else { } else {
handleError(req, res, 404, `Transaction audit not available`); return res.status(404).send(`transaction audit not available`);
return;
} }
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get transaction audit summary'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -470,49 +388,42 @@ class BitcoinRoutes {
return await this.getLegacyBlocks(req, res); return await this.getLegacyBlocks(req, res);
} }
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get blocks'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
private async getBlocksByBulk(req: Request, res: Response) { private async getBlocksByBulk(req: Request, res: Response) {
try { try {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { // Liquid - Not implemented if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { // Liquid - Not implemented
handleError(req, res, 404, `This API is only available for Bitcoin networks`); return res.status(404).send(`This API is only available for Bitcoin networks`);
return;
} }
if (config.MEMPOOL.MAX_BLOCKS_BULK_QUERY <= 0) { if (config.MEMPOOL.MAX_BLOCKS_BULK_QUERY <= 0) {
handleError(req, res, 404, `This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`); return res.status(404).send(`This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`);
return;
} }
if (!Common.indexingEnabled()) { if (!Common.indexingEnabled()) {
handleError(req, res, 404, `Indexing is required for this API`); return res.status(404).send(`Indexing is required for this API`);
return;
} }
const from = parseInt(req.params.from, 10); const from = parseInt(req.params.from, 10);
if (!req.params.from || from < 0) { if (!req.params.from || from < 0) {
handleError(req, res, 400, `Parameter 'from' must be a block height (integer)`); return res.status(400).send(`Parameter 'from' must be a block height (integer)`);
return;
} }
const to = req.params.to === undefined ? await bitcoinApi.$getBlockHeightTip() : parseInt(req.params.to, 10); const to = req.params.to === undefined ? await bitcoinApi.$getBlockHeightTip() : parseInt(req.params.to, 10);
if (to < 0) { if (to < 0) {
handleError(req, res, 400, `Parameter 'to' must be a block height (integer)`); return res.status(400).send(`Parameter 'to' must be a block height (integer)`);
return;
} }
if (from > to) { if (from > to) {
handleError(req, res, 400, `Parameter 'to' must be a higher block height than 'from'`); return res.status(400).send(`Parameter 'to' must be a higher block height than 'from'`);
return;
} }
if ((to - from + 1) > config.MEMPOOL.MAX_BLOCKS_BULK_QUERY) { if ((to - from + 1) > config.MEMPOOL.MAX_BLOCKS_BULK_QUERY) {
handleError(req, res, 400, `You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`); return res.status(400).send(`You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`);
return;
} }
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(await blocks.$getBlocksBetweenHeight(from, to)); res.json(await blocks.$getBlocksBetweenHeight(from, to));
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get blocks'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -547,15 +458,11 @@ class BitcoinRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(returnBlocks); res.json(returnBlocks);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get blocks'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
private async getBlockTransactions(req: Request, res: Response) { private async getBlockTransactions(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try { try {
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0); loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0);
@@ -576,7 +483,7 @@ class BitcoinRoutes {
res.json(transactions); res.json(transactions);
} catch (e) { } catch (e) {
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100); loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100);
handleError(req, res, 500, 'Failed to get block transactions'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -585,17 +492,13 @@ class BitcoinRoutes {
const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10)); const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10));
res.send(blockHash); res.send(blockHash);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get block at height'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
private async getAddress(req: Request, res: Response) { private async getAddress(req: Request, res: Response) {
if (config.MEMPOOL.BACKEND === 'none') { if (config.MEMPOOL.BACKEND === 'none') {
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
return;
}
if (!ADDRESS_REGEX.test(req.params.address)) {
handleError(req, res, 501, `Invalid address`);
return; return;
} }
@@ -604,20 +507,15 @@ class BitcoinRoutes {
res.json(addressData); res.json(addressData);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
handleError(req, res, 413, e.message); return res.status(413).send(e instanceof Error ? e.message : e);
return;
} }
handleError(req, res, 500, 'Failed to get address'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
private async getAddressTransactions(req: Request, res: Response): Promise<void> { private async getAddressTransactions(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND === 'none') { if (config.MEMPOOL.BACKEND === 'none') {
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
return;
}
if (!ADDRESS_REGEX.test(req.params.address)) {
handleError(req, res, 501, `Invalid address`);
return; return;
} }
@@ -630,27 +528,23 @@ class BitcoinRoutes {
res.json(transactions); res.json(transactions);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
handleError(req, res, 413, e.message); res.status(413).send(e instanceof Error ? e.message : e);
return; return;
} }
handleError(req, res, 500, 'Failed to get address transactions'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
private async getAddressTransactionSummary(req: Request, res: Response): Promise<void> { private async getAddressTransactionSummary(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND !== 'esplora') { if (config.MEMPOOL.BACKEND !== 'esplora') {
handleError(req, res, 405, 'Address summary lookups require mempool/electrs backend.'); res.status(405).send('Address summary lookups require mempool/electrs backend.');
return; return;
} }
} }
private async getScriptHash(req: Request, res: Response) { private async getScriptHash(req: Request, res: Response) {
if (config.MEMPOOL.BACKEND === 'none') { if (config.MEMPOOL.BACKEND === 'none') {
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
return;
}
if (!SCRIPT_HASH_REGEX.test(req.params.scripthash)) {
handleError(req, res, 501, `Invalid scripthash`);
return; return;
} }
@@ -661,20 +555,15 @@ class BitcoinRoutes {
res.json(addressData); res.json(addressData);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
handleError(req, res, 413, e.message); return res.status(413).send(e instanceof Error ? e.message : e);
return;
} }
handleError(req, res, 500, 'Failed to get script hash'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
private async getScriptHashTransactions(req: Request, res: Response): Promise<void> { private async getScriptHashTransactions(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND === 'none') { if (config.MEMPOOL.BACKEND === 'none') {
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
return;
}
if (!SCRIPT_HASH_REGEX.test(req.params.scripthash)) {
handleError(req, res, 501, `Invalid scripthash`);
return; return;
} }
@@ -689,26 +578,26 @@ class BitcoinRoutes {
res.json(transactions); res.json(transactions);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
handleError(req, res, 413, e.message); res.status(413).send(e instanceof Error ? e.message : e);
return; return;
} }
handleError(req, res, 500, 'Failed to get script hash transactions'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
private async getScriptHashTransactionSummary(req: Request, res: Response): Promise<void> { private async getScriptHashTransactionSummary(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND !== 'esplora') { if (config.MEMPOOL.BACKEND !== 'esplora') {
handleError(req, res, 405, 'Scripthash summary lookups require mempool/electrs backend.'); res.status(405).send('Scripthash summary lookups require mempool/electrs backend.');
return; return;
} }
} }
private async getAddressPrefix(req: Request, res: Response) { private async getAddressPrefix(req: Request, res: Response) {
try { try {
const addressPrefix = await bitcoinApi.$getAddressPrefix(req.params.prefix); const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
res.send(addressPrefix); res.send(blockHash);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get address prefix'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -735,7 +624,7 @@ class BitcoinRoutes {
const rawMempool = await bitcoinApi.$getRawMempool(); const rawMempool = await bitcoinApi.$getRawMempool();
res.send(rawMempool); res.send(rawMempool);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -743,13 +632,12 @@ class BitcoinRoutes {
try { try {
const result = blocks.getCurrentBlockHeight(); const result = blocks.getCurrentBlockHeight();
if (!result) { if (!result) {
handleError(req, res, 503, `Service Temporarily Unavailable`); return res.status(503).send(`Service Temporarily Unavailable`);
return;
} }
res.setHeader('content-type', 'text/plain'); res.setHeader('content-type', 'text/plain');
res.send(result.toString()); res.send(result.toString());
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get height at tip'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -759,55 +647,39 @@ class BitcoinRoutes {
res.setHeader('content-type', 'text/plain'); res.setHeader('content-type', 'text/plain');
res.send(result); res.send(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get hash at tip'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
private async getRawBlock(req: Request, res: Response) { private async getRawBlock(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try { try {
const result = await bitcoinApi.$getRawBlock(req.params.hash); const result = await bitcoinApi.$getRawBlock(req.params.hash);
res.setHeader('content-type', 'application/octet-stream'); res.setHeader('content-type', 'application/octet-stream');
res.send(result); res.send(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get raw block'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
private async getTxIdsForBlock(req: Request, res: Response) { private async getTxIdsForBlock(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try { try {
const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash); const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
res.json(result); res.json(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get txids for block'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
private async validateAddress(req: Request, res: Response) { private async validateAddress(req: Request, res: Response) {
if (!ADDRESS_REGEX.test(req.params.address)) {
handleError(req, res, 501, `Invalid address`);
return;
}
try { try {
const result = await bitcoinClient.validateAddress(req.params.address); const result = await bitcoinClient.validateAddress(req.params.address);
res.json(result); res.json(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to validate address'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
private async getRbfHistory(req: Request, res: Response) { private async getRbfHistory(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try { try {
const replacements = rbfCache.getRbfTree(req.params.txId) || null; const replacements = rbfCache.getRbfTree(req.params.txId) || null;
const replaces = rbfCache.getReplaces(req.params.txId) || null; const replaces = rbfCache.getReplaces(req.params.txId) || null;
@@ -816,7 +688,7 @@ class BitcoinRoutes {
replaces replaces
}); });
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get rbf history'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -825,7 +697,7 @@ class BitcoinRoutes {
const result = rbfCache.getRbfTrees(false); const result = rbfCache.getRbfTrees(false);
res.json(result); res.json(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get rbf trees'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -834,15 +706,11 @@ class BitcoinRoutes {
const result = rbfCache.getRbfTrees(true); const result = rbfCache.getRbfTrees(true);
res.json(result); res.json(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get full rbf replacements'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
private async getCachedTx(req: Request, res: Response) { private async getCachedTx(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try { try {
const result = rbfCache.getTx(req.params.txId); const result = rbfCache.getTx(req.params.txId);
if (result) { if (result) {
@@ -851,20 +719,16 @@ class BitcoinRoutes {
res.status(204).send(); res.status(204).send();
} }
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get cached tx'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
private async getTransactionOutspends(req: Request, res: Response) { private async getTransactionOutspends(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try { try {
const result = await bitcoinApi.$getOutspends(req.params.txId); const result = await bitcoinApi.$getOutspends(req.params.txId);
res.json(result); res.json(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get transaction outspends'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -874,10 +738,10 @@ class BitcoinRoutes {
if (da) { if (da) {
res.json(da); res.json(da);
} else { } else {
handleError(req, res, 503, `Service Temporarily Unavailable`); res.status(503).send(`Service Temporarily Unavailable`);
} }
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get difficulty change'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -888,8 +752,8 @@ class BitcoinRoutes {
const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx); const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx);
res.send(txIdResult); res.send(txIdResult);
} catch (e: any) { } catch (e: any) {
handleError(req, res, 400, (e.message && e.code) ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code }) res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: 'Failed to send raw transaction'); : (e.message || 'Error'));
} }
} }
@@ -900,8 +764,8 @@ class BitcoinRoutes {
const txIdResult = await bitcoinClient.sendRawTransaction(txHex); const txIdResult = await bitcoinClient.sendRawTransaction(txHex);
res.send(txIdResult); res.send(txIdResult);
} catch (e: any) { } catch (e: any) {
handleError(req, res, 400, (e.message && e.code) ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code }) res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: 'Failed to send raw transaction'); : (e.message || 'Error'));
} }
} }
@@ -912,21 +776,9 @@ class BitcoinRoutes {
const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate); const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate);
res.send(result); res.send(result);
} catch (e: any) { } catch (e: any) {
handleError(req, res, 400, (e.message && e.code) ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code }) res.setHeader('content-type', 'text/plain');
: 'Failed to test transactions'); res.status(400).send(e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
} : (e.message || 'Error'));
}
private async $submitPackage(req: Request, res: Response) {
try {
const rawTxs = Common.getTransactionsFromRequest(req);
const maxfeerate = parseFloat(req.query.maxfeerate as string);
const maxburnamount = parseFloat(req.query.maxburnamount as string);
const result = await bitcoinClient.submitPackage(rawTxs, maxfeerate ?? undefined, maxburnamount ?? undefined);
res.send(result);
} catch (e: any) {
handleError(req, res, 400, (e.message && e.code) ? 'submitpackage RPC error: ' + JSON.stringify({ code: e.code })
: 'Failed to submit package');
} }
} }

View File

@@ -179,11 +179,4 @@ export namespace IEsploraApi {
burn_count: number; burn_count: number;
} }
export interface AddressTxSummary {
txid: string;
value: number;
height: number;
time: number;
tx_position?: number;
}
} }

View File

@@ -1,12 +1,12 @@
import config from '../../config'; import config from '../../config';
import axios, { isAxiosError } from 'axios'; import axios, { AxiosResponse, isAxiosError } from 'axios';
import http from 'http'; import http from 'http';
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory'; import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
import { IEsploraApi } from './esplora-api.interface'; import { IEsploraApi } from './esplora-api.interface';
import logger from '../../logger'; import logger from '../../logger';
import { Common } from '../common'; import { Common } from '../common';
import { SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface'; import { TestMempoolAcceptResult } from './bitcoin-api.interface';
import os from 'os';
interface FailoverHost { interface FailoverHost {
host: string, host: string,
rtts: number[], rtts: number[],
@@ -20,13 +20,6 @@ interface FailoverHost {
preferred?: boolean, preferred?: boolean,
checked: boolean, checked: boolean,
lastChecked?: number, lastChecked?: number,
publicDomain: string,
hashes: {
frontend?: string,
backend?: string,
electrs?: string,
lastUpdated: number,
}
} }
class FailoverRouter { class FailoverRouter {
@@ -36,21 +29,14 @@ class FailoverRouter {
maxHeight: number = 0; maxHeight: number = 0;
hosts: FailoverHost[]; hosts: FailoverHost[];
multihost: boolean; multihost: boolean;
gitHashInterval: number = 600000; // 10 minutes pollInterval: number = 60000;
pollInterval: number = 60000; // 1 minute
pollTimer: NodeJS.Timeout | null = null; pollTimer: NodeJS.Timeout | null = null;
pollConnection = axios.create(); pollConnection = axios.create();
localHostname: string = 'localhost';
requestConnection = axios.create({ requestConnection = axios.create({
httpAgent: new http.Agent({ keepAlive: true }) httpAgent: new http.Agent({ keepAlive: true })
}); });
constructor() { constructor() {
try {
this.localHostname = os.hostname();
} catch (e) {
logger.warn('Failed to set local hostname, using "localhost"');
}
// setup list of hosts // setup list of hosts
this.hosts = (config.ESPLORA.FALLBACK || []).map(domain => { this.hosts = (config.ESPLORA.FALLBACK || []).map(domain => {
return { return {
@@ -59,10 +45,6 @@ class FailoverRouter {
rtts: [], rtts: [],
rtt: Infinity, rtt: Infinity,
failures: 0, failures: 0,
publicDomain: 'https://' + this.extractPublicDomain(domain),
hashes: {
lastUpdated: 0,
},
}; };
}); });
this.activeHost = { this.activeHost = {
@@ -73,10 +55,6 @@ class FailoverRouter {
socket: !!config.ESPLORA.UNIX_SOCKET_PATH, socket: !!config.ESPLORA.UNIX_SOCKET_PATH,
preferred: true, preferred: true,
checked: false, checked: false,
publicDomain: `http://${this.localHostname}`,
hashes: {
lastUpdated: 0,
},
}; };
this.fallbackHost = this.activeHost; this.fallbackHost = this.activeHost;
this.hosts.unshift(this.activeHost); this.hosts.unshift(this.activeHost);
@@ -128,24 +106,6 @@ class FailoverRouter {
host.outOfSync = false; host.outOfSync = false;
} }
host.unreachable = false; host.unreachable = false;
// update esplora git hash using the x-powered-by header from the height check
const poweredBy = result.headers['x-powered-by'];
if (poweredBy) {
const match = poweredBy.match(/([a-fA-F0-9]{5,40})/);
if (match && match[1]?.length) {
host.hashes.electrs = match[1];
}
}
// Check front and backend git hashes less often
if (Date.now() - host.hashes.lastUpdated > this.gitHashInterval) {
await Promise.all([
this.$updateFrontendGitHash(host),
this.$updateBackendGitHash(host)
]);
host.hashes.lastUpdated = Date.now();
}
} else { } else {
host.outOfSync = true; host.outOfSync = true;
host.unreachable = true; host.unreachable = true;
@@ -242,47 +202,6 @@ class FailoverRouter {
} }
} }
// methods for retrieving git hashes by host
private async $updateFrontendGitHash(host: FailoverHost): Promise<void> {
try {
const url = `${host.publicDomain}/resources/config.js`;
const response = await this.pollConnection.get<string>(url, { timeout: config.ESPLORA.FALLBACK_TIMEOUT });
const match = response.data.match(/GIT_COMMIT_HASH\s*=\s*['"](.*?)['"]/);
if (match && match[1]?.length) {
host.hashes.frontend = match[1];
}
} catch (e) {
// failed to get frontend build hash - do nothing
}
}
private async $updateBackendGitHash(host: FailoverHost): Promise<void> {
try {
const url = `${host.publicDomain}/api/v1/backend-info`;
const response = await this.pollConnection.get<any>(url, { timeout: config.ESPLORA.FALLBACK_TIMEOUT });
if (response.data?.gitCommit) {
host.hashes.backend = response.data.gitCommit;
}
} catch (e) {
// failed to get backend build hash - do nothing
}
}
// returns the public mempool domain corresponding to an esplora server url
// (a bit of a hack to avoid manually specifying frontend & backend URLs for each esplora server)
private extractPublicDomain(url: string): string {
// force the url to start with a valid protocol
const urlWithProtocol = url.startsWith('http') ? url : `https://${url}`;
// parse as URL and extract the hostname
try {
const parsed = new URL(urlWithProtocol);
return parsed.hostname;
} catch (e) {
// fallback to the original url
return url;
}
}
private async $query<T>(method: 'get'| 'post', path, data: any, responseType = 'json', host = this.activeHost, retry: boolean = true): Promise<T> { private async $query<T>(method: 'get'| 'post', path, data: any, responseType = 'json', host = this.activeHost, retry: boolean = true): Promise<T> {
let axiosConfig; let axiosConfig;
let url; let url;
@@ -386,7 +305,7 @@ class ElectrsApi implements AbstractBitcoinApi {
} }
$getAddress(address: string): Promise<IEsploraApi.Address> { $getAddress(address: string): Promise<IEsploraApi.Address> {
return this.failoverRouter.$get<IEsploraApi.Address>('/address/' + address); throw new Error('Method getAddress not implemented.');
} }
$getAddressTransactions(address: string, txId?: string): Promise<IEsploraApi.Transaction[]> { $getAddressTransactions(address: string, txId?: string): Promise<IEsploraApi.Transaction[]> {
@@ -413,10 +332,6 @@ class ElectrsApi implements AbstractBitcoinApi {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
$submitPackage(rawTransactions: string[]): Promise<SubmitPackageResult> {
throw new Error('Method not implemented.');
}
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> { $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
return this.failoverRouter.$get<IEsploraApi.Outspend>('/tx/' + txId + '/outspend/' + vout); return this.failoverRouter.$get<IEsploraApi.Outspend>('/tx/' + txId + '/outspend/' + vout);
} }
@@ -442,10 +357,6 @@ class ElectrsApi implements AbstractBitcoinApi {
return this.failoverRouter.$get<IEsploraApi.Transaction>('/tx/' + txid); return this.failoverRouter.$get<IEsploraApi.Transaction>('/tx/' + txid);
} }
async $getAddressTransactionSummary(address: string): Promise<IEsploraApi.AddressTxSummary[]> {
return this.failoverRouter.$get<IEsploraApi.AddressTxSummary[]>('/address/' + address + '/txs/summary');
}
public startHealthChecks(): void { public startHealthChecks(): void {
this.failoverRouter.startHealthChecks(); this.failoverRouter.startHealthChecks();
} }
@@ -462,7 +373,6 @@ class ElectrsApi implements AbstractBitcoinApi {
unreachable: !!host.unreachable, unreachable: !!host.unreachable,
checked: !!host.checked, checked: !!host.checked,
lastChecked: host.lastChecked || 0, lastChecked: host.lastChecked || 0,
hashes: host.hashes,
})); }));
} else { } else {
return []; return [];

View File

@@ -34,7 +34,6 @@ import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp';
import mempool from './mempool'; import mempool from './mempool';
import CpfpRepository from '../repositories/CpfpRepository'; import CpfpRepository from '../repositories/CpfpRepository';
import accelerationApi from './services/acceleration'; import accelerationApi from './services/acceleration';
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
class Blocks { class Blocks {
private blocks: BlockExtended[] = []; private blocks: BlockExtended[] = [];
@@ -343,12 +342,7 @@ class Blocks {
id: pool.uniqueId, id: pool.uniqueId,
name: pool.name, name: pool.name,
slug: pool.slug, slug: pool.slug,
minerNames: null,
}; };
if (extras.pool.name === 'OCEAN') {
extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw);
}
} }
extras.matchRate = null; extras.matchRate = null;
@@ -412,16 +406,8 @@ class Blocks {
} }
try { try {
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
const currentBlockHeight = blockchainInfo.blocks;
let indexingBlockAmount = Math.min(config.MEMPOOL.INDEXING_BLOCKS_AMOUNT, currentBlockHeight);
if (indexingBlockAmount <= -1) {
indexingBlockAmount = currentBlockHeight + 1;
}
const lastBlockToIndex = Math.max(0, currentBlockHeight - indexingBlockAmount + 1);
// Get all indexed block hash // Get all indexed block hash
const indexedBlocks = (await blocksRepository.$getIndexedBlocks()).filter(block => block.height >= lastBlockToIndex); const indexedBlocks = await blocksRepository.$getIndexedBlocks();
const indexedBlockSummariesHashesArray = await BlocksSummariesRepository.$getIndexedSummariesId(); const indexedBlockSummariesHashesArray = await BlocksSummariesRepository.$getIndexedSummariesId();
const indexedBlockSummariesHashes = {}; // Use a map for faster seek during the indexing loop const indexedBlockSummariesHashes = {}; // Use a map for faster seek during the indexing loop
@@ -1224,11 +1210,6 @@ class Blocks {
return summary.transactions; return summary.transactions;
} }
public async $getSingleTxFromSummary(hash: string, txid: string): Promise<TransactionClassified | null> {
const txs = await this.$getStrippedBlockTransactions(hash);
return txs.find(tx => tx.txid === txid) || null;
}
/** /**
* Get 15 blocks * Get 15 blocks
* *

View File

@@ -79,8 +79,8 @@ export class Common {
return arr; return arr;
} }
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} { static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: MempoolTransactionExtended[] } {
const matches: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = {}; const matches: { [txid: string]: MempoolTransactionExtended[] } = {};
// For small N, a naive nested loop is extremely fast, but it doesn't scale // For small N, a naive nested loop is extremely fast, but it doesn't scale
if (added.length < 1000 && deleted.length < 50 && !forceScalable) { if (added.length < 1000 && deleted.length < 50 && !forceScalable) {
@@ -95,7 +95,7 @@ export class Common {
addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout)); addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
}); });
if (foundMatches?.length) { if (foundMatches?.length) {
matches[addedTx.txid] = { replaced: [...new Set(foundMatches)], replacedBy: addedTx }; matches[addedTx.txid] = [...new Set(foundMatches)];
} }
}); });
} else { } else {
@@ -123,7 +123,7 @@ export class Common {
foundMatches.add(deletedTx); foundMatches.add(deletedTx);
} }
if (foundMatches.size) { if (foundMatches.size) {
matches[addedTx.txid] = { replaced: [...foundMatches], replacedBy: addedTx }; matches[addedTx.txid] = [...foundMatches];
} }
} }
} }
@@ -138,17 +138,17 @@ export class Common {
const replaced: Set<MempoolTransactionExtended> = new Set(); const replaced: Set<MempoolTransactionExtended> = new Set();
for (let i = 0; i < tx.vin.length; i++) { for (let i = 0; i < tx.vin.length; i++) {
const vin = tx.vin[i]; const vin = tx.vin[i];
const key = `${vin.txid}:${vin.vout}`; const match = spendMap.get(`${vin.txid}:${vin.vout}`);
const match = spendMap.get(key);
if (match && match.txid !== tx.txid) { if (match && match.txid !== tx.txid) {
replaced.add(match); replaced.add(match);
// remove this tx from the spendMap // remove this tx from the spendMap
// prevents the same tx being replaced more than once // prevents the same tx being replaced more than once
for (const replacedVin of match.vin) { for (const replacedVin of match.vin) {
const replacedKey = `${replacedVin.txid}:${replacedVin.vout}`; const key = `${replacedVin.txid}:${replacedVin.vout}`;
spendMap.delete(replacedKey); spendMap.delete(key);
} }
} }
const key = `${vin.txid}:${vin.vout}`;
spendMap.delete(key); spendMap.delete(key);
} }
if (replaced.size) { if (replaced.size) {

View File

@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2'; import { RowDataPacket } from 'mysql2';
class DatabaseMigration { class DatabaseMigration {
private static currentVersion = 94; private static currentVersion = 83;
private queryTimeout = 3600_000; private queryTimeout = 3600_000;
private statisticsAddedIndexed = false; private statisticsAddedIndexed = false;
private uniqueLogs: string[] = []; private uniqueLogs: string[] = [];
@@ -706,418 +706,10 @@ class DatabaseMigration {
await this.updateToSchemaVersion(82); await this.updateToSchemaVersion(82);
} }
if (databaseSchemaVersion < 83 && isBitcoin === true) { if (databaseSchemaVersion < 83) {
await this.$executeQuery('ALTER TABLE `blocks` ADD first_seen datetime(6) DEFAULT NULL'); await this.$addPerSecondVsizeToStatistics();
await this.updateToSchemaVersion(83); await this.updateToSchemaVersion(83);
} }
// add new pools indexes
if (databaseSchemaVersion < 84 && isBitcoin === true) {
await this.$executeQuery(`
ALTER TABLE \`pools\`
ADD INDEX \`slug\` (\`slug\`),
ADD INDEX \`unique_id\` (\`unique_id\`)
`);
await this.updateToSchemaVersion(84);
}
// lightning channels indexes
if (databaseSchemaVersion < 85 && isBitcoin === true) {
await this.$executeQuery(`
ALTER TABLE \`channels\`
ADD INDEX \`created\` (\`created\`),
ADD INDEX \`capacity\` (\`capacity\`),
ADD INDEX \`closing_reason\` (\`closing_reason\`),
ADD INDEX \`closing_resolved\` (\`closing_resolved\`)
`);
await this.updateToSchemaVersion(85);
}
// lightning nodes indexes
if (databaseSchemaVersion < 86 && isBitcoin === true) {
await this.$executeQuery(`
ALTER TABLE \`nodes\`
ADD INDEX \`status\` (\`status\`),
ADD INDEX \`channels\` (\`channels\`),
ADD INDEX \`country_id\` (\`country_id\`),
ADD INDEX \`as_number\` (\`as_number\`),
ADD INDEX \`first_seen\` (\`first_seen\`)
`);
await this.updateToSchemaVersion(86);
}
// lightning node sockets indexes
if (databaseSchemaVersion < 87 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `nodes_sockets` ADD INDEX `type` (`type`)');
await this.updateToSchemaVersion(87);
}
// lightning stats indexes
if (databaseSchemaVersion < 88 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD INDEX `added` (`added`)');
await this.updateToSchemaVersion(88);
}
// geo names indexes
if (databaseSchemaVersion < 89 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `geo_names` ADD INDEX `names` (`names`)');
await this.updateToSchemaVersion(89);
}
// hashrates indexes
if (databaseSchemaVersion < 90 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `hashrates` ADD INDEX `type` (`type`)');
await this.updateToSchemaVersion(90);
}
// block audits indexes
if (databaseSchemaVersion < 91 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `time` (`time`)');
await this.updateToSchemaVersion(91);
}
// elements_pegs indexes
if (databaseSchemaVersion < 92 && config.MEMPOOL.NETWORK === 'liquid') {
await this.$executeQuery(`
ALTER TABLE \`elements_pegs\`
ADD INDEX \`block\` (\`block\`),
ADD INDEX \`datetime\` (\`datetime\`),
ADD INDEX \`amount\` (\`amount\`),
ADD INDEX \`bitcoinaddress\` (\`bitcoinaddress\`),
ADD INDEX \`bitcointxid\` (\`bitcointxid\`)
`);
await this.updateToSchemaVersion(92);
}
// federation_txos indexes
if (databaseSchemaVersion < 93 && config.MEMPOOL.NETWORK === 'liquid') {
await this.$executeQuery(`
ALTER TABLE \`federation_txos\`
ADD INDEX \`unspent\` (\`unspent\`),
ADD INDEX \`lastblockupdate\` (\`lastblockupdate\`),
ADD INDEX \`blocktime\` (\`blocktime\`),
ADD INDEX \`emergencyKey\` (\`emergencyKey\`),
ADD INDEX \`expiredAt\` (\`expiredAt\`)
`);
await this.updateToSchemaVersion(93);
}
// Unify database schema for all mempool netwoks
// versions above 94 should not use network-specific flags
if (databaseSchemaVersion < 94) {
if (!isBitcoin) {
// Apply all the bitcoin specific migrations to non-bitcoin networks: liquid, liquidtestnet and testnet4 (!)
// Version 5
await this.$executeQuery('ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"');
// Version 6
await this.$executeQuery('ALTER TABLE blocks MODIFY `height` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `tx_count` smallint unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `size` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `weight` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `difficulty` double NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks DROP FOREIGN KEY IF EXISTS `blocks_ibfk_1`');
await this.$executeQuery('ALTER TABLE pools MODIFY `id` smallint unsigned AUTO_INCREMENT');
await this.$executeQuery('ALTER TABLE blocks MODIFY `pool_id` smallint unsigned NULL');
await this.$executeQuery('ALTER TABLE blocks ADD FOREIGN KEY (`pool_id`) REFERENCES `pools` (`id`)');
await this.$executeQuery('ALTER TABLE blocks ADD `version` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks ADD `bits` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks ADD `nonce` bigint unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks ADD `merkle_root` varchar(65) NOT NULL DEFAULT ""');
await this.$executeQuery('ALTER TABLE blocks ADD `previous_block_hash` varchar(65) NULL');
// Version 7
await this.$executeQuery('DROP table IF EXISTS hashrates;');
await this.$executeQuery(this.getCreateDailyStatsTableQuery(), await this.$checkIfTableExists('hashrates'));
// Version 8
await this.$executeQuery('ALTER TABLE `hashrates` DROP INDEX `PRIMARY`');
await this.$executeQuery('ALTER TABLE `hashrates` ADD `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST');
await this.$executeQuery('ALTER TABLE `hashrates` ADD `share` float NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `hashrates` ADD `type` enum("daily", "weekly") DEFAULT "daily"');
// Version 9
await this.$executeQuery('ALTER TABLE `state` CHANGE `name` `name` varchar(100)');
await this.$executeQuery('ALTER TABLE `hashrates` ADD UNIQUE `hashrate_timestamp_pool_id` (`hashrate_timestamp`, `pool_id`)');
// Version 10
await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `blockTimestamp` (`blockTimestamp`)');
// Version 11
await this.$executeQuery(`ALTER TABLE blocks
ADD avg_fee INT UNSIGNED NULL,
ADD avg_fee_rate INT UNSIGNED NULL
`);
await this.$executeQuery('ALTER TABLE blocks MODIFY `reward` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` INT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` INT UNSIGNED NOT NULL DEFAULT "0"');
// Version 12
await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
// Version 13
await this.$executeQuery('ALTER TABLE blocks MODIFY `difficulty` DOUBLE UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee_rate` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
// Version 14
await this.$executeQuery('ALTER TABLE `hashrates` DROP FOREIGN KEY `hashrates_ibfk_1`');
await this.$executeQuery('ALTER TABLE `hashrates` MODIFY `pool_id` SMALLINT UNSIGNED NOT NULL DEFAULT "0"');
// Version 17
await this.$executeQuery('ALTER TABLE `pools` ADD `slug` CHAR(50) NULL');
// Version 18
await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `hash` (`hash`);');
// Version 20
await this.$executeQuery(this.getCreateBlocksSummariesTableQuery(), await this.$checkIfTableExists('blocks_summaries'));
// Version 22
await this.$executeQuery('DROP TABLE IF EXISTS `difficulty_adjustments`');
await this.$executeQuery(this.getCreateDifficultyAdjustmentsTableQuery(), await this.$checkIfTableExists('difficulty_adjustments'));
// Version 24
await this.$executeQuery('DROP TABLE IF EXISTS `blocks_audits`');
await this.$executeQuery(this.getCreateBlocksAuditsTableQuery(), await this.$checkIfTableExists('blocks_audits'));
// Version 25
await this.$executeQuery(this.getCreateLightningStatisticsQuery(), await this.$checkIfTableExists('lightning_stats'));
await this.$executeQuery(this.getCreateNodesQuery(), await this.$checkIfTableExists('nodes'));
await this.$executeQuery(this.getCreateChannelsQuery(), await this.$checkIfTableExists('channels'));
await this.$executeQuery(this.getCreateNodesStatsQuery(), await this.$checkIfTableExists('node_stats'));
// Version 26
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD tor_nodes int(11) NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_nodes int(11) NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD unannounced_nodes int(11) NOT NULL DEFAULT "0"');
// Version 27
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_capacity bigint(20) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_fee_rate int(11) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_base_fee_mtokens bigint(20) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_capacity bigint(20) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_fee_rate int(11) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_base_fee_mtokens bigint(20) unsigned NOT NULL DEFAULT "0"');
// Version 28
await this.$executeQuery(`ALTER TABLE lightning_stats MODIFY added DATE`);
// Version 29
await this.$executeQuery(this.getCreateGeoNamesTableQuery(), await this.$checkIfTableExists('geo_names'));
await this.$executeQuery('ALTER TABLE `nodes` ADD as_number int(11) unsigned NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD city_id int(11) unsigned NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD country_id int(11) unsigned NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD accuracy_radius int(11) unsigned NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD subdivision_id int(11) unsigned NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD longitude double NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD latitude double NULL DEFAULT NULL');
// Version 30
await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization") NOT NULL');
// Version 31
await this.$executeQuery('ALTER TABLE `prices` ADD `id` int NULL AUTO_INCREMENT UNIQUE');
await this.$executeQuery('DROP TABLE IF EXISTS `blocks_prices`');
await this.$executeQuery(this.getCreateBlocksPricesTableQuery(), await this.$checkIfTableExists('blocks_prices'));
// Version 32
await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD `template` JSON DEFAULT "[]"');
// Version 33
await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization", "country_iso_code") NOT NULL');
// Version 34
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_tor_nodes int(11) NOT NULL DEFAULT "0"');
// Version 35
await this.$executeQuery('DELETE from `lightning_stats` WHERE added > "2021-09-19"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD CONSTRAINT added_unique UNIQUE (added);');
// Version 36
await this.$executeQuery('ALTER TABLE `nodes` ADD status TINYINT NOT NULL DEFAULT "1"');
// Version 37
await this.$executeQuery(this.getCreateLNNodesSocketsTableQuery(), await this.$checkIfTableExists('nodes_sockets'));
// Version 38
await this.$executeQuery(`TRUNCATE lightning_stats`);
await this.$executeQuery(`TRUNCATE node_stats`);
await this.$executeQuery('ALTER TABLE `lightning_stats` CHANGE `added` `added` timestamp NULL');
await this.$executeQuery('ALTER TABLE `node_stats` CHANGE `added` `added` timestamp NULL');
await this.updateToSchemaVersion(38);
// Version 39
await this.$executeQuery('ALTER TABLE `nodes` ADD alias_search TEXT NULL DEFAULT NULL AFTER `alias`');
await this.$executeQuery('ALTER TABLE nodes ADD FULLTEXT(alias_search)');
// Version 40
await this.$executeQuery('ALTER TABLE `nodes` ADD capacity bigint(20) unsigned DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD channels int(11) unsigned DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD INDEX `capacity` (`capacity`);');
// Version 41
await this.$executeQuery('UPDATE channels SET closing_reason = NULL WHERE closing_reason = 1');
// Version 42
await this.$executeQuery('ALTER TABLE `channels` ADD closing_resolved tinyint(1) DEFAULT 0');
// Version 43
await this.$executeQuery(this.getCreateLNNodeRecordsTableQuery(), await this.$checkIfTableExists('nodes_records'));
// Version 44
await this.$executeQuery('UPDATE blocks_summaries SET template = NULL');
// Version 45
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD fresh_txs JSON DEFAULT "[]"');
// Version 48
await this.$executeQuery('ALTER TABLE `channels` ADD source_checked tinyint(1) DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD closing_fee bigint(20) unsigned DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD node1_funding_balance bigint(20) unsigned DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD node2_funding_balance bigint(20) unsigned DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD node1_closing_balance bigint(20) unsigned DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD node2_closing_balance bigint(20) unsigned DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD funding_ratio float unsigned DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `channels` ADD closed_by varchar(66) DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `channels` ADD single_funded tinyint(1) DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD outputs JSON DEFAULT "[]"');
// Version 57
await this.$executeQuery(`ALTER TABLE nodes MODIFY updated_at datetime NULL`);
// Version 60
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD sigop_txs JSON DEFAULT "[]"');
// Version 61
if (! await this.$checkIfTableExists('blocks_templates')) {
await this.$executeQuery('CREATE TABLE blocks_templates AS SELECT id, template FROM blocks_summaries WHERE template != "[]"');
}
await this.$executeQuery('ALTER TABLE blocks_templates MODIFY template JSON DEFAULT "[]"');
await this.$executeQuery('ALTER TABLE blocks_templates ADD PRIMARY KEY (id)');
await this.$executeQuery('ALTER TABLE blocks_summaries DROP COLUMN template');
// Version 62
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD expected_fees BIGINT UNSIGNED DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD expected_weight BIGINT UNSIGNED DEFAULT NULL');
// Version 63
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD fullrbf_txs JSON DEFAULT "[]"');
// Version 64
await this.$executeQuery('ALTER TABLE `nodes` ADD features text NULL');
// Version 65
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD accelerated_txs JSON DEFAULT "[]"');
// Version 67
await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD version INT NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD INDEX `version` (`version`)');
await this.$executeQuery('ALTER TABLE `blocks_templates` ADD version INT NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `blocks_templates` ADD INDEX `version` (`version`)');
// Version 76
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD prioritized_txs JSON DEFAULT "[]"');
// Version 81
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD version INT NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `version` (`version`)');
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"');
// Version 83
await this.$executeQuery('ALTER TABLE `blocks` ADD first_seen datetime(6) DEFAULT NULL');
// Version 84
await this.$executeQuery(`
ALTER TABLE \`pools\`
ADD INDEX \`slug\` (\`slug\`),
ADD INDEX \`unique_id\` (\`unique_id\`)
`);
// Version 85
await this.$executeQuery(`
ALTER TABLE \`channels\`
ADD INDEX \`created\` (\`created\`),
ADD INDEX \`capacity\` (\`capacity\`),
ADD INDEX \`closing_reason\` (\`closing_reason\`),
ADD INDEX \`closing_resolved\` (\`closing_resolved\`)
`);
// Version 86
await this.$executeQuery(`
ALTER TABLE \`nodes\`
ADD INDEX \`status\` (\`status\`),
ADD INDEX \`channels\` (\`channels\`),
ADD INDEX \`country_id\` (\`country_id\`),
ADD INDEX \`as_number\` (\`as_number\`),
ADD INDEX \`first_seen\` (\`first_seen\`)
`);
// Version 87
await this.$executeQuery('ALTER TABLE `nodes_sockets` ADD INDEX `type` (`type`)');
await this.updateToSchemaVersion(87);
// Version 88
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD INDEX `added` (`added`)');
// Version 89
await this.$executeQuery('ALTER TABLE `geo_names` ADD INDEX `names` (`names`)');
// Version 90
await this.$executeQuery('ALTER TABLE `hashrates` ADD INDEX `type` (`type`)');
// Version 91
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `time` (`time`)');
}
if (config.MEMPOOL.NETWORK !== 'liquid') {
// Apply all the liquid specific migrations to all other networks
// Version 68
await this.$executeQuery('ALTER TABLE elements_pegs ADD PRIMARY KEY (txid, txindex);');
await this.$executeQuery(this.getCreateFederationAddressesTableQuery(), await this.$checkIfTableExists('federation_addresses'));
await this.$executeQuery(this.getCreateFederationTxosTableQuery(), await this.$checkIfTableExists('federation_txos'));
// Version 71
await this.$executeQuery('ALTER TABLE `federation_txos` ADD timelock INT NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `federation_txos` ADD expiredAt INT NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `federation_txos` ADD emergencyKey TINYINT NOT NULL DEFAULT 0');
// Version 92
await this.$executeQuery(`
ALTER TABLE \`elements_pegs\`
ADD INDEX \`block\` (\`block\`),
ADD INDEX \`datetime\` (\`datetime\`),
ADD INDEX \`amount\` (\`amount\`),
ADD INDEX \`bitcoinaddress\` (\`bitcoinaddress\`),
ADD INDEX \`bitcointxid\` (\`bitcointxid\`)
`);
// Version 93
await this.$executeQuery(`
ALTER TABLE \`federation_txos\`
ADD INDEX \`unspent\` (\`unspent\`),
ADD INDEX \`lastblockupdate\` (\`lastblockupdate\`),
ADD INDEX \`blocktime\` (\`blocktime\`),
ADD INDEX \`emergencyKey\` (\`emergencyKey\`),
ADD INDEX \`expiredAt\` (\`expiredAt\`)
`);
}
if (config.MEMPOOL.NETWORK !== 'mainnet') {
// Apply all the mainnet specific migrations to all other networks
// Version 69
await this.$executeQuery(this.getCreateAccelerationsTableQuery(), await this.$checkIfTableExists('accelerations'));
// Version 70
await this.$executeQuery('ALTER TABLE accelerations MODIFY COLUMN added DATETIME;');
// Version 77
await this.$executeQuery('ALTER TABLE `accelerations` ADD requested datetime DEFAULT NULL');
}
await this.updateToSchemaVersion(94);
}
} }
/** /**
@@ -1754,6 +1346,47 @@ class DatabaseMigration {
} }
} }
} }
private async $addPerSecondVsizeToStatistics(): Promise<void> {
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_1` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_2` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_3` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_4` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_5` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_6` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_8` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_10` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_12` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_15` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_20` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_30` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_40` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_50` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_60` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_70` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_80` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_90` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_100` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_125` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_150` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_175` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_200` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_250` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_300` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_350` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_400` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_500` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_600` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_700` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_800` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_900` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_1000` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_1200` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_1400` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_1600` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_1800` int(11) NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_2000` int(11) NOT NULL DEFAULT 0');
}
} }
export default new DatabaseMigration(); export default new DatabaseMigration();

View File

@@ -257,7 +257,6 @@ class DiskCache {
trees: rbfData.rbf.trees, trees: rbfData.rbf.trees,
expiring: rbfData.rbf.expiring.map(([txid, value]) => ({ key: txid, value })), expiring: rbfData.rbf.expiring.map(([txid, value]) => ({ key: txid, value })),
mempool: memPool.getMempool(), mempool: memPool.getMempool(),
spendMap: memPool.getSpendMap(),
}); });
} }
} catch (e) { } catch (e) {

View File

@@ -1,9 +1,6 @@
import config from '../../config'; import config from '../../config';
import { Application, Request, Response } from 'express'; import { Application, Request, Response } from 'express';
import channelsApi from './channels.api'; import channelsApi from './channels.api';
import { handleError } from '../../utils/api';
const TXID_REGEX = /^[a-f0-9]{64}$/i;
class ChannelsRoutes { class ChannelsRoutes {
constructor() { } constructor() { }
@@ -25,7 +22,7 @@ class ChannelsRoutes {
const channels = await channelsApi.$searchChannelsById(req.params.search); const channels = await channelsApi.$searchChannelsById(req.params.search);
res.json(channels); res.json(channels);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to search channels by id'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -41,7 +38,7 @@ class ChannelsRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channel); res.json(channel);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get channel'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -56,11 +53,11 @@ class ChannelsRoutes {
const status: string = typeof req.query.status === 'string' ? req.query.status : ''; const status: string = typeof req.query.status === 'string' ? req.query.status : '';
if (index < -1) { if (index < -1) {
handleError(req, res, 400, 'Invalid index'); res.status(400).send('Invalid index');
return; return;
} }
if (['open', 'active', 'closed'].includes(status) === false) { if (['open', 'active', 'closed'].includes(status) === false) {
handleError(req, res, 400, 'Invalid status'); res.status(400).send('Invalid status');
return; return;
} }
@@ -72,23 +69,20 @@ class ChannelsRoutes {
res.header('X-Total-Count', channelsCount.toString()); res.header('X-Total-Count', channelsCount.toString());
res.json(channels); res.json(channels);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get channels for node'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
private async $getChannelsByTransactionIds(req: Request, res: Response): Promise<void> { private async $getChannelsByTransactionIds(req: Request, res: Response): Promise<void> {
try { try {
if (!Array.isArray(req.query.txId)) { if (!Array.isArray(req.query.txId)) {
handleError(req, res, 400, 'Not an array'); res.status(400).send('Not an array');
return; return;
} }
const txIds: string[] = []; const txIds: string[] = [];
for (const _txId in req.query.txId) { for (const _txId in req.query.txId) {
if (typeof req.query.txId[_txId] === 'string') { if (typeof req.query.txId[_txId] === 'string') {
const txid = req.query.txId[_txId].toString(); txIds.push(req.query.txId[_txId].toString());
if (TXID_REGEX.test(txid)) {
txIds.push(txid);
}
} }
} }
const channels = await channelsApi.$getChannelsByTransactionId(txIds); const channels = await channelsApi.$getChannelsByTransactionId(txIds);
@@ -113,7 +107,7 @@ class ChannelsRoutes {
res.json(result); res.json(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get channels by transaction ids'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -125,7 +119,7 @@ class ChannelsRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channels); res.json(channels);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get penalty closed channels'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -138,7 +132,7 @@ class ChannelsRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channels); res.json(channels);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get channel geodata'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }

View File

@@ -3,8 +3,6 @@ import { Application, Request, Response } from 'express';
import nodesApi from './nodes.api'; import nodesApi from './nodes.api';
import channelsApi from './channels.api'; import channelsApi from './channels.api';
import statisticsApi from './statistics.api'; import statisticsApi from './statistics.api';
import { handleError } from '../../utils/api';
class GeneralLightningRoutes { class GeneralLightningRoutes {
constructor() { } constructor() { }
@@ -29,7 +27,7 @@ class GeneralLightningRoutes {
channels: channels, channels: channels,
}); });
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to search for nodes and channels'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -43,7 +41,7 @@ class GeneralLightningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(statistics); res.json(statistics);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get lightning statistics'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -52,7 +50,7 @@ class GeneralLightningRoutes {
const statistics = await statisticsApi.$getLatestStatistics(); const statistics = await statisticsApi.$getLatestStatistics();
res.json(statistics); res.json(statistics);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get lightning statistics'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
} }

View File

@@ -3,7 +3,6 @@ import { Application, Request, Response } from 'express';
import nodesApi from './nodes.api'; import nodesApi from './nodes.api';
import DB from '../../database'; import DB from '../../database';
import { INodesRanking } from '../../mempool.interfaces'; import { INodesRanking } from '../../mempool.interfaces';
import { handleError } from '../../utils/api';
class NodesRoutes { class NodesRoutes {
constructor() { } constructor() { }
@@ -32,7 +31,7 @@ class NodesRoutes {
const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search); const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search);
res.json(nodes); res.json(nodes);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to search for node'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -182,13 +181,13 @@ class NodesRoutes {
} }
} catch (e) {} } catch (e) {}
} }
res.header('Pragma', 'public'); res.header('Pragma', 'public');
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(nodes); res.json(nodes);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get node group'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -196,7 +195,7 @@ class NodesRoutes {
try { try {
const node = await nodesApi.$getNode(req.params.public_key); const node = await nodesApi.$getNode(req.params.public_key);
if (!node) { if (!node) {
handleError(req, res, 404, 'Node not found'); res.status(404).send('Node not found');
return; return;
} }
res.header('Pragma', 'public'); res.header('Pragma', 'public');
@@ -204,7 +203,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(node); res.json(node);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get node'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -216,7 +215,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(statistics); res.json(statistics);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get historical node stats'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -224,7 +223,7 @@ class NodesRoutes {
try { try {
const node = await nodesApi.$getFeeHistogram(req.params.public_key); const node = await nodesApi.$getFeeHistogram(req.params.public_key);
if (!node) { if (!node) {
handleError(req, res, 404, 'Node not found'); res.status(404).send('Node not found');
return; return;
} }
res.header('Pragma', 'public'); res.header('Pragma', 'public');
@@ -232,7 +231,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(node); res.json(node);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get fee histogram'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -248,7 +247,7 @@ class NodesRoutes {
topByChannels: topChannelsNodes, topByChannels: topChannelsNodes,
}); });
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get nodes ranking'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -260,7 +259,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes); res.json(topCapacityNodes);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get top nodes by capacity'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -272,7 +271,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes); res.json(topCapacityNodes);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get top nodes by channels'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -284,7 +283,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes); res.json(topCapacityNodes);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get oldest nodes'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -296,7 +295,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(nodesPerAs); res.json(nodesPerAs);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get ISP ranking'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -308,7 +307,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(worldNodes); res.json(worldNodes);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get world nodes'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -323,7 +322,7 @@ class NodesRoutes {
); );
if (country.length === 0) { if (country.length === 0) {
handleError(req, res, 404, `This country does not exist or does not host any lightning nodes on clearnet`); res.status(404).send(`This country does not exist or does not host any lightning nodes on clearnet`);
return; return;
} }
@@ -336,7 +335,7 @@ class NodesRoutes {
nodes: nodes, nodes: nodes,
}); });
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get nodes per country'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -350,7 +349,7 @@ class NodesRoutes {
); );
if (isp.length === 0) { if (isp.length === 0) {
handleError(req, res, 404, `This ISP does not exist or does not host any lightning nodes on clearnet`); res.status(404).send(`This ISP does not exist or does not host any lightning nodes on clearnet`);
return; return;
} }
@@ -363,7 +362,7 @@ class NodesRoutes {
nodes: nodes, nodes: nodes,
}); });
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get nodes per ISP'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -375,7 +374,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(nodesPerAs); res.json(nodesPerAs);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get nodes per country'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
} }

View File

@@ -3,7 +3,6 @@ import { Application, Request, Response } from 'express';
import config from '../../config'; import config from '../../config';
import elementsParser from './elements-parser'; import elementsParser from './elements-parser';
import icons from './icons'; import icons from './icons';
import { handleError } from '../../utils/api';
class LiquidRoutes { class LiquidRoutes {
public initRoutes(app: Application) { public initRoutes(app: Application) {
@@ -43,7 +42,7 @@ class LiquidRoutes {
res.setHeader('content-length', result.length); res.setHeader('content-length', result.length);
res.send(result); res.send(result);
} else { } else {
handleError(req, res, 404, 'Asset icon not found'); res.status(404).send('Asset icon not found');
} }
} }
@@ -52,7 +51,7 @@ class LiquidRoutes {
if (result) { if (result) {
res.json(result); res.json(result);
} else { } else {
handleError(req, res, 404, 'Asset icons not found'); res.status(404).send('Asset icons not found');
} }
} }
@@ -83,7 +82,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
res.json(pegs); res.json(pegs);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get pegs by month'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -95,7 +94,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
res.json(reserves); res.json(reserves);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get reserves by month'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -107,7 +106,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(currentSupply); res.json(currentSupply);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get pegs'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -119,7 +118,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(currentReserves); res.json(currentReserves);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get reserves'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -131,7 +130,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(auditStatus); res.json(auditStatus);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get federation audit status'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -143,7 +142,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationAddresses); res.json(federationAddresses);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get federation addresses'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -155,7 +154,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationAddresses); res.json(federationAddresses);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get federation addresses'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -167,7 +166,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationUtxos); res.json(federationUtxos);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get federation utxos'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -179,7 +178,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(expiredUtxos); res.json(expiredUtxos);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get expired utxos'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -191,7 +190,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationUtxos); res.json(federationUtxos);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get federation utxos number'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -203,7 +202,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(emergencySpentUtxos); res.json(emergencySpentUtxos);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get emergency spent utxos'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -215,7 +214,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(emergencySpentUtxos); res.json(emergencySpentUtxos);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get emergency spent utxos stats'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -227,7 +226,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(recentPegs); res.json(recentPegs);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get pegs list'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -239,7 +238,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(pegsVolume); res.json(pegsVolume);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get pegs volume daily'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -251,7 +250,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(pegsCount); res.json(pegsCount);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get pegs count'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }

View File

@@ -369,7 +369,7 @@ class MempoolBlocks {
const lastBlockIndex = blocks.length - 1; const lastBlockIndex = blocks.length - 1;
let hasBlockStack = blocks.length >= 8; let hasBlockStack = blocks.length >= 8;
let stackWeight; let stackWeight;
let feeStatsCalculator: OnlineFeeStatsCalculator | null = null; let feeStatsCalculator: OnlineFeeStatsCalculator | void;
if (hasBlockStack) { if (hasBlockStack) {
if (blockWeights && blockWeights[7] !== null) { if (blockWeights && blockWeights[7] !== null) {
stackWeight = blockWeights[7]; stackWeight = blockWeights[7];
@@ -380,36 +380,28 @@ class MempoolBlocks {
feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5, [10, 20, 30, 40, 50, 60, 70, 80, 90]); feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5, [10, 20, 30, 40, 50, 60, 70, 80, 90]);
} }
const ancestors: Ancestor[] = [];
const descendants: Ancestor[] = [];
let ancestor: MempoolTransactionExtended;
for (const cluster of clusters) { for (const cluster of clusters) {
for (const memberTxid of cluster) { for (const memberTxid of cluster) {
const mempoolTx = mempool[memberTxid]; const mempoolTx = mempool[memberTxid];
if (mempoolTx) { if (mempoolTx) {
// ugly micro-optimization to avoid allocating new arrays const ancestors: Ancestor[] = [];
ancestors.length = 0; const descendants: Ancestor[] = [];
descendants.length = 0;
let matched = false; let matched = false;
cluster.forEach(txid => { cluster.forEach(txid => {
ancestor = mempool[txid];
if (txid === memberTxid) { if (txid === memberTxid) {
matched = true; matched = true;
} else { } else {
if (!ancestor) { if (!mempool[txid]) {
console.log('txid missing from mempool! ', txid, candidates?.txs[txid]); console.log('txid missing from mempool! ', txid, candidates?.txs[txid]);
return;
} }
const relative = { const relative = {
txid: txid, txid: txid,
fee: ancestor.fee, fee: mempool[txid].fee,
weight: (ancestor.adjustedVsize * 4), weight: (mempool[txid].adjustedVsize * 4),
}; };
if (matched) { if (matched) {
descendants.push(relative); descendants.push(relative);
if (!mempoolTx.lastBoosted || (ancestor.firstSeen && ancestor.firstSeen > mempoolTx.lastBoosted)) { mempoolTx.lastBoosted = Math.max(mempoolTx.lastBoosted || 0, mempool[txid].firstSeen || 0);
mempoolTx.lastBoosted = ancestor.firstSeen;
}
} else { } else {
ancestors.push(relative); ancestors.push(relative);
} }
@@ -418,20 +410,7 @@ class MempoolBlocks {
if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) { if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) {
mempoolTx.cpfpDirty = true; mempoolTx.cpfpDirty = true;
} }
// ugly micro-optimization to avoid allocating new arrays or objects Object.assign(mempoolTx, {ancestors, descendants, bestDescendant: null, cpfpChecked: true});
if (mempoolTx.ancestors) {
mempoolTx.ancestors.length = 0;
} else {
mempoolTx.ancestors = [];
}
if (mempoolTx.descendants) {
mempoolTx.descendants.length = 0;
} else {
mempoolTx.descendants = [];
}
mempoolTx.ancestors.push(...ancestors);
mempoolTx.descendants.push(...descendants);
mempoolTx.cpfpChecked = true;
} }
} }
} }
@@ -441,10 +420,7 @@ class MempoolBlocks {
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2; const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
// update this thread's mempool with the results // update this thread's mempool with the results
let mempoolTx: MempoolTransactionExtended; let mempoolTx: MempoolTransactionExtended;
let acceleration: Acceleration; const mempoolBlocks: MempoolBlockWithTransactions[] = blocks.map((block, blockIndex) => {
const mempoolBlocks: MempoolBlockWithTransactions[] = [];
for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
const block = blocks[blockIndex];
let totalSize = 0; let totalSize = 0;
let totalVsize = 0; let totalVsize = 0;
let totalWeight = 0; let totalWeight = 0;
@@ -460,9 +436,8 @@ class MempoolBlocks {
} }
} }
for (let i = 0; i < block.length; i++) { for (const txid of block) {
const txid = block[i]; if (txid) {
if (txid in mempool) {
mempoolTx = mempool[txid]; mempoolTx = mempool[txid];
// save position in projected blocks // save position in projected blocks
mempoolTx.position = { mempoolTx.position = {
@@ -470,40 +445,30 @@ class MempoolBlocks {
vsize: totalVsize + (mempoolTx.vsize / 2), vsize: totalVsize + (mempoolTx.vsize / 2),
}; };
if (txid in accelerations) { const acceleration = accelerations[txid];
acceleration = accelerations[txid]; if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) { if (!mempoolTx.acceleration) {
if (!mempoolTx.acceleration) { mempoolTx.cpfpDirty = true;
mempoolTx.cpfpDirty = true; }
} mempoolTx.acceleration = true;
mempoolTx.acceleration = true; mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools;
mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools; mempoolTx.acceleratedAt = acceleration?.added;
mempoolTx.acceleratedAt = acceleration?.added; mempoolTx.feeDelta = acceleration?.feeDelta;
mempoolTx.feeDelta = acceleration?.feeDelta; for (const ancestor of mempoolTx.ancestors || []) {
for (const ancestor of mempoolTx.ancestors || []) { if (!mempool[ancestor.txid].acceleration) {
if (!(ancestor.txid in mempool)) { mempool[ancestor.txid].cpfpDirty = true;
continue;
}
if (!mempool[ancestor.txid].acceleration) {
mempool[ancestor.txid].cpfpDirty = true;
}
mempool[ancestor.txid].acceleration = true;
mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy;
mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt;
mempool[ancestor.txid].feeDelta = mempoolTx.feeDelta;
isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy;
}
} else {
if (mempoolTx.acceleration) {
mempoolTx.cpfpDirty = true;
delete mempoolTx.acceleration;
} }
mempool[ancestor.txid].acceleration = true;
mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy;
mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt;
mempool[ancestor.txid].feeDelta = mempoolTx.feeDelta;
isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy;
} }
} else { } else {
if (mempoolTx.acceleration) { if (mempoolTx.acceleration) {
mempoolTx.cpfpDirty = true; mempoolTx.cpfpDirty = true;
delete mempoolTx.acceleration;
} }
delete mempoolTx.acceleration;
} }
// online calculation of stack-of-blocks fee stats // online calculation of stack-of-blocks fee stats
@@ -521,7 +486,7 @@ class MempoolBlocks {
} }
} }
} }
mempoolBlocks[blockIndex] = this.dataToMempoolBlocks( return this.dataToMempoolBlocks(
block, block,
transactions, transactions,
totalSize, totalSize,
@@ -529,7 +494,7 @@ class MempoolBlocks {
totalFees, totalFees,
(hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) ? feeStatsCalculator.getRawFeeStats() : undefined, (hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) ? feeStatsCalculator.getRawFeeStats() : undefined,
); );
}; });
if (saveResults) { if (saveResults) {
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks); const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks);
@@ -691,7 +656,7 @@ class MempoolBlocks {
[pool: string]: { name: string, block: number, vsize: number, accelerations: string[], complete: boolean }; [pool: string]: { name: string, block: number, vsize: number, accelerations: string[], complete: boolean };
} = {}; } = {};
// prepare a list of accelerations in ascending order (we'll pop items off the end of the list) // prepare a list of accelerations in ascending order (we'll pop items off the end of the list)
const accQueue: { acceleration: Acceleration, rate: number, vsize: number }[] = Object.values(accelerations).filter(acc => acc.txid in mempoolCache).map(acc => { const accQueue: { acceleration: Acceleration, rate: number, vsize: number }[] = Object.values(accelerations).map(acc => {
let vsize = mempoolCache[acc.txid].vsize; let vsize = mempoolCache[acc.txid].vsize;
for (const ancestor of mempoolCache[acc.txid].ancestors || []) { for (const ancestor of mempoolCache[acc.txid].ancestors || []) {
vsize += (ancestor.weight / 4); vsize += (ancestor.weight / 4);

View File

@@ -10,9 +10,9 @@ 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 { Acceleration } from './services/acceleration'; import { Acceleration } from './services/acceleration';
import accelerationApi from './services/acceleration';
import redisCache from './redis-cache'; import redisCache from './redis-cache';
import blocks from './blocks'; import blocks from './blocks';
import { logFees } from './statistics/statistics';
class Mempool { class Mempool {
private inSync: boolean = false; private inSync: boolean = false;
@@ -20,13 +20,12 @@ class Mempool {
private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {}; private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {};
private mempoolCandidates: { [txid: string ]: boolean } = {}; private mempoolCandidates: { [txid: string ]: boolean } = {};
private spendMap = new Map<string, MempoolTransactionExtended>(); private spendMap = new Map<string, MempoolTransactionExtended>();
private recentlyDeleted: MempoolTransactionExtended[][] = []; // buffer of transactions deleted in recent mempool updates
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0, private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
maxmempool: 300000000, mempoolminfee: Common.isLiquid() ? 0.00000100 : 0.00001000, minrelaytxfee: Common.isLiquid() ? 0.00000100 : 0.00001000 }; maxmempool: 300000000, mempoolminfee: Common.isLiquid() ? 0.00000100 : 0.00001000, minrelaytxfee: Common.isLiquid() ? 0.00000100 : 0.00001000 };
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[], private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void) | undefined; deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void) | undefined;
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[], private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[],
deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[], candidates?: GbtCandidates) => Promise<void>) | undefined; deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], candidates?: GbtCandidates) => Promise<void>) | undefined;
private accelerations: { [txId: string]: Acceleration } = {}; private accelerations: { [txId: string]: Acceleration } = {};
private accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {}; private accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {};
@@ -36,6 +35,7 @@ class Mempool {
private vBytesPerSecondArray: VbytesPerSecond[] = []; private vBytesPerSecondArray: VbytesPerSecond[] = [];
private vBytesPerSecond: number = 0; private vBytesPerSecond: number = 0;
private vBytesPerSecondByFeeRate: { [feePerWU: number]: number } = {};
private mempoolProtection = 0; private mempoolProtection = 0;
private latestTransactions: any[] = []; private latestTransactions: any[] = [];
@@ -76,12 +76,12 @@ class Mempool {
} }
public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void): void { newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void): void {
this.mempoolChangedCallback = fn; this.mempoolChangedCallback = fn;
} }
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number, public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number,
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[], newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[],
candidates?: GbtCandidates) => Promise<void>): void { candidates?: GbtCandidates) => Promise<void>): void {
this.$asyncMempoolChangedCallback = fn; this.$asyncMempoolChangedCallback = fn;
} }
@@ -195,6 +195,10 @@ class Mempool {
return this.vBytesPerSecond; return this.vBytesPerSecond;
} }
public getVBytesPerSecondByFeeRate(): { [feePerWU: number]: number } {
return this.vBytesPerSecondByFeeRate;
}
public getFirstSeenForTransactions(txIds: string[]): number[] { public getFirstSeenForTransactions(txIds: string[]): number[] {
const txTimes: number[] = []; const txTimes: number[] = [];
txIds.forEach((txId: string) => { txIds.forEach((txId: string) => {
@@ -208,7 +212,7 @@ class Mempool {
return txTimes; return txTimes;
} }
public async $updateMempool(transactions: string[], accelerations: Record<string, Acceleration> | null, minFeeMempool: string[], minFeeTip: number, pollRate: number): Promise<void> { public async $updateMempool(transactions: string[], accelerations: Acceleration[] | null, minFeeMempool: string[], minFeeTip: number, 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
@@ -272,6 +276,7 @@ class Mempool {
this.vBytesPerSecondArray.push({ this.vBytesPerSecondArray.push({
unixTime: new Date().getTime(), unixTime: new Date().getTime(),
vSize: transaction.vsize, vSize: transaction.vsize,
effectiveFeePerVsize: transaction.effectiveFeePerVsize
}); });
} }
hasChange = true; hasChange = true;
@@ -355,7 +360,7 @@ 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);
const accelerationDelta = accelerations != null ? await this.updateAccelerations(accelerations) : []; const accelerationDelta = accelerations != null ? await this.$updateAccelerations(accelerations) : [];
if (accelerationDelta.length) { if (accelerationDelta.length) {
hasChange = true; hasChange = true;
} }
@@ -364,15 +369,12 @@ class Mempool {
const candidatesChanged = candidates?.added?.length || candidates?.removed?.length; const candidatesChanged = candidates?.added?.length || candidates?.removed?.length;
this.recentlyDeleted.unshift(deletedTransactions); if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
this.recentlyDeleted.length = Math.min(this.recentlyDeleted.length, 10); // truncate to the last 10 mempool updates this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta);
if (this.mempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length)) {
this.mempoolChangedCallback(this.mempoolCache, newTransactions, this.recentlyDeleted, accelerationDelta);
} }
if (this.$asyncMempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length || candidatesChanged)) { if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length || candidatesChanged)) {
this.updateTimerProgress(timer, 'running async mempool callback'); this.updateTimerProgress(timer, 'running async mempool callback');
await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, this.recentlyDeleted, accelerationDelta, candidates); await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, deletedTransactions, accelerationDelta, candidates);
this.updateTimerProgress(timer, 'completed async mempool callback'); this.updateTimerProgress(timer, 'completed async mempool callback');
} }
@@ -400,11 +402,58 @@ class Mempool {
return this.accelerations; return this.accelerations;
} }
public updateAccelerations(newAccelerationMap: Record<string, Acceleration>): string[] { public $updateAccelerations(newAccelerations: Acceleration[]): string[] {
try { try {
const accelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, newAccelerationMap); const changed: string[] = [];
const newAccelerationMap: { [txid: string]: Acceleration } = {};
for (const acceleration of newAccelerations) {
// skip transactions we don't know about
if (!this.mempoolCache[acceleration.txid]) {
continue;
}
newAccelerationMap[acceleration.txid] = acceleration;
if (this.accelerations[acceleration.txid] == null) {
// new acceleration
changed.push(acceleration.txid);
} else {
if (this.accelerations[acceleration.txid].feeDelta !== acceleration.feeDelta) {
// feeDelta changed
changed.push(acceleration.txid);
} else if (this.accelerations[acceleration.txid].pools?.length) {
let poolsChanged = false;
const pools = new Set();
this.accelerations[acceleration.txid].pools.forEach(pool => {
pools.add(pool);
});
acceleration.pools.forEach(pool => {
if (!pools.has(pool)) {
poolsChanged = true;
} else {
pools.delete(pool);
}
});
if (pools.size > 0) {
poolsChanged = true;
}
if (poolsChanged) {
// pools changed
changed.push(acceleration.txid);
}
}
}
}
for (const oldTxid of Object.keys(this.accelerations)) {
if (!newAccelerationMap[oldTxid]) {
// removed
changed.push(oldTxid);
}
}
this.accelerations = newAccelerationMap; this.accelerations = newAccelerationMap;
return accelerationDelta;
return changed;
} catch (e: any) { } catch (e: any) {
logger.debug(`Failed to update accelerations: ` + (e instanceof Error ? e.message : e)); logger.debug(`Failed to update accelerations: ` + (e instanceof Error ? e.message : e));
return []; return [];
@@ -499,7 +548,16 @@ class Mempool {
} }
} }
public handleRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void { public handleRbfTransactions(rbfTransactions: { [txid: string]: MempoolTransactionExtended[]; }): void {
for (const rbfTransaction in rbfTransactions) {
if (this.mempoolCache[rbfTransaction] && rbfTransactions[rbfTransaction]?.length) {
// Store replaced transactions
rbfCache.add(rbfTransactions[rbfTransaction], this.mempoolCache[rbfTransaction]);
}
}
}
public handleMinedRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void {
for (const rbfTransaction in rbfTransactions) { for (const rbfTransaction in rbfTransactions) {
if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) { if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) {
// Store replaced transactions // Store replaced transactions
@@ -537,6 +595,21 @@ class Mempool {
this.vBytesPerSecond = Math.round( this.vBytesPerSecond = Math.round(
this.vBytesPerSecondArray.map((data) => data.vSize).reduce((a, b) => a + b) / config.STATISTICS.TX_PER_SECOND_SAMPLE_PERIOD this.vBytesPerSecondArray.map((data) => data.vSize).reduce((a, b) => a + b) / config.STATISTICS.TX_PER_SECOND_SAMPLE_PERIOD
); );
if (!Common.isLiquid()) {
this.vBytesPerSecondByFeeRate = {};
for (const tx of this.vBytesPerSecondArray) {
for (let i = 0; i < logFees.length; i++) {
if (tx.effectiveFeePerVsize < logFees[i + 1] || i === logFees.length - 1) {
this.vBytesPerSecondByFeeRate[logFees[i]] = (this.vBytesPerSecondByFeeRate[logFees[i]] || 0) + tx.vSize;
break;
}
}
}
for (const feeRate of Object.keys(this.vBytesPerSecondByFeeRate)) {
this.vBytesPerSecondByFeeRate[feeRate] = Math.round(this.vBytesPerSecondByFeeRate[feeRate] / config.STATISTICS.TX_PER_SECOND_SAMPLE_PERIOD);
}
}
} }
} }

View File

@@ -10,7 +10,6 @@ import mining from "./mining";
import PricesRepository from '../../repositories/PricesRepository'; import PricesRepository from '../../repositories/PricesRepository';
import AccelerationRepository from '../../repositories/AccelerationRepository'; import AccelerationRepository from '../../repositories/AccelerationRepository';
import accelerationApi from '../services/acceleration'; import accelerationApi from '../services/acceleration';
import { handleError } from '../../utils/api';
class MiningRoutes { class MiningRoutes {
public initRoutes(app: Application) { public initRoutes(app: Application) {
@@ -54,12 +53,12 @@ class MiningRoutes {
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
if (['testnet', 'signet', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) { if (['testnet', 'signet', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) {
handleError(req, res, 400, 'Prices are not available on testnets.'); res.status(400).send('Prices are not available on testnets.');
return; return;
} }
const timestamp = parseInt(req.query.timestamp as string, 10) || 0; const timestamp = parseInt(req.query.timestamp as string, 10) || 0;
const currency = req.query.currency as string; const currency = req.query.currency as string;
let response; let response;
if (timestamp && currency) { if (timestamp && currency) {
response = await PricesRepository.$getNearestHistoricalPrice(timestamp, currency); response = await PricesRepository.$getNearestHistoricalPrice(timestamp, currency);
@@ -72,7 +71,7 @@ class MiningRoutes {
} }
res.status(200).send(response); res.status(200).send(response);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get historical prices'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -85,9 +84,9 @@ class MiningRoutes {
res.json(stats); res.json(stats);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
handleError(req, res, 404, e.message); res.status(404).send(e.message);
} else { } else {
handleError(req, res, 500, 'Failed to get pool'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
} }
@@ -104,9 +103,9 @@ class MiningRoutes {
res.json(poolBlocks); res.json(poolBlocks);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
handleError(req, res, 404, e.message); res.status(404).send(e.message);
} else { } else {
handleError(req, res, 500, 'Failed to get blocks for pool'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
} }
@@ -130,7 +129,7 @@ class MiningRoutes {
res.json(pools); res.json(pools);
} }
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get pools'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -144,7 +143,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(stats); res.json(stats);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get pools'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -158,7 +157,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(hashrates); res.json(hashrates);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get pools historical hashrate'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -173,9 +172,9 @@ class MiningRoutes {
res.json(hashrates); res.json(hashrates);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
handleError(req, res, 404, e.message); res.status(404).send(e.message);
} else { } else {
handleError(req, res, 500, 'Failed to get pool historical hashrate'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
} }
@@ -183,7 +182,7 @@ class MiningRoutes {
private async $getHistoricalHashrate(req: Request, res: Response) { private async $getHistoricalHashrate(req: Request, res: Response) {
let currentHashrate = 0, currentDifficulty = 0; let currentHashrate = 0, currentDifficulty = 0;
try { try {
currentHashrate = await bitcoinClient.getNetworkHashPs(1008); currentHashrate = await bitcoinClient.getNetworkHashPs();
currentDifficulty = await bitcoinClient.getDifficulty(); currentDifficulty = await bitcoinClient.getDifficulty();
} catch (e) { } catch (e) {
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate and difficulty'); logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate and difficulty');
@@ -204,7 +203,7 @@ class MiningRoutes {
currentDifficulty: currentDifficulty, currentDifficulty: currentDifficulty,
}); });
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get historical hashrate'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -218,7 +217,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFees); res.json(blockFees);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get historical block fees'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -236,7 +235,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFees); res.json(blockFees);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get historical block fees'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -250,7 +249,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockRewards); res.json(blockRewards);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get historical block rewards'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -264,7 +263,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFeeRates); res.json(blockFeeRates);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get historical block fee rates'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -282,7 +281,7 @@ class MiningRoutes {
weights: blockWeights weights: blockWeights
}); });
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get historical block size and weight'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -294,7 +293,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment])); res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment]));
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get historical difficulty adjustments'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -304,7 +303,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(response); res.json(response);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get reward stats'); res.status(500).end();
} }
} }
@@ -318,7 +317,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate])); res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate]));
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get historical blocks health'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -327,7 +326,7 @@ class MiningRoutes {
const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash); const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash);
if (!audit) { if (!audit) {
handleError(req, res, 204, `This block has not been audited.`); res.status(204).send(`This block has not been audited.`);
return; return;
} }
@@ -336,7 +335,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
res.json(audit); res.json(audit);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get block audit'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -359,7 +358,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(result); res.json(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get height from timestamp'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -372,7 +371,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15)); res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15));
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get block audit scores'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -385,7 +384,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
res.json(audit || 'null'); res.json(audit || 'null');
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get block audit score'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -395,12 +394,12 @@ class MiningRoutes {
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
handleError(req, res, 400, 'Acceleration data is not available.'); res.status(400).send('Acceleration data is not available.');
return; return;
} }
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug)); res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug));
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get accelerations by pool'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -410,13 +409,13 @@ class MiningRoutes {
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
handleError(req, res, 400, 'Acceleration data is not available.'); res.status(400).send('Acceleration data is not available.');
return; return;
} }
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10); const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height)); res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height));
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get accelerations by height'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -426,12 +425,12 @@ class MiningRoutes {
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
handleError(req, res, 400, 'Acceleration data is not available.'); res.status(400).send('Acceleration data is not available.');
return; return;
} }
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval)); res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval));
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get recent accelerations'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -441,12 +440,12 @@ class MiningRoutes {
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
handleError(req, res, 400, 'Acceleration data is not available.'); res.status(400).send('Acceleration data is not available.');
return; return;
} }
res.status(200).send(await AccelerationRepository.$getAccelerationTotals(<string>req.query.pool, <string>req.query.interval)); res.status(200).send(await AccelerationRepository.$getAccelerationTotals(<string>req.query.pool, <string>req.query.interval));
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get acceleration totals'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -456,12 +455,12 @@ class MiningRoutes {
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
handleError(req, res, 400, 'Acceleration data is not available.'); res.status(400).send('Acceleration data is not available.');
return; return;
} }
res.status(200).send(Object.values(accelerationApi.getAccelerations() || {})); res.status(200).send(accelerationApi.accelerations || []);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get active accelerations'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@@ -473,7 +472,7 @@ class MiningRoutes {
accelerationApi.accelerationRequested(req.params.txid); accelerationApi.accelerationRequested(req.params.txid);
res.status(200).send(); res.status(200).send();
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to request acceleration'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
} }

View File

@@ -136,13 +136,9 @@ class Mining {
poolsStatistics['blockCount'] = blockCount; poolsStatistics['blockCount'] = blockCount;
const totalBlock24h: number = await BlocksRepository.$blockCount(null, '24h'); const totalBlock24h: number = await BlocksRepository.$blockCount(null, '24h');
const totalBlock3d: number = await BlocksRepository.$blockCount(null, '3d');
const totalBlock1w: number = await BlocksRepository.$blockCount(null, '1w');
try { try {
poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h); poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h);
poolsStatistics['lastEstimatedHashrate3d'] = await bitcoinClient.getNetworkHashPs(totalBlock3d);
poolsStatistics['lastEstimatedHashrate1w'] = await bitcoinClient.getNetworkHashPs(totalBlock1w);
} catch (e) { } catch (e) {
poolsStatistics['lastEstimatedHashrate'] = 0; poolsStatistics['lastEstimatedHashrate'] = 0;
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate', logger.tags.mining); logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate', logger.tags.mining);

View File

@@ -1,15 +1,10 @@
import { Application, Request, Response } from 'express'; import { Application, Request, Response } from 'express';
import config from '../../config'; import config from '../../config';
import pricesUpdater from '../../tasks/price-updater'; import pricesUpdater from '../../tasks/price-updater';
import logger from '../../logger';
import PricesRepository from '../../repositories/PricesRepository';
class PricesRoutes { class PricesRoutes {
public initRoutes(app: Application): void { public initRoutes(app: Application): void {
app app.get(config.MEMPOOL.API_URL_PREFIX + 'prices', this.$getCurrentPrices.bind(this));
.get(config.MEMPOOL.API_URL_PREFIX + 'prices', this.$getCurrentPrices.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/usd-price-history', this.$getAllPrices.bind(this))
;
} }
private $getCurrentPrices(req: Request, res: Response): void { private $getCurrentPrices(req: Request, res: Response): void {
@@ -19,23 +14,6 @@ class PricesRoutes {
res.json(pricesUpdater.getLatestPrices()); res.json(pricesUpdater.getLatestPrices());
} }
private async $getAllPrices(req: Request, res: Response): Promise<void> {
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 360_0000 / config.MEMPOOL.PRICE_UPDATES_PER_HOUR).toUTCString());
try {
const usdPriceHistory = await PricesRepository.$getPricesTimesAndId();
const responseData = usdPriceHistory.map(p => {
return { time: p.time, USD: p.USD };
});
res.status(200).json(responseData);
} catch (e: any) {
logger.err(`Exception ${e} in PricesRoutes::$getAllPrices. Code: ${e.code}. Message: ${e.message}`);
res.status(403).send();
}
}
} }
export default new PricesRoutes(); export default new PricesRoutes();

View File

@@ -44,22 +44,6 @@ interface CacheEvent {
value?: any, value?: any,
} }
/**
* Singleton for tracking RBF trees
*
* Maintains a set of RBF trees, where each tree represents a sequence of
* consecutive RBF replacements.
*
* Trees are identified by the txid of the root transaction.
*
* To maintain consistency, the following invariants must be upheld:
* - Symmetry: replacedBy(A) = B <=> A in replaces(B)
* - Unique id: treeMap(treeMap(X)) = treeMap(X)
* - Unique tree: A in replaces(B) => treeMap(A) == treeMap(B)
* - Existence: X in treeMap => treeMap(X) in rbfTrees
* - Completeness: X in replacedBy => X in treeMap, Y in replaces => Y in treeMap
*/
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();
@@ -77,10 +61,6 @@ class RbfCache {
setInterval(this.cleanup.bind(this), 1000 * 60 * 10); setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
} }
/**
* Low level cache operations
*/
private addTx(txid: string, tx: MempoolTransactionExtended): void { private addTx(txid: string, tx: MempoolTransactionExtended): void {
this.txs.set(txid, tx); this.txs.set(txid, tx);
this.cacheQueue.push({ op: CacheOp.Add, type: 'tx', txid }); this.cacheQueue.push({ op: CacheOp.Add, type: 'tx', txid });
@@ -112,18 +92,8 @@ class RbfCache {
this.cacheQueue.push({ op: CacheOp.Remove, type: 'exp', txid }); this.cacheQueue.push({ op: CacheOp.Remove, type: 'exp', txid });
} }
/**
* Basic data structure operations
* must uphold tree invariants
*/
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void { public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
if ( !newTxExtended if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
|| !replaced?.length
|| this.txs.has(newTxExtended.txid)
|| !(replaced.some(tx => !this.replacedBy.has(tx.txid)))
) {
return; return;
} }
@@ -144,10 +114,6 @@ class RbfCache {
if (!replacedTx.rbf) { if (!replacedTx.rbf) {
txFullRbf = true; txFullRbf = true;
} }
if (this.replacedBy.has(replacedTx.txid)) {
// should never happen
continue;
}
this.replacedBy.set(replacedTx.txid, newTx.txid); this.replacedBy.set(replacedTx.txid, newTx.txid);
if (this.treeMap.has(replacedTx.txid)) { if (this.treeMap.has(replacedTx.txid)) {
const treeId = this.treeMap.get(replacedTx.txid); const treeId = this.treeMap.get(replacedTx.txid);
@@ -174,47 +140,18 @@ class RbfCache {
} }
} }
newTx.fullRbf = txFullRbf; newTx.fullRbf = txFullRbf;
const treeId = replacedTrees[0].tx.txid;
const newTree = { const newTree = {
tx: newTx, tx: newTx,
time: newTime, time: newTime,
fullRbf: treeFullRbf, fullRbf: treeFullRbf,
replaces: replacedTrees replaces: replacedTrees
}; };
this.addTree(newTree.tx.txid, newTree); this.addTree(treeId, newTree);
this.updateTreeMap(newTree.tx.txid, 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));
} }
public mined(txid): void {
if (!this.txs.has(txid)) {
return;
}
const treeId = this.treeMap.get(txid);
if (treeId && this.rbfTrees.has(treeId)) {
const tree = this.rbfTrees.get(treeId);
if (tree) {
this.setTreeMined(tree, txid);
tree.mined = true;
this.dirtyTrees.add(treeId);
this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId });
}
}
this.evict(txid);
}
// flag a transaction as removed from the mempool
public evict(txid: string, fast: boolean = false): void {
this.evictionCount++;
if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) {
const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours
this.addExpiration(txid, expiryTime);
}
}
/**
* Read-only public interface
*/
public has(txId: string): boolean { public has(txId: string): boolean {
return this.txs.has(txId); return this.txs.has(txId);
} }
@@ -295,6 +232,32 @@ class RbfCache {
return changes; return changes;
} }
public mined(txid): void {
if (!this.txs.has(txid)) {
return;
}
const treeId = this.treeMap.get(txid);
if (treeId && this.rbfTrees.has(treeId)) {
const tree = this.rbfTrees.get(treeId);
if (tree) {
this.setTreeMined(tree, txid);
tree.mined = true;
this.dirtyTrees.add(treeId);
this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId });
}
}
this.evict(txid);
}
// flag a transaction as removed from the mempool
public evict(txid: string, fast: boolean = false): void {
this.evictionCount++;
if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) {
const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours
this.addExpiration(txid, expiryTime);
}
}
// is the transaction involved in a full rbf replacement? // is the transaction involved in a full rbf replacement?
public isFullRbf(txid: string): boolean { public isFullRbf(txid: string): boolean {
const treeId = this.treeMap.get(txid); const treeId = this.treeMap.get(txid);
@@ -308,10 +271,6 @@ class RbfCache {
return tree?.fullRbf; return tree?.fullRbf;
} }
/**
* Cache maintenance & utility functions
*/
private cleanup(): void { private cleanup(): void {
const now = Date.now(); const now = Date.now();
for (const txid of this.expiring.keys()) { for (const txid of this.expiring.keys()) {
@@ -340,6 +299,10 @@ class RbfCache {
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.treeMap.get(tx) === tx) {
this.removeTree(tx);
}
this.remove(tx); this.remove(tx);
} }
} }
@@ -407,21 +370,14 @@ class RbfCache {
}; };
} }
public async load({ txs, trees, expiring, mempool, spendMap }): Promise<void> { public async load({ txs, trees, expiring, mempool }): Promise<void> {
try { try {
txs.forEach(txEntry => { txs.forEach(txEntry => {
this.txs.set(txEntry.value.txid, txEntry.value); this.txs.set(txEntry.value.txid, txEntry.value);
}); });
this.staleCount = 0; this.staleCount = 0;
for (const deflatedTree of trees.sort((a, b) => Object.keys(b).length - Object.keys(a).length)) { for (const deflatedTree of trees) {
const tree = await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs); await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
if (tree) {
this.addTree(tree.tx.txid, tree);
this.updateTreeMap(tree.tx.txid, tree);
if (tree.mined) {
this.evict(tree.tx.txid);
}
}
} }
expiring.forEach(expiringEntry => { expiring.forEach(expiringEntry => {
if (this.txs.has(expiringEntry.key)) { if (this.txs.has(expiringEntry.key)) {
@@ -429,31 +385,6 @@ class RbfCache {
} }
}); });
this.staleCount = 0; this.staleCount = 0;
// connect cached trees to current mempool transactions
const conflicts: Record<string, { replacedBy: MempoolTransactionExtended, replaces: Set<MempoolTransactionExtended> }> = {};
for (const tree of this.rbfTrees.values()) {
const tx = this.getTx(tree.tx.txid);
if (!tx || tree.mined) {
continue;
}
for (const vin of tx.vin) {
const conflict = spendMap.get(`${vin.txid}:${vin.vout}`);
if (conflict && conflict.txid !== tx.txid) {
if (!conflicts[conflict.txid]) {
conflicts[conflict.txid] = {
replacedBy: conflict,
replaces: new Set(),
};
}
conflicts[conflict.txid].replaces.add(tx);
}
}
}
for (const { replacedBy, replaces } of Object.values(conflicts)) {
this.add([...replaces.values()], replacedBy);
}
await this.checkTrees(); await this.checkTrees();
logger.debug(`loaded ${txs.length} txs, ${trees.length} trees into rbf cache, ${expiring.length} due to expire, ${this.staleCount} were stale`); logger.debug(`loaded ${txs.length} txs, ${trees.length} trees into rbf cache, ${expiring.length} due to expire, ${this.staleCount} were stale`);
this.cleanup(); this.cleanup();
@@ -495,12 +426,6 @@ class RbfCache {
return; return;
} }
// if this tx is already in the cache, return early
if (this.treeMap.has(txid)) {
this.removeTree(deflated.key);
return;
}
// recursively reconstruct child trees // recursively reconstruct child trees
for (const childId of treeInfo.replaces) { for (const childId of treeInfo.replaces) {
const replaced = await this.importTree(mempool, root, childId, deflated, txs, mined); const replaced = await this.importTree(mempool, root, childId, deflated, txs, mined);
@@ -532,6 +457,10 @@ class RbfCache {
fullRbf: treeInfo.fullRbf, fullRbf: treeInfo.fullRbf,
replaces, replaces,
}; };
this.treeMap.set(txid, root);
if (root === txid) {
this.addTree(root, tree);
}
return tree; return tree;
} }
@@ -582,7 +511,6 @@ class RbfCache {
processTxs(txs); processTxs(txs);
} }
// evict missing transactions
for (const txid of txids) { for (const txid of txids) {
if (!found[txid]) { if (!found[txid]) {
this.evict(txid, false); this.evict(txid, false);

View File

@@ -365,7 +365,6 @@ class RedisCache {
trees: rbfTrees.map(loadedTree => { loadedTree.value.key = loadedTree.key; return loadedTree.value; }), trees: rbfTrees.map(loadedTree => { loadedTree.value.key = loadedTree.key; return loadedTree.value; }),
expiring: rbfExpirations, expiring: rbfExpirations,
mempool: memPool.getMempool(), mempool: memPool.getMempool(),
spendMap: memPool.getSpendMap(),
}); });
} }

View File

@@ -1,10 +1,7 @@
import { WebSocket } from 'ws';
import config from '../../config'; import config from '../../config';
import logger from '../../logger'; import logger from '../../logger';
import { BlockExtended } from '../../mempool.interfaces'; import { BlockExtended } from '../../mempool.interfaces';
import axios from 'axios'; import axios from 'axios';
import mempool from '../mempool';
import websocketHandler from '../websocket-handler';
type MyAccelerationStatus = 'requested' | 'accelerating' | 'done'; type MyAccelerationStatus = 'requested' | 'accelerating' | 'done';
@@ -40,23 +37,14 @@ export interface AccelerationHistory {
}; };
class AccelerationApi { class AccelerationApi {
private ws: WebSocket | null = null;
private useWebsocket: boolean = config.MEMPOOL.OFFICIAL && config.MEMPOOL_SERVICES.ACCELERATIONS;
private startedWebsocketLoop: boolean = false;
private websocketConnected: boolean = false;
private onDemandPollingEnabled = !config.MEMPOOL_SERVICES.ACCELERATIONS; private onDemandPollingEnabled = !config.MEMPOOL_SERVICES.ACCELERATIONS;
private apiPath = config.MEMPOOL.OFFICIAL ? (config.MEMPOOL_SERVICES.API + '/accelerator/accelerations') : (config.EXTERNAL_DATA_SERVER.MEMPOOL_API + '/accelerations'); private apiPath = config.MEMPOOL.OFFICIAL ? (config.MEMPOOL_SERVICES.API + '/accelerator/accelerations') : (config.EXTERNAL_DATA_SERVER.MEMPOOL_API + '/accelerations');
private websocketPath = config.MEMPOOL_SERVICES?.API ? `${config.MEMPOOL_SERVICES.API.replace('https://', 'wss://').replace('http://', 'ws://')}/accelerator/ws` : '/'; private _accelerations: Acceleration[] | null = null;
private _accelerations: Record<string, Acceleration> = {};
private lastPoll = 0; private lastPoll = 0;
private lastPing = Date.now();
private lastPong = Date.now();
private forcePoll = false; private forcePoll = false;
private myAccelerations: Record<string, { status: MyAccelerationStatus, added: number, acceleration?: Acceleration }> = {}; private myAccelerations: Record<string, { status: MyAccelerationStatus, added: number, acceleration?: Acceleration }> = {};
public constructor() {} public get accelerations(): Acceleration[] | null {
public getAccelerations(): Record<string, Acceleration> {
return this._accelerations; return this._accelerations;
} }
@@ -84,18 +72,11 @@ class AccelerationApi {
} }
} }
public async $updateAccelerations(): Promise<Record<string, Acceleration> | null> { public async $updateAccelerations(): Promise<Acceleration[] | null> {
if (this.useWebsocket && this.websocketConnected) {
return this._accelerations;
}
if (!this.onDemandPollingEnabled) { if (!this.onDemandPollingEnabled) {
const accelerations = await this.$fetchAccelerations(); const accelerations = await this.$fetchAccelerations();
if (accelerations) { if (accelerations) {
const latestAccelerations = {}; this._accelerations = accelerations;
for (const acc of accelerations) {
latestAccelerations[acc.txid] = acc;
}
this._accelerations = latestAccelerations;
return this._accelerations; return this._accelerations;
} }
} else { } else {
@@ -104,7 +85,7 @@ class AccelerationApi {
return null; return null;
} }
private async $updateAccelerationsOnDemand(): Promise<Record<string, Acceleration> | null> { private async $updateAccelerationsOnDemand(): Promise<Acceleration[] | null> {
const shouldUpdate = this.forcePoll const shouldUpdate = this.forcePoll
|| this.countMyAccelerationsWithStatus('requested') > 0 || this.countMyAccelerationsWithStatus('requested') > 0
|| (this.countMyAccelerationsWithStatus('accelerating') > 0 && this.lastPoll < (Date.now() - (10 * 60 * 1000))); || (this.countMyAccelerationsWithStatus('accelerating') > 0 && this.lastPoll < (Date.now() - (10 * 60 * 1000)));
@@ -139,11 +120,7 @@ class AccelerationApi {
} }
} }
const latestAccelerations = {}; this._accelerations = Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[];
for (const acc of Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[]) {
latestAccelerations[acc.txid] = acc;
}
this._accelerations = latestAccelerations;
return this._accelerations; return this._accelerations;
} }
@@ -175,148 +152,6 @@ class AccelerationApi {
} }
return anyAccelerated; return anyAccelerated;
} }
// get a list of accelerations that have changed between two sets of accelerations
public getAccelerationDelta(oldAccelerationMap: Record<string, Acceleration>, newAccelerationMap: Record<string, Acceleration>): string[] {
const changed: string[] = [];
const mempoolCache = mempool.getMempool();
for (const acceleration of Object.values(newAccelerationMap)) {
// skip transactions we don't know about
if (!mempoolCache[acceleration.txid]) {
continue;
}
if (oldAccelerationMap[acceleration.txid] == null) {
// new acceleration
changed.push(acceleration.txid);
} else {
if (oldAccelerationMap[acceleration.txid].feeDelta !== acceleration.feeDelta) {
// feeDelta changed
changed.push(acceleration.txid);
} else if (oldAccelerationMap[acceleration.txid].pools?.length) {
let poolsChanged = false;
const pools = new Set();
oldAccelerationMap[acceleration.txid].pools.forEach(pool => {
pools.add(pool);
});
acceleration.pools.forEach(pool => {
if (!pools.has(pool)) {
poolsChanged = true;
} else {
pools.delete(pool);
}
});
if (pools.size > 0) {
poolsChanged = true;
}
if (poolsChanged) {
// pools changed
changed.push(acceleration.txid);
}
}
}
}
for (const oldTxid of Object.keys(oldAccelerationMap)) {
if (!newAccelerationMap[oldTxid]) {
// removed
changed.push(oldTxid);
}
}
return changed;
}
private handleWebsocketMessage(msg: any): void {
if (msg?.accelerations !== null) {
const latestAccelerations = {};
for (const acc of msg?.accelerations || []) {
latestAccelerations[acc.txid] = acc;
}
this._accelerations = latestAccelerations;
websocketHandler.handleAccelerationsChanged(this._accelerations);
}
}
public async connectWebsocket(): Promise<void> {
if (this.startedWebsocketLoop) {
return;
}
while (this.useWebsocket) {
this.startedWebsocketLoop = true;
if (!this.ws) {
this.ws = new WebSocket(this.websocketPath);
this.lastPing = 0;
this.ws.on('open', () => {
logger.info(`Acceleration websocket opened to ${this.websocketPath}`);
this.websocketConnected = true;
this.ws?.send(JSON.stringify({
'watch-accelerations': true
}));
});
this.ws.on('error', (error) => {
let errMsg = `Acceleration websocket error on ${this.websocketPath}: ${error['code']}`;
if (error['errors']) {
errMsg += ' - ' + error['errors'].join(' - ');
}
logger.err(errMsg);
this.ws = null;
this.websocketConnected = false;
});
this.ws.on('close', () => {
logger.info('Acceleration websocket closed');
this.ws = null;
this.websocketConnected = false;
});
this.ws.on('message', (data, isBinary) => {
try {
const msg = (isBinary ? data : data.toString()) as string;
const parsedMsg = msg?.length ? JSON.parse(msg) : null;
this.handleWebsocketMessage(parsedMsg);
} catch (e) {
logger.warn('Failed to parse acceleration websocket message: ' + (e instanceof Error ? e.message : e));
}
});
this.ws.on('ping', () => {
logger.debug('received ping from acceleration websocket server');
});
this.ws.on('pong', () => {
logger.debug('received pong from acceleration websocket server');
this.lastPong = Date.now();
});
} else if (this.websocketConnected) {
if (this.lastPing && this.lastPing > this.lastPong && (Date.now() - this.lastPing > 10000)) {
logger.warn('No pong received within 10 seconds, terminating connection');
try {
this.ws?.terminate();
} catch (e) {
logger.warn('failed to terminate acceleration websocket connection: ' + (e instanceof Error ? e.message : e));
} finally {
this.ws = null;
this.websocketConnected = false;
this.lastPing = 0;
}
} else if (!this.lastPing || (Date.now() - this.lastPing > 30000)) {
logger.debug('sending ping to acceleration websocket server');
if (this.ws?.readyState === WebSocket.OPEN) {
try {
this.ws?.ping();
this.lastPing = Date.now();
} catch (e) {
logger.warn('failed to send ping to acceleration websocket server: ' + (e instanceof Error ? e.message : e));
}
}
}
}
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
} }
export default new AccelerationApi(); export default new AccelerationApi();

View File

@@ -1,27 +0,0 @@
import { Application, Request, Response } from 'express';
import config from '../../config';
import WalletApi from './wallets';
import { handleError } from '../../utils/api';
class ServicesRoutes {
public initRoutes(app: Application): void {
app
.get(config.MEMPOOL.API_URL_PREFIX + 'wallet/:walletId', this.$getWallet)
;
}
private async $getWallet(req: Request, res: Response): Promise<void> {
try {
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 5).toUTCString());
const walletId = req.params.walletId;
const wallet = await WalletApi.getWallet(walletId);
res.status(200).send(wallet);
} catch (e) {
handleError(req, res, 500, 'Failed to get wallet');
}
}
}
export default new ServicesRoutes();

View File

@@ -1,105 +0,0 @@
import { WebSocket } from 'ws';
import logger from '../../logger';
import config from '../../config';
import websocketHandler from '../websocket-handler';
export interface StratumJob {
pool: number;
height: number;
coinbase: string;
scriptsig: string;
reward: number;
jobId: string;
extraNonce: string;
extraNonce2Size: number;
prevHash: string;
coinbase1: string;
coinbase2: string;
merkleBranches: string[];
version: string;
bits: string;
time: string;
timestamp: number;
cleanJobs: boolean;
received: number;
}
function isStratumJob(obj: any): obj is StratumJob {
return obj
&& typeof obj === 'object'
&& 'pool' in obj
&& 'prevHash' in obj
&& 'height' in obj
&& 'received' in obj
&& 'version' in obj
&& 'timestamp' in obj
&& 'bits' in obj
&& 'merkleBranches' in obj
&& 'cleanJobs' in obj;
}
class StratumApi {
private ws: WebSocket | null = null;
private runWebsocketLoop: boolean = false;
private startedWebsocketLoop: boolean = false;
private websocketConnected: boolean = false;
private jobs: Record<string, StratumJob> = {};
public constructor() {}
public getJobs(): Record<string, StratumJob> {
return this.jobs;
}
private handleWebsocketMessage(msg: any): void {
if (isStratumJob(msg)) {
this.jobs[msg.pool] = msg;
websocketHandler.handleNewStratumJob(this.jobs[msg.pool]);
}
}
public async connectWebsocket(): Promise<void> {
if (!config.STRATUM.ENABLED) {
return;
}
this.runWebsocketLoop = true;
if (this.startedWebsocketLoop) {
return;
}
while (this.runWebsocketLoop) {
this.startedWebsocketLoop = true;
if (!this.ws) {
this.ws = new WebSocket(`${config.STRATUM.API}`);
this.websocketConnected = true;
this.ws.on('open', () => {
logger.info('Stratum websocket opened');
});
this.ws.on('error', (error) => {
logger.err('Stratum websocket error: ' + error);
this.ws = null;
this.websocketConnected = false;
});
this.ws.on('close', () => {
logger.info('Stratum websocket closed');
this.ws = null;
this.websocketConnected = false;
});
this.ws.on('message', (data, isBinary) => {
try {
const parsedMsg = JSON.parse((isBinary ? data : data.toString()) as string);
this.handleWebsocketMessage(parsedMsg);
} catch (e) {
logger.warn('Failed to parse stratum websocket message: ' + (e instanceof Error ? e.message : e));
}
});
}
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
}
export default new StratumApi();

View File

@@ -1,153 +0,0 @@
import config from '../../config';
import logger from '../../logger';
import { IEsploraApi } from '../bitcoin/esplora-api.interface';
import bitcoinApi from '../bitcoin/bitcoin-api-factory';
import axios from 'axios';
import { TransactionExtended } from '../../mempool.interfaces';
interface WalletAddress {
address: string;
active: boolean;
stats: {
funded_txo_count: number;
funded_txo_sum: number;
spent_txo_count: number;
spent_txo_sum: number;
tx_count: number;
};
transactions: IEsploraApi.AddressTxSummary[];
lastSync: number;
}
interface Wallet {
name: string;
addresses: Record<string, WalletAddress>;
lastPoll: number;
}
const POLL_FREQUENCY = 5 * 60 * 1000; // 5 minutes
class WalletApi {
private wallets: Record<string, Wallet> = {};
private syncing = false;
constructor() {
this.wallets = config.WALLETS.ENABLED ? (config.WALLETS.WALLETS as string[]).reduce((acc, wallet) => {
acc[wallet] = { name: wallet, addresses: {}, lastPoll: 0 };
return acc;
}, {} as Record<string, Wallet>) : {};
}
public getWallet(wallet: string): Record<string, WalletAddress> {
return this.wallets?.[wallet]?.addresses || {};
}
// resync wallet addresses from the services backend
async $syncWallets(): Promise<void> {
if (!config.WALLETS.ENABLED || this.syncing) {
return;
}
this.syncing = true;
for (const walletKey of Object.keys(this.wallets)) {
const wallet = this.wallets[walletKey];
if (wallet.lastPoll < (Date.now() - POLL_FREQUENCY)) {
try {
const response = await axios.get(config.MEMPOOL_SERVICES.API + `/wallets/${wallet.name}`);
const addresses: Record<string, WalletAddress> = response.data;
const addressList: WalletAddress[] = Object.values(addresses);
// sync all current addresses
for (const address of addressList) {
await this.$syncWalletAddress(wallet, address);
}
// remove old addresses
for (const address of Object.keys(wallet.addresses)) {
if (!addresses[address]) {
delete wallet.addresses[address];
}
}
wallet.lastPoll = Date.now();
logger.debug(`Synced ${Object.keys(wallet.addresses).length} addresses for wallet ${wallet.name}`);
} catch (e) {
logger.err(`Error syncing wallet ${wallet.name}: ${(e instanceof Error ? e.message : e)}`);
}
}
}
this.syncing = false;
}
// resync address transactions from esplora
async $syncWalletAddress(wallet: Wallet, address: WalletAddress): Promise<void> {
// fetch full transaction data if the address is new or still active and hasn't been synced in the last hour
const refreshTransactions = !wallet.addresses[address.address] || (address.active && (Date.now() - wallet.addresses[address.address].lastSync) > 60 * 60 * 1000);
if (refreshTransactions) {
try {
const summary = await bitcoinApi.$getAddressTransactionSummary(address.address);
const addressInfo = await bitcoinApi.$getAddress(address.address);
const walletAddress: WalletAddress = {
address: address.address,
active: address.active,
transactions: summary,
stats: addressInfo.chain_stats,
lastSync: Date.now(),
};
wallet.addresses[address.address] = walletAddress;
} catch (e) {
logger.err(`Error syncing wallet address ${address.address}: ${(e instanceof Error ? e.message : e)}`);
}
}
}
// check a new block for transactions that affect wallet address balances, and add relevant transactions to wallets
processBlock(block: IEsploraApi.Block, blockTxs: TransactionExtended[]): Record<string, IEsploraApi.Transaction[]> {
const walletTransactions: Record<string, IEsploraApi.Transaction[]> = {};
for (const walletKey of Object.keys(this.wallets)) {
const wallet = this.wallets[walletKey];
walletTransactions[walletKey] = [];
for (const tx of blockTxs) {
const funded: Record<string, number> = {};
const spent: Record<string, number> = {};
const fundedCount: Record<string, number> = {};
const spentCount: Record<string, number> = {};
let anyMatch = false;
for (const vin of tx.vin) {
const address = vin.prevout?.scriptpubkey_address;
if (address && wallet.addresses[address]) {
anyMatch = true;
spent[address] = (spent[address] ?? 0) + (vin.prevout?.value ?? 0);
spentCount[address] = (spentCount[address] ?? 0) + 1;
}
}
for (const vout of tx.vout) {
const address = vout.scriptpubkey_address;
if (address && wallet.addresses[address]) {
anyMatch = true;
funded[address] = (funded[address] ?? 0) + (vout.value ?? 0);
fundedCount[address] = (fundedCount[address] ?? 0) + 1;
}
}
for (const address of Object.keys({ ...funded, ...spent })) {
// update address stats
wallet.addresses[address].stats.tx_count++;
wallet.addresses[address].stats.funded_txo_count += fundedCount[address] || 0;
wallet.addresses[address].stats.spent_txo_count += spentCount[address] || 0;
wallet.addresses[address].stats.funded_txo_sum += funded[address] || 0;
wallet.addresses[address].stats.spent_txo_sum += spent[address] || 0;
// add tx to summary
const txSummary: IEsploraApi.AddressTxSummary = {
txid: tx.txid,
value: (funded[address] ?? 0) - (spent[address] ?? 0),
height: block.height,
time: block.timestamp,
};
wallet.addresses[address].transactions?.push(txSummary);
}
if (anyMatch) {
walletTransactions[walletKey].push(tx);
}
}
}
return walletTransactions;
}
}
export default new WalletApi();

View File

@@ -53,7 +53,45 @@ class StatisticsApi {
vsize_1400, vsize_1400,
vsize_1600, vsize_1600,
vsize_1800, vsize_1800,
vsize_2000 vsize_2000,
vsize_ps_1,
vsize_ps_2,
vsize_ps_3,
vsize_ps_4,
vsize_ps_5,
vsize_ps_6,
vsize_ps_8,
vsize_ps_10,
vsize_ps_12,
vsize_ps_15,
vsize_ps_20,
vsize_ps_30,
vsize_ps_40,
vsize_ps_50,
vsize_ps_60,
vsize_ps_70,
vsize_ps_80,
vsize_ps_90,
vsize_ps_100,
vsize_ps_125,
vsize_ps_150,
vsize_ps_175,
vsize_ps_200,
vsize_ps_250,
vsize_ps_300,
vsize_ps_350,
vsize_ps_400,
vsize_ps_500,
vsize_ps_600,
vsize_ps_700,
vsize_ps_800,
vsize_ps_900,
vsize_ps_1000,
vsize_ps_1200,
vsize_ps_1400,
vsize_ps_1600,
vsize_ps_1800,
vsize_ps_2000
) )
VALUES (NOW(), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, VALUES (NOW(), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)`; 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)`;
@@ -67,56 +105,94 @@ class StatisticsApi {
public async $create(statistics: Statistic, convertToDatetime = false): Promise<number | undefined> { public async $create(statistics: Statistic, convertToDatetime = false): Promise<number | undefined> {
try { try {
const query = `INSERT INTO statistics( const query = `INSERT INTO statistics(
added, added,
unconfirmed_transactions, unconfirmed_transactions,
tx_per_second, tx_per_second,
vbytes_per_second, vbytes_per_second,
mempool_byte_weight, mempool_byte_weight,
fee_data, fee_data,
total_fee, total_fee,
min_fee, min_fee,
vsize_1, vsize_1,
vsize_2, vsize_2,
vsize_3, vsize_3,
vsize_4, vsize_4,
vsize_5, vsize_5,
vsize_6, vsize_6,
vsize_8, vsize_8,
vsize_10, vsize_10,
vsize_12, vsize_12,
vsize_15, vsize_15,
vsize_20, vsize_20,
vsize_30, vsize_30,
vsize_40, vsize_40,
vsize_50, vsize_50,
vsize_60, vsize_60,
vsize_70, vsize_70,
vsize_80, vsize_80,
vsize_90, vsize_90,
vsize_100, vsize_100,
vsize_125, vsize_125,
vsize_150, vsize_150,
vsize_175, vsize_175,
vsize_200, vsize_200,
vsize_250, vsize_250,
vsize_300, vsize_300,
vsize_350, vsize_350,
vsize_400, vsize_400,
vsize_500, vsize_500,
vsize_600, vsize_600,
vsize_700, vsize_700,
vsize_800, vsize_800,
vsize_900, vsize_900,
vsize_1000, vsize_1000,
vsize_1200, vsize_1200,
vsize_1400, vsize_1400,
vsize_1600, vsize_1600,
vsize_1800, vsize_1800,
vsize_2000 vsize_2000,
) vsize_ps_1,
VALUES (${convertToDatetime ? `FROM_UNIXTIME(${statistics.added})` : statistics.added}, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, vsize_ps_2,
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; vsize_ps_3,
vsize_ps_4,
vsize_ps_5,
vsize_ps_6,
vsize_ps_8,
vsize_ps_10,
vsize_ps_12,
vsize_ps_15,
vsize_ps_20,
vsize_ps_30,
vsize_ps_40,
vsize_ps_50,
vsize_ps_60,
vsize_ps_70,
vsize_ps_80,
vsize_ps_90,
vsize_ps_100,
vsize_ps_125,
vsize_ps_150,
vsize_ps_175,
vsize_ps_200,
vsize_ps_250,
vsize_ps_300,
vsize_ps_350,
vsize_ps_400,
vsize_ps_500,
vsize_ps_600,
vsize_ps_700,
vsize_ps_800,
vsize_ps_900,
vsize_ps_1000,
vsize_ps_1200,
vsize_ps_1400,
vsize_ps_1600,
vsize_ps_1800,
vsize_ps_2000
)
VALUES (${convertToDatetime ? `FROM_UNIXTIME(${statistics.added})` : statistics.added}, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
const params: (string | number)[] = [ const params: (string | number)[] = [
statistics.unconfirmed_transactions, statistics.unconfirmed_transactions,
statistics.tx_per_second, statistics.tx_per_second,
@@ -163,6 +239,44 @@ class StatisticsApi {
statistics.vsize_1600, statistics.vsize_1600,
statistics.vsize_1800, statistics.vsize_1800,
statistics.vsize_2000, statistics.vsize_2000,
statistics.vsize_ps_1,
statistics.vsize_ps_2,
statistics.vsize_ps_3,
statistics.vsize_ps_4,
statistics.vsize_ps_5,
statistics.vsize_ps_6,
statistics.vsize_ps_8,
statistics.vsize_ps_10,
statistics.vsize_ps_12,
statistics.vsize_ps_15,
statistics.vsize_ps_20,
statistics.vsize_ps_30,
statistics.vsize_ps_40,
statistics.vsize_ps_50,
statistics.vsize_ps_60,
statistics.vsize_ps_70,
statistics.vsize_ps_80,
statistics.vsize_ps_90,
statistics.vsize_ps_100,
statistics.vsize_ps_125,
statistics.vsize_ps_150,
statistics.vsize_ps_175,
statistics.vsize_ps_200,
statistics.vsize_ps_250,
statistics.vsize_ps_300,
statistics.vsize_ps_350,
statistics.vsize_ps_400,
statistics.vsize_ps_500,
statistics.vsize_ps_600,
statistics.vsize_ps_700,
statistics.vsize_ps_800,
statistics.vsize_ps_900,
statistics.vsize_ps_1000,
statistics.vsize_ps_1200,
statistics.vsize_ps_1400,
statistics.vsize_ps_1600,
statistics.vsize_ps_1800,
statistics.vsize_ps_2000,
]; ];
const [result]: any = await DB.query(query, params); const [result]: any = await DB.query(query, params);
return result.insertId; return result.insertId;
@@ -214,7 +328,45 @@ class StatisticsApi {
CAST(avg(vsize_1400) as DOUBLE) as vsize_1400, CAST(avg(vsize_1400) as DOUBLE) as vsize_1400,
CAST(avg(vsize_1600) as DOUBLE) as vsize_1600, CAST(avg(vsize_1600) as DOUBLE) as vsize_1600,
CAST(avg(vsize_1800) as DOUBLE) as vsize_1800, CAST(avg(vsize_1800) as DOUBLE) as vsize_1800,
CAST(avg(vsize_2000) as DOUBLE) as vsize_2000 \ CAST(avg(vsize_2000) as DOUBLE) as vsize_2000,
CAST(avg(vsize_ps_1) as DOUBLE) as vsize_ps_1,
CAST(avg(vsize_ps_2) as DOUBLE) as vsize_ps_2,
CAST(avg(vsize_ps_3) as DOUBLE) as vsize_ps_3,
CAST(avg(vsize_ps_4) as DOUBLE) as vsize_ps_4,
CAST(avg(vsize_ps_5) as DOUBLE) as vsize_ps_5,
CAST(avg(vsize_ps_6) as DOUBLE) as vsize_ps_6,
CAST(avg(vsize_ps_8) as DOUBLE) as vsize_ps_8,
CAST(avg(vsize_ps_10) as DOUBLE) as vsize_ps_10,
CAST(avg(vsize_ps_12) as DOUBLE) as vsize_ps_12,
CAST(avg(vsize_ps_15) as DOUBLE) as vsize_ps_15,
CAST(avg(vsize_ps_20) as DOUBLE) as vsize_ps_20,
CAST(avg(vsize_ps_30) as DOUBLE) as vsize_ps_30,
CAST(avg(vsize_ps_40) as DOUBLE) as vsize_ps_40,
CAST(avg(vsize_ps_50) as DOUBLE) as vsize_ps_50,
CAST(avg(vsize_ps_60) as DOUBLE) as vsize_ps_60,
CAST(avg(vsize_ps_70) as DOUBLE) as vsize_ps_70,
CAST(avg(vsize_ps_80) as DOUBLE) as vsize_ps_80,
CAST(avg(vsize_ps_90) as DOUBLE) as vsize_ps_90,
CAST(avg(vsize_ps_100) as DOUBLE) as vsize_ps_100,
CAST(avg(vsize_ps_125) as DOUBLE) as vsize_ps_125,
CAST(avg(vsize_ps_150) as DOUBLE) as vsize_ps_150,
CAST(avg(vsize_ps_175) as DOUBLE) as vsize_ps_175,
CAST(avg(vsize_ps_200) as DOUBLE) as vsize_ps_200,
CAST(avg(vsize_ps_250) as DOUBLE) as vsize_ps_250,
CAST(avg(vsize_ps_300) as DOUBLE) as vsize_ps_300,
CAST(avg(vsize_ps_350) as DOUBLE) as vsize_ps_350,
CAST(avg(vsize_ps_400) as DOUBLE) as vsize_ps_400,
CAST(avg(vsize_ps_500) as DOUBLE) as vsize_ps_500,
CAST(avg(vsize_ps_600) as DOUBLE) as vsize_ps_600,
CAST(avg(vsize_ps_700) as DOUBLE) as vsize_ps_700,
CAST(avg(vsize_ps_800) as DOUBLE) as vsize_ps_800,
CAST(avg(vsize_ps_900) as DOUBLE) as vsize_ps_900,
CAST(avg(vsize_ps_1000) as DOUBLE) as vsize_ps_1000,
CAST(avg(vsize_ps_1200) as DOUBLE) as vsize_ps_1200,
CAST(avg(vsize_ps_1400) as DOUBLE) as vsize_ps_1400,
CAST(avg(vsize_ps_1600) as DOUBLE) as vsize_ps_1600,
CAST(avg(vsize_ps_1800) as DOUBLE) as vsize_ps_1800,
CAST(avg(vsize_ps_2000) as DOUBLE) as vsize_ps_2000 \
FROM statistics \ FROM statistics \
${interval === 'all' ? '' : `WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`} \ ${interval === 'all' ? '' : `WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`} \
GROUP BY UNIX_TIMESTAMP(added) DIV ${div} \ GROUP BY UNIX_TIMESTAMP(added) DIV ${div} \
@@ -264,7 +416,45 @@ class StatisticsApi {
vsize_1400, vsize_1400,
vsize_1600, vsize_1600,
vsize_1800, vsize_1800,
vsize_2000 \ vsize_2000,
vsize_ps_1,
vsize_ps_2,
vsize_ps_3,
vsize_ps_4,
vsize_ps_5,
vsize_ps_6,
vsize_ps_8,
vsize_ps_10,
vsize_ps_12,
vsize_ps_15,
vsize_ps_20,
vsize_ps_30,
vsize_ps_40,
vsize_ps_50,
vsize_ps_60,
vsize_ps_70,
vsize_ps_80,
vsize_ps_90,
vsize_ps_100,
vsize_ps_125,
vsize_ps_150,
vsize_ps_175,
vsize_ps_200,
vsize_ps_250,
vsize_ps_300,
vsize_ps_350,
vsize_ps_400,
vsize_ps_500,
vsize_ps_600,
vsize_ps_700,
vsize_ps_800,
vsize_ps_900,
vsize_ps_1000,
vsize_ps_1200,
vsize_ps_1400,
vsize_ps_1600,
vsize_ps_1800,
vsize_ps_2000 \
FROM statistics \ FROM statistics \
${interval === 'all' ? '' : `WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`} \ ${interval === 'all' ? '' : `WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`} \
GROUP BY UNIX_TIMESTAMP(added) DIV ${div} \ GROUP BY UNIX_TIMESTAMP(added) DIV ${div} \
@@ -452,7 +642,47 @@ class StatisticsApi {
s.vsize_1600, s.vsize_1600,
s.vsize_1800, s.vsize_1800,
s.vsize_2000, s.vsize_2000,
] ],
vsizes_ps: [
s.vsize_ps_1,
s.vsize_ps_2,
s.vsize_ps_3,
s.vsize_ps_4,
s.vsize_ps_5,
s.vsize_ps_6,
s.vsize_ps_8,
s.vsize_ps_10,
s.vsize_ps_12,
s.vsize_ps_15,
s.vsize_ps_20,
s.vsize_ps_30,
s.vsize_ps_40,
s.vsize_ps_50,
s.vsize_ps_60,
s.vsize_ps_70,
s.vsize_ps_80,
s.vsize_ps_90,
s.vsize_ps_100,
s.vsize_ps_125,
s.vsize_ps_150,
s.vsize_ps_175,
s.vsize_ps_200,
s.vsize_ps_250,
s.vsize_ps_300,
s.vsize_ps_350,
s.vsize_ps_400,
s.vsize_ps_500,
s.vsize_ps_600,
s.vsize_ps_700,
s.vsize_ps_800,
s.vsize_ps_900,
s.vsize_ps_1000,
s.vsize_ps_1200,
s.vsize_ps_1400,
s.vsize_ps_1600,
s.vsize_ps_1800,
s.vsize_ps_2000,
],
}; };
}); });
} }
@@ -506,6 +736,44 @@ class StatisticsApi {
vsize_1600: s.vsizes[35], vsize_1600: s.vsizes[35],
vsize_1800: s.vsizes[36], vsize_1800: s.vsizes[36],
vsize_2000: s.vsizes[37], vsize_2000: s.vsizes[37],
vsize_ps_1: s.vsizes_ps?.[0] || 0,
vsize_ps_2: s.vsizes_ps?.[1] || 0,
vsize_ps_3: s.vsizes_ps?.[2] || 0,
vsize_ps_4: s.vsizes_ps?.[3] || 0,
vsize_ps_5: s.vsizes_ps?.[4] || 0,
vsize_ps_6: s.vsizes_ps?.[5] || 0,
vsize_ps_8: s.vsizes_ps?.[6] || 0,
vsize_ps_10: s.vsizes_ps?.[7] || 0,
vsize_ps_12: s.vsizes_ps?.[8] || 0,
vsize_ps_15: s.vsizes_ps?.[9] || 0,
vsize_ps_20: s.vsizes_ps?.[10] || 0,
vsize_ps_30: s.vsizes_ps?.[11] || 0,
vsize_ps_40: s.vsizes_ps?.[12] || 0,
vsize_ps_50: s.vsizes_ps?.[13] || 0,
vsize_ps_60: s.vsizes_ps?.[14] || 0,
vsize_ps_70: s.vsizes_ps?.[15] || 0,
vsize_ps_80: s.vsizes_ps?.[16] || 0,
vsize_ps_90: s.vsizes_ps?.[17] || 0,
vsize_ps_100: s.vsizes_ps?.[18] || 0,
vsize_ps_125: s.vsizes_ps?.[19] || 0,
vsize_ps_150: s.vsizes_ps?.[20] || 0,
vsize_ps_175: s.vsizes_ps?.[21] || 0,
vsize_ps_200: s.vsizes_ps?.[22] || 0,
vsize_ps_250: s.vsizes_ps?.[23] || 0,
vsize_ps_300: s.vsizes_ps?.[24] || 0,
vsize_ps_350: s.vsizes_ps?.[25] || 0,
vsize_ps_400: s.vsizes_ps?.[26] || 0,
vsize_ps_500: s.vsizes_ps?.[27] || 0,
vsize_ps_600: s.vsizes_ps?.[28] || 0,
vsize_ps_700: s.vsizes_ps?.[29] || 0,
vsize_ps_800: s.vsizes_ps?.[30] || 0,
vsize_ps_900: s.vsizes_ps?.[31] || 0,
vsize_ps_1000: s.vsizes_ps?.[32] || 0,
vsize_ps_1200: s.vsizes_ps?.[33] || 0,
vsize_ps_1400: s.vsizes_ps?.[34] || 0,
vsize_ps_1600: s.vsizes_ps?.[35] || 0,
vsize_ps_1800: s.vsizes_ps?.[36] || 0,
vsize_ps_2000: s.vsizes_ps?.[37] || 0,
} }
}); });
} }

View File

@@ -1,7 +1,7 @@
import { Application, Request, Response } from 'express'; import { Application, Request, Response } from 'express';
import config from '../../config'; import config from '../../config';
import statisticsApi from './statistics-api'; import statisticsApi from './statistics-api';
import { handleError } from '../../utils/api';
class StatisticsRoutes { class StatisticsRoutes {
public initRoutes(app: Application) { public initRoutes(app: Application) {
app app
@@ -65,7 +65,7 @@ class StatisticsRoutes {
} }
res.json(result); res.json(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get statistics'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
} }

View File

@@ -4,6 +4,9 @@ import { TransactionExtended, OptimizedStatistic } from '../../mempool.interface
import { Common } from '../common'; import { Common } from '../common';
import statisticsApi from './statistics-api'; import statisticsApi from './statistics-api';
export const logFees = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000];
class Statistics { class Statistics {
protected intervalTimer: NodeJS.Timer | undefined; protected intervalTimer: NodeJS.Timer | undefined;
protected lastRun: number = 0; protected lastRun: number = 0;
@@ -42,6 +45,7 @@ class Statistics {
const currentMempool = memPool.getMempool(); const currentMempool = memPool.getMempool();
const txPerSecond = memPool.getTxPerSecond(); const txPerSecond = memPool.getTxPerSecond();
const vBytesPerSecond = memPool.getVBytesPerSecond(); const vBytesPerSecond = memPool.getVBytesPerSecond();
const vBytesPerSecondByFeeRate = memPool.getVBytesPerSecondByFeeRate();
logger.debug('Running statistics'); logger.debug('Running statistics');
@@ -73,9 +77,6 @@ class Statistics {
const totalWeight = memPoolArray.map((tx) => tx.vsize).reduce((acc, curr) => acc + curr) * 4; const totalWeight = memPoolArray.map((tx) => tx.vsize).reduce((acc, curr) => acc + curr) * 4;
const totalFee = memPoolArray.map((tx) => tx.fee).reduce((acc, curr) => acc + curr); const totalFee = memPoolArray.map((tx) => tx.fee).reduce((acc, curr) => acc + curr);
const logFees = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000];
const weightVsizeFees: { [feePerWU: number]: number } = {}; const weightVsizeFees: { [feePerWU: number]: number } = {};
const lastItem = logFees.length - 1; const lastItem = logFees.length - 1;
@@ -147,6 +148,44 @@ class Statistics {
vsize_1600: weightVsizeFees['1600'] || 0, vsize_1600: weightVsizeFees['1600'] || 0,
vsize_1800: weightVsizeFees['1800'] || 0, vsize_1800: weightVsizeFees['1800'] || 0,
vsize_2000: weightVsizeFees['2000'] || 0, vsize_2000: weightVsizeFees['2000'] || 0,
vsize_ps_1: vBytesPerSecondByFeeRate['1'] || 0,
vsize_ps_2: vBytesPerSecondByFeeRate['2'] || 0,
vsize_ps_3: vBytesPerSecondByFeeRate['3'] || 0,
vsize_ps_4: vBytesPerSecondByFeeRate['4'] || 0,
vsize_ps_5: vBytesPerSecondByFeeRate['5'] || 0,
vsize_ps_6: vBytesPerSecondByFeeRate['6'] || 0,
vsize_ps_8: vBytesPerSecondByFeeRate['8'] || 0,
vsize_ps_10: vBytesPerSecondByFeeRate['10'] || 0,
vsize_ps_12: vBytesPerSecondByFeeRate['12'] || 0,
vsize_ps_15: vBytesPerSecondByFeeRate['15'] || 0,
vsize_ps_20: vBytesPerSecondByFeeRate['20'] || 0,
vsize_ps_30: vBytesPerSecondByFeeRate['30'] || 0,
vsize_ps_40: vBytesPerSecondByFeeRate['40'] || 0,
vsize_ps_50: vBytesPerSecondByFeeRate['50'] || 0,
vsize_ps_60: vBytesPerSecondByFeeRate['60'] || 0,
vsize_ps_70: vBytesPerSecondByFeeRate['70'] || 0,
vsize_ps_80: vBytesPerSecondByFeeRate['80'] || 0,
vsize_ps_90: vBytesPerSecondByFeeRate['90'] || 0,
vsize_ps_100: vBytesPerSecondByFeeRate['100'] || 0,
vsize_ps_125: vBytesPerSecondByFeeRate['125'] || 0,
vsize_ps_150: vBytesPerSecondByFeeRate['150'] || 0,
vsize_ps_175: vBytesPerSecondByFeeRate['175'] || 0,
vsize_ps_200: vBytesPerSecondByFeeRate['200'] || 0,
vsize_ps_250: vBytesPerSecondByFeeRate['250'] || 0,
vsize_ps_300: vBytesPerSecondByFeeRate['300'] || 0,
vsize_ps_350: vBytesPerSecondByFeeRate['350'] || 0,
vsize_ps_400: vBytesPerSecondByFeeRate['400'] || 0,
vsize_ps_500: vBytesPerSecondByFeeRate['500'] || 0,
vsize_ps_600: vBytesPerSecondByFeeRate['600'] || 0,
vsize_ps_700: vBytesPerSecondByFeeRate['700'] || 0,
vsize_ps_800: vBytesPerSecondByFeeRate['800'] || 0,
vsize_ps_900: vBytesPerSecondByFeeRate['900'] || 0,
vsize_ps_1000: vBytesPerSecondByFeeRate['1000'] || 0,
vsize_ps_1200: vBytesPerSecondByFeeRate['1200'] || 0,
vsize_ps_1400: vBytesPerSecondByFeeRate['1400'] || 0,
vsize_ps_1600: vBytesPerSecondByFeeRate['1600'] || 0,
vsize_ps_1800: vBytesPerSecondByFeeRate['1800'] || 0,
vsize_ps_2000: vBytesPerSecondByFeeRate['2000'] || 0,
}); });
if (this.newStatisticsEntryCallback && insertId) { if (this.newStatisticsEntryCallback && insertId) {

View File

@@ -121,7 +121,6 @@ class TransactionUtils {
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 = (transaction.fee || 0) / fractionalVsize; const feePerVbytes = (transaction.fee || 0) / fractionalVsize;
const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize; const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize;
const effectiveFeePerVsize = transaction['effectiveFeePerVsize'] || adjustedFeePerVsize || feePerVbytes;
const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, { const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, {
order: this.txidToOrdering(transaction.txid), order: this.txidToOrdering(transaction.txid),
vsize, vsize,
@@ -129,7 +128,7 @@ class TransactionUtils {
sigops, sigops,
feePerVsize: feePerVbytes, feePerVsize: feePerVbytes,
adjustedFeePerVsize: adjustedFeePerVsize, adjustedFeePerVsize: adjustedFeePerVsize,
effectiveFeePerVsize: effectiveFeePerVsize, effectiveFeePerVsize: adjustedFeePerVsize,
}); });
if (!transactionExtended?.status?.confirmed && !transactionExtended.firstSeen) { if (!transactionExtended?.status?.confirmed && !transactionExtended.firstSeen) {
transactionExtended.firstSeen = Math.round((Date.now() / 1000)); transactionExtended.firstSeen = Math.round((Date.now() / 1000));

View File

@@ -16,19 +16,16 @@ import transactionUtils from './transaction-utils';
import rbfCache, { ReplacementInfo } from './rbf-cache'; import rbfCache, { ReplacementInfo } from './rbf-cache';
import difficultyAdjustment from './difficulty-adjustment'; import difficultyAdjustment from './difficulty-adjustment';
import feeApi from './fee-api'; import feeApi from './fee-api';
import BlocksRepository from '../repositories/BlocksRepository';
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository'; import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository'; import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
import Audit from './audit'; import Audit from './audit';
import priceUpdater from '../tasks/price-updater'; import priceUpdater from '../tasks/price-updater';
import { ApiPrice } from '../repositories/PricesRepository'; import { ApiPrice } from '../repositories/PricesRepository';
import { Acceleration } from './services/acceleration';
import accelerationApi from './services/acceleration'; import accelerationApi from './services/acceleration';
import mempool from './mempool'; import mempool from './mempool';
import statistics from './statistics/statistics'; import statistics from './statistics/statistics';
import accelerationRepository from '../repositories/AccelerationRepository'; import accelerationRepository from '../repositories/AccelerationRepository';
import bitcoinApi from './bitcoin/bitcoin-api-factory'; import bitcoinApi from './bitcoin/bitcoin-api-factory';
import walletApi from './services/wallets';
interface AddressTransactions { interface AddressTransactions {
mempool: MempoolTransactionExtended[], mempool: MempoolTransactionExtended[],
@@ -37,8 +34,6 @@ interface AddressTransactions {
} }
import bitcoinSecondClient from './bitcoin/bitcoin-second-client'; import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import { calculateMempoolTxCpfp } from './cpfp'; import { calculateMempoolTxCpfp } from './cpfp';
import { getRecentFirstSeen } from '../utils/file-read';
import stratumApi, { StratumJob } from './services/stratum';
// valid 'want' subscriptions // valid 'want' subscriptions
const wantable = [ const wantable = [
@@ -62,8 +57,6 @@ class WebsocketHandler {
private lastRbfSummary: ReplacementInfo[] | null = null; private lastRbfSummary: ReplacementInfo[] | null = null;
private mempoolSequence: number = 0; private mempoolSequence: number = 0;
private accelerations: Record<string, Acceleration> = {};
constructor() { } constructor() { }
addWebsocketServer(wss: WebSocket.Server) { addWebsocketServer(wss: WebSocket.Server) {
@@ -312,14 +305,6 @@ class WebsocketHandler {
} }
} }
if (parsedMessage && parsedMessage['track-wallet']) {
if (parsedMessage['track-wallet'] === 'stop') {
client['track-wallet'] = null;
} else {
client['track-wallet'] = parsedMessage['track-wallet'];
}
}
if (parsedMessage && parsedMessage['track-asset']) { if (parsedMessage && parsedMessage['track-asset']) {
if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-asset'])) { if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-asset'])) {
client['track-asset'] = parsedMessage['track-asset']; client['track-asset'] = parsedMessage['track-asset'];
@@ -404,16 +389,6 @@ class WebsocketHandler {
delete client['track-mempool']; delete client['track-mempool'];
} }
if (parsedMessage && parsedMessage['track-stratum'] != null) {
if (parsedMessage['track-stratum']) {
const sub = parsedMessage['track-stratum'];
client['track-stratum'] = sub;
response['stratumJobs'] = this.socketData['stratumJobs'];
} else {
client['track-stratum'] = false;
}
}
if (Object.keys(response).length) { if (Object.keys(response).length) {
client.send(this.serializeResponse(response)); client.send(this.serializeResponse(response));
} }
@@ -509,42 +484,6 @@ class WebsocketHandler {
} }
} }
handleAccelerationsChanged(accelerations: Record<string, Acceleration>): void {
if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server has been set');
}
const websocketAccelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, accelerations);
this.accelerations = accelerations;
if (!websocketAccelerationDelta.length) {
return;
}
// pre-compute acceleration delta
const accelerationUpdate = {
added: websocketAccelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
removed: websocketAccelerationDelta.filter(txid => !accelerations[txid]),
};
try {
const response = JSON.stringify({
accelerations: accelerationUpdate,
});
for (const server of this.webSocketServers) {
server.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
client.send(response);
});
}
} catch (e) {
logger.debug(`Error sending acceleration update to websocket clients: ${e}`);
}
}
handleReorg(): void { handleReorg(): void {
if (!this.webSocketServers.length) { if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server have been set'); throw new Error('No WebSocket.Server have been set');
@@ -581,17 +520,8 @@ class WebsocketHandler {
} }
} }
/**
*
* @param newMempool
* @param mempoolSize
* @param newTransactions array of transactions added this mempool update.
* @param recentlyDeletedTransactions array of arrays of transactions removed in the last N mempool updates, most recent first.
* @param accelerationDelta
* @param candidates
*/
async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number, async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number,
newTransactions: MempoolTransactionExtended[], recentlyDeletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[], newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[],
candidates?: GbtCandidates): Promise<void> { candidates?: GbtCandidates): Promise<void> {
if (!this.webSocketServers.length) { if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server have been set'); throw new Error('No WebSocket.Server have been set');
@@ -599,8 +529,6 @@ class WebsocketHandler {
this.printLogs(); this.printLogs();
const deletedTransactions = recentlyDeletedTransactions.length ? recentlyDeletedTransactions[0] : [];
const transactionIds = (memPool.limitGBT && candidates) ? Object.keys(candidates?.txs || {}) : Object.keys(newMempool); const transactionIds = (memPool.limitGBT && candidates) ? Object.keys(candidates?.txs || {}) : Object.keys(newMempool);
let added = newTransactions; let added = newTransactions;
let removed = deletedTransactions; let removed = deletedTransactions;
@@ -619,9 +547,9 @@ class WebsocketHandler {
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas(); const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
const mempoolInfo = memPool.getMempoolInfo(); const mempoolInfo = memPool.getMempoolInfo();
const vBytesPerSecond = memPool.getVBytesPerSecond(); const vBytesPerSecond = memPool.getVBytesPerSecond();
const rbfTransactions = Common.findRbfTransactions(newTransactions, recentlyDeletedTransactions.flat()); const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
const da = difficultyAdjustment.getDifficultyAdjustment(); const da = difficultyAdjustment.getDifficultyAdjustment();
const accelerations = accelerationApi.getAccelerations(); const accelerations = memPool.getAccelerations();
memPool.handleRbfTransactions(rbfTransactions); memPool.handleRbfTransactions(rbfTransactions);
const rbfChanges = rbfCache.getRbfChanges(); const rbfChanges = rbfCache.getRbfChanges();
let rbfReplacements; let rbfReplacements;
@@ -650,7 +578,7 @@ class WebsocketHandler {
const replacedTransactions: { replaced: string, by: TransactionExtended }[] = []; const replacedTransactions: { replaced: string, by: TransactionExtended }[] = [];
for (const tx of newTransactions) { for (const tx of newTransactions) {
if (rbfTransactions[tx.txid]) { if (rbfTransactions[tx.txid]) {
for (const replaced of rbfTransactions[tx.txid].replaced) { for (const replaced of rbfTransactions[tx.txid]) {
replacedTransactions.push({ replaced: replaced.txid, by: tx }); replacedTransactions.push({ replaced: replaced.txid, by: tx });
} }
} }
@@ -729,13 +657,10 @@ class WebsocketHandler {
const addressCache = this.makeAddressCache(newTransactions); const addressCache = this.makeAddressCache(newTransactions);
const removedAddressCache = this.makeAddressCache(deletedTransactions); const removedAddressCache = this.makeAddressCache(deletedTransactions);
const websocketAccelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, accelerations);
this.accelerations = accelerations;
// pre-compute acceleration delta // pre-compute acceleration delta
const accelerationUpdate = { const accelerationUpdate = {
added: websocketAccelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null), added: accelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
removed: websocketAccelerationDelta.filter(txid => !accelerations[txid]), removed: accelerationDelta.filter(txid => !accelerations[txid]),
}; };
// TODO - Fix indentation after PR is merged // TODO - Fix indentation after PR is merged
@@ -1022,7 +947,7 @@ class WebsocketHandler {
await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions)); await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions));
const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap()); const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
memPool.handleRbfTransactions(rbfTransactions); memPool.handleMinedRbfTransactions(rbfTransactions);
memPool.removeFromSpendMap(transactions); memPool.removeFromSpendMap(transactions);
if (config.MEMPOOL.AUDIT && memPool.isInSync()) { if (config.MEMPOOL.AUDIT && memPool.isInSync()) {
@@ -1092,14 +1017,6 @@ class WebsocketHandler {
} }
} }
if (config.CORE_RPC.DEBUG_LOG_PATH && block.extras) {
const firstSeen = getRecentFirstSeen(block.id);
if (firstSeen) {
BlocksRepository.$saveFirstSeenTime(block.id, firstSeen);
block.extras.firstSeen = firstSeen;
}
}
const confirmedTxids: { [txid: string]: boolean } = {}; const confirmedTxids: { [txid: string]: boolean } = {};
// Update mempool to remove transactions included in the new block // Update mempool to remove transactions included in the new block
@@ -1174,9 +1091,6 @@ class WebsocketHandler {
replaced: replacedTransactions, replaced: replacedTransactions,
}; };
// check for wallet transactions
const walletTransactions = config.WALLETS.ENABLED ? walletApi.processBlock(block, transactions) : [];
const responseCache = { ...this.socketData }; const responseCache = { ...this.socketData };
function getCachedResponse(key, data): string { function getCachedResponse(key, data): string {
if (!responseCache[key]) { if (!responseCache[key]) {
@@ -1381,11 +1295,6 @@ class WebsocketHandler {
response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta); response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta);
} }
if (client['track-wallet']) {
const trackedWallet = client['track-wallet'];
response['wallet-transactions'] = getCachedResponse(`wallet-transactions-${trackedWallet}`, walletTransactions[trackedWallet] ?? {});
}
if (Object.keys(response).length) { if (Object.keys(response).length) {
client.send(this.serializeResponse(response)); client.send(this.serializeResponse(response));
} }
@@ -1395,23 +1304,6 @@ class WebsocketHandler {
await statistics.runStatistics(); await statistics.runStatistics();
} }
public handleNewStratumJob(job: StratumJob): void {
this.updateSocketDataFields({ 'stratumJobs': stratumApi.getJobs() });
for (const server of this.webSocketServers) {
server.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
if (client['track-stratum'] && (client['track-stratum'] === 'all' || client['track-stratum'] === job.pool)) {
client.send(JSON.stringify({
'stratumJob': job
}));
}
});
}
}
// takes a dictionary of JSON serialized values // takes a dictionary of JSON serialized values
// and zips it together into a valid JSON object // and zips it together into a valid JSON object
private serializeResponse(response): string { private serializeResponse(response): string {

View File

@@ -32,7 +32,6 @@ interface IConfig {
AUTOMATIC_POOLS_UPDATE: boolean; AUTOMATIC_POOLS_UPDATE: boolean;
POOLS_JSON_URL: string, POOLS_JSON_URL: string,
POOLS_JSON_TREE_URL: string, POOLS_JSON_TREE_URL: string,
POOLS_UPDATE_DELAY: number,
AUDIT: boolean; AUDIT: boolean;
RUST_GBT: boolean; RUST_GBT: boolean;
LIMIT_GBT: boolean; LIMIT_GBT: boolean;
@@ -86,7 +85,6 @@ interface IConfig {
TIMEOUT: number; TIMEOUT: number;
COOKIE: boolean; COOKIE: boolean;
COOKIE_PATH: string; COOKIE_PATH: string;
DEBUG_LOG_PATH: string;
}; };
SECOND_CORE_RPC: { SECOND_CORE_RPC: {
HOST: string; HOST: string;
@@ -162,14 +160,6 @@ interface IConfig {
PAID: boolean; PAID: boolean;
API_KEY: string; API_KEY: string;
}, },
WALLETS: {
ENABLED: boolean;
WALLETS: string[];
},
STRATUM: {
ENABLED: boolean;
API: string;
}
} }
const defaults: IConfig = { const defaults: IConfig = {
@@ -202,9 +192,8 @@ const defaults: IConfig = {
'AUTOMATIC_POOLS_UPDATE': false, 'AUTOMATIC_POOLS_UPDATE': false,
'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json', 'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json',
'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master', 'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
'POOLS_UPDATE_DELAY': 604800, // in seconds, default is one week
'AUDIT': false, 'AUDIT': false,
'RUST_GBT': true, 'RUST_GBT': false,
'LIMIT_GBT': false, 'LIMIT_GBT': false,
'CPFP_INDEXING': false, 'CPFP_INDEXING': false,
'MAX_BLOCKS_BULK_QUERY': 0, 'MAX_BLOCKS_BULK_QUERY': 0,
@@ -236,8 +225,7 @@ const defaults: IConfig = {
'PASSWORD': 'mempool', 'PASSWORD': 'mempool',
'TIMEOUT': 60000, 'TIMEOUT': 60000,
'COOKIE': false, 'COOKIE': false,
'COOKIE_PATH': '/bitcoin/.cookie', 'COOKIE_PATH': '/bitcoin/.cookie'
'DEBUG_LOG_PATH': '',
}, },
'SECOND_CORE_RPC': { 'SECOND_CORE_RPC': {
'HOST': '127.0.0.1', 'HOST': '127.0.0.1',
@@ -332,14 +320,6 @@ const defaults: IConfig = {
'PAID': false, 'PAID': false,
'API_KEY': '', 'API_KEY': '',
}, },
'WALLETS': {
'ENABLED': false,
'WALLETS': [],
},
'STRATUM': {
'ENABLED': false,
'API': 'http://localhost:1234',
}
}; };
class Config implements IConfig { class Config implements IConfig {
@@ -361,8 +341,6 @@ class Config implements IConfig {
MEMPOOL_SERVICES: IConfig['MEMPOOL_SERVICES']; MEMPOOL_SERVICES: IConfig['MEMPOOL_SERVICES'];
REDIS: IConfig['REDIS']; REDIS: IConfig['REDIS'];
FIAT_PRICE: IConfig['FIAT_PRICE']; FIAT_PRICE: IConfig['FIAT_PRICE'];
WALLETS: IConfig['WALLETS'];
STRATUM: IConfig['STRATUM'];
constructor() { constructor() {
const configs = this.merge(configFromFile, defaults); const configs = this.merge(configFromFile, defaults);
@@ -384,8 +362,6 @@ class Config implements IConfig {
this.MEMPOOL_SERVICES = configs.MEMPOOL_SERVICES; this.MEMPOOL_SERVICES = configs.MEMPOOL_SERVICES;
this.REDIS = configs.REDIS; this.REDIS = configs.REDIS;
this.FIAT_PRICE = configs.FIAT_PRICE; this.FIAT_PRICE = configs.FIAT_PRICE;
this.WALLETS = configs.WALLETS;
this.STRATUM = configs.STRATUM;
} }
merge = (...objects: object[]): IConfig => { merge = (...objects: object[]): IConfig => {

View File

@@ -32,7 +32,6 @@ import pricesRoutes from './api/prices/prices.routes';
import miningRoutes from './api/mining/mining-routes'; import miningRoutes from './api/mining/mining-routes';
import liquidRoutes from './api/liquid/liquid.routes'; import liquidRoutes from './api/liquid/liquid.routes';
import bitcoinRoutes from './api/bitcoin/bitcoin.routes'; import bitcoinRoutes from './api/bitcoin/bitcoin.routes';
import servicesRoutes from './api/services/services-routes';
import fundingTxFetcher from './tasks/lightning/sync-tasks/funding-tx-fetcher'; import fundingTxFetcher from './tasks/lightning/sync-tasks/funding-tx-fetcher';
import forensicsService from './tasks/lightning/forensics.service'; import forensicsService from './tasks/lightning/forensics.service';
import priceUpdater from './tasks/price-updater'; import priceUpdater from './tasks/price-updater';
@@ -47,8 +46,6 @@ import bitcoinSecondClient from './api/bitcoin/bitcoin-second-client';
import accelerationRoutes from './api/acceleration/acceleration.routes'; import accelerationRoutes from './api/acceleration/acceleration.routes';
import aboutRoutes from './api/about.routes'; import aboutRoutes from './api/about.routes';
import mempoolBlocks from './api/mempool-blocks'; import mempoolBlocks from './api/mempool-blocks';
import walletApi from './api/services/wallets';
import stratumApi from './api/services/stratum';
class Server { class Server {
private wss: WebSocket.Server | undefined; private wss: WebSocket.Server | undefined;
@@ -214,8 +211,6 @@ class Server {
} }
}); });
} }
poolsUpdater.$startService();
} }
async runMainUpdateLoop(): Promise<void> { async runMainUpdateLoop(): Promise<void> {
@@ -234,17 +229,13 @@ class Server {
const newMempool = await bitcoinApi.$getRawMempool(); const newMempool = await bitcoinApi.$getRawMempool();
const minFeeMempool = memPool.limitGBT ? await bitcoinSecondClient.getRawMemPool() : null; const minFeeMempool = memPool.limitGBT ? await bitcoinSecondClient.getRawMemPool() : null;
const minFeeTip = memPool.limitGBT ? await bitcoinSecondClient.getBlockCount() : -1; const minFeeTip = memPool.limitGBT ? await bitcoinSecondClient.getBlockCount() : -1;
const latestAccelerations = await accelerationApi.$updateAccelerations(); const newAccelerations = await accelerationApi.$updateAccelerations();
const numHandledBlocks = await blocks.$updateBlocks(); const numHandledBlocks = await blocks.$updateBlocks();
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1); const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1);
if (numHandledBlocks === 0) { if (numHandledBlocks === 0) {
await memPool.$updateMempool(newMempool, latestAccelerations, minFeeMempool, minFeeTip, pollRate); await memPool.$updateMempool(newMempool, newAccelerations, minFeeMempool, minFeeTip, pollRate);
} }
indexer.$run(); indexer.$run();
if (config.WALLETS.ENABLED) {
// might take a while, so run in the background
walletApi.$syncWallets();
}
if (config.FIAT_PRICE.ENABLED) { if (config.FIAT_PRICE.ENABLED) {
priceUpdater.$run(); priceUpdater.$run();
} }
@@ -319,18 +310,11 @@ class Server {
priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler)); priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
} }
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler)); loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
accelerationApi.connectWebsocket();
if (config.STRATUM.ENABLED) {
stratumApi.connectWebsocket();
}
} }
setUpHttpApiRoutes(): void { setUpHttpApiRoutes(): void {
bitcoinRoutes.initRoutes(this.app); bitcoinRoutes.initRoutes(this.app);
if (config.MEMPOOL.OFFICIAL) { bitcoinCoreRoutes.initRoutes(this.app);
bitcoinCoreRoutes.initRoutes(this.app);
}
pricesRoutes.initRoutes(this.app); pricesRoutes.initRoutes(this.app);
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && config.MEMPOOL.ENABLED) { if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && config.MEMPOOL.ENABLED) {
statisticsRoutes.initRoutes(this.app); statisticsRoutes.initRoutes(this.app);
@@ -349,9 +333,6 @@ class Server {
if (config.MEMPOOL_SERVICES.ACCELERATIONS) { if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
accelerationRoutes.initRoutes(this.app); accelerationRoutes.initRoutes(this.app);
} }
if (config.WALLETS.ENABLED) {
servicesRoutes.initRoutes(this.app);
}
if (!config.MEMPOOL.OFFICIAL) { if (!config.MEMPOOL.OFFICIAL) {
aboutRoutes.initRoutes(this.app); aboutRoutes.initRoutes(this.app);
} }

View File

@@ -299,7 +299,6 @@ export interface BlockExtension {
id: number; // Note - This is the `unique_id`, not to mix with the auto increment `id` id: number; // Note - This is the `unique_id`, not to mix with the auto increment `id`
name: string; name: string;
slug: string; slug: string;
minerNames: string[] | null;
}; };
avgFee: number; avgFee: number;
avgFeeRate: number; avgFeeRate: number;
@@ -320,7 +319,6 @@ export interface BlockExtension {
segwitTotalSize: number; segwitTotalSize: number;
segwitTotalWeight: number; segwitTotalWeight: number;
header: string; header: string;
firstSeen: number | null;
utxoSetChange: number; utxoSetChange: number;
// Requires coinstatsindex, will be set to NULL otherwise // Requires coinstatsindex, will be set to NULL otherwise
utxoSetSize: number | null; utxoSetSize: number | null;
@@ -441,6 +439,46 @@ export interface Statistic {
vsize_1600: number; vsize_1600: number;
vsize_1800: number; vsize_1800: number;
vsize_2000: number; vsize_2000: number;
vsize_ps_1: number;
vsize_ps_2: number;
vsize_ps_3: number;
vsize_ps_4: number;
vsize_ps_5: number;
vsize_ps_6: number;
vsize_ps_8: number;
vsize_ps_10: number;
vsize_ps_12: number;
vsize_ps_15: number;
vsize_ps_20: number;
vsize_ps_30: number;
vsize_ps_40: number;
vsize_ps_50: number;
vsize_ps_60: number;
vsize_ps_70: number;
vsize_ps_80: number;
vsize_ps_90: number;
vsize_ps_100: number;
vsize_ps_125: number;
vsize_ps_150: number;
vsize_ps_175: number;
vsize_ps_200: number;
vsize_ps_250: number;
vsize_ps_300: number;
vsize_ps_350: number;
vsize_ps_400: number;
vsize_ps_500: number;
vsize_ps_600: number;
vsize_ps_700: number;
vsize_ps_800: number;
vsize_ps_900: number;
vsize_ps_1000: number;
vsize_ps_1200: number;
vsize_ps_1400: number;
vsize_ps_1600: number;
vsize_ps_1800: number;
vsize_ps_2000: number;
} }
export interface OptimizedStatistic { export interface OptimizedStatistic {
@@ -451,6 +489,7 @@ export interface OptimizedStatistic {
mempool_byte_weight: number; mempool_byte_weight: number;
min_fee: number; min_fee: number;
vsizes: number[]; vsizes: number[];
vsizes_ps: number[];
} }
export interface TxTrackingInfo { export interface TxTrackingInfo {
@@ -483,6 +522,7 @@ export interface WebsocketResponse {
export interface VbytesPerSecond { export interface VbytesPerSecond {
unixTime: number; unixTime: number;
vSize: number; vSize: number;
effectiveFeePerVsize: number;
} }
export interface RequiredSpec { [name: string]: RequiredParams; } export interface RequiredSpec { [name: string]: RequiredParams; }

View File

@@ -14,7 +14,6 @@ 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'; import transactionUtils from '../api/transaction-utils';
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
interface DatabaseBlock { interface DatabaseBlock {
id: string; id: string;
@@ -57,7 +56,6 @@ interface DatabaseBlock {
utxoSetChange: number; utxoSetChange: number;
utxoSetSize: number; utxoSetSize: number;
totalInputAmt: number; totalInputAmt: number;
firstSeen: number;
} }
const BLOCK_DB_FIELDS = ` const BLOCK_DB_FIELDS = `
@@ -100,8 +98,7 @@ const BLOCK_DB_FIELDS = `
blocks.header, blocks.header,
blocks.utxoset_change AS utxoSetChange, blocks.utxoset_change AS utxoSetChange,
blocks.utxoset_size AS utxoSetSize, blocks.utxoset_size AS utxoSetSize,
blocks.total_input_amt AS totalInputAmt, blocks.total_input_amt AS totalInputAmt
UNIX_TIMESTAMP(blocks.first_seen) AS firstSeen
`; `;
class BlocksRepository { class BlocksRepository {
@@ -501,7 +498,7 @@ class BlocksRepository {
} }
query += ` ORDER BY height DESC query += ` ORDER BY height DESC
LIMIT 100`; LIMIT 10`;
try { try {
const [rows]: any[] = await DB.query(query, params); const [rows]: any[] = await DB.query(query, params);
@@ -1023,24 +1020,6 @@ class BlocksRepository {
} }
} }
/**
* Save block first seen time
*
* @param id
*/
public async $saveFirstSeenTime(id: string, firstSeen: number): Promise<void> {
try {
await DB.query(`
UPDATE blocks SET first_seen = FROM_UNIXTIME(?)
WHERE hash = ?`,
[firstSeen, id]
);
} catch (e) {
logger.err(`Cannot update block first seen time. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/** /**
* Convert a mysql row block into a BlockExtended. Note that you * Convert a mysql row block into a BlockExtended. Note that you
* must provide the correct field into dbBlk object param * must provide the correct field into dbBlk object param
@@ -1075,7 +1054,6 @@ class BlocksRepository {
id: dbBlk.poolId, id: dbBlk.poolId,
name: dbBlk.poolName, name: dbBlk.poolName,
slug: dbBlk.poolSlug, slug: dbBlk.poolSlug,
minerNames: null,
}; };
extras.avgFee = dbBlk.avgFee; extras.avgFee = dbBlk.avgFee;
extras.avgFeeRate = dbBlk.avgFeeRate; extras.avgFeeRate = dbBlk.avgFeeRate;
@@ -1098,7 +1076,6 @@ class BlocksRepository {
extras.utxoSetSize = dbBlk.utxoSetSize; extras.utxoSetSize = dbBlk.utxoSetSize;
extras.totalInputAmt = dbBlk.totalInputAmt; extras.totalInputAmt = dbBlk.totalInputAmt;
extras.virtualSize = dbBlk.weight / 4.0; extras.virtualSize = dbBlk.weight / 4.0;
extras.firstSeen = dbBlk.firstSeen;
// Re-org can happen after indexing so we need to always get the // Re-org can happen after indexing so we need to always get the
// latest state from core // latest state from core
@@ -1146,10 +1123,6 @@ class BlocksRepository {
} }
} }
if (extras.pool.name === 'OCEAN') {
extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw);
}
blk.extras = <BlockExtension>extras; blk.extras = <BlockExtension>extras;
return <BlockExtended>blk; return <BlockExtended>blk;
} }

View File

@@ -83,7 +83,6 @@ module.exports = {
signRawTransaction: 'signrawtransaction', // bitcoind v0.7.0+ signRawTransaction: 'signrawtransaction', // bitcoind v0.7.0+
stop: 'stop', stop: 'stop',
submitBlock: 'submitblock', // bitcoind v0.7.0+ submitBlock: 'submitblock', // bitcoind v0.7.0+
submitPackage: 'submitpackage',
validateAddress: 'validateaddress', validateAddress: 'validateaddress',
verifyChain: 'verifychain', // bitcoind v0.9.0+ verifyChain: 'verifychain', // bitcoind v0.9.0+
verifyMessage: 'verifymessage', verifyMessage: 'verifymessage',

View File

@@ -6,30 +6,16 @@ import backendInfo from '../api/backend-info';
import logger from '../logger'; import logger from '../logger';
import { SocksProxyAgent } from 'socks-proxy-agent'; import { SocksProxyAgent } from 'socks-proxy-agent';
import * as https from 'https'; import * as https from 'https';
import { Common } from '../api/common';
/** /**
* Maintain the most recent version of pools-v2.json * Maintain the most recent version of pools-v2.json
*/ */
class PoolsUpdater { class PoolsUpdater {
tag = 'PoolsUpdater';
lastRun: number = 0; lastRun: number = 0;
currentSha: string | null = null; currentSha: string | null = null;
poolsUrl: string = config.MEMPOOL.POOLS_JSON_URL; poolsUrl: string = config.MEMPOOL.POOLS_JSON_URL;
treeUrl: string = config.MEMPOOL.POOLS_JSON_TREE_URL; treeUrl: string = config.MEMPOOL.POOLS_JSON_TREE_URL;
public async $startService(): Promise<void> {
while ('Bitcoin is still alive') {
try {
await this.updatePoolsJson();
} catch (e: any) {
logger.info(`Exception ${e} in PoolsUpdater::$startService. Code: ${e.code}. Message: ${e.message}`, this.tag);
}
await Common.sleep$(10000);
}
}
public async updatePoolsJson(): Promise<void> { public async updatePoolsJson(): Promise<void> {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false || if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false ||
config.MEMPOOL.ENABLED === false config.MEMPOOL.ENABLED === false
@@ -37,8 +23,11 @@ class PoolsUpdater {
return; return;
} }
const oneWeek = 604800;
const oneDay = 86400;
const now = new Date().getTime() / 1000; const now = new Date().getTime() / 1000;
if (now - this.lastRun < config.MEMPOOL.POOLS_UPDATE_DELAY) { // Execute the PoolsUpdate only once a week, or upon restart if (now - this.lastRun < oneWeek) { // Execute the PoolsUpdate only once a week, or upon restart
return; return;
} }
@@ -54,7 +43,7 @@ class PoolsUpdater {
this.currentSha = await this.getShaFromDb(); this.currentSha = await this.getShaFromDb();
} }
logger.debug(`pools-v2.json sha | Current: ${this.currentSha} | Github: ${githubSha}`, this.tag); logger.debug(`pools-v2.json sha | Current: ${this.currentSha} | Github: ${githubSha}`);
if (this.currentSha !== null && this.currentSha === githubSha) { if (this.currentSha !== null && this.currentSha === githubSha) {
return; return;
} }
@@ -64,16 +53,16 @@ class PoolsUpdater {
config.MEMPOOL.AUTOMATIC_POOLS_UPDATE !== true && // Automatic pools update is disabled config.MEMPOOL.AUTOMATIC_POOLS_UPDATE !== true && // Automatic pools update is disabled
!process.env.npm_config_update_pools // We're not manually updating mining pool !process.env.npm_config_update_pools // We're not manually updating mining pool
) { ) {
logger.warn(`Updated mining pools data is available (${githubSha}) but AUTOMATIC_POOLS_UPDATE is disabled`, this.tag); logger.warn(`Updated mining pools data is available (${githubSha}) but AUTOMATIC_POOLS_UPDATE is disabled`);
logger.info(`You can update your mining pools using the --update-pools command flag. You may want to clear your nginx cache as well if applicable`, this.tag); logger.info(`You can update your mining pools using the --update-pools command flag. You may want to clear your nginx cache as well if applicable`);
return; return;
} }
const network = config.SOCKS5PROXY.ENABLED ? 'tor' : 'clearnet'; const network = config.SOCKS5PROXY.ENABLED ? 'tor' : 'clearnet';
if (this.currentSha === null) { if (this.currentSha === null) {
logger.info(`Downloading pools-v2.json for the first time from ${this.poolsUrl} over ${network}`, this.tag); logger.info(`Downloading pools-v2.json for the first time from ${this.poolsUrl} over ${network}`, logger.tags.mining);
} else { } else {
logger.warn(`pools-v2.json is outdated, fetching latest from ${this.poolsUrl} over ${network}`, this.tag); logger.warn(`pools-v2.json is outdated, fetching latest from ${this.poolsUrl} over ${network}`, logger.tags.mining);
} }
const poolsJson = await this.query(this.poolsUrl); const poolsJson = await this.query(this.poolsUrl);
if (poolsJson === undefined) { if (poolsJson === undefined) {
@@ -82,7 +71,7 @@ class PoolsUpdater {
poolsParser.setMiningPools(poolsJson); poolsParser.setMiningPools(poolsJson);
if (config.DATABASE.ENABLED === false) { // Don't run db operations if (config.DATABASE.ENABLED === false) { // Don't run db operations
logger.info(`Mining pools-v2.json (${githubSha}) import completed (no database)`, this.tag); logger.info(`Mining pools-v2.json (${githubSha}) import completed (no database)`);
return; return;
} }
@@ -92,14 +81,14 @@ class PoolsUpdater {
await this.updateDBSha(githubSha); await this.updateDBSha(githubSha);
await DB.query('COMMIT;'); await DB.query('COMMIT;');
} catch (e) { } catch (e) {
logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, this.tag); logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, logger.tags.mining);
await DB.query('ROLLBACK;'); await DB.query('ROLLBACK;');
} }
logger.info(`Mining pools-v2.json (${githubSha}) import completed`, this.tag); logger.info(`Mining pools-v2.json (${githubSha}) import completed`);
} catch (e) { } catch (e) {
this.lastRun = now - 600; // Try again in 10 minutes this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week
logger.err(`PoolsUpdater failed. Will try again in 10 minutes. Exception: ${JSON.stringify(e)}`, this.tag); logger.err(`PoolsUpdater failed. Will try again in 24h. Exception: ${JSON.stringify(e)}`, logger.tags.mining);
} }
} }
@@ -113,7 +102,7 @@ class PoolsUpdater {
await DB.query('DELETE FROM state where name="pools_json_sha"'); await DB.query('DELETE FROM state where name="pools_json_sha"');
await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`); await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`);
} catch (e) { } catch (e) {
logger.err('Cannot save github pools-v2.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e), this.tag); logger.err('Cannot save github pools-v2.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
} }
} }
} }
@@ -126,7 +115,7 @@ class PoolsUpdater {
const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"'); const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"');
return (rows.length > 0 ? rows[0].string : null); return (rows.length > 0 ? rows[0].string : null);
} catch (e) { } catch (e) {
logger.err('Cannot fetch pools-v2.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), this.tag); logger.err('Cannot fetch pools-v2.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
return null; return null;
} }
} }
@@ -145,7 +134,7 @@ class PoolsUpdater {
} }
} }
logger.err(`Cannot find "pools-v2.json" in git tree (${this.treeUrl})`, this.tag); logger.err(`Cannot find "pools-v2.json" in git tree (${this.treeUrl})`, logger.tags.mining);
return null; return null;
} }
@@ -197,7 +186,7 @@ class PoolsUpdater {
} }
return data.data; return data.data;
} catch (e) { } catch (e) {
logger.err('Could not connect to Github. Reason: ' + (e instanceof Error ? e.message : e), this.tag); logger.err('Could not connect to Github. Reason: ' + (e instanceof Error ? e.message : e));
retry++; retry++;
} }
await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL); await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL);

View File

@@ -1,9 +0,0 @@
import { Request, Response } from 'express';
export function handleError(req: Request, res: Response, statusCode: number, errorMessage: string | unknown): void {
if (req.accepts('json')) {
res.status(statusCode).json({ error: errorMessage });
} else {
res.status(statusCode).send(errorMessage);
}
}

View File

@@ -200,28 +200,4 @@ export function getVarIntLength(n: number): number {
} else { } else {
return 9; return 9;
} }
}
/** Extracts miner names from a DATUM coinbase transaction */
export function parseDATUMTemplateCreator(coinbaseRaw: string): string[] | null {
let bytes: number[] = [];
for (let c = 0; c < coinbaseRaw.length; c += 2) {
bytes.push(parseInt(coinbaseRaw.slice(c, c + 2), 16));
}
// Skip block height
let tagLengthByte = 1 + bytes[0];
let tagsLength = bytes[tagLengthByte];
if (tagsLength == 0x4c) {
tagLengthByte += 1;
tagsLength = bytes[tagLengthByte];
}
const tagStart = tagLengthByte + 1;
const tags = bytes.slice(tagStart, tagStart + tagsLength);
let tagString = String.fromCharCode(...tags);
tagString = tagString.replace('\x00', '');
return tagString.split('\x0f').map((name) => name.replace(/[^a-zA-Z0-9 ]/g, ''));
} }

View File

@@ -1,58 +0,0 @@
import * as fs from 'fs';
import logger from '../logger';
import config from '../config';
function readFile(filePath: string, bufferSize?: number): string[] {
const fileSize = fs.statSync(filePath).size;
const chunkSize = bufferSize || fileSize;
const fileDescriptor = fs.openSync(filePath, 'r');
const buffer = Buffer.alloc(chunkSize);
fs.readSync(fileDescriptor, buffer, 0, chunkSize, fileSize - chunkSize);
fs.closeSync(fileDescriptor);
const lines = buffer.toString('utf8', 0, chunkSize).split('\n');
return lines;
}
function extractDateFromLogLine(line: string): number | undefined {
// Extract time from log: "2021-08-31T12:34:56Z" or "2021-08-31T12:34:56.123456Z"
const dateMatch = line.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{6})?Z/);
if (!dateMatch) {
return undefined;
}
const dateStr = dateMatch[0];
const date = new Date(dateStr);
let timestamp = Math.floor(date.getTime() / 1000); // Remove decimal (microseconds are added later)
const timePart = dateStr.split('T')[1];
const microseconds = timePart.split('.')[1] || '';
if (!microseconds) {
return timestamp;
}
return parseFloat(timestamp + '.' + microseconds);
}
export function getRecentFirstSeen(hash: string): number | undefined {
const debugLogPath = config.CORE_RPC.DEBUG_LOG_PATH;
if (debugLogPath) {
try {
// Read the last few lines of debug.log
const lines = readFile(debugLogPath, 2048);
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i];
if (line && line.includes(`Saw new header hash=${hash}`)) {
return extractDateFromLogLine(line);
}
}
} catch (e) {
logger.err(`Cannot parse block first seen time from Core logs. Reason: ` + (e instanceof Error ? e.message : e));
}
}
return undefined;
}

View File

@@ -109,7 +109,6 @@ Below we list all settings from `mempool-config.json` and the corresponding over
"AUTOMATIC_POOLS_UPDATE": false, "AUTOMATIC_POOLS_UPDATE": false,
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json", "POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json",
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master", "POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
"POOLS_UPDATE_DELAY": 604800,
"CPFP_INDEXING": false, "CPFP_INDEXING": false,
"MAX_BLOCKS_BULK_QUERY": 0, "MAX_BLOCKS_BULK_QUERY": 0,
"DISK_CACHE_BLOCK_INTERVAL": 6, "DISK_CACHE_BLOCK_INTERVAL": 6,
@@ -141,7 +140,6 @@ Corresponding `docker-compose.yml` overrides:
MEMPOOL_AUTOMATIC_POOLS_UPDATE: "" MEMPOOL_AUTOMATIC_POOLS_UPDATE: ""
MEMPOOL_POOLS_JSON_URL: "" MEMPOOL_POOLS_JSON_URL: ""
MEMPOOL_POOLS_JSON_TREE_URL: "" MEMPOOL_POOLS_JSON_TREE_URL: ""
MEMPOOL_POOLS_UPDATE_DELAY: ""
MEMPOOL_CPFP_INDEXING: "" MEMPOOL_CPFP_INDEXING: ""
MEMPOOL_MAX_BLOCKS_BULK_QUERY: "" MEMPOOL_MAX_BLOCKS_BULK_QUERY: ""
MEMPOOL_DISK_CACHE_BLOCK_INTERVAL: "" MEMPOOL_DISK_CACHE_BLOCK_INTERVAL: ""

View File

@@ -36,7 +36,6 @@
"ALLOW_UNREACHABLE": __MEMPOOL_ALLOW_UNREACHABLE__, "ALLOW_UNREACHABLE": __MEMPOOL_ALLOW_UNREACHABLE__,
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__", "POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__", "POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
"POOLS_UPDATE_DELAY": __MEMPOOL_POOLS_UPDATE_DELAY__,
"PRICE_UPDATES_PER_HOUR": __MEMPOOL_PRICE_UPDATES_PER_HOUR__, "PRICE_UPDATES_PER_HOUR": __MEMPOOL_PRICE_UPDATES_PER_HOUR__,
"MAX_TRACKED_ADDRESSES": __MEMPOOL_MAX_TRACKED_ADDRESSES__ "MAX_TRACKED_ADDRESSES": __MEMPOOL_MAX_TRACKED_ADDRESSES__
}, },
@@ -47,8 +46,7 @@
"PASSWORD": "__CORE_RPC_PASSWORD__", "PASSWORD": "__CORE_RPC_PASSWORD__",
"TIMEOUT": __CORE_RPC_TIMEOUT__, "TIMEOUT": __CORE_RPC_TIMEOUT__,
"COOKIE": __CORE_RPC_COOKIE__, "COOKIE": __CORE_RPC_COOKIE__,
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__", "COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__"
"DEBUG_LOG_PATH": "__CORE_RPC_DEBUG_LOG_PATH__"
}, },
"ELECTRUM": { "ELECTRUM": {
"HOST": "__ELECTRUM_HOST__", "HOST": "__ELECTRUM_HOST__",
@@ -148,10 +146,6 @@
"API": "__MEMPOOL_SERVICES_API__", "API": "__MEMPOOL_SERVICES_API__",
"ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__ "ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__
}, },
"STRATUM": {
"ENABLED": __STRATUM_ENABLED__,
"API": "__STRATUM_API__"
},
"REDIS": { "REDIS": {
"ENABLED": __REDIS_ENABLED__, "ENABLED": __REDIS_ENABLED__,
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__", "UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__",

View File

@@ -29,9 +29,8 @@ __MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info}
__MEMPOOL_AUTOMATIC_POOLS_UPDATE__=${MEMPOOL_AUTOMATIC_POOLS_UPDATE:=false} __MEMPOOL_AUTOMATIC_POOLS_UPDATE__=${MEMPOOL_AUTOMATIC_POOLS_UPDATE:=false}
__MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json} __MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json}
__MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master} __MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master}
__MEMPOOL_POOLS_UPDATE_DELAY__=${MEMPOOL_POOLS_UPDATE_DELAY:=604800}
__MEMPOOL_AUDIT__=${MEMPOOL_AUDIT:=false} __MEMPOOL_AUDIT__=${MEMPOOL_AUDIT:=false}
__MEMPOOL_RUST_GBT__=${MEMPOOL_RUST_GBT:=true} __MEMPOOL_RUST_GBT__=${MEMPOOL_RUST_GBT:=false}
__MEMPOOL_LIMIT_GBT__=${MEMPOOL_LIMIT_GBT:=false} __MEMPOOL_LIMIT_GBT__=${MEMPOOL_LIMIT_GBT:=false}
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false} __MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
__MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0} __MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0}
@@ -49,7 +48,6 @@ __CORE_RPC_PASSWORD__=${CORE_RPC_PASSWORD:=mempool}
__CORE_RPC_TIMEOUT__=${CORE_RPC_TIMEOUT:=60000} __CORE_RPC_TIMEOUT__=${CORE_RPC_TIMEOUT:=60000}
__CORE_RPC_COOKIE__=${CORE_RPC_COOKIE:=false} __CORE_RPC_COOKIE__=${CORE_RPC_COOKIE:=false}
__CORE_RPC_COOKIE_PATH__=${CORE_RPC_COOKIE_PATH:=""} __CORE_RPC_COOKIE_PATH__=${CORE_RPC_COOKIE_PATH:=""}
__CORE_RPC_DEBUG_LOG_PATH__=${CORE_RPC_DEBUG_LOG_PATH:=""}
# ELECTRUM # ELECTRUM
__ELECTRUM_HOST__=${ELECTRUM_HOST:=127.0.0.1} __ELECTRUM_HOST__=${ELECTRUM_HOST:=127.0.0.1}
@@ -149,10 +147,6 @@ __REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:="https://mempool.space/api/v1/services"} __MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:="https://mempool.space/api/v1/services"}
__MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false} __MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
# STRATUM
__STRATUM_ENABLED__=${STRATUM_ENABLED:=false}
__STRATUM_API__=${STRATUM_API:="http://localhost:1234"}
# REDIS # REDIS
__REDIS_ENABLED__=${REDIS_ENABLED:=false} __REDIS_ENABLED__=${REDIS_ENABLED:=false}
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=""} __REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=""}
@@ -193,7 +187,6 @@ sed -i "s!__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__!${__MEMPOOL_STDOUT_LOG_MIN_PRIORIT
sed -i "s!__MEMPOOL_AUTOMATIC_POOLS_UPDATE__!${__MEMPOOL_AUTOMATIC_POOLS_UPDATE__}!g" mempool-config.json sed -i "s!__MEMPOOL_AUTOMATIC_POOLS_UPDATE__!${__MEMPOOL_AUTOMATIC_POOLS_UPDATE__}!g" mempool-config.json
sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-config.json sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_POOLS_UPDATE_DELAY__!${__MEMPOOL_POOLS_UPDATE_DELAY__}!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_RUST_GBT__!${__MEMPOOL_RUST_GBT__}!g" mempool-config.json sed -i "s!__MEMPOOL_RUST_GBT__!${__MEMPOOL_RUST_GBT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_LIMIT_GBT__!${__MEMPOOL_LIMIT_GBT__}!g" mempool-config.json sed -i "s!__MEMPOOL_LIMIT_GBT__!${__MEMPOOL_LIMIT_GBT__}!g" mempool-config.json
@@ -212,7 +205,6 @@ sed -i "s!__CORE_RPC_PASSWORD__!${__CORE_RPC_PASSWORD__}!g" mempool-config.json
sed -i "s!__CORE_RPC_TIMEOUT__!${__CORE_RPC_TIMEOUT__}!g" mempool-config.json sed -i "s!__CORE_RPC_TIMEOUT__!${__CORE_RPC_TIMEOUT__}!g" mempool-config.json
sed -i "s!__CORE_RPC_COOKIE__!${__CORE_RPC_COOKIE__}!g" mempool-config.json sed -i "s!__CORE_RPC_COOKIE__!${__CORE_RPC_COOKIE__}!g" mempool-config.json
sed -i "s!__CORE_RPC_COOKIE_PATH__!${__CORE_RPC_COOKIE_PATH__}!g" mempool-config.json sed -i "s!__CORE_RPC_COOKIE_PATH__!${__CORE_RPC_COOKIE_PATH__}!g" mempool-config.json
sed -i "s!__CORE_RPC_DEBUG_LOG_PATH__!${__CORE_RPC_DEBUG_LOG_PATH__}!g" mempool-config.json
sed -i "s!__ELECTRUM_HOST__!${__ELECTRUM_HOST__}!g" mempool-config.json sed -i "s!__ELECTRUM_HOST__!${__ELECTRUM_HOST__}!g" mempool-config.json
sed -i "s!__ELECTRUM_PORT__!${__ELECTRUM_PORT__}!g" mempool-config.json sed -i "s!__ELECTRUM_PORT__!${__ELECTRUM_PORT__}!g" mempool-config.json
@@ -304,10 +296,6 @@ sed -i "s!__REPLICATION_SERVERS__!${__REPLICATION_SERVERS__}!g" mempool-config.j
sed -i "s!__MEMPOOL_SERVICES_API__!${__MEMPOOL_SERVICES_API__}!g" mempool-config.json sed -i "s!__MEMPOOL_SERVICES_API__!${__MEMPOOL_SERVICES_API__}!g" mempool-config.json
sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS__}!g" mempool-config.json sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS__}!g" mempool-config.json
# STRATUM
sed -i "s!__STRATUM_ENABLED__!${__STRATUM_ENABLED__}!g" mempool-config.json
sed -i "s!__STRATUM_API__!${__STRATUM_API__}!g" mempool-config.json
# REDIS # REDIS
sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json
sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json

View File

@@ -1,48 +0,0 @@
{
"theme": "wiz",
"enterprise": "bitb",
"branding": {
"name": "bitb",
"title": "BITB",
"site_id": 20,
"header_img": "/resources/bitblogo.svg",
"footer_img": "/resources/bitblogo.svg"
},
"dashboard": {
"widgets": [
{
"component": "fees",
"mobileOrder": 4
},
{
"component": "walletBalance",
"mobileOrder": 1,
"props": {
"wallet": "BITB"
}
},
{
"component": "goggles",
"mobileOrder": 5
},
{
"component": "wallet",
"mobileOrder": 2,
"props": {
"wallet": "BITB",
"period": "all"
}
},
{
"component": "blocks"
},
{
"component": "walletTransactions",
"mobileOrder": 3,
"props": {
"wallet": "BITB"
}
}
]
}
}

View File

@@ -1,51 +0,0 @@
{
"theme": "contrast",
"enterprise": "meta",
"branding": {
"name": "metaplanet",
"title": "Metaplanet",
"site_id": 21,
"header_img": "/resources/metalogo.svg",
"footer_img": "/resources/metalogo.svg"
},
"dashboard": {
"widgets": [
{
"component": "fees",
"mobileOrder": 4
},
{
"component": "walletBalance",
"mobileOrder": 1,
"props": {
"wallet": "3350"
}
},
{
"component": "twitter",
"mobileOrder": 5,
"props": {
"handle": "Metaplanet_JP"
}
},
{
"component": "wallet",
"mobileOrder": 2,
"props": {
"wallet": "3350",
"period": "all"
}
},
{
"component": "blocks"
},
{
"component": "walletTransactions",
"mobileOrder": 3,
"props": {
"wallet": "3350"
}
}
]
}
}

View File

@@ -344,9 +344,7 @@ describe('Mainnet', () => {
cy.visit('/'); cy.visit('/');
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
//TODO(knorrium): add a check for the proxied server cy.changeNetwork('testnet4');
// cy.changeNetwork('testnet4');
cy.changeNetwork('signet'); cy.changeNetwork('signet');
cy.changeNetwork('mainnet'); cy.changeNetwork('mainnet');
}); });

View File

@@ -27,6 +27,5 @@
"ACCELERATOR": false, "ACCELERATOR": false,
"ACCELERATOR_BUTTON": true, "ACCELERATOR_BUTTON": true,
"PUBLIC_ACCELERATIONS": false, "PUBLIC_ACCELERATIONS": false,
"STRATUM_ENABLED": false,
"SERVICES_API": "https://mempool.space/api/v1/services" "SERVICES_API": "https://mempool.space/api/v1/services"
} }

File diff suppressed because it is too large Load Diff

View File

@@ -76,9 +76,9 @@
"@angular/router": "^17.3.1", "@angular/router": "^17.3.1",
"@angular/ssr": "^17.3.1", "@angular/ssr": "^17.3.1",
"@fortawesome/angular-fontawesome": "~0.14.1", "@fortawesome/angular-fontawesome": "~0.14.1",
"@fortawesome/fontawesome-common-types": "~6.7.2", "@fortawesome/fontawesome-common-types": "~6.6.0",
"@fortawesome/fontawesome-svg-core": "~6.7.2", "@fortawesome/fontawesome-svg-core": "~6.6.0",
"@fortawesome/free-solid-svg-icons": "~6.7.2", "@fortawesome/free-solid-svg-icons": "~6.6.0",
"@mempool/mempool.js": "2.3.0", "@mempool/mempool.js": "2.3.0",
"@ng-bootstrap/ng-bootstrap": "^16.0.0", "@ng-bootstrap/ng-bootstrap": "^16.0.0",
"@types/qrcode": "~1.5.0", "@types/qrcode": "~1.5.0",
@@ -87,14 +87,15 @@
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"domino": "^2.1.6", "domino": "^2.1.6",
"echarts": "~5.5.0", "echarts": "~5.5.0",
"lightweight-charts": "~3.8.0",
"ngx-echarts": "~17.2.0", "ngx-echarts": "~17.2.0",
"ngx-infinite-scroll": "^17.0.0", "ngx-infinite-scroll": "^17.0.0",
"qrcode": "1.5.1", "qrcode": "1.5.1",
"rxjs": "~7.8.1", "rxjs": "~7.8.1",
"esbuild": "^0.24.0", "esbuild": "^0.23.0",
"tinyify": "^4.0.0", "tinyify": "^4.0.0",
"tlite": "^0.1.9", "tlite": "^0.1.9",
"tslib": "~2.8.0", "tslib": "~2.7.0",
"zone.js": "~0.14.4" "zone.js": "~0.14.4"
}, },
"devDependencies": { "devDependencies": {
@@ -104,7 +105,7 @@
"@typescript-eslint/eslint-plugin": "^7.4.0", "@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^7.4.0", "@typescript-eslint/parser": "^7.4.0",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"browser-sync": "^3.0.3", "browser-sync": "^3.0.0",
"http-proxy-middleware": "~2.0.6", "http-proxy-middleware": "~2.0.6",
"prettier": "^3.0.0", "prettier": "^3.0.0",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
@@ -114,7 +115,7 @@
"optionalDependencies": { "optionalDependencies": {
"@cypress/schematic": "^2.5.0", "@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3", "@types/cypress": "^1.1.3",
"cypress": "^13.17.0", "cypress": "^13.14.0",
"cypress-fail-on-console-error": "~5.1.0", "cypress-fail-on-console-error": "~5.1.0",
"cypress-wait-until": "^2.0.1", "cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1", "mock-socket": "~9.3.1",

View File

@@ -3,10 +3,8 @@ const fs = require('fs');
let PROXY_CONFIG = require('./proxy.conf'); let PROXY_CONFIG = require('./proxy.conf');
PROXY_CONFIG.forEach(entry => { PROXY_CONFIG.forEach(entry => {
const hostname = process.env.CYPRESS_REROUTE_TESTNET === 'true' ? 'mempool-staging.fra.mempool.space' : 'node201.fmt.mempool.space'; entry.target = entry.target.replace("mempool.space", "mempool-staging.fra.mempool.space");
console.log(`e2e tests running against ${hostname}`); entry.target = entry.target.replace("liquid.network", "liquid-staging.fra.mempool.space");
entry.target = entry.target.replace("mempool.space", hostname);
entry.target = entry.target.replace("liquid.network", "liquid-staging.fmt.mempool.space");
}); });
module.exports = PROXY_CONFIG; module.exports = PROXY_CONFIG;

View File

@@ -1,15 +1,15 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router'; import { Routes, RouterModule } from '@angular/router';
import { AppPreloadingStrategy } from '@app/app.preloading-strategy' import { AppPreloadingStrategy } from './app.preloading-strategy'
import { BlockViewComponent } from '@components/block-view/block-view.component'; import { BlockViewComponent } from './components/block-view/block-view.component';
import { EightBlocksComponent } from '@components/eight-blocks/eight-blocks.component'; import { EightBlocksComponent } from './components/eight-blocks/eight-blocks.component';
import { MempoolBlockViewComponent } from '@components/mempool-block-view/mempool-block-view.component'; import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component';
import { ClockComponent } from '@components/clock/clock.component'; import { ClockComponent } from './components/clock/clock.component';
import { StatusViewComponent } from '@components/status-view/status-view.component'; import { StatusViewComponent } from './components/status-view/status-view.component';
import { AddressGroupComponent } from '@components/address-group/address-group.component'; import { AddressGroupComponent } from './components/address-group/address-group.component';
import { TrackerComponent } from '@components/tracker/tracker.component'; import { TrackerComponent } from './components/tracker/tracker.component';
import { AccelerateCheckout } from '@components/accelerate-checkout/accelerate-checkout.component'; import { AccelerateCheckout } from './components/accelerate-checkout/accelerate-checkout.component';
import { TrackerGuard } from '@app/route-guards'; import { TrackerGuard } from './route-guards';
const browserWindow = window || {}; const browserWindow = window || {};
// @ts-ignore // @ts-ignore
@@ -22,50 +22,12 @@ let routes: Routes = [
{ {
path: '', path: '',
pathMatch: 'full', pathMatch: 'full',
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
path: '', path: '',
loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule), loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
data: { preload: true },
},
{
path: 'widget/wallet',
children: [],
component: AddressGroupComponent,
data: {
networkSpecific: true,
}
},
{
path: 'status',
data: { networks: ['bitcoin', 'liquid'] },
component: StatusViewComponent
},
{
path: '',
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: '**',
redirectTo: '/testnet'
},
]
},
{
path: 'testnet4',
children: [
{
path: '',
pathMatch: 'full',
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: '',
loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
@@ -83,7 +45,45 @@ let routes: Routes = [
}, },
{ {
path: '', path: '',
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: '**',
redirectTo: '/testnet'
},
]
},
{
path: 'testnet4',
children: [
{
path: '',
pathMatch: 'full',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: '',
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
data: { preload: true },
},
{
path: 'wallet',
children: [],
component: AddressGroupComponent,
data: {
networkSpecific: true,
}
},
{
path: 'status',
data: { networks: ['bitcoin', 'liquid'] },
component: StatusViewComponent
},
{
path: '',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
@@ -103,16 +103,16 @@ let routes: Routes = [
{ {
path: '', path: '',
pathMatch: 'full', pathMatch: 'full',
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
path: '', path: '',
loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule), loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
path: 'widget/wallet', path: 'wallet',
children: [], children: [],
component: AddressGroupComponent, component: AddressGroupComponent,
data: { data: {
@@ -126,7 +126,7 @@ let routes: Routes = [
}, },
{ {
path: '', path: '',
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
@@ -138,22 +138,22 @@ let routes: Routes = [
{ {
path: '', path: '',
pathMatch: 'full', pathMatch: 'full',
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
path: 'tx', path: 'tx',
canMatch: [TrackerGuard], canMatch: [TrackerGuard],
runGuardsAndResolvers: 'always', runGuardsAndResolvers: 'always',
loadChildren: () => import('@components/tracker/tracker.module').then(m => m.TrackerModule), loadChildren: () => import('./components/tracker/tracker.module').then(m => m.TrackerModule),
}, },
{ {
path: '', path: '',
loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule), loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
path: 'widget/wallet', path: 'wallet',
children: [], children: [],
component: AddressGroupComponent, component: AddressGroupComponent,
data: { data: {
@@ -165,19 +165,19 @@ let routes: Routes = [
children: [ children: [
{ {
path: '', path: '',
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule) loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
}, },
{ {
path: 'testnet', path: 'testnet',
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule) loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
}, },
{ {
path: 'testnet4', path: 'testnet4',
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule) loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
}, },
{ {
path: 'signet', path: 'signet',
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule) loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
}, },
], ],
}, },
@@ -212,7 +212,7 @@ let routes: Routes = [
}, },
{ {
path: '', path: '',
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
]; ];
@@ -225,16 +225,16 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
{ {
path: '', path: '',
pathMatch: 'full', pathMatch: 'full',
loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
path: '', path: '',
loadChildren: () => import ('@app/liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule), loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
path: 'widget/wallet', path: 'wallet',
children: [], children: [],
component: AddressGroupComponent, component: AddressGroupComponent,
data: { data: {
@@ -248,7 +248,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
}, },
{ {
path: '', path: '',
loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
@@ -260,16 +260,16 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
{ {
path: '', path: '',
pathMatch: 'full', pathMatch: 'full',
loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
path: '', path: '',
loadChildren: () => import ('@app/liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule), loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
path: 'widget/wallet', path: 'wallet',
children: [], children: [],
component: AddressGroupComponent, component: AddressGroupComponent,
data: { data: {
@@ -281,11 +281,11 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
children: [ children: [
{ {
path: '', path: '',
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule) loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
}, },
{ {
path: 'testnet', path: 'testnet',
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule) loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
}, },
], ],
}, },
@@ -296,7 +296,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
}, },
{ {
path: '', path: '',
loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
]; ];

View File

@@ -439,39 +439,4 @@ export const fiatCurrencies = {
code: 'ZAR', code: 'ZAR',
indexed: true, indexed: true,
}, },
}; };
export interface Timezone {
offset: string;
name: string;
}
export const timezones: Timezone[] = [
{ offset: '-12', name: 'Anywhere on Earth (AoE)' },
{ offset: '-11', name: 'Samoa Standard Time (SST)' },
{ offset: '-10', name: 'Hawaii Standard Time (HST)' },
{ offset: '-9', name: 'Alaska Standard Time (AKST)' },
{ offset: '-8', name: 'Pacific Standard Time (PST)' },
{ offset: '-7', name: 'Mountain Standard Time (MST)' },
{ offset: '-6', name: 'Central Standard Time (CST)' },
{ offset: '-5', name: 'Eastern Standard Time (EST)' },
{ offset: '-4', name: 'Atlantic Standard Time (AST)' },
{ offset: '-3', name: 'Argentina Time (ART)' },
{ offset: '-2', name: 'Fernando de Noronha Time (FNT)' },
{ offset: '-1', name: 'Azores Time (AZOT)' },
{ offset: '+0', name: 'Greenwich Mean Time (GMT)' },
{ offset: '+1', name: 'Central European Time (CET)' },
{ offset: '+2', name: 'Eastern European Time (EET)' },
{ offset: '+3', name: 'Moscow Standard Time (MSK)' },
{ offset: '+4', name: 'Armenia Time (AMT)' },
{ offset: '+5', name: 'Pakistan Standard Time (PKT)' },
{ offset: '+6', name: 'Xinjiang Time (XJT)' },
{ offset: '+7', name: 'Indochina Time (ICT)' },
{ offset: '+8', name: 'Hong Kong Time (HKT)' },
{ offset: '+9', name: 'Japan Standard Time (JST)' },
{ offset: '+10', name: 'Australian Eastern Standard Time (AEST)' },
{ offset: '+11', name: 'Norfolk Time (NFT)' },
{ offset: '+12', name: 'New Zealand Standard Time (NZST)' },
{ offset: '+13', name: 'Tonga Time (TOT)' },
{ offset: '+14', name: 'Line Islands Time (LINT)' }
];

View File

@@ -2,11 +2,11 @@ import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server'; import { ServerModule } from '@angular/platform-server';
import { ZONE_SERVICE } from '@app/injection-tokens'; import { ZONE_SERVICE } from './injection-tokens';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { AppComponent } from '@components/app/app.component'; import { AppComponent } from './components/app/app.component';
import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor'; import { HttpCacheInterceptor } from './services/http-cache.interceptor';
import { ZoneService } from '@app/services/zone.service'; import { ZoneService } from './services/zone.service';
@NgModule({ @NgModule({
@@ -20,4 +20,4 @@ import { ZoneService } from '@app/services/zone.service';
], ],
bootstrap: [AppComponent], bootstrap: [AppComponent],
}) })
export class AppServerModule {} export class AppServerModule {}

View File

@@ -2,38 +2,35 @@ import { BrowserModule } from '@angular/platform-browser';
import { ModuleWithProviders, NgModule } from '@angular/core'; import { ModuleWithProviders, NgModule } from '@angular/core';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ZONE_SERVICE } from '@app/injection-tokens'; import { ZONE_SERVICE } from './injection-tokens';
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from '@components/app/app.component'; import { AppComponent } from './components/app/app.component';
import { ElectrsApiService } from '@app/services/electrs-api.service'; import { ElectrsApiService } from './services/electrs-api.service';
import { OrdApiService } from '@app/services/ord-api.service'; import { StateService } from './services/state.service';
import { StateService } from '@app/services/state.service'; import { CacheService } from './services/cache.service';
import { CacheService } from '@app/services/cache.service'; import { PriceService } from './services/price.service';
import { PriceService } from '@app/services/price.service'; import { EnterpriseService } from './services/enterprise.service';
import { EnterpriseService } from '@app/services/enterprise.service'; import { WebsocketService } from './services/websocket.service';
import { WebsocketService } from '@app/services/websocket.service'; import { AudioService } from './services/audio.service';
import { AudioService } from '@app/services/audio.service'; import { PreloadService } from './services/preload.service';
import { PreloadService } from '@app/services/preload.service'; import { SeoService } from './services/seo.service';
import { SeoService } from '@app/services/seo.service'; import { OpenGraphService } from './services/opengraph.service';
import { OpenGraphService } from '@app/services/opengraph.service'; import { ZoneService } from './services/zone-shim.service';
import { ZoneService } from '@app/services/zone-shim.service'; import { SharedModule } from './shared/shared.module';
import { SharedModule } from '@app/shared/shared.module'; import { StorageService } from './services/storage.service';
import { StorageService } from '@app/services/storage.service'; import { HttpCacheInterceptor } from './services/http-cache.interceptor';
import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor'; import { LanguageService } from './services/language.service';
import { LanguageService } from '@app/services/language.service'; import { ThemeService } from './services/theme.service';
import { ThemeService } from '@app/services/theme.service'; import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe';
import { TimeService } from '@app/services/time.service'; import { FiatCurrencyPipe } from './shared/pipes/fiat-currency.pipe';
import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe'; import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe'; import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe';
import { ShortenStringPipe } from '@app/shared/pipes/shorten-string-pipe/shorten-string.pipe'; import { AppPreloadingStrategy } from './app.preloading-strategy';
import { CapAddressPipe } from '@app/shared/pipes/cap-address-pipe/cap-address-pipe'; import { ServicesApiServices } from './services/services-api.service';
import { AppPreloadingStrategy } from '@app/app.preloading-strategy';
import { ServicesApiServices } from '@app/services/services-api.service';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
const providers = [ const providers = [
ElectrsApiService, ElectrsApiService,
OrdApiService,
StateService, StateService,
CacheService, CacheService,
PriceService, PriceService,
@@ -45,7 +42,6 @@ const providers = [
EnterpriseService, EnterpriseService,
LanguageService, LanguageService,
ThemeService, ThemeService,
TimeService,
ShortenStringPipe, ShortenStringPipe,
FiatShortenerPipe, FiatShortenerPipe,
FiatCurrencyPipe, FiatCurrencyPipe,

View File

@@ -1,13 +1,13 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Routes, RouterModule } from '@angular/router'; import { Routes, RouterModule } from '@angular/router';
import { MasterPageComponent } from '@components/master-page/master-page.component'; import { MasterPageComponent } from './components/master-page/master-page.component';
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: '',
component: MasterPageComponent, component: MasterPageComponent,
loadChildren: () => import('@app/graphs/graphs.module').then(m => m.GraphsModule), loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule),
data: { preload: true }, data: { preload: true },
} }
]; ];

View File

@@ -1,5 +1,5 @@
import { Transaction, Vin } from '@interfaces/electrs.interface'; import { Transaction, Vin } from './interfaces/electrs.interface';
import { Hash } from '@app/shared/sha256'; import { Hash } from './shared/sha256';
const P2SH_P2WPKH_COST = 21 * 4; // the WU cost for the non-witness part of P2SH-P2WPKH const P2SH_P2WPKH_COST = 21 * 4; // the WU cost for the non-witness part of P2SH-P2WPKH
const P2SH_P2WSH_COST = 35 * 4; // the WU cost for the non-witness part of P2SH-P2WSH const P2SH_P2WSH_COST = 35 * 4; // the WU cost for the non-witness part of P2SH-P2WSH
@@ -303,4 +303,4 @@ export async function calcScriptHash$(script: string): Promise<string> {
return hashArray return hashArray
.map((bytes) => bytes.toString(16).padStart(2, '0')) .map((bytes) => bytes.toString(16).padStart(2, '0'))
.join(''); .join('');
} }

View File

@@ -1,5 +1,5 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { EnterpriseService } from '@app/services/enterprise.service'; import { EnterpriseService } from '../../services/enterprise.service';
@Component({ @Component({
selector: 'app-about-sponsors', selector: 'app-about-sponsors',

View File

@@ -201,17 +201,12 @@
<img class="image" src="/resources/profile/leather.svg" /> <img class="image" src="/resources/profile/leather.svg" />
<span>Leather</span> <span>Leather</span>
</a> </a>
<a href="https://taprootwizards.com/" target="_blank" title="Taproot Wizards">
<img class="image" src="/resources/profile/wizardhat.png" />
<span>Taproot Wizards</span>
</a>
</div> </div>
</div> </div>
<ng-container> <ng-container>
<div *ngIf="profiles$ | async as profiles" id="community-sponsors-anchor"> <div *ngIf="profiles$ | async as profiles" id="community-sponsors-anchor">
<div class="community-sponsor whale-sponsor" style="margin-bottom: 68px" *ngIf="profiles.whales.length > 0"> <div class="community-sponsor" style="margin-bottom: 68px" *ngIf="profiles.whales.length > 0">
<h3 i18n="about.sponsors.withHeart">Whale Sponsors</h3> <h3 i18n="about.sponsors.withHeart">Whale Sponsors</h3>
<div class="wrapper"> <div class="wrapper">
<ng-container> <ng-container>

View File

@@ -92,13 +92,6 @@
} }
} }
.whale-sponsor {
img {
width: 70px;
height: 70px;
}
}
.alliances { .alliances {
margin-bottom: 100px; margin-bottom: 100px;
a { a {

View File

@@ -1,16 +1,16 @@
import { ChangeDetectionStrategy, Component, ElementRef, Inject, LOCALE_ID, OnInit, ViewChild } from '@angular/core'; import { ChangeDetectionStrategy, Component, ElementRef, Inject, LOCALE_ID, OnInit, ViewChild } from '@angular/core';
import { WebsocketService } from '@app/services/websocket.service'; import { WebsocketService } from '../../services/websocket.service';
import { SeoService } from '@app/services/seo.service'; import { SeoService } from '../../services/seo.service';
import { OpenGraphService } from '@app/services/opengraph.service'; import { OpenGraphService } from '../../services/opengraph.service';
import { StateService } from '@app/services/state.service'; import { StateService } from '../../services/state.service';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ApiService } from '@app/services/api.service'; import { ApiService } from '../../services/api.service';
import { IBackendInfo } from '@interfaces/websocket.interface'; import { IBackendInfo } from '../../interfaces/websocket.interface';
import { Router, ActivatedRoute } from '@angular/router'; import { Router, ActivatedRoute } from '@angular/router';
import { map, share, tap } from 'rxjs/operators'; import { map, share, tap } from 'rxjs/operators';
import { ITranslators } from '@interfaces/node-api.interface'; import { ITranslators } from '../../interfaces/node-api.interface';
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { EnterpriseService } from '@app/services/enterprise.service'; import { EnterpriseService } from '../../services/enterprise.service';
@Component({ @Component({
selector: 'app-about', selector: 'app-about',

View File

@@ -1,9 +1,9 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Routes, RouterModule } from '@angular/router'; import { Routes, RouterModule } from '@angular/router';
import { AboutComponent } from '@components/about/about.component'; import { AboutComponent } from './about.component';
import { AboutSponsorsComponent } from '@components/about/about-sponsors.component'; import { AboutSponsorsComponent } from './about-sponsors.component';
import { SharedModule } from '@app/shared/shared.module'; import { SharedModule } from '../../shared/shared.module';
const routes: Routes = [ const routes: Routes = [
{ {

View File

@@ -1,18 +1,10 @@
<div class="box card w-100 accelerate-checkout-inner" [class.input-disabled]="isCheckoutLocked > 0" style="background: var(--box-bg)" id=acceleratePreviewAnchor> <div class="box card w-100" style="background: var(--box-bg)" id=acceleratePreviewAnchor>
@if (accelerateError) { @if (accelerateError) {
@if (accelerateError.includes('Payment declined')) { <div class="row mb-1 text-center">
<div class="row mb-1 text-center"> <div class="col-sm">
<div class="col-sm"> <h1 style="font-size: larger;" i18n="accelerator.sorry-error-title">Sorry, something went wrong!</h1>
<h1 style="font-size: larger;">{{ accelerateError }}</h1>
</div>
</div> </div>
} @else { </div>
<div class="row mb-1 text-center">
<div class="col-sm">
<h1 style="font-size: larger;" i18n="accelerator.sorry-error-title">Sorry, something went wrong!</h1>
</div>
</div>
}
<div class="row text-center mt-1"> <div class="row text-center mt-1">
<div class="col-sm"> <div class="col-sm">
<div class="d-flex flex-row justify-content-center align-items-center"> <div class="d-flex flex-row justify-content-center align-items-center">
@@ -369,7 +361,7 @@
<div class="row text-center justify-content-center mx-2"> <div class="row text-center justify-content-center mx-2">
<p i18n="accelerator.payment-to-mempool-space">Payment to mempool.space for acceleration of txid <a [routerLink]="'/tx/' + tx.txid" target="_blank">{{ tx.txid.substr(0, 10) }}..{{ tx.txid.substr(-10) }}</a></p> <p i18n="accelerator.payment-to-mempool-space">Payment to mempool.space for acceleration of txid <a [routerLink]="'/tx/' + tx.txid" target="_blank">{{ tx.txid.substr(0, 10) }}..{{ tx.txid.substr(-10) }}</a></p>
</div> </div>
@if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay)) { @if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp)) {
<div class="row"> <div class="row">
<div class="col-sm text-center d-flex flex-column justify-content-center align-items-center"> <div class="col-sm text-center d-flex flex-column justify-content-center align-items-center">
<p><ng-container i18n="accelerator.your-account-will-be-debited">Your account will be debited no more than</ng-container>&nbsp;<small style="font-family: monospace;">{{ cost | number }}</small>&nbsp;<span class="symbol" i18n="shared.sats">sats</span></p> <p><ng-container i18n="accelerator.your-account-will-be-debited">Your account will be debited no more than</ng-container>&nbsp;<small style="font-family: monospace;">{{ cost | number }}</small>&nbsp;<span class="symbol" i18n="shared.sats">sats</span></p>
@@ -492,11 +484,6 @@
</div> </div>
} }
</div> </div>
@if (isTokenizing > 0) {
<div class="d-flex flex-row justify-content-center">
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
</div>
}
</div> </div>
</div> </div>

View File

@@ -8,13 +8,6 @@
color: var(--green) color: var(--green)
} }
.accelerate-checkout-inner {
&.input-disabled {
pointer-events: none;
opacity: 0.75;
}
}
.paymentMethod { .paymentMethod {
padding: 10px; padding: 10px;
background-color: var(--secondary); background-color: var(--secondary);
@@ -179,6 +172,10 @@
background-color: var(--tertiary); background-color: var(--tertiary);
} }
.btn-small-height {
line-height: 1;
}
.summary-row { .summary-row {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View File

@@ -1,16 +1,16 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core'; import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core';
import { Subscription, tap, of, catchError, Observable, switchMap } from 'rxjs'; import { Subscription, tap, of, catchError, Observable, switchMap } from 'rxjs';
import { ServicesApiServices } from '@app/services/services-api.service'; import { ServicesApiServices } from '../../services/services-api.service';
import { md5 } from '@app/shared/common.utils'; import { md5, insecureRandomUUID } from '../../shared/common.utils';
import { StateService } from '@app/services/state.service'; import { StateService } from '../../services/state.service';
import { AudioService } from '@app/services/audio.service'; import { AudioService } from '../../services/audio.service';
import { ETA, EtaService } from '@app/services/eta.service'; import { ETA, EtaService } from '../../services/eta.service';
import { Transaction } from '@interfaces/electrs.interface'; import { Transaction } from '../../interfaces/electrs.interface';
import { MiningStats } from '@app/services/mining.service'; import { MiningStats } from '../../services/mining.service';
import { IAuth, AuthServiceMempool } from '@app/services/auth.service'; import { IAuth, AuthServiceMempool } from '../../services/auth.service';
import { EnterpriseService } from '@app/services/enterprise.service'; import { EnterpriseService } from '../../services/enterprise.service';
import { ApiService } from '@app/services/api.service'; import { ApiService } from '../../services/api.service';
import { isDevMode } from '@angular/core'; import { isDevMode } from '@angular/core';
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay'; export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay';
@@ -75,9 +75,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
@Output() changeMode = new EventEmitter<boolean>(); @Output() changeMode = new EventEmitter<boolean>();
calculating = true; calculating = true;
processing = false;
isCheckoutLocked = 0; // reference counter, 0 = unlocked, >0 = locked
isTokenizing = 0; // reference counter, 0 = false, >0 = true
selectedOption: 'wait' | 'accel'; selectedOption: 'wait' | 'accel';
cantPayReason = ''; cantPayReason = '';
quoteError = ''; // error fetching estimate or initial data quoteError = ''; // error fetching estimate or initial data
@@ -86,7 +83,13 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
timePaid: number = 0; // time acceleration requested timePaid: number = 0; // time acceleration requested
math = Math; math = Math;
isMobile: boolean = window.innerWidth <= 767.98; isMobile: boolean = window.innerWidth <= 767.98;
isProdDomain = false; isProdDomain = ['mempool.space',
'mempool-staging.va1.mempool.space',
'mempool-staging.fmt.mempool.space',
'mempool-staging.fra.mempool.space',
'mempool-staging.tk7.mempool.space',
'mempool-staging.sg1.mempool.space'
].indexOf(document.location.hostname) > -1;
private _step: CheckoutStep = 'summary'; private _step: CheckoutStep = 'summary';
simpleMode: boolean = true; simpleMode: boolean = true;
@@ -96,6 +99,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
auth: IAuth | null = null; auth: IAuth | null = null;
// accelerator stuff // accelerator stuff
accelerationUUID: string;
accelerationSubscription: Subscription; accelerationSubscription: Subscription;
difficultySubscription: Subscription; difficultySubscription: Subscription;
estimateSubscription: Subscription; estimateSubscription: Subscription;
@@ -138,7 +142,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
private authService: AuthServiceMempool, private authService: AuthServiceMempool,
private enterpriseService: EnterpriseService, private enterpriseService: EnterpriseService,
) { ) {
this.isProdDomain = this.stateService.env.PROD_DOMAINS.indexOf(document.location.hostname) > -1; this.accelerationUUID = insecureRandomUUID();
// Check if Apple Pay available // Check if Apple Pay available
// https://developer.apple.com/documentation/apple_pay_on_the_web/apple_pay_js_api/checking_for_apple_pay_availability#overview // https://developer.apple.com/documentation/apple_pay_on_the_web/apple_pay_js_api/checking_for_apple_pay_availability#overview
@@ -156,7 +160,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.accelerateError = null; this.accelerateError = null;
this.timePaid = 0; this.timePaid = 0;
this.btcpayInvoiceFailed = false; this.btcpayInvoiceFailed = false;
this.moveToStep('summary', true); this.moveToStep('summary');
} else { } else {
this.auth = auth; this.auth = auth;
} }
@@ -165,11 +169,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('cash_request_id')) { // Redirected from cashapp if (urlParams.get('cash_request_id')) { // Redirected from cashapp
this.moveToStep('processing', true); this.moveToStep('processing');
this.insertSquare(); this.insertSquare();
this.setupSquare(); this.setupSquare();
} else { } else {
this.moveToStep('summary', true); this.moveToStep('summary');
} }
this.conversionsSubscription = this.stateService.conversions$.subscribe( this.conversionsSubscription = this.stateService.conversions$.subscribe(
@@ -192,20 +196,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
if (changes.scrollEvent && this.scrollEvent) { if (changes.scrollEvent && this.scrollEvent) {
this.scrollToElement('acceleratePreviewAnchor', 'start'); this.scrollToElement('acceleratePreviewAnchor', 'start');
} }
if (changes.accelerating && this.accelerating) { if (changes.accelerating) {
if (this.step === 'processing' || this.step === 'paid') { if ((this.step === 'processing' || this.step === 'paid') && this.accelerating) {
this.moveToStep('success', true); this.moveToStep('success');
} else { // Edge case where the transaction gets accelerated by someone else or on another session
this.closeModal();
} }
} }
} }
moveToStep(step: CheckoutStep, force: boolean = false): void { moveToStep(step: CheckoutStep): void {
if (this.isCheckoutLocked > 0 && !force) {
return;
}
this.processing = false;
this._step = step; this._step = step;
if (this.timeoutTimer) { if (this.timeoutTimer) {
clearTimeout(this.timeoutTimer); clearTimeout(this.timeoutTimer);
@@ -247,7 +245,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
closeModal(): void { closeModal(): void {
this.completed.emit(true); this.completed.emit(true);
this.moveToStep('summary', true); this.moveToStep('summary');
} }
/** /**
@@ -373,7 +371,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.selectFeeRateIndex = index; this.selectFeeRateIndex = index;
this.userBid = Math.max(0, fee); this.userBid = Math.max(0, fee);
this.cost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee; this.cost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
this.validateChoice();
} }
} }
@@ -381,27 +378,25 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
* Account-based acceleration request * Account-based acceleration request
*/ */
accelerateWithMempoolAccount(): void { accelerateWithMempoolAccount(): void {
if (!this.canPay || this.calculating || this.processing) { if (!this.canPay || this.calculating) {
return; return;
} }
this.processing = true;
if (this.accelerationSubscription) { if (this.accelerationSubscription) {
this.accelerationSubscription.unsubscribe(); this.accelerationSubscription.unsubscribe();
} }
this.accelerationSubscription = this.servicesApiService.accelerate$( this.accelerationSubscription = this.servicesApiService.accelerate$(
this.tx.txid, this.tx.txid,
this.userBid, this.userBid,
this.accelerationUUID
).subscribe({ ).subscribe({
next: () => { next: () => {
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon'); this.audioService.playSound('ascend-chime-cartoon');
this.showSuccess = true; this.showSuccess = true;
this.estimateSubscription.unsubscribe(); this.estimateSubscription.unsubscribe();
this.moveToStep('paid', true); this.moveToStep('paid');
}, },
error: (response) => { error: (response) => {
this.processing = false;
this.accelerateError = response.error; this.accelerateError = response.error;
} }
}); });
@@ -471,14 +466,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
* APPLE PAY * APPLE PAY
*/ */
async requestApplePayPayment(): Promise<void> { async requestApplePayPayment(): Promise<void> {
if (this.processing) {
return;
}
if (this.conversionsSubscription) { if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe(); this.conversionsSubscription.unsubscribe();
} }
this.processing = true;
this.conversionsSubscription = this.stateService.conversions$.subscribe( this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => { async (conversions) => {
this.conversions = conversions; this.conversions = conversions;
@@ -503,84 +494,59 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
console.error(`Unable to find apple pay button id='apple-pay-button'`); console.error(`Unable to find apple pay button id='apple-pay-button'`);
// Try again // Try again
setTimeout(this.requestApplePayPayment.bind(this), 500); setTimeout(this.requestApplePayPayment.bind(this), 500);
this.processing = false;
return; return;
} }
this.loadingApplePay = false; this.loadingApplePay = false;
applePayButton.addEventListener('click', async event => { applePayButton.addEventListener('click', async event => {
if (this.isCheckoutLocked > 0 || this.isTokenizing > 0) {
return;
}
event.preventDefault(); event.preventDefault();
try { const tokenResult = await this.applePay.tokenize();
// lock the checkout UI and show a loading spinner until the square modals are finished if (tokenResult?.status === 'OK') {
this.isCheckoutLocked++; const card = tokenResult.details?.card;
this.isTokenizing++; if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
const tokenResult = await this.applePay.tokenize(); console.error(`Cannot retreive payment card details`);
if (tokenResult?.status === 'OK') { this.accelerateError = 'apple_pay_no_card_details';
const card = tokenResult.details?.card; return;
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
console.error(`Cannot retreive payment card details`);
this.accelerateError = 'apple_pay_no_card_details';
this.processing = false;
return;
}
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
// keep checkout in loading state until the acceleration request completes
this.isTokenizing++;
this.isCheckoutLocked++;
this.servicesApiService.accelerateWithApplePay$(
this.tx.txid,
tokenResult.token,
cardTag,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
costUSD
).subscribe({
next: () => {
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
if (this.applePay) {
this.applePay.destroy();
}
setTimeout(() => {
this.isTokenizing--;
this.isCheckoutLocked--;
this.moveToStep('paid', true);
}, 1000);
},
error: (response) => {
this.processing = false;
this.accelerateError = response.error;
if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => {
this.isTokenizing--;
this.isCheckoutLocked--;
// Reset everything by reloading the page :D, can be improved
const urlParams = new URLSearchParams(window.location.search);
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
}, 10000);
}
}
});
} else {
this.processing = false;
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
if (tokenResult.errors) {
errorMessage += ` and errors: ${JSON.stringify(
tokenResult.errors,
)}`;
}
throw new Error(errorMessage);
} }
} finally { const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
// always unlock the checkout once we're finished this.servicesApiService.accelerateWithApplePay$(
this.isTokenizing--; this.tx.txid,
this.isCheckoutLocked--; tokenResult.token,
cardTag,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
this.accelerationUUID
).subscribe({
next: () => {
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
if (this.applePay) {
this.applePay.destroy();
}
setTimeout(() => {
this.moveToStep('paid');
}, 1000);
},
error: (response) => {
this.accelerateError = response.error;
if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => {
// Reset everything by reloading the page :D, can be improved
const urlParams = new URLSearchParams(window.location.search);
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
}, 3000);
}
}
});
} else {
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
if (tokenResult.errors) {
errorMessage += ` and errors: ${JSON.stringify(
tokenResult.errors,
)}`;
}
throw new Error(errorMessage);
} }
}); });
} catch (e) { } catch (e) {
this.processing = false;
console.error(e); console.error(e);
} }
} }
@@ -591,14 +557,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
* GOOGLE PAY * GOOGLE PAY
*/ */
async requestGooglePayPayment(): Promise<void> { async requestGooglePayPayment(): Promise<void> {
if (this.processing) {
return;
}
if (this.conversionsSubscription) { if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe(); this.conversionsSubscription.unsubscribe();
} }
this.processing = true;
this.conversionsSubscription = this.stateService.conversions$.subscribe( this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => { async (conversions) => {
this.conversions = conversions; this.conversions = conversions;
@@ -626,84 +588,52 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.loadingGooglePay = false; this.loadingGooglePay = false;
document.getElementById('google-pay-button').addEventListener('click', async event => { document.getElementById('google-pay-button').addEventListener('click', async event => {
if (this.isCheckoutLocked > 0 || this.isTokenizing > 0) {
return;
}
event.preventDefault(); event.preventDefault();
try { const tokenResult = await this.googlePay.tokenize();
// lock the checkout UI and show a loading spinner until the square modals are finished if (tokenResult?.status === 'OK') {
this.isCheckoutLocked++; const card = tokenResult.details?.card;
this.isTokenizing++; if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
const tokenResult = await this.googlePay.tokenize(); console.error(`Cannot retreive payment card details`);
if (tokenResult?.status === 'OK') { this.accelerateError = 'apple_pay_no_card_details';
const card = tokenResult.details?.card; return;
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
console.error(`Cannot retreive payment card details`);
this.accelerateError = 'apple_pay_no_card_details';
this.processing = false;
return;
}
const verificationToken = await this.$verifyBuyer(this.payments, tokenResult.token, tokenResult.details, costUSD.toFixed(2));
if (!verificationToken || !verificationToken.token) {
console.error(`SCA verification failed`);
this.accelerateError = 'SCA Verification Failed. Payment Declined.';
this.processing = false;
return;
}
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
// keep checkout in loading state until the acceleration request completes
this.isCheckoutLocked++;
this.isTokenizing++;
this.servicesApiService.accelerateWithGooglePay$(
this.tx.txid,
tokenResult.token,
verificationToken.token,
cardTag,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
costUSD,
verificationToken.userChallenged
).subscribe({
next: () => {
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
if (this.googlePay) {
this.googlePay.destroy();
}
setTimeout(() => {
this.isTokenizing--;
this.isCheckoutLocked--;
this.moveToStep('paid', true);
}, 1000);
},
error: (response) => {
this.processing = false;
this.accelerateError = response.error;
this.isTokenizing--;
this.isCheckoutLocked--;
if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => {
// Reset everything by reloading the page :D, can be improved
const urlParams = new URLSearchParams(window.location.search);
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
}, 10000);
}
}
});
} else {
this.processing = false;
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
if (tokenResult.errors) {
errorMessage += ` and errors: ${JSON.stringify(
tokenResult.errors,
)}`;
}
throw new Error(errorMessage);
} }
} finally { const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
// always unlock the checkout once we're finished this.servicesApiService.accelerateWithGooglePay$(
this.isTokenizing--; this.tx.txid,
this.isCheckoutLocked--; tokenResult.token,
cardTag,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
this.accelerationUUID
).subscribe({
next: () => {
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
if (this.googlePay) {
this.googlePay.destroy();
}
setTimeout(() => {
this.moveToStep('paid');
}, 1000);
},
error: (response) => {
this.accelerateError = response.error;
if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => {
// Reset everything by reloading the page :D, can be improved
const urlParams = new URLSearchParams(window.location.search);
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
}, 3000);
}
}
});
} else {
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
if (tokenResult.errors) {
errorMessage += ` and errors: ${JSON.stringify(
tokenResult.errors,
)}`;
}
throw new Error(errorMessage);
} }
}); });
} }
@@ -714,14 +644,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
* CASHAPP * CASHAPP
*/ */
async requestCashAppPayment(): Promise<void> { async requestCashAppPayment(): Promise<void> {
if (this.processing) {
return;
}
if (this.conversionsSubscription) { if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe(); this.conversionsSubscription.unsubscribe();
} }
this.processing = true;
this.conversionsSubscription = this.stateService.conversions$.subscribe( this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => { async (conversions) => {
this.conversions = conversions; this.conversions = conversions;
@@ -752,7 +678,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.cashAppPay.addEventListener('ontokenization', event => { this.cashAppPay.addEventListener('ontokenization', event => {
const { tokenResult, error } = event.detail; const { tokenResult, error } = event.detail;
if (error) { if (error) {
this.processing = false;
this.accelerateError = error; this.accelerateError = error;
} else if (tokenResult.status === 'OK') { } else if (tokenResult.status === 'OK') {
this.servicesApiService.accelerateWithCashApp$( this.servicesApiService.accelerateWithCashApp$(
@@ -760,17 +685,16 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
tokenResult.token, tokenResult.token,
tokenResult.details.cashAppPay.cashtag, tokenResult.details.cashAppPay.cashtag,
tokenResult.details.cashAppPay.referenceId, tokenResult.details.cashAppPay.referenceId,
costUSD this.accelerationUUID
).subscribe({ ).subscribe({
next: () => { next: () => {
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon'); this.audioService.playSound('ascend-chime-cartoon');
if (this.cashAppPay) { if (this.cashAppPay) {
this.cashAppPay.destroy(); this.cashAppPay.destroy();
} }
setTimeout(() => { setTimeout(() => {
this.moveToStep('paid', true); this.moveToStep('paid');
if (window.history.replaceState) { if (window.history.replaceState) {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
window.history.replaceState(null, null, window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, '')); window.history.replaceState(null, null, window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ''));
@@ -778,14 +702,13 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}, 1000); }, 1000);
}, },
error: (response) => { error: (response) => {
this.processing = false;
this.accelerateError = response.error; this.accelerateError = response.error;
if (!(response.status === 403 && response.error === 'not_available')) { if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => { setTimeout(() => {
// Reset everything by reloading the page :D, can be improved // Reset everything by reloading the page :D, can be improved
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``)); window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
}, 10000); }, 3000);
} }
} }
}); });
@@ -795,32 +718,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
); );
} }
/**
* https://developer.squareup.com/docs/sca-overview
*/
async $verifyBuyer(payments, token, details, amount): Promise<{token: string, userChallenged: boolean}> {
const verificationDetails = {
amount: amount,
currencyCode: 'USD',
intent: 'CHARGE',
billingContact: {
givenName: details.card?.billing?.givenName,
familyName: details.card?.billing?.familyName,
phone: details.card?.billing?.phone,
addressLines: details.card?.billing?.addressLines,
city: details.card?.billing?.city,
state: details.card?.billing?.state,
countryCode: details.card?.billing?.countryCode,
},
};
const verificationResults = await payments.verifyBuyer(
token,
verificationDetails,
);
return verificationResults;
}
/** /**
* BTCPay * BTCPay
*/ */
@@ -844,7 +741,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon'); this.audioService.playSound('ascend-chime-cartoon');
this.estimateSubscription.unsubscribe(); this.estimateSubscription.unsubscribe();
this.moveToStep('paid', true); this.moveToStep('paid');
} }
isLoggedIn(): boolean { isLoggedIn(): boolean {

View File

@@ -12,7 +12,7 @@
</p> </p>
</div> </div>
<div class="spacer"></div> <div class="spacer"></div>
<span class="fee">{{ bar.class === 'tx' ? '' : '+' }}{{ bar.fee | number }} <span class="symbol" i18n="shared.sats">sats</span></span> <span class="fee">{{ bar.class === 'tx' ? '' : '+' }}{{ bar.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></span>
<div class="spacer"></div> <div class="spacer"></div>
<div class="spacer"></div> <div class="spacer"></div>
</div> </div>

View File

@@ -1,6 +1,6 @@
import { Component, Input, Output, OnChanges, EventEmitter, HostListener, OnInit, ViewChild, ElementRef, AfterViewInit, OnDestroy, ChangeDetectorRef } from '@angular/core'; import { Component, Input, Output, OnChanges, EventEmitter, HostListener, OnInit, ViewChild, ElementRef, AfterViewInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
import { Transaction } from '@interfaces/electrs.interface'; import { Transaction } from '../../interfaces/electrs.interface';
import { AccelerationEstimate, RateOption } from '@components/accelerate-checkout/accelerate-checkout.component'; import { AccelerationEstimate, RateOption } from './accelerate-checkout.component';
interface GraphBar { interface GraphBar {
rate: number; rate: number;

View File

@@ -21,14 +21,14 @@
</tr> </tr>
<tr *ngIf="accelerationInfo.fee"> <tr *ngIf="accelerationInfo.fee">
<td class="label" i18n="transaction.fee|Transaction fee">Fee</td> <td class="label" i18n="transaction.fee|Transaction fee">Fee</td>
<td class="value">{{ accelerationInfo.fee | number }} <span class="symbol" i18n="shared.sats">sats</span></td> <td class="value">{{ accelerationInfo.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
</tr> </tr>
<tr *ngIf="accelerationInfo.bidBoost >= 0 || accelerationInfo.feeDelta"> <tr *ngIf="accelerationInfo.bidBoost >= 0 || accelerationInfo.feeDelta">
<td class="label" i18n="transaction.out-of-band-fees">Out-of-band fees</td> <td class="label" i18n="transaction.out-of-band-fees">Out-of-band fees</td>
@if (accelerationInfo.status === 'accelerated') { @if (accelerationInfo.status === 'accelerated') {
<td class="value oobFees">{{ accelerationInfo.feeDelta | number }} <span class="symbol" i18n="shared.sats">sats</span></td> <td class="value oobFees">{{ accelerationInfo.feeDelta | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
} @else { } @else {
<td class="value oobFees">{{ accelerationInfo.bidBoost | number }} <span class="symbol" i18n="shared.sats">sats</span></td> <td class="value oobFees">{{ accelerationInfo.bidBoost | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
} }
</tr> </tr>
<tr *ngIf="accelerationInfo.fee && accelerationInfo.weight"> <tr *ngIf="accelerationInfo.fee && accelerationInfo.weight">
@@ -47,14 +47,13 @@
<tr *ngIf="['accelerated', 'mined'].includes(accelerationInfo.status) && hasPoolsData()"> <tr *ngIf="['accelerated', 'mined'].includes(accelerationInfo.status) && hasPoolsData()">
<td class="label" i18n="transaction.accelerated-by-hashrate|Accelerated to hashrate">Accelerated by</td> <td class="label" i18n="transaction.accelerated-by-hashrate|Accelerated to hashrate">Accelerated by</td>
<td class="value" *ngIf="accelerationInfo.pools"> <td class="value" *ngIf="accelerationInfo.pools">
<ng-container *ngFor="let pool of accelerationInfo.pools; let i = index;"> <ng-container *ngFor="let pool of accelerationInfo.pools">
<img *ngIf="accelerationInfo.poolsData[pool]" <img *ngIf="accelerationInfo.poolsData[pool]"
class="pool-logo" class="pool-logo"
[style.opacity]="accelerationInfo?.minedByPoolUniqueId && pool !== accelerationInfo?.minedByPoolUniqueId ? '0.3' : '1'" [style.opacity]="accelerationInfo?.minedByPoolUniqueId && pool !== accelerationInfo?.minedByPoolUniqueId ? '0.3' : '1'"
[src]="'/resources/mining-pools/' + accelerationInfo.poolsData[pool].slug + '.svg'" [src]="'/resources/mining-pools/' + accelerationInfo.poolsData[pool].slug + '.svg'"
onError="this.src = '/resources/mining-pools/default.svg'" onError="this.src = '/resources/mining-pools/default.svg'"
[alt]="'Logo of ' + pool.name + ' mining pool'"> [alt]="'Logo of ' + pool.name + ' mining pool'">
<br *ngIf="i % 6 === 5">
</ng-container> </ng-container>
</td> </td>
</tr> </tr>

View File

@@ -23,7 +23,6 @@
.label { .label {
padding-right: 30px; padding-right: 30px;
vertical-align: top;
} }
.pool-logo { .pool-logo {
@@ -31,8 +30,7 @@
height: 22px; height: 22px;
position: relative; position: relative;
top: -1px; top: -1px;
margin-right: 4px; margin-right: 3px;
margin-bottom: 4px;
} }
.oobFees { .oobFees {

View File

@@ -1,6 +1,6 @@
<div class="acceleration-timeline box" [class.lower-padding]="!tx.status.confirmed"> <div class="acceleration-timeline box" [class.lower-padding]="!tx.status.confirmed">
<div class="timeline-wrapper"> <div class="timeline-wrapper">
@if (!tx.status.confirmed || canceled) { @if (!tx.status.confirmed) {
<div class="timeline"> <div class="timeline">
<div class="intervals"> <div class="intervals">
<div class="node-spacer"></div> <div class="node-spacer"></div>
@@ -8,8 +8,8 @@
<div class="node-spacer"></div> <div class="node-spacer"></div>
<div class="interval"> <div class="interval">
<div class="interval-time"> <div class="interval-time">
@if (eta && !canceled) { @if (eta) {
~<app-time [time]="eta?.wait / 1000"></app-time> ~<app-time [time]="eta?.wait / 1000"></app-time> <!-- <span *ngIf="accelerateRatio > 1" class="compare"> ({{ accelerateRatio }}x faster)</span> -->
} }
</div> </div>
</div> </div>
@@ -19,20 +19,16 @@
<div class="node-spacer"></div> <div class="node-spacer"></div>
<div class="interval-spacer"></div> <div class="interval-spacer"></div>
<div class="node"> <div class="node">
<div class="acc-to-confirmed right go-faster" [class.no-animation]="canceled"></div> <div class="acc-to-confirmed right go-faster"></div>
</div> </div>
<div class="interval-spacer"> <div class="interval-spacer">
</div> </div>
<div class="node" [id]="'confirmed'"> <div class="node" [id]="'confirmed'">
<div class="acc-to-confirmed left go-faster" [class.no-animation]="canceled"></div> <div class="acc-to-confirmed left go-faster"></div>
<div class="shape-border waiting"> <div class="shape-border waiting">
<div class="shape"></div> <div class="shape"></div>
</div> </div>
@if (canceled) { <div class="status"><span class="badge badge-waiting" i18n="transaction.rbf.mined">Mined</span></div>
<div class="status"><span class="badge badge-danger" i18n="accelerator.canceled">Canceled</span></div>
} @else {
<div class="status"><span class="badge badge-waiting" i18n="transaction.rbf.mined">Mined</span></div>
}
</div> </div>
</div> </div>
</div> </div>
@@ -42,16 +38,18 @@
<div class="node-spacer"></div> <div class="node-spacer"></div>
<div class="interval"> <div class="interval">
<div class="interval-time"> <div class="interval-time">
<app-time [time]="firstSeenToAccelerated"></app-time> <app-time [time]="acceleratedAt - transactionTime"></app-time>
</div> </div>
</div> </div>
<div class="node-spacer"></div> <div class="node-spacer"></div>
<div class="interval"> <div class="interval">
<div class="interval-time"> <div class="interval-time">
@if (tx.status.confirmed) { @if (tx.status.confirmed) {
<app-time [time]="acceleratedToMined"></app-time> <div class="interval-time">
} @else if (eta && canceled) { <app-time [time]="tx.status.block_time - acceleratedAt"></app-time>
~<app-time [time]="eta?.wait / 1000"></app-time> </div>
} @else if (standardETA && !tx.status.confirmed) {
<!-- ~<app-time [time]="standardETA / 1000 - now"></app-time> -->
} }
</div> </div>
</div> </div>
@@ -75,42 +73,42 @@
<div class="interval-spacer"> <div class="interval-spacer">
<div class="seen-to-acc"></div> <div class="seen-to-acc"></div>
</div> </div>
<div class="node" [class.accelerated]="!tx.status.confirmed && !canceled" [id]="'accelerated'"> <div class="node" [class.accelerated]="!tx.status.confirmed" [id]="'accelerated'">
<div class="seen-to-acc left"></div> <div class="seen-to-acc left"></div>
@if (tx.status.confirmed && !canceled) { @if (tx.status.confirmed) {
<div class="acc-to-confirmed right"></div> <div class="acc-to-confirmed right"></div>
} @else { } @else {
<div class="seen-to-acc right"></div> <div class="seen-to-acc right"></div>
} }
<div class="shape-border hovering" (pointerover)="onHover($event, 'accelerated');" (pointerout)="onBlur($event);"> <div class="shape-border hovering" (pointerover)="onHover($event, 'accelerated');" (pointerout)="onBlur($event);">
<div class="shape"></div> <div class="shape"></div>
@if (!tx.status.confirmed || canceled) { @if (!tx.status.confirmed) {
<div class="connector down" [class.loading]="!canceled"></div> <div class="connector down loading"></div>
} }
</div> </div>
@if (tx.status.confirmed && !canceled) { @if (tx.status.confirmed) {
<div class="status"><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></div> <div class="status"><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></div>
} }
<div class="time" [class.no-margin]="!tx.status.confirmed || canceled" [class.offset-left]="!tx.status.confirmed || canceled"> <div class="time" [class.no-margin]="!tx.status.confirmed" [class.offset-left]="!tx.status.confirmed">
@if (!tx.status.confirmed) { @if (!tx.status.confirmed) {
<span i18n="transaction.audit.accelerated">Accelerated</span>{{ "" }} <span i18n="transaction.audit.accelerated">Accelerated</span>{{ "" }}
} }
@if (useAbsoluteTime) { @if (useAbsoluteTime) {
<span>{{ acceleratedAt * 1000 | date }}</span> <span>{{ acceleratedAt * 1000 | date }}</span>
} @else { } @else {
<app-time kind="since" [time]="acceleratedAt" [lowercaseStart]="!tx.status.confirmed || canceled"></app-time> <app-time kind="since" [time]="acceleratedAt" [lowercaseStart]="!tx.status.confirmed"></app-time>
} }
</div> </div>
</div> </div>
<div class="interval-spacer"> <div class="interval-spacer">
@if (tx.status.confirmed && !canceled) { @if (tx.status.confirmed) {
<div class="acc-to-confirmed"></div> <div class="acc-to-confirmed"></div>
} @else { } @else {
<div class="seen-to-acc"></div> <div class="seen-to-acc"></div>
} }
</div> </div>
<div class="node" [class.selected]="tx.status.confirmed" [id]="'confirmed'"> <div class="node" [class.selected]="tx.status.confirmed" [id]="'confirmed'">
@if (tx.status.confirmed && !canceled) { @if (tx.status.confirmed) {
<div class="acc-to-confirmed left"></div> <div class="acc-to-confirmed left"></div>
} @else { } @else {
<div class="seen-to-acc left"></div> <div class="seen-to-acc left"></div>

View File

@@ -129,9 +129,6 @@
margin-left: calc(-4em + 5px); margin-left: calc(-4em + 5px);
animation: goFasterLeft 0.8s infinite linear; animation: goFasterLeft 0.8s infinite linear;
} }
&.no-animation {
animation: none;
}
} }
&.left { &.left {

View File

@@ -1,8 +1,8 @@
import { Component, Input, OnInit, OnChanges, HostListener } from '@angular/core'; import { Component, Input, OnInit, OnChanges, HostListener } from '@angular/core';
import { ETA } from '@app/services/eta.service'; import { ETA } from '../../services/eta.service';
import { Transaction } from '@interfaces/electrs.interface'; import { Transaction } from '../../interfaces/electrs.interface';
import { Acceleration, SinglePoolStats } from '@interfaces/node-api.interface'; import { Acceleration, SinglePoolStats } from '../../interfaces/node-api.interface';
import { MiningService } from '@app/services/mining.service'; import { MiningService } from '../../services/mining.service';
@Component({ @Component({
selector: 'app-acceleration-timeline', selector: 'app-acceleration-timeline',
@@ -11,17 +11,19 @@ import { MiningService } from '@app/services/mining.service';
}) })
export class AccelerationTimelineComponent implements OnInit, OnChanges { export class AccelerationTimelineComponent implements OnInit, OnChanges {
@Input() transactionTime: number; @Input() transactionTime: number;
@Input() acceleratedAt: number;
@Input() tx: Transaction; @Input() tx: Transaction;
@Input() accelerationInfo: Acceleration; @Input() accelerationInfo: Acceleration;
@Input() eta: ETA; @Input() eta: ETA;
@Input() canceled: boolean; // A mined transaction has standard ETA and accelerated ETA undefined
// A transaction in mempool has either standardETA defined (if accelerated) or acceleratedETA defined (if not accelerated yet)
@Input() standardETA: number;
@Input() acceleratedETA: number;
acceleratedAt: number;
now: number; now: number;
accelerateRatio: number; accelerateRatio: number;
useAbsoluteTime: boolean = false; useAbsoluteTime: boolean = false;
firstSeenToAccelerated: number; interval: number;
acceleratedToMined: number;
tooltipPosition = null; tooltipPosition = null;
hoverInfo: any = null; hoverInfo: any = null;
@@ -32,24 +34,38 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
this.updateTimes(); this.acceleratedAt = this.tx.acceleratedAt ?? new Date().getTime() / 1000;
this.now = Math.floor(new Date().getTime() / 1000);
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
this.miningService.getPools().subscribe(pools => { this.miningService.getPools().subscribe(pools => {
for (const pool of pools) { for (const pool of pools) {
this.poolsData[pool.unique_id] = pool; this.poolsData[pool.unique_id] = pool;
} }
}); });
this.interval = window.setInterval(() => {
this.now = Math.floor(new Date().getTime() / 1000);
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
}, 60000);
} }
ngOnChanges(changes): void { ngOnChanges(changes): void {
this.updateTimes(); // Hide standard ETA while we don't have a proper standard ETA calculation, see https://github.com/mempool/mempool/issues/65
// if (changes?.eta?.currentValue || changes?.standardETA?.currentValue || changes?.acceleratedETA?.currentValue) {
// if (changes?.eta?.currentValue) {
// if (changes?.acceleratedETA?.currentValue) {
// this.accelerateRatio = Math.floor((Math.floor(changes.eta.currentValue.time / 1000) - this.now) / (Math.floor(changes.acceleratedETA.currentValue / 1000) - this.now));
// } else if (changes?.standardETA?.currentValue) {
// this.accelerateRatio = Math.floor((Math.floor(changes.standardETA.currentValue / 1000) - this.now) / (Math.floor(changes.eta.currentValue.time / 1000) - this.now));
// }
// }
// }
} }
updateTimes(): void { ngOnDestroy(): void {
this.now = Math.floor(new Date().getTime() / 1000); clearInterval(this.interval);
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
this.firstSeenToAccelerated = Math.max(0, this.acceleratedAt - this.transactionTime);
this.acceleratedToMined = Math.max(0, this.tx.status.block_time - this.acceleratedAt);
} }
onHover(event, status: string): void { onHover(event, status: string): void {

View File

@@ -1,18 +1,18 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
import { EChartsOption } from '@app/graphs/echarts'; import { EChartsOption } from '../../../graphs/echarts';
import { Observable, Subject, Subscription, combineLatest, fromEvent, merge, share } from 'rxjs'; import { Observable, Subject, Subscription, combineLatest, fromEvent, merge, share } from 'rxjs';
import { startWith, switchMap, tap } from 'rxjs/operators'; import { startWith, switchMap, tap } from 'rxjs/operators';
import { SeoService } from '@app/services/seo.service'; import { SeoService } from '../../../services/seo.service';
import { formatNumber } from '@angular/common'; import { formatNumber } from '@angular/common';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '@app/shared/graphs.utils'; import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../../shared/graphs.utils';
import { StorageService } from '@app/services/storage.service'; import { StorageService } from '../../../services/storage.service';
import { MiningService } from '@app/services/mining.service'; import { MiningService } from '../../../services/mining.service';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Acceleration } from '@interfaces/node-api.interface'; import { Acceleration } from '../../../interfaces/node-api.interface';
import { ServicesApiServices } from '@app/services/services-api.service'; import { ServicesApiServices } from '../../../services/services-api.service';
import { StateService } from '@app/services/state.service'; import { StateService } from '../../../services/state.service';
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe'; import { RelativeUrlPipe } from '../../../shared/pipes/relative-url/relative-url.pipe';
@Component({ @Component({
selector: 'app-acceleration-fees-graph', selector: 'app-acceleration-fees-graph',
@@ -46,8 +46,6 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
aggregatedHistory$: Observable<any>; aggregatedHistory$: Observable<any>;
statsSubscription: Subscription; statsSubscription: Subscription;
aggregatedHistorySubscription: Subscription;
fragmentSubscription: Subscription;
isLoading = true; isLoading = true;
formatNumber = formatNumber; formatNumber = formatNumber;
timespan = ''; timespan = '';
@@ -81,8 +79,8 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
} }
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference }); this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference); this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
this.fragmentSubscription = this.route.fragment.subscribe((fragment) => { this.route.fragment.subscribe((fragment) => {
if (['24h', '3d', '1w', '1m', '3m', 'all'].indexOf(fragment) > -1) { if (['24h', '3d', '1w', '1m', '3m', 'all'].indexOf(fragment) > -1) {
this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false }); this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
} }
@@ -115,7 +113,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
share(), share(),
); );
this.aggregatedHistorySubscription = this.aggregatedHistory$.subscribe(); this.aggregatedHistory$.subscribe();
} }
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
@@ -266,7 +264,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
type: 'bar', type: 'bar',
barWidth: '90%', barWidth: '90%',
large: true, large: true,
barMinHeight: 3, barMinHeight: 1,
}, },
], ],
dataZoom: (this.widget || data.length === 0 )? undefined : [{ dataZoom: (this.widget || data.length === 0 )? undefined : [{
@@ -337,8 +335,8 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.aggregatedHistorySubscription?.unsubscribe(); if (this.statsSubscription) {
this.fragmentSubscription?.unsubscribe(); this.statsSubscription.unsubscribe();
this.statsSubscription?.unsubscribe(); }
} }
} }

View File

@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ServicesApiServices } from '@app/services/services-api.service'; import { ServicesApiServices } from '../../../services/services-api.service';
export type AccelerationStats = { export type AccelerationStats = {
totalRequested: number; totalRequested: number;

View File

@@ -4,7 +4,7 @@
<div class="clearfix"></div> <div class="clearfix"></div>
<div class="acceleration-list" *ngIf="{ accelerations: accelerationList$ | async } as state"> <div class="acceleration-list">
<table *ngIf="nonEmptyAccelerations; else noData" class="table table-borderless table-fixed"> <table *ngIf="nonEmptyAccelerations; else noData" class="table table-borderless table-fixed">
<thead> <thead>
<th class="txid text-left" i18n="dashboard.latest-transactions.txid">TXID</th> <th class="txid text-left" i18n="dashboard.latest-transactions.txid">TXID</th>
@@ -21,8 +21,8 @@
<th class="date text-right" i18n="accelerator.requested" *ngIf="!this.widget">Requested</th> <th class="date text-right" i18n="accelerator.requested" *ngIf="!this.widget">Requested</th>
</ng-container> </ng-container>
</thead> </thead>
<tbody *ngIf="state.accelerations && nonEmptyAccelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''"> <tbody *ngIf="accelerationList$ | async as accelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<tr *ngFor="let acceleration of state.accelerations; let i= index;"> <tr *ngFor="let acceleration of accelerations; let i= index;">
<td class="txid text-left"> <td class="txid text-left">
<a [routerLink]="['/tx' | relativeUrl, acceleration.txid]"> <a [routerLink]="['/tx' | relativeUrl, acceleration.txid]">
<app-truncate [text]="acceleration.txid" [lastChars]="5"></app-truncate> <app-truncate [text]="acceleration.txid" [lastChars]="5"></app-truncate>
@@ -33,7 +33,7 @@
<app-fee-rate [fee]="acceleration.effectiveFee" [weight]="acceleration.effectiveVsize * 4"></app-fee-rate> <app-fee-rate [fee]="acceleration.effectiveFee" [weight]="acceleration.effectiveVsize * 4"></app-fee-rate>
</td> </td>
<td class="bid text-right"> <td class="bid text-right">
{{ (acceleration.feeDelta) | number }} <span class="symbol" i18n="shared.sats">sats</span> {{ (acceleration.feeDelta) | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
</td> </td>
<td class="time text-right"> <td class="time text-right">
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time> <app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
@@ -41,7 +41,7 @@
</ng-container> </ng-container>
<ng-container *ngIf="!pending"> <ng-container *ngIf="!pending">
<td *ngIf="acceleration.boost != null" class="fee text-right"> <td *ngIf="acceleration.boost != null" class="fee text-right">
{{ acceleration.boost | number }} <span class="symbol" i18n="shared.sats">sats</span> {{ acceleration.boost | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
</td> </td>
<td *ngIf="acceleration.boost == null" class="fee text-right"> <td *ngIf="acceleration.boost == null" class="fee text-right">
~ ~
@@ -64,7 +64,7 @@
<span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="accelerator.pending">Pending</span> <span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="accelerator.pending">Pending</span>
<span *ngIf="acceleration.status.includes('completed') && acceleration.minedByPoolUniqueId && pools[acceleration.minedByPoolUniqueId]" class="badge badge-success"><ng-container i18n="accelerator.completed">Completed</ng-container><span *ngIf="acceleration.status === 'completed_provisional'">&nbsp;</span></span> <span *ngIf="acceleration.status.includes('completed') && acceleration.minedByPoolUniqueId && pools[acceleration.minedByPoolUniqueId]" class="badge badge-success"><ng-container i18n="accelerator.completed">Completed</ng-container><span *ngIf="acceleration.status === 'completed_provisional'">&nbsp;</span></span>
<span *ngIf="acceleration.status.includes('completed') && (!acceleration.minedByPoolUniqueId || !pools[acceleration.minedByPoolUniqueId])" class="badge badge-success"><ng-container i18n="transaction.rbf.mined">Mined</ng-container><span *ngIf="acceleration.status === 'completed_provisional'">&nbsp;</span></span> <span *ngIf="acceleration.status.includes('completed') && (!acceleration.minedByPoolUniqueId || !pools[acceleration.minedByPoolUniqueId])" class="badge badge-success"><ng-container i18n="transaction.rbf.mined">Mined</ng-container><span *ngIf="acceleration.status === 'completed_provisional'">&nbsp;</span></span>
<span *ngIf="acceleration.status.includes('failed')" class="badge badge-danger"><ng-container i18n="accelerator.canceled">Canceled</ng-container><span *ngIf="acceleration.status === 'failed_provisional'">&nbsp;</span></span> <span *ngIf="acceleration.status.includes('failed')" class="badge badge-danger"><ng-container i18n="accelerator.canceled">Failed</ng-container><span *ngIf="acceleration.status === 'failed_provisional'">&nbsp;</span></span>
</td> </td>
<td class="date text-right" *ngIf="!this.widget"> <td class="date text-right" *ngIf="!this.widget">
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time> <app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>

View File

@@ -1,12 +1,12 @@
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy, Inject, LOCALE_ID } from '@angular/core'; import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy, Inject, LOCALE_ID } from '@angular/core';
import { BehaviorSubject, Observable, Subscription, catchError, combineLatest, filter, of, switchMap, tap, throttleTime, timer } from 'rxjs'; import { BehaviorSubject, Observable, Subscription, catchError, filter, of, switchMap, tap, throttleTime } from 'rxjs';
import { Acceleration, BlockExtended, SinglePoolStats } from '@interfaces/node-api.interface'; import { Acceleration, BlockExtended, SinglePoolStats } from '../../../interfaces/node-api.interface';
import { StateService } from '@app/services/state.service'; import { StateService } from '../../../services/state.service';
import { WebsocketService } from '@app/services/websocket.service'; import { WebsocketService } from '../../../services/websocket.service';
import { ServicesApiServices } from '@app/services/services-api.service'; import { ServicesApiServices } from '../../../services/services-api.service';
import { SeoService } from '@app/services/seo.service'; import { SeoService } from '../../../services/seo.service';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { MiningService } from '@app/services/mining.service'; import { MiningService } from '../../../services/mining.service';
@Component({ @Component({
selector: 'app-accelerations-list', selector: 'app-accelerations-list',
@@ -61,11 +61,8 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
this.websocketService.want(['blocks']); this.websocketService.want(['blocks']);
this.seoService.setTitle($localize`:@@02573b6980a2d611b4361a2595a4447e390058cd:Accelerations`); this.seoService.setTitle($localize`:@@02573b6980a2d611b4361a2595a4447e390058cd:Accelerations`);
this.paramSubscription = combineLatest([ this.paramSubscription = this.route.params.pipe(
this.route.params, tap(params => {
timer(0),
]).pipe(
tap(([params]) => {
this.page = +params['page'] || 1; this.page = +params['page'] || 1;
this.pageSubject.next(this.page); this.pageSubject.next(this.page);
}) })
@@ -151,4 +148,4 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
this.paramSubscription?.unsubscribe(); this.paramSubscription?.unsubscribe();
this.keyNavigationSubscription?.unsubscribe(); this.keyNavigationSubscription?.unsubscribe();
} }
} }

View File

@@ -1,18 +1,18 @@
import { ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; import { ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
import { SeoService } from '@app/services/seo.service'; import { SeoService } from '../../../services/seo.service';
import { OpenGraphService } from '@app/services/opengraph.service'; import { OpenGraphService } from '../../../services/opengraph.service';
import { WebsocketService } from '@app/services/websocket.service'; import { WebsocketService } from '../../../services/websocket.service';
import { Acceleration, BlockExtended } from '@interfaces/node-api.interface'; import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface';
import { StateService } from '@app/services/state.service'; import { StateService } from '../../../services/state.service';
import { Observable, Subscription, catchError, combineLatest, distinctUntilChanged, map, of, share, switchMap, tap } from 'rxjs'; import { Observable, Subscription, catchError, combineLatest, distinctUntilChanged, map, of, share, switchMap, tap } from 'rxjs';
import { Color } from '@components/block-overview-graph/sprite-types'; import { Color } from '../../block-overview-graph/sprite-types';
import { hexToColor } from '@components/block-overview-graph/utils'; import { hexToColor } from '../../block-overview-graph/utils';
import TxView from '@components/block-overview-graph/tx-view'; import TxView from '../../block-overview-graph/tx-view';
import { feeLevels, defaultMempoolFeeColors, contrastMempoolFeeColors } from '@app/app.constants'; import { feeLevels, defaultMempoolFeeColors, contrastMempoolFeeColors } from '../../../app.constants';
import { ServicesApiServices } from '@app/services/services-api.service'; import { ServicesApiServices } from '../../../services/services-api.service';
import { detectWebGL } from '@app/shared/graphs.utils'; import { detectWebGL } from '../../../shared/graphs.utils';
import { AudioService } from '@app/services/audio.service'; import { AudioService } from '../../../services/audio.service';
import { ThemeService } from '@app/services/theme.service'; import { ThemeService } from '../../../services/theme.service';
const acceleratedColor: Color = hexToColor('8F5FF6'); const acceleratedColor: Color = hexToColor('8F5FF6');
const normalColors = defaultMempoolFeeColors.map(hex => hexToColor(hex + '5F')); const normalColors = defaultMempoolFeeColors.map(hex => hexToColor(hex + '5F'));

View File

@@ -10,17 +10,17 @@
</td> </td>
<td class="field-value" [class]="chartPositionLeft ? 'chart-left' : ''"> <td class="field-value" [class]="chartPositionLeft ? 'chart-left' : ''">
<div class="effective-fee-container"> <div class="effective-fee-container">
@if (accelerationInfo?.acceleratedFeeRate && (!effectiveFeeRate || accelerationInfo.acceleratedFeeRate >= effectiveFeeRate)) { @if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize)) {
<app-fee-rate class="oobFees" [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate> <app-fee-rate class="oobFees" [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
} @else { } @else {
<app-fee-rate class="oobFees" [fee]="effectiveFeeRate"></app-fee-rate> <app-fee-rate class="oobFees" [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
} }
</div> </div>
</td> </td>
<td class="pie-chart" rowspan="2" *ngIf="!chartPositionLeft"> <td class="pie-chart" rowspan="2" *ngIf="!chartPositionLeft">
<div class="d-flex justify-content-between align-items-start"> <div class="d-flex justify-content-between align-items-start">
@if (hasCpfp) { @if (hasCpfp) {
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP</button> <button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
} }
<ng-container *ngTemplateOutlet="pieChart"></ng-container> <ng-container *ngTemplateOutlet="pieChart"></ng-container>
</div> </div>
@@ -36,7 +36,7 @@
<tr> <tr>
<td colspan="3" class="pt-0"> <td colspan="3" class="pt-0">
<div class="d-flex justify-content-end align-items-start"> <div class="d-flex justify-content-end align-items-start">
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP</button> <button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
</div> </div>
</td> </td>
</tr> </tr>

View File

@@ -1,8 +1,8 @@
import { Component, ChangeDetectionStrategy, Input, Output, OnChanges, SimpleChanges, EventEmitter, ChangeDetectorRef } from '@angular/core'; import { Component, ChangeDetectionStrategy, Input, Output, OnChanges, SimpleChanges, EventEmitter } from '@angular/core';
import { Transaction } from '@interfaces/electrs.interface'; import { Transaction } from '../../../interfaces/electrs.interface';
import { Acceleration, SinglePoolStats } from '@interfaces/node-api.interface'; import { Acceleration, SinglePoolStats } from '../../../interfaces/node-api.interface';
import { EChartsOption, PieSeriesOption } from '@app/graphs/echarts'; import { EChartsOption, PieSeriesOption } from '../../../graphs/echarts';
import { MiningStats } from '@app/services/mining.service'; import { MiningStats } from '../../../services/mining.service';
function lighten(color, p): { r, g, b } { function lighten(color, p): { r, g, b } {
return { return {
@@ -23,8 +23,7 @@ function toRGB({r,g,b}): string {
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ActiveAccelerationBox implements OnChanges { export class ActiveAccelerationBox implements OnChanges {
@Input() acceleratedBy?: number[]; @Input() tx: Transaction;
@Input() effectiveFeeRate?: number;
@Input() accelerationInfo: Acceleration; @Input() accelerationInfo: Acceleration;
@Input() miningStats: MiningStats; @Input() miningStats: MiningStats;
@Input() pools: number[]; @Input() pools: number[];
@@ -42,12 +41,10 @@ export class ActiveAccelerationBox implements OnChanges {
timespan = ''; timespan = '';
chartInstance: any = undefined; chartInstance: any = undefined;
constructor( constructor() {}
private cd: ChangeDetectorRef,
) {}
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
const pools = this.pools || this.accelerationInfo?.pools || this.acceleratedBy; const pools = this.pools || this.accelerationInfo?.pools || this.tx.acceleratedBy;
if (pools && this.miningStats) { if (pools && this.miningStats) {
this.prepareChartOptions(pools); this.prepareChartOptions(pools);
} }
@@ -76,21 +73,15 @@ export class ActiveAccelerationBox implements OnChanges {
acceleratingPools.forEach((poolId, index) => { acceleratingPools.forEach((poolId, index) => {
const pool = pools[poolId]; const pool = pools[poolId];
const poolShare = ((pool.lastEstimatedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1); const poolShare = ((pool.lastEstimatedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1);
let color = 'white';
if (index >= firstSignificantPool) {
if (numSignificantPools > 1) {
color = toRGB(lighten({ r: 147, g: 57, b: 244 }, 1 - (index - firstSignificantPool) / Math.max((numSignificantPools - 1), 1)));
} else {
color = toRGB({ r: 147, g: 57, b: 244 });
}
}
data.push(getDataItem( data.push(getDataItem(
pool.lastEstimatedHashrate, pool.lastEstimatedHashrate,
color, index >= firstSignificantPool
? toRGB(lighten({ r: 147, g: 57, b: 244 }, 1 - (index - firstSignificantPool) / (numSignificantPools - 1)))
: 'white',
`<b style="color: white">${pool.name} (${poolShare}%)</b>`, `<b style="color: white">${pool.name} (${poolShare}%)</b>`,
true, true,
) as PieSeriesOption); ) as PieSeriesOption);
}); })
this.acceleratedByPercentage = ((totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1) + '%'; this.acceleratedByPercentage = ((totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1) + '%';
const notAcceleratedByPercentage = ((1 - (totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate)) * 100).toFixed(1) + '%'; const notAcceleratedByPercentage = ((1 - (totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate)) * 100).toFixed(1) + '%';
data.push(getDataItem( data.push(getDataItem(
@@ -141,7 +132,6 @@ export class ActiveAccelerationBox implements OnChanges {
} }
] ]
}; };
this.cd.markForCheck();
} }
onChartInit(ec) { onChartInit(ec) {
@@ -154,4 +144,4 @@ export class ActiveAccelerationBox implements OnChanges {
onToggleCpfp(): void { onToggleCpfp(): void {
this.toggleCpfp.emit(); this.toggleCpfp.emit();
} }
} }

View File

@@ -1,9 +1,9 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { switchMap } from 'rxjs/operators'; import { switchMap } from 'rxjs/operators';
import { Acceleration } from '@interfaces/node-api.interface'; import { Acceleration } from '../../../interfaces/node-api.interface';
import { StateService } from '@app/services/state.service'; import { StateService } from '../../../services/state.service';
import { WebsocketService } from '@app/services/websocket.service'; import { WebsocketService } from '../../../services/websocket.service';
@Component({ @Component({
selector: 'app-pending-stats', selector: 'app-pending-stats',

View File

@@ -1,15 +1,16 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
import { echarts, EChartsOption } from '@app/graphs/echarts'; import { echarts, EChartsOption } from '../../graphs/echarts';
import { BehaviorSubject, Observable, Subscription, combineLatest, of } from 'rxjs'; import { BehaviorSubject, Observable, Subscription, combineLatest, of } from 'rxjs';
import { catchError, map, switchMap, tap } from 'rxjs/operators'; import { catchError, map, switchMap, tap } from 'rxjs/operators';
import { AddressTxSummary, ChainStats } from '@interfaces/electrs.interface'; import { AddressTxSummary, ChainStats } from '../../interfaces/electrs.interface';
import { ElectrsApiService } from '@app/services/electrs-api.service'; import { ElectrsApiService } from '../../services/electrs-api.service';
import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe'; import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe'; import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { StateService } from '@app/services/state.service'; import { StateService } from '../../services/state.service';
import { PriceService } from '@app/services/price.service'; import { PriceService } from '../../services/price.service';
import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe'; import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe';
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
const periodSeconds = { const periodSeconds = {
'1d': (60 * 60 * 24), '1d': (60 * 60 * 24),
@@ -44,18 +45,14 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
@Input() right: number | string = 10; @Input() right: number | string = 10;
@Input() left: number | string = 70; @Input() left: number | string = 70;
@Input() widget: boolean = false; @Input() widget: boolean = false;
@Input() defaultFiat: boolean = false;
@Input() showLegend: boolean = true;
@Input() showYAxis: boolean = true;
adjustedLeft: number;
adjustedRight: number;
data: any[] = []; data: any[] = [];
fiatData: any[] = []; fiatData: any[] = [];
hoverData: any[] = []; hoverData: any[] = [];
conversions: any; conversions: any;
allowZoom: boolean = false; allowZoom: boolean = false;
initialRight = this.right;
initialLeft = this.left;
selected = { [$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`]: true, 'Fiat': false }; selected = { [$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`]: true, 'Fiat': false };
subscription: Subscription; subscription: Subscription;
@@ -80,17 +77,15 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
private relativeUrlPipe: RelativeUrlPipe, private relativeUrlPipe: RelativeUrlPipe,
private priceService: PriceService, private priceService: PriceService,
private fiatCurrencyPipe: FiatCurrencyPipe, private fiatCurrencyPipe: FiatCurrencyPipe,
private fiatShortenerPipe: FiatShortenerPipe,
private zone: NgZone, private zone: NgZone,
) {} ) {}
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
this.isLoading = true; this.isLoading = true;
if (!this.addressSummary$ && (!this.address || !this.stats)) { if (!this.address || !this.stats) {
return; return;
} }
if (changes.defaultFiat) {
this.selected['Fiat'] = !!this.defaultFiat;
}
if (changes.address || changes.isPubkey || changes.addressSummary$ || changes.stats) { if (changes.address || changes.isPubkey || changes.addressSummary$ || changes.stats) {
if (this.subscription) { if (this.subscription) {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
@@ -123,7 +118,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
} else if (this.conversions && this.conversions['USD']) { } else if (this.conversions && this.conversions['USD']) {
price = this.conversions['USD']; price = this.conversions['USD'];
} }
return { ...item, price: price }; return { ...item, price: price }
}); });
} }
}), }),
@@ -149,16 +144,15 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
} }
prepareChartOptions(summary: AddressTxSummary[]) { prepareChartOptions(summary: AddressTxSummary[]) {
if (!summary) { if (!summary || !this.stats) {
return; return;
} }
const total = this.stats ? (this.stats.funded_txo_sum - this.stats.spent_txo_sum) : summary.reduce((acc, tx) => acc + tx.value, 0); let total = (this.stats.funded_txo_sum - this.stats.spent_txo_sum);
let runningTotal = total;
const processData = summary.map(d => { const processData = summary.map(d => {
const balance = runningTotal; const balance = total;
const fiatBalance = runningTotal * d.price / 100_000_000; const fiatBalance = total * d.price / 100_000_000;
runningTotal -= d.value; total -= d.value;
return { return {
time: d.time * 1000, time: d.time * 1000,
balance, balance,
@@ -166,7 +160,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
d d
}; };
}).reverse(); }).reverse();
this.data = processData.filter(({ d }) => d.txid !== undefined).map(({ time, balance, d }) => [time, balance, d]); this.data = processData.filter(({ d }) => d.txid !== undefined).map(({ time, balance, d }) => [time, balance, d]);
this.fiatData = processData.map(({ time, fiatBalance, balance, d }) => [time, fiatBalance, d, balance]); this.fiatData = processData.map(({ time, fiatBalance, balance, d }) => [time, fiatBalance, d, balance]);
@@ -178,15 +172,12 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
this.fiatData = this.fiatData.filter(d => d[0] >= startFiat); this.fiatData = this.fiatData.filter(d => d[0] >= startFiat);
} }
this.data.push( this.data.push(
{value: [now, total], symbol: 'none', tooltip: { show: false }} {value: [now, this.stats.funded_txo_sum - this.stats.spent_txo_sum], symbol: 'none', tooltip: { show: false }}
); );
const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1] ?? d.value[1])), 0); const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1] ?? d.value[1])), 0);
const minValue = this.data.reduce((acc, d) => Math.min(acc, Math.abs(d[1] ?? d.value[1])), maxValue); const minValue = this.data.reduce((acc, d) => Math.min(acc, Math.abs(d[1] ?? d.value[1])), maxValue);
this.adjustedRight = this.selected['Fiat'] ? +this.right + 40 : +this.right;
this.adjustedLeft = this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`] ? +this.left : +this.left - 40;
this.chartOptions = { this.chartOptions = {
color: [ color: [
new echarts.graphic.LinearGradient(0, 0, 0, 1, [ new echarts.graphic.LinearGradient(0, 0, 0, 1, [
@@ -202,10 +193,10 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
grid: { grid: {
top: 20, top: 20,
bottom: this.allowZoom ? 65 : 20, bottom: this.allowZoom ? 65 : 20,
right: this.adjustedRight, right: this.right,
left: this.adjustedLeft, left: this.left,
}, },
legend: (this.showLegend && !this.stateService.isAnyTestnet()) ? { legend: !this.stateService.isAnyTestnet() ? {
data: [ data: [
{ {
name: $localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`, name: $localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`,
@@ -253,22 +244,21 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
let tooltip = '<div>'; let tooltip = '<div>';
const hasTx = data[0].data[2].txid; const hasTx = data[0].data[2].txid;
const date = new Date(data[0].data[0]).toLocaleTimeString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
tooltip += `<div>
<div style="text-align: right;">
<div><b>${date}</b></div>`;
if (hasTx) { if (hasTx) {
const header = data.length === 1 const header = data.length === 1
? `${data[0].data[2].txid.slice(0, 6)}...${data[0].data[2].txid.slice(-6)}` ? `${data[0].data[2].txid.slice(0, 6)}...${data[0].data[2].txid.slice(-6)}`
: `${data.length} transactions`; : `${data.length} transactions`;
tooltip += `<div><b>${header}</b></div>`; tooltip += `<span><b>${header}</b></span>`;
} }
const date = new Date(data[0].data[0]).toLocaleTimeString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
tooltip += `<div>
<div style="text-align: right;">`;
const formatBTC = (val, decimal) => (val / 100_000_000).toFixed(decimal); const formatBTC = (val, decimal) => (val / 100_000_000).toFixed(decimal);
const formatFiat = (val) => this.fiatCurrencyPipe.transform(val, null, 'USD'); const formatFiat = (val) => this.fiatCurrencyPipe.transform(val, null, 'USD');
const btcVal = btcData.reduce((total, d) => total + d.data[2].value, 0); const btcVal = btcData.reduce((total, d) => total + d.data[2].value, 0);
const fiatVal = fiatData.reduce((total, d) => total + d.data[2].value * d.data[2].price / 100_000_000, 0); const fiatVal = fiatData.reduce((total, d) => total + d.data[2].value * d.data[2].price / 100_000_000, 0);
const btcColor = btcVal === 0 ? '' : (btcVal > 0 ? 'var(--green)' : 'var(--red)'); const btcColor = btcVal === 0 ? '' : (btcVal > 0 ? 'var(--green)' : 'var(--red)');
@@ -300,7 +290,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
} }
} }
tooltip += `</div></div>`; tooltip += `</div><span>${date}</span></div>`;
return tooltip; return tooltip;
}.bind(this) }.bind(this)
}, },
@@ -316,26 +306,22 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
type: 'value', type: 'value',
position: 'left', position: 'left',
axisLabel: { axisLabel: {
show: this.showYAxis,
color: 'rgb(110, 112, 121)', color: 'rgb(110, 112, 121)',
formatter: (val): string => { formatter: (val): string => {
let valSpan = maxValue - (this.period === 'all' ? 0 : minValue); let valSpan = maxValue - (this.period === 'all' ? 0 : minValue);
if (valSpan > 100_000_000_000) { if (valSpan > 100_000_000_000) {
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 0, undefined, true)} BTC`; return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 0)} BTC`;
} }
else if (valSpan > 1_000_000_000) { else if (valSpan > 1_000_000_000) {
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 2, undefined, true)} BTC`; return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 2)} BTC`;
} else if (valSpan > 100_000_000) { } else if (valSpan > 100_000_000) {
return `${(val / 100_000_000).toFixed(1)} BTC`; return `${(val / 100_000_000).toFixed(1)} BTC`;
} else if (valSpan > 10_000_000) { } else if (valSpan > 10_000_000) {
return `${(val / 100_000_000).toFixed(2)} BTC`; return `${(val / 100_000_000).toFixed(2)} BTC`;
} else if (valSpan > 1_000_000) { } else if (valSpan > 1_000_000) {
if (maxValue > 100_000_000_000) {
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 3, undefined, true)} BTC`;
}
return `${(val / 100_000_000).toFixed(3)} BTC`; return `${(val / 100_000_000).toFixed(3)} BTC`;
} else { } else {
return `${this.amountShortenerPipe.transform(val, 0, undefined, true)} sats`; return `${this.amountShortenerPipe.transform(val, 0)} sats`;
} }
} }
}, },
@@ -347,10 +333,9 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
{ {
type: 'value', type: 'value',
axisLabel: { axisLabel: {
show: this.showYAxis,
color: 'rgb(110, 112, 121)', color: 'rgb(110, 112, 121)',
formatter: function(val) { formatter: function(val) {
return `$${this.amountShortenerPipe.transform(val, 3, undefined, true, true)}`; return this.fiatShortenerPipe.transform(val, null, 'USD');
}.bind(this) }.bind(this)
}, },
splitLine: { splitLine: {
@@ -404,8 +389,8 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
type: 'slider', type: 'slider',
brushSelect: false, brushSelect: false,
realtime: true, realtime: true,
left: this.adjustedLeft, left: this.left,
right: this.adjustedRight, right: this.right,
selectedDataBackground: { selectedDataBackground: {
lineStyle: { lineStyle: {
color: '#fff', color: '#fff',
@@ -418,7 +403,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
onChartClick(e) { onChartClick(e) {
if (this.hoverData?.length && this.hoverData[0]?.[2]?.txid) { if (this.hoverData?.length && this.hoverData[0]?.[2]?.txid) {
this.zone.run(() => { this.zone.run(() => {
const url = this.relativeUrlPipe.transform(`/tx/${this.hoverData[0][2].txid}`); const url = this.relativeUrlPipe.transform(`/tx/${this.hoverData[0][2].txid}`);
if (e.event.event.shiftKey || e.event.event.ctrlKey || e.event.event.metaKey) { if (e.event.event.shiftKey || e.event.event.ctrlKey || e.event.event.metaKey) {
window.open(url); window.open(url);
@@ -435,26 +420,26 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
onLegendSelectChanged(e) { onLegendSelectChanged(e) {
this.selected = e.selected; this.selected = e.selected;
this.adjustedRight = this.selected['Fiat'] ? +this.right + 40 : +this.right; this.right = this.selected['Fiat'] ? +this.initialRight + 40 : this.initialRight;
this.adjustedLeft = this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`] ? +this.left : +this.left - 40; this.left = this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`] ? this.initialLeft : +this.initialLeft - 40;
this.chartOptions = { this.chartOptions = {
grid: { grid: {
right: this.adjustedRight, right: this.right,
left: this.adjustedLeft, left: this.left,
}, },
legend: { legend: {
selected: this.selected, selected: this.selected,
}, },
dataZoom: this.allowZoom ? [{ dataZoom: this.allowZoom ? [{
left: this.adjustedLeft, left: this.left,
right: this.adjustedRight, right: this.right,
}, { }, {
left: this.adjustedLeft, left: this.left,
right: this.adjustedRight, right: this.right,
}] : undefined }] : undefined
}; };
if (this.chartInstance) { if (this.chartInstance) {
this.chartInstance.setOption(this.chartOptions); this.chartInstance.setOption(this.chartOptions);
} }
@@ -478,30 +463,25 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
} }
extendSummary(summary) { extendSummary(summary) {
const extendedSummary = summary.slice(); let extendedSummary = summary.slice();
// Add a point at today's date to make the graph end at the current time // Add a point at today's date to make the graph end at the current time
extendedSummary.unshift({ time: Date.now() / 1000, value: 0 }); extendedSummary.unshift({ time: Date.now() / 1000, value: 0 });
extendedSummary.reverse();
let maxTime = Date.now() / 1000;
let oneHour = 60 * 60;
const oneHour = 60 * 60;
// Fill gaps longer than interval // Fill gaps longer than interval
for (let i = 0; i < extendedSummary.length - 1; i++) { for (let i = 0; i < extendedSummary.length - 1; i++) {
if (extendedSummary[i].time > maxTime) { let hours = Math.floor((extendedSummary[i + 1].time - extendedSummary[i].time) / oneHour);
extendedSummary[i].time = maxTime - 30;
}
maxTime = extendedSummary[i].time;
const hours = Math.floor((extendedSummary[i].time - extendedSummary[i + 1].time) / oneHour);
if (hours > 1) { if (hours > 1) {
for (let j = 1; j < hours; j++) { for (let j = 1; j < hours; j++) {
const newTime = extendedSummary[i].time - oneHour * j; let newTime = extendedSummary[i].time + oneHour * j;
extendedSummary.splice(i + j, 0, { time: newTime, value: 0 }); extendedSummary.splice(i + j, 0, { time: newTime, value: 0 });
} }
i += hours - 1; i += hours - 1;
} }
} }
return extendedSummary; return extendedSummary.reverse();
} }
} }

View File

@@ -1,15 +1,15 @@
import { Component, OnInit, OnDestroy, ChangeDetectorRef, HostListener } from '@angular/core'; import { Component, OnInit, OnDestroy, ChangeDetectorRef, HostListener } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router'; import { ActivatedRoute, ParamMap } from '@angular/router';
import { ElectrsApiService } from '@app/services/electrs-api.service'; import { ElectrsApiService } from '../../services/electrs-api.service';
import { switchMap, catchError } from 'rxjs/operators'; import { switchMap, catchError } from 'rxjs/operators';
import { Address, Transaction } from '@interfaces/electrs.interface'; import { Address, Transaction } from '../../interfaces/electrs.interface';
import { WebsocketService } from '@app/services/websocket.service'; import { WebsocketService } from '../../services/websocket.service';
import { StateService } from '@app/services/state.service'; import { StateService } from '../../services/state.service';
import { AudioService } from '@app/services/audio.service'; import { AudioService } from '../../services/audio.service';
import { ApiService } from '@app/services/api.service'; import { ApiService } from '../../services/api.service';
import { of, Subscription, forkJoin } from 'rxjs'; import { of, Subscription, forkJoin } from 'rxjs';
import { SeoService } from '@app/services/seo.service'; import { SeoService } from '../../services/seo.service';
import { AddressInformation } from '@interfaces/node-api.interface'; import { AddressInformation } from '../../interfaces/node-api.interface';
@Component({ @Component({
selector: 'app-address-group', selector: 'app-address-group',

View File

@@ -1,7 +1,7 @@
import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core'; import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core';
import { Vin, Vout } from '@interfaces/electrs.interface'; import { Vin, Vout } from '../../interfaces/electrs.interface';
import { StateService } from '@app/services/state.service'; import { StateService } from '../../services/state.service';
import { AddressType, AddressTypeInfo } from '@app/shared/address-utils'; import { AddressType, AddressTypeInfo } from '../../shared/address-utils';
@Component({ @Component({
selector: 'app-address-labels', selector: 'app-address-labels',

View File

@@ -12,7 +12,7 @@
<app-truncate [text]="transaction.txid" [lastChars]="5"></app-truncate> <app-truncate [text]="transaction.txid" [lastChars]="5"></app-truncate>
</a> </a>
</td> </td>
<td class="table-cell-satoshis"><app-amount [satoshis]="transaction.value" [digitsInfo]="getAmountDigits(transaction.value)" [noFiat]="true"></app-amount></td> <td class="table-cell-satoshis"><app-amount [satoshis]="transaction.value" digitsInfo="1.2-4" [noFiat]="true"></app-amount></td>
<td class="table-cell-fiat" ><app-fiat [value]="transaction.value" [blockConversion]="transaction.price" digitsInfo="1.0-0"></app-fiat></td> <td class="table-cell-fiat" ><app-fiat [value]="transaction.value" [blockConversion]="transaction.price" digitsInfo="1.0-0"></app-fiat></td>
<td class="table-cell-date"><app-time kind="since" [time]="transaction.time" [fastRender]="true" [showTooltip]="true"></app-time></td> <td class="table-cell-date"><app-time kind="since" [time]="transaction.time" [fastRender]="true" [showTooltip]="true"></app-time></td>
</tr> </tr>

View File

@@ -1,9 +1,9 @@
import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
import { StateService } from '@app/services/state.service'; import { StateService } from '../../services/state.service';
import { Address, AddressTxSummary } from '@interfaces/electrs.interface'; import { Address, AddressTxSummary } from '../../interfaces/electrs.interface';
import { ElectrsApiService } from '@app/services/electrs-api.service'; import { ElectrsApiService } from '../../services/electrs-api.service';
import { Observable, Subscription, catchError, map, of, switchMap, zip } from 'rxjs'; import { Observable, Subscription, catchError, map, of, switchMap, zip } from 'rxjs';
import { PriceService } from '@app/services/price.service'; import { PriceService } from '../../services/price.service';
@Component({ @Component({
selector: 'app-address-transactions-widget', selector: 'app-address-transactions-widget',
@@ -43,7 +43,7 @@ export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, On
startAddressSubscription(): void { startAddressSubscription(): void {
this.isLoading = true; this.isLoading = true;
if (!this.addressSummary$ && (!this.address || !this.addressInfo)) { if (!this.address || !this.addressInfo) {
return; return;
} }
this.transactions$ = (this.addressSummary$ || (this.isPubkey this.transactions$ = (this.addressSummary$ || (this.isPubkey
@@ -55,7 +55,7 @@ export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, On
}) })
)).pipe( )).pipe(
map(summary => { map(summary => {
return summary?.filter(tx => Math.abs(tx.value) >= 1000000)?.slice(0, 6); return summary?.slice(0, 6);
}), }),
switchMap(txs => { switchMap(txs => {
return (zip(txs.map(tx => this.priceService.getBlockPrice$(tx.time, txs.length < 3, this.currency).pipe( return (zip(txs.map(tx => this.priceService.getBlockPrice$(tx.time, txs.length < 3, this.currency).pipe(
@@ -68,12 +68,6 @@ export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, On
)))); ))));
}) })
); );
}
getAmountDigits(value: number): string {
const decimals = Math.max(0, 4 - Math.ceil(Math.log10(Math.abs(value / 100_000_000))));
return `1.${decimals}-${decimals}`;
} }
ngOnDestroy(): void { ngOnDestroy(): void {

View File

@@ -1,16 +1,16 @@
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router'; import { ActivatedRoute, ParamMap } from '@angular/router';
import { ElectrsApiService } from '@app/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, Transaction } from '../../interfaces/electrs.interface';
import { StateService } from '@app/services/state.service'; import { StateService } from '../../services/state.service';
import { OpenGraphService } from '@app/services/opengraph.service'; import { OpenGraphService } from '../../services/opengraph.service';
import { AudioService } from '@app/services/audio.service'; import { AudioService } from '../../services/audio.service';
import { ApiService } from '@app/services/api.service'; import { ApiService } from '../../services/api.service';
import { of, merge, Subscription, Observable } from 'rxjs'; import { of, merge, Subscription, Observable } from 'rxjs';
import { SeoService } from '@app/services/seo.service'; import { SeoService } from '../../services/seo.service';
import { seoDescriptionNetwork } from '@app/shared/common.utils'; import { seoDescriptionNetwork } from '../../shared/common.utils';
import { AddressInformation } from '@interfaces/node-api.interface'; import { AddressInformation } from '../../interfaces/node-api.interface';
@Component({ @Component({
selector: 'app-address-preview', selector: 'app-address-preview',

View File

@@ -94,20 +94,6 @@
</div> </div>
</ng-container> </ng-container>
<ng-container *ngIf="(stateService.backend$ | async) === 'esplora' && address && utxos && utxos.length > 2">
<br>
<div class="title-tx">
<h2 class="text-left" i18n="address.unspent-outputs">Unspent Outputs</h2>
</div>
<div class="box">
<div class="row">
<div class="col-md">
<app-utxo-graph [utxos]="utxos" left="80" />
</div>
</div>
</div>
</ng-container>
<br> <br>
<div class="title-tx"> <div class="title-tx">
<h2 class="text-left"> <h2 class="text-left">
@@ -117,7 +103,7 @@
</h2> </h2>
</div> </div>
<app-transactions-list [transactions]="transactions" [showConfirmations]="true" [addresses]="[address.address]" (loadMore)="loadMore()"></app-transactions-list> <app-transactions-list [transactions]="transactions" [showConfirmations]="true" [address]="address.address" (loadMore)="loadMore()"></app-transactions-list>
<div class="text-center"> <div class="text-center">
<ng-template [ngIf]="isLoadingTransactions"> <ng-template [ngIf]="isLoadingTransactions">

View File

@@ -1,17 +1,17 @@
import { Component, OnInit, OnDestroy, HostListener } from '@angular/core'; import { Component, OnInit, OnDestroy, HostListener } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router'; import { ActivatedRoute, ParamMap } from '@angular/router';
import { ElectrsApiService } from '@app/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, ChainStats, Transaction, Utxo, Vin } from '@interfaces/electrs.interface'; import { Address, ChainStats, Transaction, Vin } from '../../interfaces/electrs.interface';
import { WebsocketService } from '@app/services/websocket.service'; import { WebsocketService } from '../../services/websocket.service';
import { StateService } from '@app/services/state.service'; import { StateService } from '../../services/state.service';
import { AudioService } from '@app/services/audio.service'; import { AudioService } from '../../services/audio.service';
import { ApiService } from '@app/services/api.service'; import { ApiService } from '../../services/api.service';
import { of, merge, Subscription, Observable, forkJoin } from 'rxjs'; import { of, merge, Subscription, Observable } from 'rxjs';
import { SeoService } from '@app/services/seo.service'; import { SeoService } from '../../services/seo.service';
import { seoDescriptionNetwork } from '@app/shared/common.utils'; import { seoDescriptionNetwork } from '../../shared/common.utils';
import { AddressInformation } from '@interfaces/node-api.interface'; import { AddressInformation } from '../../interfaces/node-api.interface';
import { AddressTypeInfo } from '@app/shared/address-utils'; import { AddressTypeInfo } from '../../shared/address-utils';
class AddressStats implements ChainStats { class AddressStats implements ChainStats {
address: string; address: string;
@@ -104,7 +104,6 @@ export class AddressComponent implements OnInit, OnDestroy {
addressString: string; addressString: string;
isLoadingAddress = true; isLoadingAddress = true;
transactions: Transaction[]; transactions: Transaction[];
utxos: Utxo[];
isLoadingTransactions = true; isLoadingTransactions = true;
retryLoadMore = false; retryLoadMore = false;
error: any; error: any;
@@ -160,7 +159,6 @@ export class AddressComponent implements OnInit, OnDestroy {
this.address = null; this.address = null;
this.isLoadingTransactions = true; this.isLoadingTransactions = true;
this.transactions = null; this.transactions = null;
this.utxos = null;
this.addressInfo = null; this.addressInfo = null;
this.exampleChannel = null; this.exampleChannel = null;
document.body.scrollTo(0, 0); document.body.scrollTo(0, 0);
@@ -214,23 +212,11 @@ export class AddressComponent implements OnInit, OnDestroy {
this.updateChainStats(); this.updateChainStats();
this.isLoadingAddress = false; this.isLoadingAddress = false;
this.isLoadingTransactions = true; this.isLoadingTransactions = true;
const utxoCount = this.chainStats.utxos + this.mempoolStats.utxos; return address.is_pubkey
return forkJoin([
address.is_pubkey
? this.electrsApiService.getScriptHashTransactions$((address.address.length === 66 ? '21' : '41') + address.address + 'ac') ? this.electrsApiService.getScriptHashTransactions$((address.address.length === 66 ? '21' : '41') + address.address + 'ac')
: this.electrsApiService.getAddressTransactions$(address.address), : this.electrsApiService.getAddressTransactions$(address.address);
(utxoCount > 2 && utxoCount <= 500 ? (address.is_pubkey
? this.electrsApiService.getScriptHashUtxos$((address.address.length === 66 ? '21' : '41') + address.address + 'ac')
: this.electrsApiService.getAddressUtxos$(address.address)) : of(null)).pipe(
catchError(() => {
return of(null);
})
)
]);
}), }),
switchMap(([transactions, utxos]) => { switchMap((transactions) => {
this.utxos = utxos;
this.tempTransactions = transactions; this.tempTransactions = transactions;
if (transactions.length) { if (transactions.length) {
this.lastTransactionTxId = transactions[transactions.length - 1].txid; this.lastTransactionTxId = transactions[transactions.length - 1].txid;
@@ -323,7 +309,6 @@ export class AddressComponent implements OnInit, OnDestroy {
this.transactions = this.transactions.slice(); this.transactions = this.transactions.slice();
this.mempoolStats.removeTx(transaction); this.mempoolStats.removeTx(transaction);
this.audioService.playSound('magic'); this.audioService.playSound('magic');
this.confirmTransaction(tx);
} else { } else {
if (this.addTransaction(transaction, false)) { if (this.addTransaction(transaction, false)) {
this.audioService.playSound('magic'); this.audioService.playSound('magic');
@@ -349,31 +334,6 @@ export class AddressComponent implements OnInit, OnDestroy {
} }
} }
// update utxos in-place
if (this.utxos != null) {
let utxosChanged = false;
for (const vin of transaction.vin) {
const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === vin.txid && utxo.vout === vin.vout);
if (utxoIndex !== -1) {
this.utxos.splice(utxoIndex, 1);
utxosChanged = true;
}
}
for (const [index, vout] of transaction.vout.entries()) {
if (vout.scriptpubkey_address === this.address.address) {
this.utxos.push({
txid: transaction.txid,
vout: index,
value: vout.value,
status: JSON.parse(JSON.stringify(transaction.status)),
});
utxosChanged = true;
}
}
if (utxosChanged) {
this.utxos = this.utxos.slice();
}
}
return true; return true;
} }
@@ -386,65 +346,9 @@ export class AddressComponent implements OnInit, OnDestroy {
this.transactions.splice(index, 1); this.transactions.splice(index, 1);
this.transactions = this.transactions.slice(); this.transactions = this.transactions.slice();
// update utxos in-place
if (this.utxos != null) {
let utxosChanged = false;
for (const vin of transaction.vin) {
if (vin.prevout?.scriptpubkey_address === this.address.address) {
this.utxos.push({
txid: vin.txid,
vout: vin.vout,
value: vin.prevout.value,
status: { confirmed: true }, // Assuming the input was confirmed
});
utxosChanged = true;
}
}
for (const [index, vout] of transaction.vout.entries()) {
if (vout.scriptpubkey_address === this.address.address) {
const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === transaction.txid && utxo.vout === index);
if (utxoIndex !== -1) {
this.utxos.splice(utxoIndex, 1);
utxosChanged = true;
}
}
}
if (utxosChanged) {
this.utxos = this.utxos.slice();
}
}
return true; return true;
} }
confirmTransaction(transaction: Transaction): void {
// update utxos in-place
if (this.utxos != null) {
let utxosChanged = false;
for (const vin of transaction.vin) {
if (vin.prevout?.scriptpubkey_address === this.address.address) {
const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === vin.txid && utxo.vout === vin.vout);
if (utxoIndex !== -1) {
this.utxos[utxoIndex].status = JSON.parse(JSON.stringify(transaction.status));
utxosChanged = true;
}
}
}
for (const [index, vout] of transaction.vout.entries()) {
if (vout.scriptpubkey_address === this.address.address) {
const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === transaction.txid && utxo.vout === index);
if (utxoIndex !== -1) {
this.utxos[utxoIndex].status = JSON.parse(JSON.stringify(transaction.status));
utxosChanged = true;
}
}
}
if (utxosChanged) {
this.utxos = this.utxos.slice();
}
}
}
loadMore(): void { loadMore(): void {
if (this.isLoadingTransactions || this.fullyLoaded) { if (this.isLoadingTransactions || this.fullyLoaded) {
return; return;

View File

@@ -1,10 +0,0 @@
<div class="addresses-treemap-container">
<div *ngIf="addresses" style="height: 300px">
<div *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)">
</div>
</div>
<div *ngIf="!stateService.isBrowser || isLoading" class="text-center loading-spinner">
<div class="spinner-border text-light"></div>
</div>
</div>

View File

@@ -1,17 +0,0 @@
.node-channels-container {
position: relative;
}
.loading-spinner {
position: absolute;
top: 0;
left: 0;
right: 0;
width: 100%;
z-index: 100;
}
.spinner-border {
position: relative;
top: 225px;
}

View File

@@ -1,150 +0,0 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges } from '@angular/core';
import { Router } from '@angular/router';
import { EChartsOption, TreemapSeriesOption } from '@app/graphs/echarts';
import { lerpColor } from '@app/shared/graphs.utils';
import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe';
import { LightningApiService } from '@app/lightning/lightning-api.service';
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
import { StateService } from '@app/services/state.service';
import { Address } from '@interfaces/electrs.interface';
import { formatNumber } from '@angular/common';
@Component({
selector: 'app-addresses-treemap',
templateUrl: './addresses-treemap.component.html',
styleUrls: ['./addresses-treemap.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AddressesTreemap implements OnChanges {
@Input() addresses: Address[];
@Input() isLoading: boolean = false;
chartInstance: any;
chartOptions: EChartsOption = {};
chartInitOptions = {
renderer: 'svg',
};
constructor(
@Inject(LOCALE_ID) public locale: string,
private lightningApiService: LightningApiService,
private amountShortenerPipe: AmountShortenerPipe,
private zone: NgZone,
private router: Router,
public stateService: StateService,
) {}
ngOnChanges(): void {
this.prepareChartOptions();
}
prepareChartOptions(): void {
const data = this.addresses.map(address => ({
address: address.address,
value: address.chain_stats.funded_txo_sum - address.chain_stats.spent_txo_sum,
stats: address.chain_stats,
}));
// only consider visible items for the color gradient
const totalValue = data.reduce((acc, address) => acc + address.value, 0);
const maxTxs = data.filter(address => address.value > (totalValue / 2000)).reduce((max, address) => Math.max(max, address.stats.tx_count), 0);
const dataItems = data.map(address => ({
...address,
itemStyle: {
color: lerpColor('#1E88E5', '#D81B60', address.stats.tx_count / maxTxs),
}
}));
this.chartOptions = {
tooltip: {
trigger: 'item',
textStyle: {
align: 'left',
}
},
series: <TreemapSeriesOption[]>[
{
height: 300,
left: 0,
right: 0,
bottom: 0,
top: 0,
roam: false,
type: 'treemap',
data: dataItems,
nodeClick: 'link',
progressive: 100,
tooltip: {
show: true,
backgroundColor: 'rgba(17, 19, 31, 1)',
borderRadius: 4,
shadowColor: 'rgba(0, 0, 0, 0.5)',
textStyle: {
color: '#b1b1b1',
},
borderColor: '#000',
formatter: (value): string => {
if (!value.data.address) {
return '';
}
return `
<table style="table-layout: fixed;">
<tbody>
<tr>
<td colspan="2"><b style="color: white; margin-left: 2px">${value.data.address}</b></td>
</tr>
<tr>
<td>Received</td>
<td style="text-align: right">${this.formatValue(value.data.stats.funded_txo_sum)}</td>
</tr>
<tr>
<td>Sent</td>
<td style="text-align: right">${this.formatValue(value.data.stats.spent_txo_sum)}</td>
</tr>
<tr>
<td>Balance</td>
<td style="text-align: right">${this.formatValue(value.data.stats.funded_txo_sum - value.data.stats.spent_txo_sum)}</td>
</tr>
<tr>
<td>Transaction count</td>
<td style="text-align: right">${value.data.stats.tx_count}</td>
</tr>
</tbody>
</table>
`;
}
},
itemStyle: {
borderColor: 'black',
borderWidth: 1,
},
breadcrumb: {
show: false,
}
}
]
};
}
formatValue(sats: number): string {
if (sats > 100000000) {
return formatNumber(sats / 100000000, this.locale, '1.2-2') + ' BTC';
} else {
return this.amountShortenerPipe.transform(sats, 2) + ' sats';
}
}
onChartInit(ec: any): void {
this.chartInstance = ec;
this.chartInstance.on('click', (e) => {
//@ts-ignore
if (!e.data.address) {
return;
}
this.zone.run(() => {
//@ts-ignore
const url = new RelativeUrlPipe(this.stateService).transform(`/address/${e.data.address}`);
this.router.navigate([url]);
});
});
}
}

View File

@@ -1,7 +0,0 @@
<div [formGroup]="amountForm" class="text-small text-center">
<select formControlName="mode" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 70px;" (change)="changeMode()">
<option value="btc" i18n="shared.btc|BTC">BTC</option>
<option value="sats" i18n="shared.sats">sats</option>
<option value="fiat" i18n="shared.fiat|Fiat">Fiat</option>
</select>
</div>

Some files were not shown because too many files have changed in this diff Show More