Compare commits

..

6 Commits

Author SHA1 Message Date
transifex-integration[bot]
683d45c67a
Translate frontend/src/locale/messages.xlf in pl
100% reviewed source file: 'frontend/src/locale/messages.xlf'
on 'pl'.
2024-08-08 13:56:07 +00:00
transifex-integration[bot]
ff55070941
Translate frontend/src/locale/messages.xlf in pl
100% reviewed source file: 'frontend/src/locale/messages.xlf'
on 'pl'.
2024-08-08 13:54:42 +00:00
transifex-integration[bot]
fa1c3cf350
Translate frontend/src/locale/messages.xlf in pl
100% reviewed source file: 'frontend/src/locale/messages.xlf'
on 'pl'.
2024-08-08 13:47:35 +00:00
transifex-integration[bot]
8f764bc840
Translate frontend/src/locale/messages.xlf in pl
100% reviewed source file: 'frontend/src/locale/messages.xlf'
on 'pl'.
2024-08-08 13:46:23 +00:00
transifex-integration[bot]
f6c3752b27
Translate frontend/src/locale/messages.xlf in pl
100% reviewed source file: 'frontend/src/locale/messages.xlf'
on 'pl'.
2024-08-08 13:31:40 +00:00
transifex-integration[bot]
76c797a2db
Translate frontend/src/locale/messages.xlf in pl
100% reviewed source file: 'frontend/src/locale/messages.xlf'
on 'pl'.
2024-08-08 13:31:09 +00:00
481 changed files with 4983 additions and 16232 deletions

View File

@ -251,7 +251,17 @@ jobs:
strategy:
fail-fast: false
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 }}
steps:
@ -300,10 +310,8 @@ jobs:
- name: Unzip assets before building (src/resources)
run: unzip -o promo-video-assets.zip -d ${{ matrix.module }}/frontend/src/resources/promo-video
# mempool
- name: Chrome browser tests (${{ matrix.module }})
if: ${{ matrix.module == 'mempool' }}
uses: cypress-io/github-action@v5
with:
tag: ${{ github.event_name }}
@ -314,9 +322,7 @@ jobs:
wait-on-timeout: 120
record: true
parallel: true
spec: |
cypress/e2e/mainnet/*.spec.ts
cypress/e2e/signet/*.spec.ts
spec: ${{ matrix.spec }}
group: Tests on Chrome (${{ matrix.module }})
browser: "chrome"
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
@ -326,56 +332,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
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:
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
runs-on: "ubuntu-latest"
@ -403,4 +359,4 @@ jobs:
- name: Validate JSON syntax
run: |
cat mempool-config.json | jq
working-directory: docker/docker/backend
working-directory: docker/docker/backend

12
LICENSE
View File

@ -1,5 +1,5 @@
The Mempool Open Source Project®
Copyright (c) 2019-2024 Mempool Space K.K. and other shadowy super-coders
Copyright (c) 2019-2023 Mempool Space K.K. and other shadowy super-coders
This program is free software; you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
@ -12,12 +12,10 @@ or any other contributor to The Mempool Open Source Project.
The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®,
Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full
Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square Logo,
the mempool block visualization Logo, the mempool Blocks Logo, the mempool
transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo,
the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are
registered trademarks or trademarks of Mempool Space K.K in Japan,
the United States, and/or other countries.
Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square logo,
the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical
Logo, and the mempool.space Horizontal logo are registered trademarks or trademarks
of Mempool Space K.K in Japan, the United States, and/or other countries.
See our full Trademark Policy and Guidelines for more details, published on
<https://mempool.space/trademark-policy>.

View File

@ -77,7 +77,7 @@ Query OK, 0 rows affected (0.00 sec)
#### Build
_Make sure to use Node.js 20.x and npm 9.x or newer_
_Make sure to use Node.js 16.10 and npm 7._
_The build process requires [Rust](https://www.rust-lang.org/tools/install) to be installed._

View File

@ -7,7 +7,7 @@ const config: Config.InitialOptions = {
automock: false,
collectCoverage: true,
collectCoverageFrom: ["./src/**/**.ts"],
coverageProvider: "v8",
coverageProvider: "babel",
coverageThreshold: {
global: {
lines: 1

View File

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

View File

@ -1,23 +1,23 @@
{
"name": "mempool-backend",
"version": "3.1.0-dev",
"version": "3.0.0-beta",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "mempool-backend",
"version": "3.1.0-dev",
"version": "3.0.0-beta",
"hasInstallScript": true,
"license": "GNU Affero General Public License v3.0",
"dependencies": {
"@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3",
"axios": "1.7.2",
"axios": "~1.7.2",
"bitcoinjs-lib": "~6.1.3",
"crypto-js": "~4.2.0",
"express": "~4.21.1",
"express": "~4.19.2",
"maxmind": "~4.3.11",
"mysql2": "~3.12.0",
"mysql2": "~3.11.0",
"redis": "^4.7.0",
"rust-gbt": "file:./rust-gbt",
"socks-proxy-agent": "~7.0.0",
@ -25,6 +25,8 @@
"ws": "~8.18.0"
},
"devDependencies": {
"@babel/code-frame": "^7.18.6",
"@babel/core": "^7.25.2",
"@types/compression": "^1.7.2",
"@types/crypto-js": "^4.1.1",
"@types/express": "^4.17.17",
@ -2278,7 +2280,6 @@
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
@ -2487,9 +2488,9 @@
}
},
"node_modules/body-parser": {
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
"dependencies": {
"bytes": "3.1.2",
"content-type": "~1.0.5",
@ -2499,7 +2500,7 @@
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.13.0",
"qs": "6.11.0",
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
@ -2824,9 +2825,9 @@
"dev": true
},
"node_modules/cookie": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"engines": {
"node": ">= 0.6"
}
@ -3028,9 +3029,9 @@
"dev": true
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"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"
}
@ -3458,36 +3459,36 @@
}
},
"node_modules/express": {
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"version": "4.19.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.3",
"body-parser": "1.20.2",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.7.1",
"cookie": "0.6.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~2.0.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.3.1",
"finalhandler": "1.2.0",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.3",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.10",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"qs": "6.11.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.19.0",
"serve-static": "1.16.2",
"send": "0.18.0",
"serve-static": "1.15.0",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
@ -3600,12 +3601,12 @@
}
},
"node_modules/finalhandler": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~2.0.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
@ -5997,21 +5998,6 @@
"yallist": "^3.0.2"
}
},
"node_modules/lru.min": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.1.tgz",
"integrity": "sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q==",
"license": "MIT",
"engines": {
"bun": ">=1.0.0",
"deno": ">=1.30.0",
"node": ">=8.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wellwelwel"
}
},
"node_modules/make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@ -6064,12 +6050,9 @@
}
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
},
"node_modules/merge-stream": {
"version": "2.0.0",
@ -6173,17 +6156,16 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/mysql2": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.12.0.tgz",
"integrity": "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==",
"license": "MIT",
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.11.0.tgz",
"integrity": "sha512-J9phbsXGvTOcRVPR95YedzVSxJecpW5A5+cQ57rhHIFXteTP10HCs+VBjS7DHIKfEaI1zQ5tlVrquCd64A6YvA==",
"dependencies": {
"aws-ssl-profiles": "^1.1.1",
"denque": "^2.1.0",
"generate-function": "^2.3.1",
"iconv-lite": "^0.6.3",
"long": "^5.2.1",
"lru.min": "^1.0.0",
"lru-cache": "^8.0.0",
"named-placeholders": "^1.1.3",
"seq-queue": "^0.0.5",
"sqlstring": "^2.3.2"
@ -6203,6 +6185,14 @@
"node": ">=0.10.0"
}
},
"node_modules/mysql2/node_modules/lru-cache": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz",
"integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==",
"engines": {
"node": ">=16.14"
}
},
"node_modules/named-placeholders": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz",
@ -6276,12 +6266,9 @@
}
},
"node_modules/object-inspect": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
"integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
"engines": {
"node": ">= 0.4"
},
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@ -6449,9 +6436,9 @@
"dev": true
},
"node_modules/path-to-regexp": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
},
"node_modules/path-type": {
"version": "4.0.0",
@ -6659,11 +6646,11 @@
]
},
"node_modules/qs": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"dependencies": {
"side-channel": "^1.0.6"
"side-channel": "^1.0.4"
},
"engines": {
"node": ">=0.6"
@ -6884,9 +6871,9 @@
}
},
"node_modules/send": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
@ -6919,14 +6906,6 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"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": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -6938,14 +6917,14 @@
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
},
"node_modules/serve-static": {
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
"dependencies": {
"encodeurl": "~2.0.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.19.0"
"send": "0.18.0"
},
"engines": {
"node": ">= 0.8.0"
@ -9624,9 +9603,9 @@
}
},
"body-parser": {
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
"requires": {
"bytes": "3.1.2",
"content-type": "~1.0.5",
@ -9636,7 +9615,7 @@
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.13.0",
"qs": "6.11.0",
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
@ -9870,9 +9849,9 @@
"dev": true
},
"cookie": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="
},
"cookie-signature": {
"version": "1.0.6",
@ -10017,9 +9996,9 @@
"dev": true
},
"encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
},
"error-ex": {
"version": "1.3.2",
@ -10324,36 +10303,36 @@
}
},
"express": {
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"version": "4.19.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
"requires": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.3",
"body-parser": "1.20.2",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.7.1",
"cookie": "0.6.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~2.0.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.3.1",
"finalhandler": "1.2.0",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.3",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.10",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"qs": "6.11.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.19.0",
"serve-static": "1.16.2",
"send": "0.18.0",
"serve-static": "1.15.0",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
@ -10455,12 +10434,12 @@
}
},
"finalhandler": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
"requires": {
"debug": "2.6.9",
"encodeurl": "~2.0.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
@ -12218,11 +12197,6 @@
"yallist": "^3.0.2"
}
},
"lru.min": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.1.tgz",
"integrity": "sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q=="
},
"make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@ -12262,9 +12236,9 @@
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="
},
"merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
},
"merge-stream": {
"version": "2.0.0",
@ -12337,16 +12311,16 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"mysql2": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.12.0.tgz",
"integrity": "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==",
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.11.0.tgz",
"integrity": "sha512-J9phbsXGvTOcRVPR95YedzVSxJecpW5A5+cQ57rhHIFXteTP10HCs+VBjS7DHIKfEaI1zQ5tlVrquCd64A6YvA==",
"requires": {
"aws-ssl-profiles": "^1.1.1",
"denque": "^2.1.0",
"generate-function": "^2.3.1",
"iconv-lite": "^0.6.3",
"long": "^5.2.1",
"lru.min": "^1.0.0",
"lru-cache": "^8.0.0",
"named-placeholders": "^1.1.3",
"seq-queue": "^0.0.5",
"sqlstring": "^2.3.2"
@ -12359,6 +12333,11 @@
"requires": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
}
},
"lru-cache": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz",
"integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA=="
}
}
},
@ -12422,9 +12401,9 @@
}
},
"object-inspect": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
"integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g=="
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ=="
},
"on-finished": {
"version": "2.4.1",
@ -12541,9 +12520,9 @@
"dev": true
},
"path-to-regexp": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
},
"path-type": {
"version": "4.0.0",
@ -12685,11 +12664,11 @@
"dev": true
},
"qs": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"requires": {
"side-channel": "^1.0.6"
"side-channel": "^1.0.4"
}
},
"queue-microtask": {
@ -12823,9 +12802,9 @@
"dev": true
},
"send": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
"requires": {
"debug": "2.6.9",
"depd": "2.0.0",
@ -12857,11 +12836,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": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -12875,14 +12849,14 @@
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
},
"serve-static": {
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
"requires": {
"encodeurl": "~2.0.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.19.0"
"send": "0.18.0"
}
},
"set-function-length": {

View File

@ -1,6 +1,6 @@
{
"name": "mempool-backend",
"version": "3.1.0-dev",
"version": "3.0.0-beta",
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
"license": "GNU Affero General Public License v3.0",
"homepage": "https://mempool.space",
@ -39,14 +39,15 @@
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\""
},
"dependencies": {
"@babel/core": "^7.25.2",
"@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3",
"axios": "1.7.2",
"axios": "~1.7.2",
"bitcoinjs-lib": "~6.1.3",
"crypto-js": "~4.2.0",
"express": "~4.21.1",
"express": "~4.19.2",
"maxmind": "~4.3.11",
"mysql2": "~3.12.0",
"mysql2": "~3.11.0",
"rust-gbt": "file:./rust-gbt",
"redis": "^4.7.0",
"socks-proxy-agent": "~7.0.0",
@ -54,6 +55,8 @@
"ws": "~8.18.0"
},
"devDependencies": {
"@babel/code-frame": "^7.18.6",
"@babel/core": "^7.25.2",
"@types/compression": "^1.7.2",
"@types/crypto-js": "^4.1.1",
"@types/express": "^4.17.17",

View File

@ -28,7 +28,6 @@
"INDEXING_BLOCKS_AMOUNT": 14,
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
"POOLS_UPDATE_DELAY": 604800,
"AUDIT": true,
"RUST_GBT": false,
"LIMIT_GBT": false,
@ -47,8 +46,7 @@
"PASSWORD": "__CORE_RPC_PASSWORD__",
"TIMEOUT": 1000,
"COOKIE": false,
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__",
"DEBUG_LOG_PATH": "__CORE_RPC_DEBUG_LOG_PATH__"
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__"
},
"ELECTRUM": {
"HOST": "__ELECTRUM_HOST__",
@ -151,9 +149,5 @@
"ENABLED": true,
"PAID": false,
"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 { MempoolTransactionExtended, TransactionExtended } from '../../mempool.interfaces';
import { MempoolTransactionExtended } from '../../mempool.interfaces';
const randomTransactions = require('./test-data/transactions-random.json');
const replacedTransactions = require('./test-data/transactions-replaced.json');
@ -10,14 +10,14 @@ describe('Common', () => {
describe('RBF', () => {
const newTransactions = rbfTransactions.concat(randomTransactions);
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(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
});
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(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');

View File

@ -41,9 +41,8 @@ describe('Mempool Backend Config', () => {
STDOUT_LOG_MIN_PRIORITY: 'debug',
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_UPDATE_DELAY: 604800,
AUDIT: false,
RUST_GBT: true,
RUST_GBT: false,
LIMIT_GBT: false,
CPFP_INDEXING: false,
MAX_BLOCKS_BULK_QUERY: 0,
@ -74,8 +73,7 @@ describe('Mempool Backend Config', () => {
PASSWORD: 'mempool',
TIMEOUT: 60000,
COOKIE: false,
COOKIE_PATH: '/bitcoin/.cookie',
DEBUG_LOG_PATH: '',
COOKIE_PATH: '/bitcoin/.cookie'
});
expect(config.SECOND_CORE_RPC).toStrictEqual({
@ -159,11 +157,6 @@ describe('Mempool Backend Config', () => {
PAID: false,
API_KEY: '',
});
expect(config.STRATUM).toStrictEqual({
ENABLED: false,
API: 'http://localhost:1234',
});
});
});

View File

@ -2,7 +2,6 @@ import config from '../config';
import logger from '../logger';
import { MempoolTransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
import rbfCache from './rbf-cache';
import transactionUtils from './transaction-utils';
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
@ -16,8 +15,7 @@ class Audit {
const matches: string[] = []; // present in both mined block and template
const added: string[] = []; // present in mined block, not in template
const unseen: string[] = []; // present in the mined block, not in our mempool
let prioritized: string[] = []; // higher in the block than would be expected by in-band feerate alone
let deprioritized: string[] = []; // lower in the block than would be expected by in-band feerate alone
const prioritized: string[] = []; // higher in the block than would be expected by in-band feerate alone
const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
const accelerated: string[] = []; // prioritized by the mempool accelerator
@ -135,7 +133,23 @@ class Audit {
totalWeight += tx.weight;
}
({ prioritized, deprioritized } = transactionUtils.identifyPrioritizedTransactions(transactions, 'effectiveFeePerVsize'));
// identify "prioritized" transactions
let lastEffectiveRate = 0;
// Iterate over the mined template from bottom to top (excluding the coinbase)
// Transactions should appear in ascending order of mining priority.
for (let i = transactions.length - 1; i > 0; i--) {
const blockTx = transactions[i];
// If a tx has a lower in-band effective fee rate than the previous tx,
// it must have been prioritized out-of-band (in order to have a higher mining priority)
// so exclude from the analysis.
if ((blockTx.effectiveFeePerVsize || 0) < lastEffectiveRate) {
prioritized.push(blockTx.txid);
// accelerated txs may or may not have their prioritized fee rate applied, so don't use them as a reference
} else if (!isAccelerated[blockTx.txid]) {
lastEffectiveRate = blockTx.effectiveFeePerVsize || 0;
}
}
// transactions missing from near the end of our template are probably not being censored
let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight);

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

View File

@ -218,21 +218,3 @@ export interface TestMempoolAcceptResult {
},
['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 { 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 blocks from '../blocks';
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> {
const txOut = await this.bitcoindClient.getTxOut(txId, vout, false);
return {
@ -255,10 +251,6 @@ class BitcoinApi implements AbstractBitcoinApi {
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> {
// 120 is the default block span in Core
return this.bitcoindClient.getNetworkHashPs(120, blockHeight);
@ -331,7 +323,6 @@ class BitcoinApi implements AbstractBitcoinApi {
'witness_v1_taproot': 'v1_p2tr',
'nonstandard': 'nonstandard',
'multisig': 'multisig',
'anchor': 'anchor',
'nulldata': 'op_return'
};

View File

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

View File

@ -20,13 +20,6 @@ import difficultyAdjustment from '../difficulty-adjustment';
import transactionRepository from '../../repositories/TransactionRepository';
import rbfCache from '../rbf-cache';
import { calculateMempoolTxCpfp } from '../cpfp';
import { handleError } from '../../utils/api';
import poolsUpdater from '../../tasks/pools-updater';
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 {
public initRoutes(app: Application) {
@ -48,19 +41,12 @@ class BitcoinRoutes {
.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/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/tx/:txid/audit', this.$getBlockTxAuditSummary)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
.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/: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)
// Internal routes
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/blocks/definition/list', this.getBlockDefinitionHashes)
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/blocks/definition/current', this.getCurrentBlockDefinitionHash)
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/blocks/:definitionHash', this.getBlocksByDefinitionHash)
;
if (config.MEMPOOL.BACKEND !== 'esplora') {
@ -100,7 +86,7 @@ class BitcoinRoutes {
res.set('Content-Type', 'application/json');
res.send(result);
} catch (e) {
handleError(req, res, 500, 'Failed to get init data');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -119,22 +105,19 @@ class BitcoinRoutes {
const result = mempoolBlocks.getMempoolBlocks();
res.json(result);
} 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) {
if (!Array.isArray(req.query.txId)) {
handleError(req, res, 500, 'Not an array');
res.status(500).send('Not an array');
return;
}
const txIds: string[] = [];
for (const _txId in req.query.txId) {
if (typeof req.query.txId[_txId] === 'string') {
const txid = req.query.txId[_txId].toString();
if (TXID_REGEX.test(txid)) {
txIds.push(txid);
}
txIds.push(req.query.txId[_txId].toString());
}
}
@ -145,16 +128,12 @@ class BitcoinRoutes {
private async $getBatchedOutspends(req: Request, res: Response): Promise<IEsploraApi.Outspend[][] | void> {
const txids_csv = req.query.txids;
if (!txids_csv || typeof txids_csv !== 'string') {
handleError(req, res, 500, 'Invalid txids format');
res.status(500).send('Invalid txids format');
return;
}
const txids = txids_csv.split(',');
if (txids.length > 50) {
handleError(req, res, 400, 'Too many txids requested');
return;
}
if (txids.some((txid) => !TXID_REGEX.test(txid))) {
handleError(req, res, 400, 'Invalid txids format');
res.status(400).send('Too many txids requested');
return;
}
@ -162,13 +141,13 @@ class BitcoinRoutes {
const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids);
res.json(batchedOutspends);
} 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) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
res.status(501).send(`Invalid transaction ID.`);
return;
}
@ -201,7 +180,7 @@ class BitcoinRoutes {
try {
cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
} catch (e) {
handleError(req, res, 500, 'Failed to get CPFP info');
res.status(500).send('failed to get CPFP info');
return;
}
}
@ -222,10 +201,6 @@ class BitcoinRoutes {
}
private async getTransaction(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true, false, false, true);
res.json(transaction);
@ -233,18 +208,12 @@ class BitcoinRoutes {
let statusCode = 500;
if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
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) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(req.params.txId, true);
res.setHeader('content-type', 'text/plain');
@ -253,10 +222,8 @@ class BitcoinRoutes {
let statusCode = 500;
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
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);
}
}
@ -317,22 +284,18 @@ class BitcoinRoutes {
// Not modified
// 422 Unprocessable Entity
// 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) {
if (e instanceof Error && new RegExp(notFoundError).test(e.message)) {
handleError(req, res, 404, notFoundError);
res.status(404).send(e.message);
} 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) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true);
res.json(transaction.status);
@ -340,54 +303,22 @@ class BitcoinRoutes {
let statusCode = 500;
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
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) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try {
const transactions = await blocks.$getStrippedBlockTransactions(req.params.hash);
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(transactions);
} catch (e) {
handleError(req, res, 500, 'Failed to get block summary');
}
}
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');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getBlock(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try {
const block = await blocks.$getBlock(req.params.hash);
@ -399,69 +330,51 @@ class BitcoinRoutes {
} else if (blockAge > 30 * day) {
cacheDuration = 10 * day;
} else {
cacheDuration = 600;
cacheDuration = 600
}
res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString());
res.json(block);
} 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) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try {
const blockHeader = await bitcoinApi.$getBlockHeader(req.params.hash);
res.setHeader('content-type', 'text/plain');
res.send(blockHeader);
} 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) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try {
const auditSummary = await blocks.$getBlockAuditSummary(req.params.hash);
if (auditSummary) {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(auditSummary);
} else {
handleError(req, res, 404, `Audit not available`);
return;
return res.status(404).send(`audit not available`);
}
} 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) {
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 auditSummary = await blocks.$getBlockTxAuditSummary(req.params.hash, req.params.txid);
if (auditSummary) {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(auditSummary);
} else {
handleError(req, res, 404, `Transaction audit not available`);
return;
return res.status(404).send(`transaction audit not available`);
}
} catch (e) {
handleError(req, res, 500, 'Failed to get transaction audit summary');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -475,49 +388,42 @@ class BitcoinRoutes {
return await this.getLegacyBlocks(req, res);
}
} 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) {
try {
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;
return res.status(404).send(`This API is only available for Bitcoin networks`);
}
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;
return res.status(404).send(`This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`);
}
if (!Common.indexingEnabled()) {
handleError(req, res, 404, `Indexing is required for this API`);
return;
return res.status(404).send(`Indexing is required for this API`);
}
const from = parseInt(req.params.from, 10);
if (!req.params.from || from < 0) {
handleError(req, res, 400, `Parameter 'from' must be a block height (integer)`);
return;
return res.status(400).send(`Parameter 'from' must be a block height (integer)`);
}
const to = req.params.to === undefined ? await bitcoinApi.$getBlockHeightTip() : parseInt(req.params.to, 10);
if (to < 0) {
handleError(req, res, 400, `Parameter 'to' must be a block height (integer)`);
return;
return res.status(400).send(`Parameter 'to' must be a block height (integer)`);
}
if (from > to) {
handleError(req, res, 400, `Parameter 'to' must be a higher block height than 'from'`);
return;
return res.status(400).send(`Parameter 'to' must be a higher block height than 'from'`);
}
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;
return res.status(400).send(`You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`);
}
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(await blocks.$getBlocksBetweenHeight(from, to));
} catch (e) {
handleError(req, res, 500, 'Failed to get blocks');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -552,15 +458,11 @@ class BitcoinRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(returnBlocks);
} 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) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try {
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0);
@ -581,7 +483,7 @@ class BitcoinRoutes {
res.json(transactions);
} catch (e) {
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);
}
}
@ -590,17 +492,13 @@ class BitcoinRoutes {
const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10));
res.send(blockHash);
} 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) {
if (config.MEMPOOL.BACKEND === 'none') {
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return;
}
if (!ADDRESS_REGEX.test(req.params.address)) {
handleError(req, res, 501, `Invalid address`);
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
return;
}
@ -609,20 +507,15 @@ class BitcoinRoutes {
res.json(addressData);
} catch (e) {
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;
return res.status(413).send(e instanceof Error ? e.message : e);
}
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> {
if (config.MEMPOOL.BACKEND === 'none') {
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return;
}
if (!ADDRESS_REGEX.test(req.params.address)) {
handleError(req, res, 501, `Invalid address`);
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
return;
}
@ -635,27 +528,23 @@ class BitcoinRoutes {
res.json(transactions);
} catch (e) {
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;
}
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> {
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;
}
}
private async getScriptHash(req: Request, res: Response) {
if (config.MEMPOOL.BACKEND === 'none') {
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return;
}
if (!SCRIPT_HASH_REGEX.test(req.params.scripthash)) {
handleError(req, res, 501, `Invalid scripthash`);
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
return;
}
@ -666,20 +555,15 @@ class BitcoinRoutes {
res.json(addressData);
} catch (e) {
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;
return res.status(413).send(e instanceof Error ? e.message : e);
}
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> {
if (config.MEMPOOL.BACKEND === 'none') {
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return;
}
if (!SCRIPT_HASH_REGEX.test(req.params.scripthash)) {
handleError(req, res, 501, `Invalid scripthash`);
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
return;
}
@ -694,26 +578,26 @@ class BitcoinRoutes {
res.json(transactions);
} catch (e) {
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;
}
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> {
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;
}
}
private async getAddressPrefix(req: Request, res: Response) {
try {
const addressPrefix = await bitcoinApi.$getAddressPrefix(req.params.prefix);
res.send(addressPrefix);
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
res.send(blockHash);
} catch (e) {
handleError(req, res, 500, 'Failed to get address prefix');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -740,53 +624,7 @@ class BitcoinRoutes {
const rawMempool = await bitcoinApi.$getRawMempool();
res.send(rawMempool);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private async getBlockDefinitionHashes(req: Request, res: Response): Promise<void> {
try {
const result = await blocks.$getBlockDefinitionHashes();
if (!result) {
handleError(req, res, 503, `Service Temporarily Unavailable`);
return;
}
res.setHeader('content-type', 'application/json');
res.send(result);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private async getCurrentBlockDefinitionHash(req: Request, res: Response): Promise<void> {
try {
const currentSha = await poolsUpdater.getShaFromDb();
if (!currentSha) {
handleError(req, res, 503, `Service Temporarily Unavailable`);
return;
}
res.setHeader('content-type', 'text/plain');
res.send(currentSha);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private async getBlocksByDefinitionHash(req: Request, res: Response): Promise<void> {
try {
if (typeof(req.params.definitionHash) !== 'string') {
res.status(400).send('Parameter "hash" must be a valid string');
return;
}
const blocksHash = await blocks.$getBlocksByDefinitionHash(req.params.definitionHash as string);
if (!blocksHash) {
handleError(req, res, 503, `Service Temporarily Unavailable`);
return;
}
res.setHeader('content-type', 'application/json');
res.send(blocksHash);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -794,13 +632,12 @@ class BitcoinRoutes {
try {
const result = blocks.getCurrentBlockHeight();
if (!result) {
handleError(req, res, 503, `Service Temporarily Unavailable`);
return;
return res.status(503).send(`Service Temporarily Unavailable`);
}
res.setHeader('content-type', 'text/plain');
res.send(result.toString());
} catch (e) {
handleError(req, res, 500, 'Failed to get height at tip');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -810,55 +647,39 @@ class BitcoinRoutes {
res.setHeader('content-type', 'text/plain');
res.send(result);
} 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) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try {
const result = await bitcoinApi.$getRawBlock(req.params.hash);
res.setHeader('content-type', 'application/octet-stream');
res.send(result);
} 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) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try {
const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
res.json(result);
} 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) {
if (!ADDRESS_REGEX.test(req.params.address)) {
handleError(req, res, 501, `Invalid address`);
return;
}
try {
const result = await bitcoinClient.validateAddress(req.params.address);
res.json(result);
} 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) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const replacements = rbfCache.getRbfTree(req.params.txId) || null;
const replaces = rbfCache.getReplaces(req.params.txId) || null;
@ -867,7 +688,7 @@ class BitcoinRoutes {
replaces
});
} catch (e) {
handleError(req, res, 500, 'Failed to get rbf history');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -876,7 +697,7 @@ class BitcoinRoutes {
const result = rbfCache.getRbfTrees(false);
res.json(result);
} catch (e) {
handleError(req, res, 500, 'Failed to get rbf trees');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -885,15 +706,11 @@ class BitcoinRoutes {
const result = rbfCache.getRbfTrees(true);
res.json(result);
} 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) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const result = rbfCache.getTx(req.params.txId);
if (result) {
@ -902,20 +719,16 @@ class BitcoinRoutes {
res.status(204).send();
}
} 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) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const result = await bitcoinApi.$getOutspends(req.params.txId);
res.json(result);
} catch (e) {
handleError(req, res, 500, 'Failed to get transaction outspends');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -925,10 +738,10 @@ class BitcoinRoutes {
if (da) {
res.json(da);
} else {
handleError(req, res, 503, `Service Temporarily Unavailable`);
res.status(503).send(`Service Temporarily Unavailable`);
}
} catch (e) {
handleError(req, res, 500, 'Failed to get difficulty change');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -939,8 +752,8 @@ class BitcoinRoutes {
const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx);
res.send(txIdResult);
} catch (e: any) {
handleError(req, res, 400, (e.message && e.code) ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code })
: 'Failed to send raw transaction');
res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error'));
}
}
@ -951,8 +764,8 @@ class BitcoinRoutes {
const txIdResult = await bitcoinClient.sendRawTransaction(txHex);
res.send(txIdResult);
} catch (e: any) {
handleError(req, res, 400, (e.message && e.code) ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code })
: 'Failed to send raw transaction');
res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error'));
}
}
@ -963,21 +776,9 @@ class BitcoinRoutes {
const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate);
res.send(result);
} catch (e: any) {
handleError(req, res, 400, (e.message && e.code) ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code })
: 'Failed to test transactions');
}
}
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');
res.setHeader('content-type', 'text/plain');
res.status(400).send(e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error'));
}
}

View File

@ -179,11 +179,4 @@ export namespace IEsploraApi {
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 axios, { isAxiosError } from 'axios';
import axios, { AxiosResponse, isAxiosError } from 'axios';
import http from 'http';
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
import { IEsploraApi } from './esplora-api.interface';
import logger from '../../logger';
import { Common } from '../common';
import { SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface';
import os from 'os';
import { TestMempoolAcceptResult } from './bitcoin-api.interface';
interface FailoverHost {
host: string,
rtts: number[],
@ -20,13 +20,6 @@ interface FailoverHost {
preferred?: boolean,
checked: boolean,
lastChecked?: number,
publicDomain: string,
hashes: {
frontend?: string,
backend?: string,
electrs?: string,
lastUpdated: number,
}
}
class FailoverRouter {
@ -36,21 +29,14 @@ class FailoverRouter {
maxHeight: number = 0;
hosts: FailoverHost[];
multihost: boolean;
gitHashInterval: number = 600000; // 10 minutes
pollInterval: number = 60000; // 1 minute
pollInterval: number = 60000;
pollTimer: NodeJS.Timeout | null = null;
pollConnection = axios.create();
localHostname: string = 'localhost';
requestConnection = axios.create({
httpAgent: new http.Agent({ keepAlive: true })
});
constructor() {
try {
this.localHostname = os.hostname();
} catch (e) {
logger.warn('Failed to set local hostname, using "localhost"');
}
// setup list of hosts
this.hosts = (config.ESPLORA.FALLBACK || []).map(domain => {
return {
@ -59,10 +45,6 @@ class FailoverRouter {
rtts: [],
rtt: Infinity,
failures: 0,
publicDomain: 'https://' + this.extractPublicDomain(domain),
hashes: {
lastUpdated: 0,
},
};
});
this.activeHost = {
@ -73,10 +55,6 @@ class FailoverRouter {
socket: !!config.ESPLORA.UNIX_SOCKET_PATH,
preferred: true,
checked: false,
publicDomain: `http://${this.localHostname}`,
hashes: {
lastUpdated: 0,
},
};
this.fallbackHost = this.activeHost;
this.hosts.unshift(this.activeHost);
@ -128,24 +106,6 @@ class FailoverRouter {
host.outOfSync = 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 {
host.outOfSync = 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> {
let axiosConfig;
let url;
@ -386,7 +305,7 @@ class ElectrsApi implements AbstractBitcoinApi {
}
$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[]> {
@ -413,10 +332,6 @@ class ElectrsApi implements AbstractBitcoinApi {
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> {
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);
}
async $getAddressTransactionSummary(address: string): Promise<IEsploraApi.AddressTxSummary[]> {
return this.failoverRouter.$get<IEsploraApi.AddressTxSummary[]>('/address/' + address + '/txs/summary');
}
public startHealthChecks(): void {
this.failoverRouter.startHealthChecks();
}
@ -462,7 +373,6 @@ class ElectrsApi implements AbstractBitcoinApi {
unreachable: !!host.unreachable,
checked: !!host.checked,
lastChecked: host.lastChecked || 0,
hashes: host.hashes,
}));
} else {
return [];

View File

@ -33,8 +33,7 @@ import AccelerationRepository from '../repositories/AccelerationRepository';
import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp';
import mempool from './mempool';
import CpfpRepository from '../repositories/CpfpRepository';
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
import database from '../database';
import accelerationApi from './services/acceleration';
class Blocks {
private blocks: BlockExtended[] = [];
@ -220,10 +219,10 @@ class Blocks {
};
}
public summarizeBlockTransactions(hash: string, height: number, transactions: TransactionExtended[]): BlockSummary {
public summarizeBlockTransactions(hash: string, transactions: TransactionExtended[]): BlockSummary {
return {
id: hash,
transactions: Common.classifyTransactions(transactions, height),
transactions: Common.classifyTransactions(transactions),
};
}
@ -343,12 +342,7 @@ class Blocks {
id: pool.uniqueId,
name: pool.name,
slug: pool.slug,
minerNames: null,
};
if (extras.pool.name === 'OCEAN') {
extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw);
}
}
extras.matchRate = null;
@ -412,16 +406,8 @@ class Blocks {
}
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
const indexedBlocks = (await blocksRepository.$getIndexedBlocks()).filter(block => block.height >= lastBlockToIndex);
const indexedBlocks = await blocksRepository.$getIndexedBlocks();
const indexedBlockSummariesHashesArray = await BlocksSummariesRepository.$getIndexedSummariesId();
const indexedBlockSummariesHashes = {}; // Use a map for faster seek during the indexing loop
@ -630,7 +616,7 @@ class Blocks {
// add CPFP
const cpfpSummary = calculateGoodBlockCpfp(height, txs, []);
// classify
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions);
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 2);
if (unclassifiedBlocks[height].version < 2 && targetSummaryVersion === 2) {
const cpfpClusters = await CpfpRepository.$getClustersAt(height);
@ -667,7 +653,7 @@ class Blocks {
}
const cpfpSummary = calculateGoodBlockCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as MempoolTransactionExtended[], []);
// classify
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions);
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
const classifiedTxMap: { [txid: string]: TransactionClassified } = {};
for (const tx of classifiedTxs) {
classifiedTxMap[tx.txid] = tx;
@ -926,7 +912,7 @@ class Blocks {
}
const cpfpSummary: CpfpSummary = calculateGoodBlockCpfp(block.height, transactions, accelerations.map(a => ({ txid: a.txid, max_bid: a.feeDelta })));
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, block.height, cpfpSummary.transactions);
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions);
this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`);
if (Common.indexingEnabled()) {
@ -1183,7 +1169,7 @@ class Blocks {
transactions: cpfpSummary.transactions.map(tx => {
let flags: number = 0;
try {
flags = Common.getTransactionFlags(tx, height);
flags = Common.getTransactionFlags(tx);
} catch (e) {
logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e));
}
@ -1202,7 +1188,7 @@ class Blocks {
} else {
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
summary = this.summarizeBlockTransactions(hash, height || 0, txs);
summary = this.summarizeBlockTransactions(hash, txs);
summaryVersion = 1;
} else {
// Call Core RPC
@ -1224,11 +1210,6 @@ class Blocks {
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
*
@ -1343,7 +1324,7 @@ class Blocks {
let summaryVersion = 0;
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx));
summary = this.summarizeBlockTransactions(cleanBlock.hash, cleanBlock.height, txs);
summary = this.summarizeBlockTransactions(cleanBlock.hash, txs);
summaryVersion = 1;
} else {
// Call Core RPC
@ -1462,36 +1443,6 @@ class Blocks {
// not a fatal error, we'll try again next time the indexer runs
}
}
public async $getBlockDefinitionHashes(): Promise<string[] | null> {
try {
const [rows]: any = await database.query(`SELECT DISTINCT(definition_hash) FROM blocks`);
if (rows && Array.isArray(rows)) {
return rows.map(r => r.definition_hash);
} else {
logger.debug(`Unable to retreive list of blocks.definition_hash from db (no result)`);
return null;
}
} catch (e) {
logger.debug(`Unable to retreive list of blocks.definition_hash from db (exception: ${e})`);
return null;
}
}
public async $getBlocksByDefinitionHash(definitionHash: string): Promise<string[] | null> {
try {
const [rows]: any = await database.query(`SELECT hash FROM blocks WHERE definition_hash = ?`, [definitionHash]);
if (rows && Array.isArray(rows)) {
return rows.map(r => r.hash);
} else {
logger.debug(`Unable to retreive list of blocks for definition hash ${definitionHash} from db (no result)`);
return null;
}
} catch (e) {
logger.debug(`Unable to retreive list of blocks for definition hash ${definitionHash} from db (exception: ${e})`);
return null;
}
}
}
export default new Blocks();

View File

@ -10,6 +10,7 @@ import logger from '../logger';
import { getVarIntLength, opcodes, parseMultisigScript } from '../utils/bitcoin-script';
// Bitcoin Core default policy settings
const TX_MAX_STANDARD_VERSION = 2;
const MAX_STANDARD_TX_WEIGHT = 400_000;
const MAX_BLOCK_SIGOPS_COST = 80_000;
const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5);
@ -79,8 +80,8 @@ export class Common {
return arr;
}
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} {
const matches: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = {};
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: MempoolTransactionExtended[] } {
const matches: { [txid: string]: MempoolTransactionExtended[] } = {};
// For small N, a naive nested loop is extremely fast, but it doesn't scale
if (added.length < 1000 && deleted.length < 50 && !forceScalable) {
@ -95,7 +96,7 @@ export class Common {
addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
});
if (foundMatches?.length) {
matches[addedTx.txid] = { replaced: [...new Set(foundMatches)], replacedBy: addedTx };
matches[addedTx.txid] = [...new Set(foundMatches)];
}
});
} else {
@ -123,7 +124,7 @@ export class Common {
foundMatches.add(deletedTx);
}
if (foundMatches.size) {
matches[addedTx.txid] = { replaced: [...foundMatches], replacedBy: addedTx };
matches[addedTx.txid] = [...foundMatches];
}
}
}
@ -138,17 +139,17 @@ export class Common {
const replaced: Set<MempoolTransactionExtended> = new Set();
for (let i = 0; i < tx.vin.length; i++) {
const vin = tx.vin[i];
const key = `${vin.txid}:${vin.vout}`;
const match = spendMap.get(key);
const match = spendMap.get(`${vin.txid}:${vin.vout}`);
if (match && match.txid !== tx.txid) {
replaced.add(match);
// remove this tx from the spendMap
// prevents the same tx being replaced more than once
for (const replacedVin of match.vin) {
const replacedKey = `${replacedVin.txid}:${replacedVin.vout}`;
spendMap.delete(replacedKey);
const key = `${replacedVin.txid}:${replacedVin.vout}`;
spendMap.delete(key);
}
}
const key = `${vin.txid}:${vin.vout}`;
spendMap.delete(key);
}
if (replaced.size) {
@ -199,13 +200,10 @@ export class Common {
*
* returns true early if any standardness rule is violated, otherwise false
* (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced)
*
* As standardness rules change, we'll need to apply the rules in force *at the time* to older blocks.
* For now, just pull out individual rules into versioned functions where necessary.
*/
static isNonStandard(tx: TransactionExtended, height?: number): boolean {
static isNonStandard(tx: TransactionExtended): boolean {
// version
if (this.isNonStandardVersion(tx, height)) {
if (tx.version > TX_MAX_STANDARD_VERSION) {
return true;
}
@ -252,8 +250,6 @@ export class Common {
}
} else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
return true;
} else if (this.isNonStandardAnchor(tx, height)) {
return true;
}
// TODO: bad-witness-nonstandard
}
@ -339,49 +335,6 @@ export class Common {
return false;
}
// Individual versioned standardness rules
static V3_STANDARDNESS_ACTIVATION_HEIGHT = {
'testnet4': 42_000,
'testnet': 2_900_000,
'signet': 211_000,
'': 863_500,
};
static isNonStandardVersion(tx: TransactionExtended, height?: number): boolean {
let TX_MAX_STANDARD_VERSION = 3;
if (
height != null
&& this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
&& height <= this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
) {
// V3 transactions were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
TX_MAX_STANDARD_VERSION = 2;
}
if (tx.version > TX_MAX_STANDARD_VERSION) {
return true;
}
return false;
}
static ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT = {
'testnet4': 42_000,
'testnet': 2_900_000,
'signet': 211_000,
'': 863_500,
};
static isNonStandardAnchor(tx: TransactionExtended, height?: number): boolean {
if (
height != null
&& this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
&& height <= this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
) {
// anchor outputs were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
return true;
}
return false;
}
static getNonWitnessSize(tx: TransactionExtended): number {
let weight = tx.weight;
let hasWitness = false;
@ -462,7 +415,7 @@ export class Common {
return flags;
}
static getTransactionFlags(tx: TransactionExtended, height?: number): number {
static getTransactionFlags(tx: TransactionExtended): number {
let flags = tx.flags ? BigInt(tx.flags) : 0n;
// Update variable flags (CPFP, RBF)
@ -595,7 +548,7 @@ export class Common {
if (hasFakePubkey) {
flags |= TransactionFlags.fake_pubkey;
}
// fast but bad heuristic to detect possible coinjoins
// (at least 5 inputs and 5 outputs, less than half of which are unique amounts, with no address reuse)
const addressReuse = Object.keys(reusedOutputAddresses).reduce((acc, key) => Math.max(acc, (reusedInputAddresses[key] || 0) + (reusedOutputAddresses[key] || 0)), 0) > 1;
@ -611,17 +564,17 @@ export class Common {
flags |= TransactionFlags.batch_payout;
}
if (this.isNonStandard(tx, height)) {
if (this.isNonStandard(tx)) {
flags |= TransactionFlags.nonstandard;
}
return Number(flags);
}
static classifyTransaction(tx: TransactionExtended, height?: number): TransactionClassified {
static classifyTransaction(tx: TransactionExtended): TransactionClassified {
let flags = 0;
try {
flags = Common.getTransactionFlags(tx, height);
flags = Common.getTransactionFlags(tx);
} catch (e) {
logger.warn('Failed to add classification flags to transaction: ' + (e instanceof Error ? e.message : e));
}
@ -632,8 +585,8 @@ export class Common {
};
}
static classifyTransactions(txs: TransactionExtended[], height?: number): TransactionClassified[] {
return txs.map(tx => Common.classifyTransaction(tx, height));
static classifyTransactions(txs: TransactionExtended[]): TransactionClassified[] {
return txs.map(Common.classifyTransaction);
}
static stripTransaction(tx: TransactionExtended): TransactionStripped {

View File

@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2';
class DatabaseMigration {
private static currentVersion = 95;
private static currentVersion = 81;
private queryTimeout = 3600_000;
private statisticsAddedIndexed = false;
private uniqueLogs: string[] = [];
@ -700,436 +700,6 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"');
await this.updateToSchemaVersion(81);
}
if (databaseSchemaVersion < 82 && isBitcoin === true && config.MEMPOOL.NETWORK === 'mainnet') {
await this.$fixBadV1AuditBlocks();
await this.updateToSchemaVersion(82);
}
if (databaseSchemaVersion < 83 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks` ADD first_seen datetime(6) DEFAULT NULL');
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);
}
// blocks pools-v2.json hash
if (databaseSchemaVersion < 95) {
let poolJsonSha = 'f737d86571d190cf1a1a3cf5fd86b33ba9624254';
const [poolJsonShaDb]: any[] = await DB.query(`SELECT string FROM state WHERE name = 'pools_json_sha'`);
if (poolJsonShaDb?.length > 0) {
poolJsonSha = poolJsonShaDb[0].string;
}
await this.$executeQuery(`ALTER TABLE blocks ADD definition_hash varchar(255) NOT NULL DEFAULT "${poolJsonSha}"`);
await this.$executeQuery('ALTER TABLE blocks ADD INDEX `definition_hash` (`definition_hash`)');
await this.updateToSchemaVersion(95);
}
}
/**
@ -1744,28 +1314,6 @@ class DatabaseMigration {
logger.warn(`Failed to migrate cpfp transaction data`);
}
}
private async $fixBadV1AuditBlocks(): Promise<void> {
const badBlocks = [
'000000000000000000011ad49227fc8c9ba0ca96ad2ebce41a862f9a244478dc',
'000000000000000000010ac1f68b3080153f2826ffddc87ceffdd68ed97d6960',
'000000000000000000024cbdafeb2660ae8bd2947d166e7fe15d1689e86b2cf7',
'00000000000000000002e1dbfbf6ae057f331992a058b822644b368034f87286',
'0000000000000000000019973b2778f08ad6d21e083302ff0833d17066921ebb',
];
for (const hash of badBlocks) {
try {
await this.$executeQuery(`
UPDATE blocks_audits
SET prioritized_txs = '[]'
WHERE hash = '${hash}'
`, true);
} catch (e) {
continue;
}
}
}
}
export default new DatabaseMigration();

View File

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

View File

@ -1,9 +1,6 @@
import config from '../../config';
import { Application, Request, Response } from 'express';
import channelsApi from './channels.api';
import { handleError } from '../../utils/api';
const TXID_REGEX = /^[a-f0-9]{64}$/i;
class ChannelsRoutes {
constructor() { }
@ -25,7 +22,7 @@ class ChannelsRoutes {
const channels = await channelsApi.$searchChannelsById(req.params.search);
res.json(channels);
} 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.json(channel);
} 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 : '';
if (index < -1) {
handleError(req, res, 400, 'Invalid index');
res.status(400).send('Invalid index');
return;
}
if (['open', 'active', 'closed'].includes(status) === false) {
handleError(req, res, 400, 'Invalid status');
res.status(400).send('Invalid status');
return;
}
@ -72,23 +69,20 @@ class ChannelsRoutes {
res.header('X-Total-Count', channelsCount.toString());
res.json(channels);
} 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> {
try {
if (!Array.isArray(req.query.txId)) {
handleError(req, res, 400, 'Not an array');
res.status(400).send('Not an array');
return;
}
const txIds: string[] = [];
for (const _txId in req.query.txId) {
if (typeof req.query.txId[_txId] === 'string') {
const txid = req.query.txId[_txId].toString();
if (TXID_REGEX.test(txid)) {
txIds.push(txid);
}
txIds.push(req.query.txId[_txId].toString());
}
}
const channels = await channelsApi.$getChannelsByTransactionId(txIds);
@ -113,7 +107,7 @@ class ChannelsRoutes {
res.json(result);
} 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.json(channels);
} 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.json(channels);
} 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 channelsApi from './channels.api';
import statisticsApi from './statistics.api';
import { handleError } from '../../utils/api';
class GeneralLightningRoutes {
constructor() { }
@ -29,7 +27,7 @@ class GeneralLightningRoutes {
channels: channels,
});
} 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.json(statistics);
} 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();
res.json(statistics);
} 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 DB from '../../database';
import { INodesRanking } from '../../mempool.interfaces';
import { handleError } from '../../utils/api';
class NodesRoutes {
constructor() { }
@ -32,7 +31,7 @@ class NodesRoutes {
const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search);
res.json(nodes);
} 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) {}
}
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(nodes);
} 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 {
const node = await nodesApi.$getNode(req.params.public_key);
if (!node) {
handleError(req, res, 404, 'Node not found');
res.status(404).send('Node not found');
return;
}
res.header('Pragma', 'public');
@ -204,7 +203,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(node);
} 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.json(statistics);
} 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 {
const node = await nodesApi.$getFeeHistogram(req.params.public_key);
if (!node) {
handleError(req, res, 404, 'Node not found');
res.status(404).send('Node not found');
return;
}
res.header('Pragma', 'public');
@ -232,7 +231,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(node);
} 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,
});
} 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.json(topCapacityNodes);
} 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.json(topCapacityNodes);
} 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.json(topCapacityNodes);
} 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.json(nodesPerAs);
} 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.json(worldNodes);
} 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) {
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;
}
@ -336,7 +335,7 @@ class NodesRoutes {
nodes: nodes,
});
} 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) {
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;
}
@ -363,7 +362,7 @@ class NodesRoutes {
nodes: nodes,
});
} 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.json(nodesPerAs);
} 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 elementsParser from './elements-parser';
import icons from './icons';
import { handleError } from '../../utils/api';
class LiquidRoutes {
public initRoutes(app: Application) {
@ -43,7 +42,7 @@ class LiquidRoutes {
res.setHeader('content-length', result.length);
res.send(result);
} 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) {
res.json(result);
} 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.json(pegs);
} 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.json(reserves);
} 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.json(currentSupply);
} 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.json(currentReserves);
} 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.json(auditStatus);
} 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.json(federationAddresses);
} 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.json(federationAddresses);
} 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.json(federationUtxos);
} 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.json(expiredUtxos);
} 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.json(federationUtxos);
} 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.json(emergencySpentUtxos);
} 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.json(emergencySpentUtxos);
} 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.json(recentPegs);
} 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.json(pegsVolume);
} 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.json(pegsCount);
} 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;
let hasBlockStack = blocks.length >= 8;
let stackWeight;
let feeStatsCalculator: OnlineFeeStatsCalculator | null = null;
let feeStatsCalculator: OnlineFeeStatsCalculator | void;
if (hasBlockStack) {
if (blockWeights && blockWeights[7] !== null) {
stackWeight = blockWeights[7];
@ -380,36 +380,28 @@ class MempoolBlocks {
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 memberTxid of cluster) {
const mempoolTx = mempool[memberTxid];
if (mempoolTx) {
// ugly micro-optimization to avoid allocating new arrays
ancestors.length = 0;
descendants.length = 0;
const ancestors: Ancestor[] = [];
const descendants: Ancestor[] = [];
let matched = false;
cluster.forEach(txid => {
ancestor = mempool[txid];
if (txid === memberTxid) {
matched = true;
} else {
if (!ancestor) {
if (!mempool[txid]) {
console.log('txid missing from mempool! ', txid, candidates?.txs[txid]);
return;
}
const relative = {
txid: txid,
fee: ancestor.fee,
weight: (ancestor.adjustedVsize * 4),
fee: mempool[txid].fee,
weight: (mempool[txid].adjustedVsize * 4),
};
if (matched) {
descendants.push(relative);
if (!mempoolTx.lastBoosted || (ancestor.firstSeen && ancestor.firstSeen > mempoolTx.lastBoosted)) {
mempoolTx.lastBoosted = ancestor.firstSeen;
}
mempoolTx.lastBoosted = Math.max(mempoolTx.lastBoosted || 0, mempool[txid].firstSeen || 0);
} else {
ancestors.push(relative);
}
@ -418,20 +410,7 @@ class MempoolBlocks {
if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) {
mempoolTx.cpfpDirty = true;
}
// ugly micro-optimization to avoid allocating new arrays or objects
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;
Object.assign(mempoolTx, {ancestors, descendants, bestDescendant: null, cpfpChecked: true});
}
}
}
@ -441,10 +420,7 @@ class MempoolBlocks {
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
// update this thread's mempool with the results
let mempoolTx: MempoolTransactionExtended;
let acceleration: Acceleration;
const mempoolBlocks: MempoolBlockWithTransactions[] = [];
for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
const block = blocks[blockIndex];
const mempoolBlocks: MempoolBlockWithTransactions[] = blocks.map((block, blockIndex) => {
let totalSize = 0;
let totalVsize = 0;
let totalWeight = 0;
@ -460,9 +436,8 @@ class MempoolBlocks {
}
}
for (let i = 0; i < block.length; i++) {
const txid = block[i];
if (txid in mempool) {
for (const txid of block) {
if (txid) {
mempoolTx = mempool[txid];
// save position in projected blocks
mempoolTx.position = {
@ -470,40 +445,30 @@ class MempoolBlocks {
vsize: totalVsize + (mempoolTx.vsize / 2),
};
if (txid in accelerations) {
acceleration = accelerations[txid];
if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
if (!mempoolTx.acceleration) {
mempoolTx.cpfpDirty = true;
}
mempoolTx.acceleration = true;
mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools;
mempoolTx.acceleratedAt = acceleration?.added;
mempoolTx.feeDelta = acceleration?.feeDelta;
for (const ancestor of mempoolTx.ancestors || []) {
if (!(ancestor.txid in mempool)) {
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;
const acceleration = accelerations[txid];
if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
if (!mempoolTx.acceleration) {
mempoolTx.cpfpDirty = true;
}
mempoolTx.acceleration = true;
mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools;
mempoolTx.acceleratedAt = acceleration?.added;
mempoolTx.feeDelta = acceleration?.feeDelta;
for (const ancestor of mempoolTx.ancestors || []) {
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;
}
delete mempoolTx.acceleration;
}
// online calculation of stack-of-blocks fee stats
@ -521,7 +486,7 @@ class MempoolBlocks {
}
}
}
mempoolBlocks[blockIndex] = this.dataToMempoolBlocks(
return this.dataToMempoolBlocks(
block,
transactions,
totalSize,
@ -529,7 +494,7 @@ class MempoolBlocks {
totalFees,
(hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) ? feeStatsCalculator.getRawFeeStats() : undefined,
);
};
});
if (saveResults) {
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 };
} = {};
// 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;
for (const ancestor of mempoolCache[acc.txid].ancestors || []) {
vsize += (ancestor.weight / 4);

View File

@ -10,7 +10,6 @@ import bitcoinClient from './bitcoin/bitcoin-client';
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import rbfCache from './rbf-cache';
import { Acceleration } from './services/acceleration';
import accelerationApi from './services/acceleration';
import redisCache from './redis-cache';
import blocks from './blocks';
@ -20,13 +19,12 @@ class Mempool {
private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {};
private mempoolCandidates: { [txid: string ]: boolean } = {};
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,
maxmempool: 300000000, mempoolminfee: Common.isLiquid() ? 0.00000100 : 0.00001000, minrelaytxfee: Common.isLiquid() ? 0.00000100 : 0.00001000 };
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void) | undefined;
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void) | undefined;
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 accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {};
@ -76,12 +74,12 @@ class Mempool {
}
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;
}
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 {
this.$asyncMempoolChangedCallback = fn;
}
@ -208,7 +206,7 @@ class Mempool {
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...`);
// warn if this run stalls the main loop for more than 2 minutes
@ -355,7 +353,7 @@ class Mempool {
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
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) {
hasChange = true;
}
@ -364,15 +362,12 @@ class Mempool {
const candidatesChanged = candidates?.added?.length || candidates?.removed?.length;
this.recentlyDeleted.unshift(deletedTransactions);
this.recentlyDeleted.length = Math.min(this.recentlyDeleted.length, 10); // truncate to the last 10 mempool updates
if (this.mempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length)) {
this.mempoolChangedCallback(this.mempoolCache, newTransactions, this.recentlyDeleted, accelerationDelta);
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta);
}
if (this.$asyncMempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length || candidatesChanged)) {
if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length || candidatesChanged)) {
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');
}
@ -400,11 +395,58 @@ class Mempool {
return this.accelerations;
}
public updateAccelerations(newAccelerationMap: Record<string, Acceleration>): string[] {
public $updateAccelerations(newAccelerations: Acceleration[]): string[] {
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;
return accelerationDelta;
return changed;
} catch (e: any) {
logger.debug(`Failed to update accelerations: ` + (e instanceof Error ? e.message : e));
return [];
@ -499,7 +541,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) {
if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) {
// Store replaced transactions

View File

@ -10,7 +10,6 @@ import mining from "./mining";
import PricesRepository from '../../repositories/PricesRepository';
import AccelerationRepository from '../../repositories/AccelerationRepository';
import accelerationApi from '../services/acceleration';
import { handleError } from '../../utils/api';
class MiningRoutes {
public initRoutes(app: Application) {
@ -54,12 +53,12 @@ class MiningRoutes {
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
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;
}
const timestamp = parseInt(req.query.timestamp as string, 10) || 0;
const currency = req.query.currency as string;
let response;
if (timestamp && currency) {
response = await PricesRepository.$getNearestHistoricalPrice(timestamp, currency);
@ -72,7 +71,7 @@ class MiningRoutes {
}
res.status(200).send(response);
} 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);
} catch (e) {
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 {
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);
} catch (e) {
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 {
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);
}
} 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.json(stats);
} 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.json(hashrates);
} 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);
} catch (e) {
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 {
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) {
let currentHashrate = 0, currentDifficulty = 0;
try {
currentHashrate = await bitcoinClient.getNetworkHashPs(1008);
currentHashrate = await bitcoinClient.getNetworkHashPs();
currentDifficulty = await bitcoinClient.getDifficulty();
} catch (e) {
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate and difficulty');
@ -204,7 +203,7 @@ class MiningRoutes {
currentDifficulty: currentDifficulty,
});
} 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.json(blockFees);
} 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.json(blockFees);
} 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.json(blockRewards);
} 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.json(blockFeeRates);
} 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
});
} 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.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment]));
} 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.json(response);
} 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.json(blocksHealth.map(health => [health.time, health.height, health.match_rate]));
} 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);
if (!audit) {
handleError(req, res, 204, `This block has not been audited.`);
res.status(204).send(`This block has not been audited.`);
return;
}
@ -336,7 +335,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
res.json(audit);
} 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.json(result);
} 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.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15));
} 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.json(audit || 'null');
} 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
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;
}
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug));
} 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.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
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;
}
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height));
} 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
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;
}
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval));
} 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
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;
}
res.status(200).send(await AccelerationRepository.$getAccelerationTotals(<string>req.query.pool, <string>req.query.interval));
} 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
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;
}
res.status(200).send(Object.values(accelerationApi.getAccelerations() || {}));
res.status(200).send(accelerationApi.accelerations || []);
} 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);
res.status(200).send();
} 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;
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 {
poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h);
poolsStatistics['lastEstimatedHashrate3d'] = await bitcoinClient.getNetworkHashPs(totalBlock3d);
poolsStatistics['lastEstimatedHashrate1w'] = await bitcoinClient.getNetworkHashPs(totalBlock1w);
} catch (e) {
poolsStatistics['lastEstimatedHashrate'] = 0;
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate', logger.tags.mining);

View File

@ -19,6 +19,15 @@ class PoolsParser {
'addresses': '[]',
'slug': 'unknown'
};
private uniqueLogs: string[] = [];
private uniqueLog(loggerFunction: any, msg: string): void {
if (this.uniqueLogs.includes(msg)) {
return;
}
this.uniqueLogs.push(msg);
loggerFunction(msg);
}
public setMiningPools(pools): void {
for (const pool of pools) {

View File

@ -1,15 +1,10 @@
import { Application, Request, Response } from 'express';
import config from '../../config';
import pricesUpdater from '../../tasks/price-updater';
import logger from '../../logger';
import PricesRepository from '../../repositories/PricesRepository';
class PricesRoutes {
public initRoutes(app: Application): void {
app
.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))
;
app.get(config.MEMPOOL.API_URL_PREFIX + 'prices', this.$getCurrentPrices.bind(this));
}
private $getCurrentPrices(req: Request, res: Response): void {
@ -19,23 +14,6 @@ class PricesRoutes {
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();

View File

@ -44,22 +44,6 @@ interface CacheEvent {
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 {
private replacedBy: 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);
}
/**
* Low level cache operations
*/
private addTx(txid: string, tx: MempoolTransactionExtended): void {
this.txs.set(txid, tx);
this.cacheQueue.push({ op: CacheOp.Add, type: 'tx', txid });
@ -112,18 +92,8 @@ class RbfCache {
this.cacheQueue.push({ op: CacheOp.Remove, type: 'exp', txid });
}
/**
* Basic data structure operations
* must uphold tree invariants
*/
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
if ( !newTxExtended
|| !replaced?.length
|| this.txs.has(newTxExtended.txid)
|| !(replaced.some(tx => !this.replacedBy.has(tx.txid)))
) {
if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
return;
}
@ -144,10 +114,6 @@ class RbfCache {
if (!replacedTx.rbf) {
txFullRbf = true;
}
if (this.replacedBy.has(replacedTx.txid)) {
// should never happen
continue;
}
this.replacedBy.set(replacedTx.txid, newTx.txid);
if (this.treeMap.has(replacedTx.txid)) {
const treeId = this.treeMap.get(replacedTx.txid);
@ -174,47 +140,18 @@ class RbfCache {
}
}
newTx.fullRbf = txFullRbf;
const treeId = replacedTrees[0].tx.txid;
const newTree = {
tx: newTx,
time: newTime,
fullRbf: treeFullRbf,
replaces: replacedTrees
};
this.addTree(newTree.tx.txid, newTree);
this.updateTreeMap(newTree.tx.txid, newTree);
this.addTree(treeId, newTree);
this.updateTreeMap(treeId, newTree);
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 {
return this.txs.has(txId);
}
@ -295,6 +232,32 @@ class RbfCache {
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?
public isFullRbf(txid: string): boolean {
const treeId = this.treeMap.get(txid);
@ -308,10 +271,6 @@ class RbfCache {
return tree?.fullRbf;
}
/**
* Cache maintenance & utility functions
*/
private cleanup(): void {
const now = Date.now();
for (const txid of this.expiring.keys()) {
@ -340,6 +299,10 @@ class RbfCache {
for (const tx of (replaces || [])) {
// recursively remove prior versions from the cache
this.replacedBy.delete(tx);
// if this is the id of a tree, remove that too
if (this.treeMap.get(tx) === tx) {
this.removeTree(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 {
txs.forEach(txEntry => {
this.txs.set(txEntry.value.txid, txEntry.value);
});
this.staleCount = 0;
for (const deflatedTree of trees.sort((a, b) => Object.keys(b).length - Object.keys(a).length)) {
const tree = 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);
}
}
for (const deflatedTree of trees) {
await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
}
expiring.forEach(expiringEntry => {
if (this.txs.has(expiringEntry.key)) {
@ -429,31 +385,6 @@ class RbfCache {
}
});
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();
logger.debug(`loaded ${txs.length} txs, ${trees.length} trees into rbf cache, ${expiring.length} due to expire, ${this.staleCount} were stale`);
this.cleanup();
@ -495,12 +426,6 @@ class RbfCache {
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
for (const childId of treeInfo.replaces) {
const replaced = await this.importTree(mempool, root, childId, deflated, txs, mined);
@ -532,6 +457,10 @@ class RbfCache {
fullRbf: treeInfo.fullRbf,
replaces,
};
this.treeMap.set(txid, root);
if (root === txid) {
this.addTree(root, tree);
}
return tree;
}
@ -582,7 +511,6 @@ class RbfCache {
processTxs(txs);
}
// evict missing transactions
for (const txid of txids) {
if (!found[txid]) {
this.evict(txid, false);

View File

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

View File

@ -1,10 +1,7 @@
import { WebSocket } from 'ws';
import config from '../../config';
import logger from '../../logger';
import { BlockExtended } from '../../mempool.interfaces';
import axios from 'axios';
import mempool from '../mempool';
import websocketHandler from '../websocket-handler';
type MyAccelerationStatus = 'requested' | 'accelerating' | 'done';
@ -40,23 +37,14 @@ export interface AccelerationHistory {
};
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 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: Record<string, Acceleration> = {};
private _accelerations: Acceleration[] | null = null;
private lastPoll = 0;
private lastPing = Date.now();
private lastPong = Date.now();
private forcePoll = false;
private myAccelerations: Record<string, { status: MyAccelerationStatus, added: number, acceleration?: Acceleration }> = {};
public constructor() {}
public getAccelerations(): Record<string, Acceleration> {
public get accelerations(): Acceleration[] | null {
return this._accelerations;
}
@ -84,18 +72,11 @@ class AccelerationApi {
}
}
public async $updateAccelerations(): Promise<Record<string, Acceleration> | null> {
if (this.useWebsocket && this.websocketConnected) {
return this._accelerations;
}
public async $updateAccelerations(): Promise<Acceleration[] | null> {
if (!this.onDemandPollingEnabled) {
const accelerations = await this.$fetchAccelerations();
if (accelerations) {
const latestAccelerations = {};
for (const acc of accelerations) {
latestAccelerations[acc.txid] = acc;
}
this._accelerations = latestAccelerations;
this._accelerations = accelerations;
return this._accelerations;
}
} else {
@ -104,7 +85,7 @@ class AccelerationApi {
return null;
}
private async $updateAccelerationsOnDemand(): Promise<Record<string, Acceleration> | null> {
private async $updateAccelerationsOnDemand(): Promise<Acceleration[] | null> {
const shouldUpdate = this.forcePoll
|| this.countMyAccelerationsWithStatus('requested') > 0
|| (this.countMyAccelerationsWithStatus('accelerating') > 0 && this.lastPoll < (Date.now() - (10 * 60 * 1000)));
@ -139,11 +120,7 @@ class AccelerationApi {
}
}
const latestAccelerations = {};
for (const acc of Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[]) {
latestAccelerations[acc.txid] = acc;
}
this._accelerations = latestAccelerations;
this._accelerations = Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[];
return this._accelerations;
}
@ -175,148 +152,6 @@ class AccelerationApi {
}
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();

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

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

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 feePerVbytes = (transaction.fee || 0) / fractionalVsize;
const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize;
const effectiveFeePerVsize = transaction['effectiveFeePerVsize'] || adjustedFeePerVsize || feePerVbytes;
const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, {
order: this.txidToOrdering(transaction.txid),
vsize,
@ -129,7 +128,7 @@ class TransactionUtils {
sigops,
feePerVsize: feePerVbytes,
adjustedFeePerVsize: adjustedFeePerVsize,
effectiveFeePerVsize: effectiveFeePerVsize,
effectiveFeePerVsize: adjustedFeePerVsize,
});
if (!transactionExtended?.status?.confirmed && !transactionExtended.firstSeen) {
transactionExtended.firstSeen = Math.round((Date.now() / 1000));
@ -339,87 +338,6 @@ class TransactionUtils {
const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2;
return witness[positionOfScript];
}
// calculate the most parsimonious set of prioritizations given a list of block transactions
// (i.e. the most likely prioritizations and deprioritizations)
public identifyPrioritizedTransactions(transactions: any[], rateKey: string): { prioritized: string[], deprioritized: string[] } {
// find the longest increasing subsequence of transactions
// (adapted from https://en.wikipedia.org/wiki/Longest_increasing_subsequence#Efficient_algorithms)
// should be O(n log n)
const X = transactions.slice(1).reverse().map((tx) => ({ txid: tx.txid, rate: tx[rateKey] })); // standard block order is by *decreasing* effective fee rate, but we want to iterate in increasing order (and skip the coinbase)
if (X.length < 2) {
return { prioritized: [], deprioritized: [] };
}
const N = X.length;
const P: number[] = new Array(N);
const M: number[] = new Array(N + 1);
M[0] = -1; // undefined so can be set to any value
let L = 0;
for (let i = 0; i < N; i++) {
// Binary search for the smallest positive l ≤ L
// such that X[M[l]].effectiveFeePerVsize > X[i].effectiveFeePerVsize
let lo = 1;
let hi = L + 1;
while (lo < hi) {
const mid = lo + Math.floor((hi - lo) / 2); // lo <= mid < hi
if (X[M[mid]].rate > X[i].rate) {
hi = mid;
} else { // if X[M[mid]].effectiveFeePerVsize < X[i].effectiveFeePerVsize
lo = mid + 1;
}
}
// After searching, lo == hi is 1 greater than the
// length of the longest prefix of X[i]
const newL = lo;
// The predecessor of X[i] is the last index of
// the subsequence of length newL-1
P[i] = M[newL - 1];
M[newL] = i;
if (newL > L) {
// If we found a subsequence longer than any we've
// found yet, update L
L = newL;
}
}
// Reconstruct the longest increasing subsequence
// It consists of the values of X at the L indices:
// ..., P[P[M[L]]], P[M[L]], M[L]
const LIS: any[] = new Array(L);
let k = M[L];
for (let j = L - 1; j >= 0; j--) {
LIS[j] = X[k];
k = P[k];
}
const lisMap = new Map<string, number>();
LIS.forEach((tx, index) => lisMap.set(tx.txid, index));
const prioritized: string[] = [];
const deprioritized: string[] = [];
let lastRate = X[0].rate;
for (const tx of X) {
if (lisMap.has(tx.txid)) {
lastRate = tx.rate;
} else {
if (Math.abs(tx.rate - lastRate) < 0.1) {
// skip if the rate is almost the same as the previous transaction
} else if (tx.rate <= lastRate) {
prioritized.push(tx.txid);
} else {
deprioritized.push(tx.txid);
}
}
}
return { prioritized, deprioritized };
}
}
export default new TransactionUtils();

View File

@ -16,19 +16,16 @@ import transactionUtils from './transaction-utils';
import rbfCache, { ReplacementInfo } from './rbf-cache';
import difficultyAdjustment from './difficulty-adjustment';
import feeApi from './fee-api';
import BlocksRepository from '../repositories/BlocksRepository';
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
import Audit from './audit';
import priceUpdater from '../tasks/price-updater';
import { ApiPrice } from '../repositories/PricesRepository';
import { Acceleration } from './services/acceleration';
import accelerationApi from './services/acceleration';
import mempool from './mempool';
import statistics from './statistics/statistics';
import accelerationRepository from '../repositories/AccelerationRepository';
import bitcoinApi from './bitcoin/bitcoin-api-factory';
import walletApi from './services/wallets';
interface AddressTransactions {
mempool: MempoolTransactionExtended[],
@ -37,8 +34,6 @@ interface AddressTransactions {
}
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import { calculateMempoolTxCpfp } from './cpfp';
import { getRecentFirstSeen } from '../utils/file-read';
import stratumApi, { StratumJob } from './services/stratum';
// valid 'want' subscriptions
const wantable = [
@ -62,8 +57,6 @@ class WebsocketHandler {
private lastRbfSummary: ReplacementInfo[] | null = null;
private mempoolSequence: number = 0;
private accelerations: Record<string, Acceleration> = {};
constructor() { }
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 (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-asset'])) {
client['track-asset'] = parsedMessage['track-asset'];
@ -404,16 +389,6 @@ class WebsocketHandler {
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) {
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 {
if (!this.webSocketServers.length) {
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,
newTransactions: MempoolTransactionExtended[], recentlyDeletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[],
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[],
candidates?: GbtCandidates): Promise<void> {
if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server have been set');
@ -599,8 +529,6 @@ class WebsocketHandler {
this.printLogs();
const deletedTransactions = recentlyDeletedTransactions.length ? recentlyDeletedTransactions[0] : [];
const transactionIds = (memPool.limitGBT && candidates) ? Object.keys(candidates?.txs || {}) : Object.keys(newMempool);
let added = newTransactions;
let removed = deletedTransactions;
@ -619,9 +547,9 @@ class WebsocketHandler {
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
const mempoolInfo = memPool.getMempoolInfo();
const vBytesPerSecond = memPool.getVBytesPerSecond();
const rbfTransactions = Common.findRbfTransactions(newTransactions, recentlyDeletedTransactions.flat());
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
const da = difficultyAdjustment.getDifficultyAdjustment();
const accelerations = accelerationApi.getAccelerations();
const accelerations = memPool.getAccelerations();
memPool.handleRbfTransactions(rbfTransactions);
const rbfChanges = rbfCache.getRbfChanges();
let rbfReplacements;
@ -650,7 +578,7 @@ class WebsocketHandler {
const replacedTransactions: { replaced: string, by: TransactionExtended }[] = [];
for (const tx of newTransactions) {
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 });
}
}
@ -729,13 +657,10 @@ class WebsocketHandler {
const addressCache = this.makeAddressCache(newTransactions);
const removedAddressCache = this.makeAddressCache(deletedTransactions);
const websocketAccelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, accelerations);
this.accelerations = accelerations;
// pre-compute acceleration delta
const accelerationUpdate = {
added: websocketAccelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
removed: websocketAccelerationDelta.filter(txid => !accelerations[txid]),
added: accelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
removed: accelerationDelta.filter(txid => !accelerations[txid]),
};
// TODO - Fix indentation after PR is merged
@ -1022,7 +947,7 @@ class WebsocketHandler {
await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions));
const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
memPool.handleRbfTransactions(rbfTransactions);
memPool.handleMinedRbfTransactions(rbfTransactions);
memPool.removeFromSpendMap(transactions);
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 } = {};
// Update mempool to remove transactions included in the new block
@ -1174,9 +1091,6 @@ class WebsocketHandler {
replaced: replacedTransactions,
};
// check for wallet transactions
const walletTransactions = config.WALLETS.ENABLED ? walletApi.processBlock(block, transactions) : [];
const responseCache = { ...this.socketData };
function getCachedResponse(key, data): string {
if (!responseCache[key]) {
@ -1381,11 +1295,6 @@ class WebsocketHandler {
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) {
client.send(this.serializeResponse(response));
}
@ -1395,23 +1304,6 @@ class WebsocketHandler {
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
// and zips it together into a valid JSON object
private serializeResponse(response): string {

View File

@ -32,7 +32,6 @@ interface IConfig {
AUTOMATIC_POOLS_UPDATE: boolean;
POOLS_JSON_URL: string,
POOLS_JSON_TREE_URL: string,
POOLS_UPDATE_DELAY: number,
AUDIT: boolean;
RUST_GBT: boolean;
LIMIT_GBT: boolean;
@ -86,7 +85,6 @@ interface IConfig {
TIMEOUT: number;
COOKIE: boolean;
COOKIE_PATH: string;
DEBUG_LOG_PATH: string;
};
SECOND_CORE_RPC: {
HOST: string;
@ -162,14 +160,6 @@ interface IConfig {
PAID: boolean;
API_KEY: string;
},
WALLETS: {
ENABLED: boolean;
WALLETS: string[];
},
STRATUM: {
ENABLED: boolean;
API: string;
}
}
const defaults: IConfig = {
@ -202,9 +192,8 @@ const defaults: IConfig = {
'AUTOMATIC_POOLS_UPDATE': false,
'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json',
'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
'POOLS_UPDATE_DELAY': 604800, // in seconds, default is one week
'AUDIT': false,
'RUST_GBT': true,
'RUST_GBT': false,
'LIMIT_GBT': false,
'CPFP_INDEXING': false,
'MAX_BLOCKS_BULK_QUERY': 0,
@ -236,8 +225,7 @@ const defaults: IConfig = {
'PASSWORD': 'mempool',
'TIMEOUT': 60000,
'COOKIE': false,
'COOKIE_PATH': '/bitcoin/.cookie',
'DEBUG_LOG_PATH': '',
'COOKIE_PATH': '/bitcoin/.cookie'
},
'SECOND_CORE_RPC': {
'HOST': '127.0.0.1',
@ -332,14 +320,6 @@ const defaults: IConfig = {
'PAID': false,
'API_KEY': '',
},
'WALLETS': {
'ENABLED': false,
'WALLETS': [],
},
'STRATUM': {
'ENABLED': false,
'API': 'http://localhost:1234',
}
};
class Config implements IConfig {
@ -361,8 +341,6 @@ class Config implements IConfig {
MEMPOOL_SERVICES: IConfig['MEMPOOL_SERVICES'];
REDIS: IConfig['REDIS'];
FIAT_PRICE: IConfig['FIAT_PRICE'];
WALLETS: IConfig['WALLETS'];
STRATUM: IConfig['STRATUM'];
constructor() {
const configs = this.merge(configFromFile, defaults);
@ -384,8 +362,6 @@ class Config implements IConfig {
this.MEMPOOL_SERVICES = configs.MEMPOOL_SERVICES;
this.REDIS = configs.REDIS;
this.FIAT_PRICE = configs.FIAT_PRICE;
this.WALLETS = configs.WALLETS;
this.STRATUM = configs.STRATUM;
}
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 liquidRoutes from './api/liquid/liquid.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 forensicsService from './tasks/lightning/forensics.service';
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 aboutRoutes from './api/about.routes';
import mempoolBlocks from './api/mempool-blocks';
import walletApi from './api/services/wallets';
import stratumApi from './api/services/stratum';
class Server {
private wss: WebSocket.Server | undefined;
@ -214,8 +211,6 @@ class Server {
}
});
}
poolsUpdater.$startService();
}
async runMainUpdateLoop(): Promise<void> {
@ -234,17 +229,13 @@ class Server {
const newMempool = await bitcoinApi.$getRawMempool();
const minFeeMempool = memPool.limitGBT ? await bitcoinSecondClient.getRawMemPool() : null;
const minFeeTip = memPool.limitGBT ? await bitcoinSecondClient.getBlockCount() : -1;
const latestAccelerations = await accelerationApi.$updateAccelerations();
const newAccelerations = await accelerationApi.$updateAccelerations();
const numHandledBlocks = await blocks.$updateBlocks();
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1);
if (numHandledBlocks === 0) {
await memPool.$updateMempool(newMempool, latestAccelerations, minFeeMempool, minFeeTip, pollRate);
await memPool.$updateMempool(newMempool, newAccelerations, minFeeMempool, minFeeTip, pollRate);
}
indexer.$run();
if (config.WALLETS.ENABLED) {
// might take a while, so run in the background
walletApi.$syncWallets();
}
if (config.FIAT_PRICE.ENABLED) {
priceUpdater.$run();
}
@ -319,18 +310,11 @@ class Server {
priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
}
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
accelerationApi.connectWebsocket();
if (config.STRATUM.ENABLED) {
stratumApi.connectWebsocket();
}
}
setUpHttpApiRoutes(): void {
bitcoinRoutes.initRoutes(this.app);
if (config.MEMPOOL.OFFICIAL) {
bitcoinCoreRoutes.initRoutes(this.app);
}
bitcoinCoreRoutes.initRoutes(this.app);
pricesRoutes.initRoutes(this.app);
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && config.MEMPOOL.ENABLED) {
statisticsRoutes.initRoutes(this.app);
@ -349,9 +333,6 @@ class Server {
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
accelerationRoutes.initRoutes(this.app);
}
if (config.WALLETS.ENABLED) {
servicesRoutes.initRoutes(this.app);
}
if (!config.MEMPOOL.OFFICIAL) {
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`
name: string;
slug: string;
minerNames: string[] | null;
};
avgFee: number;
avgFeeRate: number;
@ -320,13 +319,10 @@ export interface BlockExtension {
segwitTotalSize: number;
segwitTotalWeight: number;
header: string;
firstSeen: number | null;
utxoSetChange: number;
// Requires coinstatsindex, will be set to NULL otherwise
utxoSetSize: number | null;
totalInputAmt: number | null;
// pools-v2.json git hash
definitionHash: string | undefined;
}
/**

View File

@ -132,12 +132,11 @@ class BlocksAuditRepositories {
firstSeen = tx.time;
}
});
const wasSeen = blockAudit.version === 1 ? !blockAudit.unseenTxs.includes(txid) : (isExpected || isPrioritized || isAccelerated);
return {
seen: wasSeen,
seen: isExpected || isPrioritized || isAccelerated,
expected: isExpected,
added: isAdded && (blockAudit.version === 0 || !wasSeen),
added: isAdded,
prioritized: isPrioritized,
conflict: isConflict,
accelerated: isAccelerated,

View File

@ -14,8 +14,6 @@ import chainTips from '../api/chain-tips';
import blocks from '../api/blocks';
import BlocksAuditsRepository from './BlocksAuditsRepository';
import transactionUtils from '../api/transaction-utils';
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
import poolsUpdater from '../tasks/pools-updater';
interface DatabaseBlock {
id: string;
@ -58,7 +56,6 @@ interface DatabaseBlock {
utxoSetChange: number;
utxoSetSize: number;
totalInputAmt: number;
firstSeen: number;
}
const BLOCK_DB_FIELDS = `
@ -101,8 +98,7 @@ const BLOCK_DB_FIELDS = `
blocks.header,
blocks.utxoset_change AS utxoSetChange,
blocks.utxoset_size AS utxoSetSize,
blocks.total_input_amt AS totalInputAmt,
UNIX_TIMESTAMP(blocks.first_seen) AS firstSeen
blocks.total_input_amt AS totalInputAmt
`;
class BlocksRepository {
@ -115,16 +111,16 @@ class BlocksRepository {
try {
const query = `INSERT INTO blocks(
height, hash, blockTimestamp, size,
weight, tx_count, coinbase_raw, difficulty,
pool_id, fees, fee_span, median_fee,
reward, version, bits, nonce,
merkle_root, previous_block_hash, avg_fee, avg_fee_rate,
median_timestamp, header, coinbase_address, coinbase_addresses,
coinbase_signature, utxoset_size, utxoset_change, avg_tx_size,
total_inputs, total_outputs, total_input_amt, total_output_amt,
fee_percentiles, segwit_total_txs, segwit_total_size, segwit_total_weight,
median_fee_amt, coinbase_signature_ascii, definition_hash
height, hash, blockTimestamp, size,
weight, tx_count, coinbase_raw, difficulty,
pool_id, fees, fee_span, median_fee,
reward, version, bits, nonce,
merkle_root, previous_block_hash, avg_fee, avg_fee_rate,
median_timestamp, header, coinbase_address, coinbase_addresses,
coinbase_signature, utxoset_size, utxoset_change, avg_tx_size,
total_inputs, total_outputs, total_input_amt, total_output_amt,
fee_percentiles, segwit_total_txs, segwit_total_size, segwit_total_weight,
median_fee_amt, coinbase_signature_ascii
) VALUE (
?, ?, FROM_UNIXTIME(?), ?,
?, ?, ?, ?,
@ -135,7 +131,7 @@ class BlocksRepository {
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?
?, ?
)`;
const poolDbId = await PoolsRepository.$getPoolByUniqueId(block.extras.pool.id);
@ -182,7 +178,6 @@ class BlocksRepository {
block.extras.segwitTotalWeight,
block.extras.medianFeeAmt,
truncatedCoinbaseSignatureAscii,
poolsUpdater.currentSha
];
await DB.query(query, params);
@ -503,7 +498,7 @@ class BlocksRepository {
}
query += ` ORDER BY height DESC
LIMIT 100`;
LIMIT 10`;
try {
const [rows]: any[] = await DB.query(query, params);
@ -1015,9 +1010,9 @@ class BlocksRepository {
public async $savePool(id: string, poolId: number): Promise<void> {
try {
await DB.query(`
UPDATE blocks SET pool_id = ?, definition_hash = ?
UPDATE blocks SET pool_id = ?
WHERE hash = ?`,
[poolId, poolsUpdater.currentSha, id]
[poolId, id]
);
} catch (e) {
logger.err(`Cannot update block pool. Reason: ` + (e instanceof Error ? e.message : e));
@ -1025,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
* must provide the correct field into dbBlk object param
@ -1077,7 +1054,6 @@ class BlocksRepository {
id: dbBlk.poolId,
name: dbBlk.poolName,
slug: dbBlk.poolSlug,
minerNames: null,
};
extras.avgFee = dbBlk.avgFee;
extras.avgFeeRate = dbBlk.avgFeeRate;
@ -1100,7 +1076,6 @@ class BlocksRepository {
extras.utxoSetSize = dbBlk.utxoSetSize;
extras.totalInputAmt = dbBlk.totalInputAmt;
extras.virtualSize = dbBlk.weight / 4.0;
extras.firstSeen = dbBlk.firstSeen;
// Re-org can happen after indexing so we need to always get the
// latest state from core
@ -1131,7 +1106,7 @@ class BlocksRepository {
let summaryVersion = 0;
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(dbBlk.id)).map(tx => transactionUtils.extendTransaction(tx));
summary = blocks.summarizeBlockTransactions(dbBlk.id, dbBlk.height, txs);
summary = blocks.summarizeBlockTransactions(dbBlk.id, txs);
summaryVersion = 1;
} else {
// Call Core RPC
@ -1148,10 +1123,6 @@ class BlocksRepository {
}
}
if (extras.pool.name === 'OCEAN') {
extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw);
}
blk.extras = <BlockExtension>extras;
return <BlockExtended>blk;
}

View File

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

View File

@ -6,30 +6,16 @@ import backendInfo from '../api/backend-info';
import logger from '../logger';
import { SocksProxyAgent } from 'socks-proxy-agent';
import * as https from 'https';
import { Common } from '../api/common';
/**
* Maintain the most recent version of pools-v2.json
*/
class PoolsUpdater {
tag = 'PoolsUpdater';
lastRun: number = 0;
currentSha: string | null = null;
poolsUrl: string = config.MEMPOOL.POOLS_JSON_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> {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false ||
config.MEMPOOL.ENABLED === false
@ -37,8 +23,11 @@ class PoolsUpdater {
return;
}
const oneWeek = 604800;
const oneDay = 86400;
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;
}
@ -54,7 +43,7 @@ class PoolsUpdater {
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) {
return;
}
@ -64,16 +53,16 @@ class PoolsUpdater {
config.MEMPOOL.AUTOMATIC_POOLS_UPDATE !== true && // Automatic pools update is disabled
!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.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.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`);
return;
}
const network = config.SOCKS5PROXY.ENABLED ? 'tor' : 'clearnet';
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 {
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);
if (poolsJson === undefined) {
@ -82,24 +71,24 @@ class PoolsUpdater {
poolsParser.setMiningPools(poolsJson);
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;
}
try {
await DB.query('START TRANSACTION;');
await this.updateDBSha(githubSha);
await poolsParser.migratePoolsJson();
await this.updateDBSha(githubSha);
await DB.query('COMMIT;');
} 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;');
}
logger.info(`Mining pools-v2.json (${githubSha}) import completed`, this.tag);
logger.info(`Mining pools-v2.json (${githubSha}) import completed`);
} catch (e) {
this.lastRun = now - 600; // Try again in 10 minutes
logger.err(`PoolsUpdater failed. Will try again in 10 minutes. Exception: ${JSON.stringify(e)}`, this.tag);
this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week
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(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`);
} 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);
}
}
}
@ -121,12 +110,12 @@ class PoolsUpdater {
/**
* Fetch our latest pools-v2.json sha from the db
*/
public async getShaFromDb(): Promise<string | null> {
private async getShaFromDb(): Promise<string | null> {
try {
const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"');
return (rows.length > 0 ? rows[0].string : null);
} 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;
}
}
@ -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;
}
@ -197,7 +186,7 @@ class PoolsUpdater {
}
return data.data;
} 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++;
}
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

@ -158,7 +158,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
if (!opN) {
return;
}
if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
if (!opN.startsWith('OP_PUSHNUM_')) {
return;
}
const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10);
@ -178,7 +178,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
if (!opM) {
return;
}
if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
if (!opM.startsWith('OP_PUSHNUM_')) {
return;
}
const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10);
@ -200,28 +200,4 @@ export function getVarIntLength(n: number): number {
} else {
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,
"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_UPDATE_DELAY": 604800,
"CPFP_INDEXING": false,
"MAX_BLOCKS_BULK_QUERY": 0,
"DISK_CACHE_BLOCK_INTERVAL": 6,
@ -141,7 +140,6 @@ Corresponding `docker-compose.yml` overrides:
MEMPOOL_AUTOMATIC_POOLS_UPDATE: ""
MEMPOOL_POOLS_JSON_URL: ""
MEMPOOL_POOLS_JSON_TREE_URL: ""
MEMPOOL_POOLS_UPDATE_DELAY: ""
MEMPOOL_CPFP_INDEXING: ""
MEMPOOL_MAX_BLOCKS_BULK_QUERY: ""
MEMPOOL_DISK_CACHE_BLOCK_INTERVAL: ""

View File

@ -36,7 +36,6 @@
"ALLOW_UNREACHABLE": __MEMPOOL_ALLOW_UNREACHABLE__,
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
"POOLS_UPDATE_DELAY": __MEMPOOL_POOLS_UPDATE_DELAY__,
"PRICE_UPDATES_PER_HOUR": __MEMPOOL_PRICE_UPDATES_PER_HOUR__,
"MAX_TRACKED_ADDRESSES": __MEMPOOL_MAX_TRACKED_ADDRESSES__
},
@ -47,8 +46,7 @@
"PASSWORD": "__CORE_RPC_PASSWORD__",
"TIMEOUT": __CORE_RPC_TIMEOUT__,
"COOKIE": __CORE_RPC_COOKIE__,
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__",
"DEBUG_LOG_PATH": "__CORE_RPC_DEBUG_LOG_PATH__"
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__"
},
"ELECTRUM": {
"HOST": "__ELECTRUM_HOST__",
@ -148,10 +146,6 @@
"API": "__MEMPOOL_SERVICES_API__",
"ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__
},
"STRATUM": {
"ENABLED": __STRATUM_ENABLED__,
"API": "__STRATUM_API__"
},
"REDIS": {
"ENABLED": __REDIS_ENABLED__,
"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_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_UPDATE_DELAY__=${MEMPOOL_POOLS_UPDATE_DELAY:=604800}
__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_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
__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_COOKIE__=${CORE_RPC_COOKIE:=false}
__CORE_RPC_COOKIE_PATH__=${CORE_RPC_COOKIE_PATH:=""}
__CORE_RPC_DEBUG_LOG_PATH__=${CORE_RPC_DEBUG_LOG_PATH:=""}
# ELECTRUM
__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_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
# STRATUM
__STRATUM_ENABLED__=${STRATUM_ENABLED:=false}
__STRATUM_API__=${STRATUM_API:="http://localhost:1234"}
# REDIS
__REDIS_ENABLED__=${REDIS_ENABLED:=false}
__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_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_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_RUST_GBT__!${__MEMPOOL_RUST_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_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_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_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_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
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

View File

@ -11,14 +11,10 @@ services:
stop_grace_period: 1m
command: "./wait-for db:3306 --timeout=720 -- nginx -g 'daemon off;'"
ports:
- 8080:8080
- 80:8080
api:
environment:
MEMPOOL_BACKEND: "electrum"
ELECTRUM_HOST: "172.27.0.1"
ELECTRUM_PORT: "50001"
ELECTRUM_TLS_ENABLED: "false"
MEMPOOL_BACKEND: "none"
CORE_RPC_HOST: "172.27.0.1"
CORE_RPC_PORT: "8332"
CORE_RPC_USERNAME: "mempool"

View File

@ -45,7 +45,6 @@ __SERVICES_API__=${SERVICES_API:=https://mempool.space/api/v1/services}
__PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false}
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
__ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false}
__STRATUM_ENABLED__=${STRATUM_ENABLED:=false}
# Export as environment variables to be used by envsubst
export __MAINNET_ENABLED__
@ -77,7 +76,6 @@ export __SERVICES_API__
export __PUBLIC_ACCELERATIONS__
export __HISTORICAL_PRICE__
export __ADDITIONAL_CURRENCIES__
export __STRATUM_ENABLED__
folder=$(find /var/www/mempool -name "config.js" | xargs dirname)
echo ${folder}

View File

@ -13,8 +13,8 @@ localhostIP="127.0.0.1"
cp ./docker/frontend/* ./frontend
cp ./nginx.conf ./frontend/
cp ./nginx-mempool.conf ./frontend/
# sed -i"" -e "s/${localhostIP}:80/0.0.0.0:__MEMPOOL_FRONTEND_HTTP_PORT__/g" ./frontend/nginx.conf
# sed -i"" -e "s/${localhostIP}/0.0.0.0/g" ./frontend/nginx.conf
sed -i"" -e "s/${localhostIP}:80/0.0.0.0:__MEMPOOL_FRONTEND_HTTP_PORT__/g" ./frontend/nginx.conf
sed -i"" -e "s/${localhostIP}/0.0.0.0/g" ./frontend/nginx.conf
sed -i"" -e "s/user nobody;//g" ./frontend/nginx.conf
sed -i"" -e "s!/etc/nginx/nginx-mempool.conf!/etc/nginx/conf.d/nginx-mempool.conf!g" ./frontend/nginx.conf
sed -i"" -e "s/${localhostIP}:8999/__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__:__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__/g" ./frontend/nginx-mempool.conf

View File

@ -33,7 +33,7 @@ $ npm run config:defaults:liquid
### 3. Run the Frontend
_Make sure to use Node.js 20.x and npm 9.x or newer._
_Make sure to use Node.js 16.10 and npm 7._
Install project dependencies and run the frontend server:
@ -70,7 +70,7 @@ Set up the [Mempool backend](../backend/) first, if you haven't already.
### 1. Build the Frontend
_Make sure to use Node.js 20.x and npm 9.x or newer._
_Make sure to use Node.js 16.10 and npm 7._
Build the frontend:

View File

@ -54,10 +54,6 @@
"translation": "src/locale/messages.fr.xlf",
"baseHref": "/fr/"
},
"hr": {
"translation": "src/locale/messages.hr.xlf",
"baseHref": "/hr/"
},
"ja": {
"translation": "src/locale/messages.ja.xlf",
"baseHref": "/ja/"

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.waitForSkeletonGone();
//TODO(knorrium): add a check for the proxied server
// cy.changeNetwork('testnet4');
cy.changeNetwork('testnet4');
cy.changeNetwork('signet');
cy.changeNetwork('mainnet');
});

View File

@ -750,7 +750,7 @@
},
"backendInfo": {
"hostname": "node205.tk7.mempool.space",
"version": "3.1.0-dev",
"version": "3.0.0-beta",
"gitCommit": "abbc8a134",
"lightning": false
},

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "mempool-frontend",
"version": "3.1.0-dev",
"version": "3.0.0-beta",
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
"license": "GNU Affero General Public License v3.0",
"homepage": "https://mempool.space",
@ -76,9 +76,9 @@
"@angular/router": "^17.3.1",
"@angular/ssr": "^17.3.1",
"@fortawesome/angular-fontawesome": "~0.14.1",
"@fortawesome/fontawesome-common-types": "~6.7.2",
"@fortawesome/fontawesome-svg-core": "~6.7.2",
"@fortawesome/free-solid-svg-icons": "~6.7.2",
"@fortawesome/fontawesome-common-types": "~6.6.0",
"@fortawesome/fontawesome-svg-core": "~6.6.0",
"@fortawesome/free-solid-svg-icons": "~6.6.0",
"@mempool/mempool.js": "2.3.0",
"@ng-bootstrap/ng-bootstrap": "^16.0.0",
"@types/qrcode": "~1.5.0",
@ -86,15 +86,16 @@
"browserify": "^17.0.0",
"clipboard": "^2.0.11",
"domino": "^2.1.6",
"echarts": "~5.6.0",
"echarts": "~5.5.0",
"lightweight-charts": "~3.8.0",
"ngx-echarts": "~17.2.0",
"ngx-infinite-scroll": "^17.0.0",
"qrcode": "1.5.1",
"rxjs": "~7.8.1",
"esbuild": "^0.24.0",
"esbuild": "^0.23.0",
"tinyify": "^4.0.0",
"tlite": "^0.1.9",
"tslib": "~2.8.0",
"tslib": "~2.6.0",
"zone.js": "~0.14.4"
},
"devDependencies": {
@ -104,7 +105,7 @@
"@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^7.4.0",
"eslint": "^8.57.0",
"browser-sync": "^3.0.3",
"browser-sync": "^3.0.0",
"http-proxy-middleware": "~2.0.6",
"prettier": "^3.0.0",
"source-map-support": "^0.5.21",
@ -114,7 +115,7 @@
"optionalDependencies": {
"@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3",
"cypress": "^13.17.0",
"cypress": "^13.13.0",
"cypress-fail-on-console-error": "~5.1.0",
"cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1",

View File

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

View File

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

View File

@ -151,7 +151,7 @@ export const languages: Language[] = [
{ code: 'fr', name: 'Français' }, // French
// { code: 'gl', name: 'Galego' }, // Galician
{ code: 'ko', name: '한국어' }, // Korean
{ code: 'hr', name: 'Hrvatski' }, // Croatian
// { code: 'hr', name: 'Hrvatski' }, // Croatian
// { code: 'id', name: 'Bahasa Indonesia' },// Indonesian
{ code: 'hi', name: 'हिन्दी' }, // Hindi
{ code: 'ne', name: 'नेपाली' }, // Nepalese
@ -439,39 +439,4 @@ export const fiatCurrencies = {
code: 'ZAR',
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 { ServerModule } from '@angular/platform-server';
import { ZONE_SERVICE } from '@app/injection-tokens';
import { ZONE_SERVICE } from './injection-tokens';
import { AppModule } from './app.module';
import { AppComponent } from '@components/app/app.component';
import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor';
import { ZoneService } from '@app/services/zone.service';
import { AppComponent } from './components/app/app.component';
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
import { ZoneService } from './services/zone.service';
@NgModule({
@ -20,4 +20,4 @@ import { ZoneService } from '@app/services/zone.service';
],
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 { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
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 { AppComponent } from '@components/app/app.component';
import { ElectrsApiService } from '@app/services/electrs-api.service';
import { OrdApiService } from '@app/services/ord-api.service';
import { StateService } from '@app/services/state.service';
import { CacheService } from '@app/services/cache.service';
import { PriceService } from '@app/services/price.service';
import { EnterpriseService } from '@app/services/enterprise.service';
import { WebsocketService } from '@app/services/websocket.service';
import { AudioService } from '@app/services/audio.service';
import { PreloadService } from '@app/services/preload.service';
import { SeoService } from '@app/services/seo.service';
import { OpenGraphService } from '@app/services/opengraph.service';
import { ZoneService } from '@app/services/zone-shim.service';
import { SharedModule } from '@app/shared/shared.module';
import { StorageService } from '@app/services/storage.service';
import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor';
import { LanguageService } from '@app/services/language.service';
import { ThemeService } from '@app/services/theme.service';
import { TimeService } from '@app/services/time.service';
import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe';
import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe';
import { ShortenStringPipe } from '@app/shared/pipes/shorten-string-pipe/shorten-string.pipe';
import { CapAddressPipe } from '@app/shared/pipes/cap-address-pipe/cap-address-pipe';
import { AppPreloadingStrategy } from '@app/app.preloading-strategy';
import { ServicesApiServices } from '@app/services/services-api.service';
import { AppComponent } from './components/app/app.component';
import { ElectrsApiService } from './services/electrs-api.service';
import { StateService } from './services/state.service';
import { CacheService } from './services/cache.service';
import { PriceService } from './services/price.service';
import { EnterpriseService } from './services/enterprise.service';
import { WebsocketService } from './services/websocket.service';
import { AudioService } from './services/audio.service';
import { PreloadService } from './services/preload.service';
import { SeoService } from './services/seo.service';
import { OpenGraphService } from './services/opengraph.service';
import { ZoneService } from './services/zone-shim.service';
import { SharedModule } from './shared/shared.module';
import { StorageService } from './services/storage.service';
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
import { LanguageService } from './services/language.service';
import { ThemeService } from './services/theme.service';
import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe';
import { FiatCurrencyPipe } from './shared/pipes/fiat-currency.pipe';
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe';
import { AppPreloadingStrategy } from './app.preloading-strategy';
import { ServicesApiServices } from './services/services-api.service';
import { DatePipe } from '@angular/common';
const providers = [
ElectrsApiService,
OrdApiService,
StateService,
CacheService,
PriceService,
@ -45,7 +42,6 @@ const providers = [
EnterpriseService,
LanguageService,
ThemeService,
TimeService,
ShortenStringPipe,
FiatShortenerPipe,
FiatCurrencyPipe,

View File

@ -1,13 +1,13 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
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 = [
{
path: '',
component: MasterPageComponent,
loadChildren: () => import('@app/graphs/graphs.module').then(m => m.GraphsModule),
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule),
data: { preload: true },
}
];

View File

@ -1,5 +1,5 @@
import { Transaction, Vin } from '@interfaces/electrs.interface';
import { Hash } from '@app/shared/sha256';
import { Transaction, Vin } from './interfaces/electrs.interface';
import { Hash } from './shared/sha256';
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
@ -135,7 +135,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
return;
}
const opN = ops.pop();
if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
if (!opN.startsWith('OP_PUSHNUM_')) {
return;
}
const n = parseInt(opN.match(/[0-9]+/)[0], 10);
@ -152,7 +152,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
}
}
const opM = ops.pop();
if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
if (!opM.startsWith('OP_PUSHNUM_')) {
return;
}
const m = parseInt(opM.match(/[0-9]+/)[0], 10);
@ -303,4 +303,4 @@ export async function calcScriptHash$(script: string): Promise<string> {
return hashArray
.map((bytes) => bytes.toString(16).padStart(2, '0'))
.join('');
}
}

View File

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

View File

@ -53,7 +53,7 @@
<span>Spiral</span>
</a>
<a href="https://foundrydigital.com/" target="_blank" title="Foundry">
<svg xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" style="zoom: 1;" width="32" height="90" viewBox="0 -5 32 90" class="image">
<svg xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" style="zoom: 1;" width="32" height="76" viewBox="0 0 32 76">
<defs>
<style>
.d {
@ -125,14 +125,17 @@
<span>Blockstream</span>
</a>
<a href="https://unchained.com/" target="_blank" title="Unchained">
<svg id="Layer_1" width="78" height="78" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 156.68 156.68" class="image">
<defs><style>.cls-unchained-1{fill:#fff;}</style></defs><path class="cls-unchained-1" d="m78.34,0C35.07,0,0,35.07,0,78.34s35.07,78.34,78.34,78.34,78.34-35.07,78.34-78.34S121.6,0,78.34,0ZM20.23,109.5c-4.99-9.28-7.81-19.89-7.81-31.16C12.42,41.93,41.93,12.42,78.34,12.42c33.15,0,60.58,24.46,65.23,56.32h-37.48c-45.29,0-71.19,20.05-85.85,40.76Zm58.11,34.76c-12.42,0-24.04-3.44-33.96-9.41,3.94-8.85,9.11-18.7,15.84-28.9,20.99-31.8,52.2-31.19,76.49-31.19h7.45c.06,1.18.1,2.38.1,3.58,0,36.41-29.51,65.92-65.92,65.92Z"/><path class="cls-unchained-1" d="m91.98,42.4l-3.62-1.18c-3.94-1.29-7.03-4.38-8.32-8.32l-1.18-3.63c-.13-.39-.68-.39-.81,0l-1.18,3.63c-1.29,3.94-4.38,7.03-8.32,8.32l-3.62,1.18c-.39.13-.39.68,0,.81l3.62,1.18c3.94,1.29,7.03,4.38,8.32,8.32l1.18,3.63c.13.39.68.39.81,0l1.18-3.63c1.29-3.94,4.38-7.03,8.32-8.32l3.62-1.18c.39-.13.39-.68,0-.81Z"/>
</svg>
<svg id="Layer_1" width="78" height="78" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 156.68 156.68"><defs><style>.cls-unchained-1{fill:#fff;}</style></defs><path class="cls-unchained-1" d="m78.34,0C35.07,0,0,35.07,0,78.34s35.07,78.34,78.34,78.34,78.34-35.07,78.34-78.34S121.6,0,78.34,0ZM20.23,109.5c-4.99-9.28-7.81-19.89-7.81-31.16C12.42,41.93,41.93,12.42,78.34,12.42c33.15,0,60.58,24.46,65.23,56.32h-37.48c-45.29,0-71.19,20.05-85.85,40.76Zm58.11,34.76c-12.42,0-24.04-3.44-33.96-9.41,3.94-8.85,9.11-18.7,15.84-28.9,20.99-31.8,52.2-31.19,76.49-31.19h7.45c.06,1.18.1,2.38.1,3.58,0,36.41-29.51,65.92-65.92,65.92Z"/><path class="cls-unchained-1" d="m91.98,42.4l-3.62-1.18c-3.94-1.29-7.03-4.38-8.32-8.32l-1.18-3.63c-.13-.39-.68-.39-.81,0l-1.18,3.63c-1.29,3.94-4.38,7.03-8.32,8.32l-3.62,1.18c-.39.13-.39.68,0,.81l3.62,1.18c3.94,1.29,7.03,4.38,8.32,8.32l1.18,3.63c.13.39.68.39.81,0l1.18-3.63c1.29-3.94,4.38-7.03,8.32-8.32l3.62-1.18c.39-.13.39-.68,0-.81Z"/></svg>
<span>Unchained</span>
</a>
<a href="https://bitkey.world/" target="_blank" title="Bitkey">
<img class="image" src="/resources/profile/bitkey.svg" />
<span>Bitkey</span>
<a href="https://gemini.com/" target="_blank" title="Gemini">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360" class="image">
<rect style="fill: black" width="360" height="360" />
<g transform="matrix(0.62 0 0 0.62 180 180)">
<path style="fill: rgb(0,220,250)" transform=" translate(-162, -162)" d="M 211.74 0 C 154.74 0 106.35 43.84 100.25 100.25 C 43.84 106.35 1.4210854715202004e-14 154.76 1.4210854715202004e-14 211.74 C 0.044122601308501076 273.7212006364817 50.27879936351834 323.95587739869154 112.26 324 C 169.26 324 217.84 280.15999999999997 223.75 223.75 C 280.15999999999997 217.65 324 169.24 324 112.26 C 323.95587739869154 50.278799363518324 273.72120063648174 0.04412260130848722 211.74 -1.4210854715202004e-14 z M 297.74 124.84 C 291.9644950552469 162.621439649343 262.2969457716857 192.26062994820046 224.51 198 L 224.51 124.84 z M 26.3 199.16 C 31.986912917108594 161.30935034910615 61.653433460549415 131.56986937804106 99.48999999999998 125.78999999999999 L 99.49 199 L 26.3 199 z M 198.21 224.51 C 191.87736076583954 267.0991541201681 155.312384597087 298.62923417787493 112.255 298.62923417787493 C 69.19761540291302 298.62923417787493 32.63263923416048 267.0991541201682 26.3 224.51 z M 199.16 124.83999999999999 L 199.16 199 L 124.84 199 L 124.84 124.84 z M 297.7 99.48999999999998 L 125.78999999999999 99.48999999999998 C 132.12263923416046 56.90084587983182 168.687615402913 25.37076582212505 211.745 25.37076582212505 C 254.80238459708698 25.37076582212505 291.3673607658395 56.900845879831834 297.7 99.49 z" stroke-linecap="round" />
</g>
</svg>
<span>Gemini</span>
</a>
<a href="https://bullbitcoin.com/" target="_blank" title="Bull Bitcoin">
<svg aria-hidden="true" class="image" viewBox="0 -5 40 40" xmlns="http://www.w3.org/2000/svg">
@ -147,7 +150,7 @@
<span>Bull Bitcoin</span>
</a>
<a href="https://exodus.com/" target="_blank" title="Exodus">
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg" class="image">
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="250" cy="250" r="250" fill="#1F2033"/>
<g clip-path="url(#clip0_2_14)">
<path d="M411.042 178.303L271.79 87V138.048L361.121 196.097L350.612 229.351H271.79V271.648H350.612L361.121 304.903L271.79 362.952V414L411.042 322.989L388.271 250.646L411.042 178.303Z" fill="url(#paint0_linear_2_14)"/>
@ -188,36 +191,18 @@
</svg>
<span>Exodus</span>
</a>
<a href="https://gemini.com/" target="_blank" title="Gemini">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360" class="image">
<rect style="fill: black" width="360" height="360" />
<g transform="matrix(0.62 0 0 0.62 180 180)">
<path style="fill: rgb(0,220,250)" transform=" translate(-162, -162)" d="M 211.74 0 C 154.74 0 106.35 43.84 100.25 100.25 C 43.84 106.35 1.4210854715202004e-14 154.76 1.4210854715202004e-14 211.74 C 0.044122601308501076 273.7212006364817 50.27879936351834 323.95587739869154 112.26 324 C 169.26 324 217.84 280.15999999999997 223.75 223.75 C 280.15999999999997 217.65 324 169.24 324 112.26 C 323.95587739869154 50.278799363518324 273.72120063648174 0.04412260130848722 211.74 -1.4210854715202004e-14 z M 297.74 124.84 C 291.9644950552469 162.621439649343 262.2969457716857 192.26062994820046 224.51 198 L 224.51 124.84 z M 26.3 199.16 C 31.986912917108594 161.30935034910615 61.653433460549415 131.56986937804106 99.48999999999998 125.78999999999999 L 99.49 199 L 26.3 199 z M 198.21 224.51 C 191.87736076583954 267.0991541201681 155.312384597087 298.62923417787493 112.255 298.62923417787493 C 69.19761540291302 298.62923417787493 32.63263923416048 267.0991541201682 26.3 224.51 z M 199.16 124.83999999999999 L 199.16 199 L 124.84 199 L 124.84 124.84 z M 297.7 99.48999999999998 L 125.78999999999999 99.48999999999998 C 132.12263923416046 56.90084587983182 168.687615402913 25.37076582212505 211.745 25.37076582212505 C 254.80238459708698 25.37076582212505 291.3673607658395 56.900845879831834 297.7 99.49 z" stroke-linecap="round" />
</g>
</svg>
<span>Gemini</span>
</a>
<a href="https://leather.io/" target="_blank" title="Leather">
<img class="image" src="/resources/profile/leather.svg" />
<span>Leather</span>
</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>
<ng-container>
<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>
<div class="wrapper">
<ng-container>
<ng-template ngFor let-sponsor [ngForOf]="profiles.whales">
<a [href]="'https://x.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '/md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
</a>
</ng-template>
</ng-container>
@ -229,7 +214,7 @@
<div class="wrapper">
<ng-template ngFor let-sponsor [ngForOf]="profiles.chads">
<a [href]="'https://x.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '/md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
</a>
</ng-template>
</div>
@ -450,7 +435,7 @@
Trademark Notice<br>
</div>
<p>
The Mempool Open Source Project&reg;, Mempool Accelerator&trade;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, the mempool Logo, the mempool Square Logo, the mempool block visualization Logo, the mempool Blocks Logo, the mempool transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo, the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
The Mempool Open Source Project&reg;, Mempool Accelerator&trade;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, the mempool logo, the mempool Square logo, the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical Logo, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
</p>
<p>
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on &lt;https://mempool.space/trademark-policy&gt;.

View File

@ -13,6 +13,8 @@
.image.not-rounded {
border-radius: 0;
width: 60px;
height: 60px;
}
.intro {
@ -92,13 +94,6 @@
}
}
.whale-sponsor {
img {
width: 70px;
height: 70px;
}
}
.alliances {
margin-bottom: 100px;
a {
@ -163,8 +158,9 @@
margin: 40px 29px 10px;
&.image.coldcard {
border-radius: 0;
height: auto;
margin: 20px 29px 20px;
width: auto;
max-height: 50px;
margin: 40px 29px 14px 29px;
}
}
}
@ -258,12 +254,3 @@
width: 64px;
height: 64px;
}
.enterprise-sponsor {
.wrapper {
display: flex;
flex-wrap: wrap;
justify-content: center;
max-width: 800px;
}
}

View File

@ -1,16 +1,16 @@
import { ChangeDetectionStrategy, Component, ElementRef, Inject, LOCALE_ID, OnInit, ViewChild } from '@angular/core';
import { WebsocketService } from '@app/services/websocket.service';
import { SeoService } from '@app/services/seo.service';
import { OpenGraphService } from '@app/services/opengraph.service';
import { StateService } from '@app/services/state.service';
import { WebsocketService } from '../../services/websocket.service';
import { SeoService } from '../../services/seo.service';
import { OpenGraphService } from '../../services/opengraph.service';
import { StateService } from '../../services/state.service';
import { Observable } from 'rxjs';
import { ApiService } from '@app/services/api.service';
import { IBackendInfo } from '@interfaces/websocket.interface';
import { ApiService } from '../../services/api.service';
import { IBackendInfo } from '../../interfaces/websocket.interface';
import { Router, ActivatedRoute } from '@angular/router';
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 { EnterpriseService } from '@app/services/enterprise.service';
import { EnterpriseService } from '../../services/enterprise.service';
@Component({
selector: 'app-about',

View File

@ -1,9 +1,9 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Routes, RouterModule } from '@angular/router';
import { AboutComponent } from '@components/about/about.component';
import { AboutSponsorsComponent } from '@components/about/about-sponsors.component';
import { SharedModule } from '@app/shared/shared.module';
import { AboutComponent } from './about.component';
import { AboutSponsorsComponent } from './about-sponsors.component';
import { SharedModule } from '../../shared/shared.module';
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.includes('Payment declined')) {
<div class="row mb-1 text-center">
<div class="col-sm">
<h1 style="font-size: larger;">{{ accelerateError }}</h1>
</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>
} @else {
<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>
<div class="row text-center mt-1">
<div class="col-sm">
<div class="d-flex flex-row justify-content-center align-items-center">
@ -365,11 +357,11 @@
<app-active-acceleration-box [miningStats]="miningStats" [pools]="estimate.pools" [chartOnly]="true" class="ml-2"></app-active-acceleration-box>
</div>
</div>
<div class="payment-area" style="font-size: 14px;">
<div class="payment-area mt-2 p-2" style="font-size: 14px;">
<div class="row text-center justify-content-center mx-2">
<span 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></span>
<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>
@if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay)) {
@if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp)) {
<div class="row">
<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>
@ -386,12 +378,9 @@
<p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container>&nbsp;<span><small style="font-family: monospace;">{{ ((invoice.btcDue * 100_000_000) || cost) | number }}</small>&nbsp;<span class="symbol" i18n="shared.sats">sats</span></span></p>
<app-bitcoin-invoice style="width: 100%;" [invoice]="invoice" [minimal]="true" (completed)="bitcoinPaymentCompleted()"></app-bitcoin-invoice>
} @else if (btcpayInvoiceFailed) {
<div class="btcpay-invoice">
<fa-icon style="font-size: 20px; color: var(--red)" [icon]="['fas', 'circle-xmark']"></fa-icon>
<span i18n="accelerator.failed-to-load-invoice">Failed to load invoice</span>
@if (!loadingBtcpayInvoice) {
<button class="btn btn-sm btn-secondary mt-0 mt-md-1" (click)="requestBTCPayInvoice()">Retry ↻</button>
}
<p i18n="accelerator.failed-to-load-invoice">Failed to load invoice</p>
<div class="d-flex flex-column align-items-center justify-content-center" style="width: 100%; height: 292px;">
<fa-icon style="font-size: 24px; color: var(--red)" [icon]="['fas', 'circle-xmark']"></fa-icon>
</div>
} @else {
<p i18n="accelerator.loading-invoice">Loading invoice...</p>
@ -400,13 +389,13 @@
</div>
}
</div>
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay || canPayWithCardOnFile) {
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) {
<div class="col-sm text-center flex-grow-0 d-flex flex-column justify-content-center align-items-center">
<p class="text-nowrap">&mdash;&mdash;<span i18n="or"> OR </span>&mdash;&mdash;</p>
<p class="text-nowrap">&mdash;<span i18n="or">OR</span>&mdash;</p>
</div>
}
}
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay || canPayWithCardOnFile) {
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) {
<div class="col-sm text-center d-flex flex-column justify-content-center align-items-center">
<p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container>&nbsp;<app-fiat [value]="cost"></app-fiat> with</p>
@if (canPayWithCashapp) {
@ -424,17 +413,6 @@
<img src="/resources/google-pay.png" height=37>
</div>
}
@if (canPayWithCardOnFile) {
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) { <span class="mt-1 mb-1"></span> }
<div class="paymentMethod mx-2 d-flex justify-content-center align-items-center" style="width: 200px; height: 55px" (click)="moveToStep('cardonfile')">
@if (['VISA', 'MASTERCARD', 'JCB', 'DISCOVER', 'DISCOVER_DINERS', 'AMERICAN_EXPRESS'].includes(estimate?.availablePaymentMethods?.cardOnFile?.card?.brand)) {
<app-svg-images [name]="estimate?.availablePaymentMethods?.cardOnFile?.card?.brand" height="33" class="mr-2"></app-svg-images>
} @else {
<app-svg-images name="OTHER_BRAND" height="33" class="mr-2"></app-svg-images>
}
<span style="font-size: 22px; padding-bottom: 3px">{{ estimate?.availablePaymentMethods?.cardOnFile?.card?.last_4 }}</span>
</div>
}
</div>
}
</div>
@ -457,7 +435,7 @@
<button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="moveToStep('summary')" i18n="go-back">Go back</button>
</div>
</div>
} @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay' || step === 'cardonfile') {
} @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay') {
<!-- Show checkout page -->
<div class="row mb-md-1 text-center" id="confirm-title">
<div class="col-sm" id="confirm-payment-title">
@ -473,7 +451,7 @@
</div>
</div>
@if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay || step === 'googlepay' && !loadingGooglePay || step === 'cardonfile' && !loadingCardOnFile) {
@if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay || step === 'googlepay' && !loadingGooglePay) {
<div class="row text-center mt-1">
<div class="col-sm">
<div class="form-group w-100">
@ -498,24 +476,14 @@
<div id="cash-app-pay" class="d-inline-block" style="height: 50px" [style]="loadingCashapp ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
} @else if (step === 'googlepay') {
<div id="google-pay-button" class="d-inline-block" style="height: 50px" [style]="loadingGooglePay ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
} @else if (step === 'cardonfile') {
<div class="paymentMethod mx-2 d-flex justify-content-center align-items-center ml-auto mr-auto" style="width: 200px; height: 55px" (click)="requestCardOnFilePayment()" [style]="loadingCardOnFile ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''">
<fa-icon style="font-size: 24px; color: white" [icon]="['fas', 'credit-card']"></fa-icon>
<span class="ml-2" style="font-size: 22px">{{ estimate?.availablePaymentMethods?.cardOnFile?.card?.brand }} {{ estimate?.availablePaymentMethods?.cardOnFile?.card?.last_4 }}</span>
</div>
}
@if (loadingCashapp || loadingApplePay || loadingGooglePay || loadingCardOnFile) {
@if (loadingCashapp || loadingApplePay || loadingGooglePay) {
<div display="d-flex flex-row justify-content-center">
<span i18n="accelerator.loading-payment-method">Loading payment method...</span>
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></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>

View File

@ -8,13 +8,6 @@
color: var(--green)
}
.accelerate-checkout-inner {
&.input-disabled {
pointer-events: none;
opacity: 0.75;
}
}
.paymentMethod {
padding: 10px;
background-color: var(--secondary);
@ -153,11 +146,6 @@
.payment-area {
background: var(--bg);
margin-top: 0.5rem;
padding: 0.5rem;
@media (max-width: 575px) {
padding-bottom: 1.25rem;
}
}
.col.pie {
@ -184,6 +172,10 @@
background-color: var(--tertiary);
}
.btn-small-height {
line-height: 1;
}
.summary-row {
display: flex;
flex-direction: row;
@ -224,17 +216,4 @@
}
.apple-pay-button-white-with-line {
-apple-pay-button-style: white-outline;
}
.btcpay-invoice {
display: flex;
height: 292px;
flex-direction: column;
justify-content: center;
align-items: center;
@media (max-width: 575px) {
height: 75px;
flex-direction: row;
gap: 5px;
}
}

View File

@ -1,19 +1,19 @@
/* eslint-disable no-console */
import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core';
import { Subscription, tap, of, catchError, Observable, switchMap } from 'rxjs';
import { ServicesApiServices } from '@app/services/services-api.service';
import { md5 } from '@app/shared/common.utils';
import { StateService } from '@app/services/state.service';
import { AudioService } from '@app/services/audio.service';
import { ETA, EtaService } from '@app/services/eta.service';
import { Transaction } from '@interfaces/electrs.interface';
import { MiningStats } from '@app/services/mining.service';
import { IAuth, AuthServiceMempool } from '@app/services/auth.service';
import { EnterpriseService } from '@app/services/enterprise.service';
import { ApiService } from '@app/services/api.service';
import { ServicesApiServices } from '../../services/services-api.service';
import { md5, insecureRandomUUID } from '../../shared/common.utils';
import { StateService } from '../../services/state.service';
import { AudioService } from '../../services/audio.service';
import { ETA, EtaService } from '../../services/eta.service';
import { Transaction } from '../../interfaces/electrs.interface';
import { MiningStats } from '../../services/mining.service';
import { IAuth, AuthServiceMempool } from '../../services/auth.service';
import { EnterpriseService } from '../../services/enterprise.service';
import { ApiService } from '../../services/api.service';
import { isDevMode } from '@angular/core';
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay' | 'cardOnFile';
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay';
export type AccelerationEstimate = {
hasAccess: boolean;
@ -26,7 +26,7 @@ export type AccelerationEstimate = {
mempoolBaseFee: number;
vsizeFee: number;
pools: number[];
availablePaymentMethods: Record<PaymentMethod, {min: number, max: number, card?: {card_id: string, last_4: string, brand: string, name: string, billing: any}}>;
availablePaymentMethods: Record<PaymentMethod, {min: number, max: number}>;
unavailable?: boolean;
options: { // recommended bid options
fee: number; // recommended userBid in sats
@ -49,7 +49,7 @@ export const MIN_BID_RATIO = 1;
export const DEFAULT_BID_RATIO = 2;
export const MAX_BID_RATIO = 4;
type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'applepay' | 'googlepay' | 'cardonfile' | 'processing' | 'paid' | 'success';
type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'applepay' | 'googlepay' | 'processing' | 'paid' | 'success';
@Component({
selector: 'app-accelerate-checkout',
@ -62,9 +62,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
@Input() miningStats: MiningStats;
@Input() eta: ETA;
@Input() scrollEvent: boolean;
@Input() cashappEnabled: boolean = true;
@Input() applePayEnabled: boolean = false;
@Input() googlePayEnabled: boolean = true;
@Input() cardOnFileEnabled: boolean = true;
@Input() advancedEnabled: boolean = false;
@Input() forceMobile: boolean = false;
@Input() showDetails: boolean = false;
@ -75,9 +75,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
@Output() changeMode = new EventEmitter<boolean>();
calculating = true;
processing = false;
isCheckoutLocked = 0; // reference counter, 0 = unlocked, >0 = locked
isTokenizing = 0; // reference counter, 0 = false, >0 = true
selectedOption: 'wait' | 'accel';
cantPayReason = '';
quoteError = ''; // error fetching estimate or initial data
@ -86,7 +83,13 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
timePaid: number = 0; // time acceleration requested
math = Math;
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';
simpleMode: boolean = true;
@ -96,6 +99,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
auth: IAuth | null = null;
// accelerator stuff
accelerationUUID: string;
accelerationSubscription: Subscription;
difficultySubscription: Subscription;
estimateSubscription: Subscription;
@ -117,7 +121,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
loadingCashapp = false;
loadingApplePay = false;
loadingGooglePay = false;
loadingCardOnFile = false;
payments: any;
cashAppPay: any;
applePay: any;
@ -139,7 +142,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
private authService: AuthServiceMempool,
private enterpriseService: EnterpriseService,
) {
this.isProdDomain = this.stateService.env.PROD_DOMAINS.indexOf(document.location.hostname) > -1;
this.accelerationUUID = insecureRandomUUID();
// 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
@ -157,7 +160,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.accelerateError = null;
this.timePaid = 0;
this.btcpayInvoiceFailed = false;
this.moveToStep('summary', true);
this.moveToStep('summary');
} else {
this.auth = auth;
}
@ -166,11 +169,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('cash_request_id')) { // Redirected from cashapp
this.moveToStep('processing', true);
this.moveToStep('processing');
this.insertSquare();
this.setupSquare();
} else {
this.moveToStep('summary', true);
this.moveToStep('summary');
}
this.conversionsSubscription = this.stateService.conversions$.subscribe(
@ -193,25 +196,19 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
if (changes.scrollEvent && this.scrollEvent) {
this.scrollToElement('acceleratePreviewAnchor', 'start');
}
if (changes.accelerating && this.accelerating) {
if (this.step === 'processing' || this.step === 'paid') {
this.moveToStep('success', true);
} else { // Edge case where the transaction gets accelerated by someone else or on another session
this.closeModal();
if (changes.accelerating) {
if ((this.step === 'processing' || this.step === 'paid') && this.accelerating) {
this.moveToStep('success');
}
}
}
moveToStep(step: CheckoutStep, force: boolean = false): void {
if (this.isCheckoutLocked > 0 && !force) {
return;
}
this.processing = false;
moveToStep(step: CheckoutStep): void {
this._step = step;
if (this.timeoutTimer) {
clearTimeout(this.timeoutTimer);
}
if (!this.estimate && ['quote', 'summary', 'checkout', 'processing'].includes(this.step)) {
if (!this.estimate && ['quote', 'summary', 'checkout'].includes(this.step)) {
this.fetchEstimate();
}
if (this._step === 'checkout') {
@ -220,9 +217,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
if (this._step === 'checkout' && this.canPayWithBitcoin) {
this.btcpayInvoiceFailed = false;
this.loadingBtcpayInvoice = true;
this.invoice = null;
this.requestBTCPayInvoice();
} else if (this._step === 'cashapp') {
} else if (this._step === 'cashapp' && this.cashappEnabled) {
this.loadingCashapp = true;
this.setupSquare();
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
@ -234,10 +232,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.loadingGooglePay = true;
this.setupSquare();
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
} else if (this._step === 'cardonfile' && this.cardOnFileEnabled) {
this.loadingCardOnFile = true;
this.setupSquare();
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
} else if (this._step === 'paid') {
this.timePaid = Date.now();
this.timeoutTimer = setTimeout(() => {
@ -251,7 +245,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
closeModal(): void {
this.completed.emit(true);
this.moveToStep('summary', true);
this.moveToStep('summary');
}
/**
@ -332,6 +326,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
if (this.step === 'checkout' && this.canPayWithBitcoin && !this.loadingBtcpayInvoice) {
this.loadingBtcpayInvoice = true;
this.requestBTCPayInvoice();
}
@ -376,7 +371,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.selectFeeRateIndex = index;
this.userBid = Math.max(0, fee);
this.cost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
this.validateChoice();
}
}
@ -384,27 +378,25 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
* Account-based acceleration request
*/
accelerateWithMempoolAccount(): void {
if (!this.canPay || this.calculating || this.processing) {
if (!this.canPay || this.calculating) {
return;
}
this.processing = true;
if (this.accelerationSubscription) {
this.accelerationSubscription.unsubscribe();
}
this.accelerationSubscription = this.servicesApiService.accelerate$(
this.tx.txid,
this.userBid,
this.accelerationUUID
).subscribe({
next: () => {
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
this.showSuccess = true;
this.estimateSubscription.unsubscribe();
this.moveToStep('paid', true);
this.moveToStep('paid');
},
error: (response) => {
this.processing = false;
this.accelerateError = response.error;
}
});
@ -457,8 +449,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
await this.requestApplePayPayment();
} else if (this._step === 'googlepay') {
await this.requestGooglePayPayment();
} else if (this._step === 'cardonfile') {
this.loadingCardOnFile = false;
}
},
error: () => {
@ -476,14 +466,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
* APPLE PAY
*/
async requestApplePayPayment(): Promise<void> {
if (this.processing) {
return;
}
if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe();
}
this.processing = true;
this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => {
this.conversions = conversions;
@ -508,84 +494,59 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
console.error(`Unable to find apple pay button id='apple-pay-button'`);
// Try again
setTimeout(this.requestApplePayPayment.bind(this), 500);
this.processing = false;
return;
}
this.loadingApplePay = false;
applePayButton.addEventListener('click', async event => {
if (this.isCheckoutLocked > 0 || this.isTokenizing > 0) {
return;
}
event.preventDefault();
try {
// lock the checkout UI and show a loading spinner until the square modals are finished
this.isCheckoutLocked++;
this.isTokenizing++;
const tokenResult = await this.applePay.tokenize();
if (tokenResult?.status === 'OK') {
const card = tokenResult.details?.card;
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);
const tokenResult = await this.applePay.tokenize();
if (tokenResult?.status === 'OK') {
const card = tokenResult.details?.card;
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
console.error(`Cannot retreive payment card details`);
this.accelerateError = 'apple_pay_no_card_details';
return;
}
} finally {
// always unlock the checkout once we're finished
this.isTokenizing--;
this.isCheckoutLocked--;
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
this.servicesApiService.accelerateWithApplePay$(
this.tx.txid,
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) {
this.processing = false;
console.error(e);
}
}
@ -596,14 +557,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
* GOOGLE PAY
*/
async requestGooglePayPayment(): Promise<void> {
if (this.processing) {
return;
}
if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe();
}
this.processing = true;
this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => {
this.conversions = conversions;
@ -631,205 +588,66 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.loadingGooglePay = false;
document.getElementById('google-pay-button').addEventListener('click', async event => {
if (this.isCheckoutLocked > 0 || this.isTokenizing > 0) {
return;
}
event.preventDefault();
try {
// lock the checkout UI and show a loading spinner until the square modals are finished
this.isCheckoutLocked++;
this.isTokenizing++;
const tokenResult = await this.googlePay.tokenize();
if (tokenResult?.status === 'OK') {
const card = tokenResult.details?.card;
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);
const tokenResult = await this.googlePay.tokenize();
if (tokenResult?.status === 'OK') {
const card = tokenResult.details?.card;
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
console.error(`Cannot retreive payment card details`);
this.accelerateError = 'apple_pay_no_card_details';
return;
}
} finally {
// always unlock the checkout once we're finished
this.isTokenizing--;
this.isCheckoutLocked--;
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
this.servicesApiService.accelerateWithGooglePay$(
this.tx.txid,
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);
}
});
}
);
}
/**
* Card On File
*/
async requestCardOnFilePayment(): Promise<void> {
if (this.processing) {
return;
}
if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe();
}
this.processing = true;
this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => {
this.conversions = conversions;
const costUSD = this.cost / 100_000_000 * conversions.USD;
if (this.isCheckoutLocked > 0) {
return;
}
const cardOnFile = this.estimate?.availablePaymentMethods?.cardOnFile;
if (!cardOnFile?.card) {
this.accelerateError = 'card_on_file_not_found';
return;
}
this.loadingCardOnFile = false;
try {
this.isCheckoutLocked += 2;
this.isTokenizing += 2;
const nameParts = cardOnFile.card.name.split(' ');
const assumedGivenName = nameParts[0];
const assumedFamilyName = nameParts.length > 1 ? nameParts[1] : undefined;
const verificationDetails = {
card: {
billing: {
givenName: assumedGivenName,
familyName: assumedFamilyName,
addressLines: [cardOnFile.card.billing.addressLine1 ?? ''],
city: cardOnFile.card.billing.locality ?? '',
state: cardOnFile.card.billing.administrativeDistrictLevel1 ?? '',
countyCode: cardOnFile.card.billing.country,
}
}
};
const verificationToken = await this.$verifyBuyer(this.payments, cardOnFile.card.card_id, verificationDetails, costUSD.toFixed(2));
if (!verificationToken || !verificationToken.token) {
console.error(`SCA verification failed`);
this.accelerateError = 'SCA Verification Failed. Payment Declined.';
this.processing = false;
return;
}
this.servicesApiService.accelerateWithCardOnFile$(
this.tx.txid,
cardOnFile.card.card_id,
verificationToken.token,
`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');
setTimeout(() => {
this.isCheckoutLocked--;
this.isTokenizing--;
this.moveToStep('paid', true);
}, 1000);
},
error: (response) => {
this.processing = false;
this.accelerateError = response.error;
this.isCheckoutLocked--;
this.isTokenizing--;
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);
}
}
});
} catch (e) {
console.log(e);
this.isCheckoutLocked--;
this.isTokenizing--;
this.processing = false;
this.accelerateError = e.message;
} finally {
// always unlock the checkout once we're finished
this.isCheckoutLocked--;
this.isTokenizing--;
}
}
);
}
/**
* CASHAPP
*/
async requestCashAppPayment(): Promise<void> {
if (this.processing) {
return;
}
if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe();
}
this.processing = true;
this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => {
this.conversions = conversions;
@ -838,7 +656,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
const redirectHostname = document.location.hostname === 'localhost' ? `http://localhost:4200`: `https://${document.location.hostname}`;
const costUSD = this.cost / 100_000_000 * conversions.USD;
const costUSD = this.step === 'processing' ? 69.69 : (this.cost / 100_000_000 * conversions.USD); // When we're redirected to this component, the payment data is already linked to the payment token, so does not matter what amonut we put in there, therefore it's 69.69
const paymentRequest = this.payments.paymentRequest({
countryCode: 'US',
currencyCode: 'USD',
@ -860,7 +678,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.cashAppPay.addEventListener('ontokenization', event => {
const { tokenResult, error } = event.detail;
if (error) {
this.processing = false;
this.accelerateError = error;
} else if (tokenResult.status === 'OK') {
this.servicesApiService.accelerateWithCashApp$(
@ -868,17 +685,16 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
tokenResult.token,
tokenResult.details.cashAppPay.cashtag,
tokenResult.details.cashAppPay.referenceId,
costUSD
this.accelerationUUID
).subscribe({
next: () => {
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
if (this.cashAppPay) {
this.cashAppPay.destroy();
}
setTimeout(() => {
this.moveToStep('paid', true);
this.moveToStep('paid');
if (window.history.replaceState) {
const urlParams = new URLSearchParams(window.location.search);
window.history.replaceState(null, null, window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ''));
@ -886,14 +702,13 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}, 1000);
},
error: (response) => {
this.processing = false;
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')}`, ``));
}, 10000);
}, 3000);
}
}
});
@ -903,49 +718,20 @@ 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
*/
async requestBTCPayInvoice(): Promise<void> {
this.loadingBtcpayInvoice = true;
this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid, this.userBid).pipe(
switchMap(response => {
return this.servicesApiService.retreiveInvoice$(response.btcpayInvoiceId);
}),
catchError(error => {
console.log(error);
this.loadingBtcpayInvoice = false;
this.btcpayInvoiceFailed = true;
return of(null);
})
).subscribe((invoice) => {
this.loadingBtcpayInvoice = false;
this.invoice = invoice;
this.cd.markForCheck();
});
@ -955,7 +741,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
this.estimateSubscription.unsubscribe();
this.moveToStep('paid', true);
this.moveToStep('paid');
}
isLoggedIn(): boolean {
@ -982,6 +768,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
get couldPayWithCashapp(): boolean {
if (!this.cashappEnabled) {
return false;
}
return !!this.estimate?.availablePaymentMethods?.cashapp;
}
@ -1016,7 +805,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
get canPayWithCashapp(): boolean {
if (!this.conversions || (!this.isProdDomain && !isDevMode())) {
if (!this.cashappEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) {
return false;
}
@ -1063,22 +852,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
return false;
}
get canPayWithCardOnFile(): boolean {
if (!this.cardOnFileEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) {
return false;
}
const paymentMethod = this.estimate?.availablePaymentMethods?.cardOnFile;
if (paymentMethod) {
const costUSD = (this.cost / 100_000_000 * this.conversions.USD);
if (costUSD >= paymentMethod.min && costUSD <= paymentMethod.max) {
return true;
}
}
return false;
}
get canPayWithBalance(): boolean {
if (!this.hasAccessToBalanceMode) {
return false;

View File

@ -12,7 +12,7 @@
</p>
</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>

View File

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

View File

@ -21,14 +21,14 @@
</tr>
<tr *ngIf="accelerationInfo.fee">
<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 *ngIf="accelerationInfo.bidBoost >= 0 || accelerationInfo.feeDelta">
<td class="label" i18n="transaction.out-of-band-fees">Out-of-band fees</td>
@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 {
<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 *ngIf="accelerationInfo.fee && accelerationInfo.weight">
@ -47,14 +47,13 @@
<tr *ngIf="['accelerated', 'mined'].includes(accelerationInfo.status) && hasPoolsData()">
<td class="label" i18n="transaction.accelerated-by-hashrate|Accelerated to hashrate">Accelerated by</td>
<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]"
class="pool-logo"
[style.opacity]="accelerationInfo?.minedByPoolUniqueId && pool !== accelerationInfo?.minedByPoolUniqueId ? '0.3' : '1'"
[src]="'/resources/mining-pools/' + accelerationInfo.poolsData[pool].slug + '.svg'"
onError="this.src = '/resources/mining-pools/default.svg'"
[alt]="'Logo of ' + pool.name + ' mining pool'">
<br *ngIf="i % 6 === 5">
</ng-container>
</td>
</tr>

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
import { Component, Input, OnInit, OnChanges, HostListener } from '@angular/core';
import { ETA } from '@app/services/eta.service';
import { Transaction } from '@interfaces/electrs.interface';
import { Acceleration, SinglePoolStats } from '@interfaces/node-api.interface';
import { MiningService } from '@app/services/mining.service';
import { ETA } from '../../services/eta.service';
import { Transaction } from '../../interfaces/electrs.interface';
import { Acceleration, SinglePoolStats } from '../../interfaces/node-api.interface';
import { MiningService } from '../../services/mining.service';
@Component({
selector: 'app-acceleration-timeline',
@ -11,17 +11,19 @@ import { MiningService } from '@app/services/mining.service';
})
export class AccelerationTimelineComponent implements OnInit, OnChanges {
@Input() transactionTime: number;
@Input() acceleratedAt: number;
@Input() tx: Transaction;
@Input() accelerationInfo: Acceleration;
@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;
accelerateRatio: number;
useAbsoluteTime: boolean = false;
firstSeenToAccelerated: number;
acceleratedToMined: number;
interval: number;
tooltipPosition = null;
hoverInfo: any = null;
@ -32,24 +34,38 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
) {}
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 => {
for (const pool of pools) {
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 {
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 {
this.now = Math.floor(new Date().getTime() / 1000);
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);
ngOnDestroy(): void {
clearInterval(this.interval);
}
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 { EChartsOption } from '@app/graphs/echarts';
import { EChartsOption } from '../../../graphs/echarts';
import { Observable, Subject, Subscription, combineLatest, fromEvent, merge, share } from 'rxjs';
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 { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '@app/shared/graphs.utils';
import { StorageService } from '@app/services/storage.service';
import { MiningService } from '@app/services/mining.service';
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../../shared/graphs.utils';
import { StorageService } from '../../../services/storage.service';
import { MiningService } from '../../../services/mining.service';
import { ActivatedRoute, Router } from '@angular/router';
import { Acceleration } from '@interfaces/node-api.interface';
import { ServicesApiServices } from '@app/services/services-api.service';
import { StateService } from '@app/services/state.service';
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
import { Acceleration } from '../../../interfaces/node-api.interface';
import { ServicesApiServices } from '../../../services/services-api.service';
import { StateService } from '../../../services/state.service';
import { RelativeUrlPipe } from '../../../shared/pipes/relative-url/relative-url.pipe';
@Component({
selector: 'app-acceleration-fees-graph',
@ -46,8 +46,6 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
aggregatedHistory$: Observable<any>;
statsSubscription: Subscription;
aggregatedHistorySubscription: Subscription;
fragmentSubscription: Subscription;
isLoading = true;
formatNumber = formatNumber;
timespan = '';
@ -81,8 +79,8 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
}
this.radioGroupForm = this.formBuilder.group({ dateSpan: 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) {
this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
}
@ -115,7 +113,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
share(),
);
this.aggregatedHistorySubscription = this.aggregatedHistory$.subscribe();
this.aggregatedHistory$.subscribe();
}
ngOnChanges(changes: SimpleChanges): void {
@ -266,7 +264,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
type: 'bar',
barWidth: '90%',
large: true,
barMinHeight: 3,
barMinHeight: 1,
},
],
dataZoom: (this.widget || data.length === 0 )? undefined : [{
@ -337,8 +335,8 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
}
ngOnDestroy(): void {
this.aggregatedHistorySubscription?.unsubscribe();
this.fragmentSubscription?.unsubscribe();
this.statsSubscription?.unsubscribe();
if (this.statsSubscription) {
this.statsSubscription.unsubscribe();
}
}
}

View File

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

View File

@ -4,7 +4,7 @@
<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">
<thead>
<th class="txid text-left" i18n="dashboard.latest-transactions.txid">TXID</th>
@ -14,15 +14,15 @@
<th class="time text-right" i18n="accelerator.requested">Requested</th>
</ng-container>
<ng-container *ngIf="!pending">
<th class="fee text-right text-truncate" i18n="transaction.bid-boost|Bid Boost">Bid Boost</th>
<th class="fee text-right" i18n="transaction.bid-boost|Bid Boost">Bid Boost</th>
<th class="block text-right" i18n="shared.block-title">Block</th>
<th class="pool text-right" i18n="mining.pool-name" *ngIf="!this.widget">Pool</th>
<th class="status text-right" i18n="transaction.status|Transaction Status">Status</th>
<th class="date text-right" i18n="accelerator.requested" *ngIf="!this.widget">Requested</th>
</ng-container>
</thead>
<tbody *ngIf="state.accelerations && nonEmptyAccelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<tr *ngFor="let acceleration of state.accelerations; let i= index;">
<tbody *ngIf="accelerationList$ | async as accelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<tr *ngFor="let acceleration of accelerations; let i= index;">
<td class="txid text-left">
<a [routerLink]="['/tx' | relativeUrl, acceleration.txid]">
<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>
</td>
<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 class="time text-right">
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
@ -41,7 +41,7 @@
</ng-container>
<ng-container *ngIf="!pending">
<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 *ngIf="acceleration.boost == null" class="fee text-right">
~
@ -64,8 +64,7 @@
<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="transaction.rbf.mined">Mined</ng-container><span *ngIf="acceleration.status === 'completed_provisional'">&nbsp;</span></span>
<span *ngIf="acceleration.status.includes('failed') && acceleration.canceled" 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') && !acceleration.canceled" class="badge badge-danger"><ng-container i18n="accelerator.canceled">Failed</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 class="date text-right" *ngIf="!this.widget">
<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 { BehaviorSubject, Observable, Subscription, catchError, combineLatest, filter, of, switchMap, tap, throttleTime, timer } from 'rxjs';
import { Acceleration, BlockExtended, SinglePoolStats } from '@interfaces/node-api.interface';
import { StateService } from '@app/services/state.service';
import { WebsocketService } from '@app/services/websocket.service';
import { ServicesApiServices } from '@app/services/services-api.service';
import { SeoService } from '@app/services/seo.service';
import { BehaviorSubject, Observable, Subscription, catchError, filter, of, switchMap, tap, throttleTime } from 'rxjs';
import { Acceleration, BlockExtended, SinglePoolStats } from '../../../interfaces/node-api.interface';
import { StateService } from '../../../services/state.service';
import { WebsocketService } from '../../../services/websocket.service';
import { ServicesApiServices } from '../../../services/services-api.service';
import { SeoService } from '../../../services/seo.service';
import { ActivatedRoute, Router } from '@angular/router';
import { MiningService } from '@app/services/mining.service';
import { MiningService } from '../../../services/mining.service';
@Component({
selector: 'app-accelerations-list',
@ -61,11 +61,8 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
this.websocketService.want(['blocks']);
this.seoService.setTitle($localize`:@@02573b6980a2d611b4361a2595a4447e390058cd:Accelerations`);
this.paramSubscription = combineLatest([
this.route.params,
timer(0),
]).pipe(
tap(([params]) => {
this.paramSubscription = this.route.params.pipe(
tap(params => {
this.page = +params['page'] || 1;
this.pageSubject.next(this.page);
})
@ -151,4 +148,4 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
this.paramSubscription?.unsubscribe();
this.keyNavigationSubscription?.unsubscribe();
}
}
}

View File

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

View File

@ -1,7 +1,7 @@
@if (chartOnly) {
<ng-container *ngTemplateOutlet="pieChart"></ng-container>
} @else {
<table style="width: 100%;">
<table>
<tbody>
<tr>
<td class="td-width field-label" [class]="chartPositionLeft ? 'chart-left' : ''" i18n="transaction.accelerated-to-feerate|Accelerated to feerate">Accelerated to</td>
@ -10,17 +10,17 @@
</td>
<td class="field-value" [class]="chartPositionLeft ? 'chart-left' : ''">
<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>
} @else {
<app-fee-rate class="oobFees" [fee]="effectiveFeeRate"></app-fee-rate>
<app-fee-rate class="oobFees" [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
}
</div>
</td>
<td class="pie-chart" rowspan="2" *ngIf="!chartPositionLeft">
<div class="d-flex justify-content-between align-items-start">
@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>
</div>
@ -36,7 +36,7 @@
<tr>
<td colspan="3" class="pt-0">
<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>
</td>
</tr>

View File

@ -1,8 +1,8 @@
import { Component, ChangeDetectionStrategy, Input, Output, OnChanges, SimpleChanges, EventEmitter, ChangeDetectorRef } from '@angular/core';
import { Transaction } from '@interfaces/electrs.interface';
import { Acceleration, SinglePoolStats } from '@interfaces/node-api.interface';
import { EChartsOption, PieSeriesOption } from '@app/graphs/echarts';
import { MiningStats } from '@app/services/mining.service';
import { Component, ChangeDetectionStrategy, Input, Output, OnChanges, SimpleChanges, EventEmitter } from '@angular/core';
import { Transaction } from '../../../interfaces/electrs.interface';
import { Acceleration, SinglePoolStats } from '../../../interfaces/node-api.interface';
import { EChartsOption, PieSeriesOption } from '../../../graphs/echarts';
import { MiningStats } from '../../../services/mining.service';
function lighten(color, p): { r, g, b } {
return {
@ -23,8 +23,7 @@ function toRGB({r,g,b}): string {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ActiveAccelerationBox implements OnChanges {
@Input() acceleratedBy?: number[];
@Input() effectiveFeeRate?: number;
@Input() tx: Transaction;
@Input() accelerationInfo: Acceleration;
@Input() miningStats: MiningStats;
@Input() pools: number[];
@ -42,12 +41,10 @@ export class ActiveAccelerationBox implements OnChanges {
timespan = '';
chartInstance: any = undefined;
constructor(
private cd: ChangeDetectorRef,
) {}
constructor() {}
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) {
this.prepareChartOptions(pools);
}
@ -70,27 +67,17 @@ export class ActiveAccelerationBox implements OnChanges {
const acceleratingPools = (poolList || []).filter(id => pools[id]).sort((a,b) => pools[a].lastEstimatedHashrate - pools[b].lastEstimatedHashrate);
const totalAcceleratedHashrate = acceleratingPools.reduce((total, pool) => total + pools[pool].lastEstimatedHashrate, 0);
// Find the first pool with at least 1% of the total network hashrate
const firstSignificantPool = acceleratingPools.findIndex(pool => pools[pool].lastEstimatedHashrate > this.miningStats.lastEstimatedHashrate / 100);
const numSignificantPools = acceleratingPools.length - firstSignificantPool;
const lightenStep = acceleratingPools.length ? (0.48 / acceleratingPools.length) : 0;
acceleratingPools.forEach((poolId, index) => {
const pool = pools[poolId];
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(
pool.lastEstimatedHashrate,
color,
toRGB(lighten({ r: 147, g: 57, b: 244 }, index * lightenStep)),
`<b style="color: white">${pool.name} (${poolShare}%)</b>`,
true,
) as PieSeriesOption);
});
})
this.acceleratedByPercentage = ((totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1) + '%';
const notAcceleratedByPercentage = ((1 - (totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate)) * 100).toFixed(1) + '%';
data.push(getDataItem(
@ -141,7 +128,6 @@ export class ActiveAccelerationBox implements OnChanges {
}
]
};
this.cd.markForCheck();
}
onChartInit(ec) {
@ -154,4 +140,4 @@ export class ActiveAccelerationBox implements OnChanges {
onToggleCpfp(): void {
this.toggleCpfp.emit();
}
}
}

View File

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

View File

@ -1,5 +0,0 @@
<div class="sparkles" #sparkleAnchor>
<div *ngFor="let sparkle of sparkles" class="sparkle" [style]="sparkle.style">
<span class="inner-sparkle" [style]="sparkle.rotation">+</span>
</div>
</div>

View File

@ -1,45 +0,0 @@
.sparkles {
position: absolute;
top: var(--block-size);
height: 50px;
right: 0;
}
.sparkle {
position: absolute;
color: rgba(152, 88, 255, 0.75);
opacity: 0;
transform: scale(0.8) rotate(0deg);
animation: pop ease 2000ms forwards, sparkle ease 500ms infinite;
}
.inner-sparkle {
display: block;
}
@keyframes pop {
0% {
transform: scale(0.8) rotate(0deg);
opacity: 0;
}
20% {
transform: scale(1) rotate(72deg);
opacity: 1;
}
100% {
transform: scale(0) rotate(360deg);
opacity: 0;
}
}
@keyframes sparkle {
0% {
color: rgba(152, 88, 255, 0.75);
}
50% {
color: rgba(198, 162, 255, 0.75);
}
100% {
color: rgba(152, 88, 255, 0.75);
}
}

View File

@ -1,73 +0,0 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core';
@Component({
selector: 'app-acceleration-sparkles',
templateUrl: './acceleration-sparkles.component.html',
styleUrls: ['./acceleration-sparkles.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AccelerationSparklesComponent implements OnChanges {
@Input() arrow: ElementRef<HTMLDivElement>;
@Input() run: boolean = false;
@ViewChild('sparkleAnchor')
sparkleAnchor: ElementRef<HTMLDivElement>;
constructor(
private cd: ChangeDetectorRef,
) {}
endTimeout: any;
lastSparkle: number = 0;
sparkleWidth: number = 0;
sparkles: any[] = [];
ngOnChanges(changes: SimpleChanges): void {
if (changes.run) {
if (this.endTimeout) {
clearTimeout(this.endTimeout);
this.endTimeout = null;
}
if (this.run) {
this.doSparkle();
} else {
this.endTimeout = setTimeout(() => {
this.sparkles = [];
}, 2000);
}
}
}
doSparkle(): void {
if (this.run) {
const now = performance.now();
if (now - this.lastSparkle > 20) {
this.lastSparkle = now;
if (this.arrow?.nativeElement && this.sparkleAnchor?.nativeElement) {
const anchor = this.sparkleAnchor.nativeElement.getBoundingClientRect().right;
const right = this.arrow.nativeElement.getBoundingClientRect().right;
const dx = (anchor - right) + 30;
const numSparkles = Math.ceil(Math.random() * 3);
for (let i = 0; i < numSparkles; i++) {
this.sparkles.push({
style: {
right: (dx + (Math.random() * 10)) + 'px',
top: (15 + (Math.random() * 30)) + 'px',
},
rotation: {
transform: `rotate(${Math.random() * 360}deg)`,
}
});
}
while (this.sparkles.length > 200) {
this.sparkles.shift();
}
this.cd.markForCheck();
}
}
requestAnimationFrame(() => {
this.doSparkle();
});
}
}
}

View File

@ -1,15 +1,16 @@
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 { catchError, map, switchMap, tap } from 'rxjs/operators';
import { AddressTxSummary, ChainStats } from '@interfaces/electrs.interface';
import { ElectrsApiService } from '@app/services/electrs-api.service';
import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe';
import { AddressTxSummary, ChainStats } from '../../interfaces/electrs.interface';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
import { Router } from '@angular/router';
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
import { StateService } from '@app/services/state.service';
import { PriceService } from '@app/services/price.service';
import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { StateService } from '../../services/state.service';
import { PriceService } from '../../services/price.service';
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe';
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
const periodSeconds = {
'1d': (60 * 60 * 24),
@ -44,18 +45,14 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
@Input() right: number | string = 10;
@Input() left: number | string = 70;
@Input() widget: boolean = false;
@Input() defaultFiat: boolean = false;
@Input() showLegend: boolean = true;
@Input() showYAxis: boolean = true;
adjustedLeft: number;
adjustedRight: number;
data: any[] = [];
fiatData: any[] = [];
hoverData: any[] = [];
conversions: any;
allowZoom: boolean = false;
initialRight = this.right;
initialLeft = this.left;
selected = { [$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`]: true, 'Fiat': false };
subscription: Subscription;
@ -80,17 +77,15 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
private relativeUrlPipe: RelativeUrlPipe,
private priceService: PriceService,
private fiatCurrencyPipe: FiatCurrencyPipe,
private fiatShortenerPipe: FiatShortenerPipe,
private zone: NgZone,
) {}
ngOnChanges(changes: SimpleChanges): void {
this.isLoading = true;
if (!this.addressSummary$ && (!this.address || !this.stats)) {
if (!this.address || !this.stats) {
return;
}
if (changes.defaultFiat) {
this.selected['Fiat'] = !!this.defaultFiat;
}
if (changes.address || changes.isPubkey || changes.addressSummary$ || changes.stats) {
if (this.subscription) {
this.subscription.unsubscribe();
@ -123,7 +118,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
} else if (this.conversions && 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[]) {
if (!summary) {
if (!summary || !this.stats) {
return;
}
const total = this.stats ? (this.stats.funded_txo_sum - this.stats.spent_txo_sum) : summary.reduce((acc, tx) => acc + tx.value, 0);
let runningTotal = total;
let total = (this.stats.funded_txo_sum - this.stats.spent_txo_sum);
const processData = summary.map(d => {
const balance = runningTotal;
const fiatBalance = runningTotal * d.price / 100_000_000;
runningTotal -= d.value;
const balance = total;
const fiatBalance = total * d.price / 100_000_000;
total -= d.value;
return {
time: d.time * 1000,
balance,
@ -166,7 +160,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
d
};
}).reverse();
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]);
@ -178,15 +172,12 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
this.fiatData = this.fiatData.filter(d => d[0] >= startFiat);
}
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 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 = {
color: [
new echarts.graphic.LinearGradient(0, 0, 0, 1, [
@ -202,10 +193,10 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
grid: {
top: 20,
bottom: this.allowZoom ? 65 : 20,
right: this.adjustedRight,
left: this.adjustedLeft,
right: this.right,
left: this.left,
},
legend: (this.showLegend && !this.stateService.isAnyTestnet()) ? {
legend: !this.stateService.isAnyTestnet() ? {
data: [
{
name: $localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`,
@ -253,22 +244,21 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
let tooltip = '<div>';
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) {
const header = data.length === 1
? `${data[0].data[2].txid.slice(0, 6)}...${data[0].data[2].txid.slice(-6)}`
: `${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 formatFiat = (val) => this.fiatCurrencyPipe.transform(val, null, 'USD');
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 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;
}.bind(this)
},
@ -316,26 +306,22 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
type: 'value',
position: 'left',
axisLabel: {
show: this.showYAxis,
color: 'rgb(110, 112, 121)',
formatter: (val): string => {
let valSpan = maxValue - (this.period === 'all' ? 0 : minValue);
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) {
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) {
return `${(val / 100_000_000).toFixed(1)} BTC`;
} else if (valSpan > 10_000_000) {
return `${(val / 100_000_000).toFixed(2)} BTC`;
} 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`;
} 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',
axisLabel: {
show: this.showYAxis,
color: 'rgb(110, 112, 121)',
formatter: function(val) {
return `$${this.amountShortenerPipe.transform(val, 3, undefined, true, true)}`;
return this.fiatShortenerPipe.transform(val, null, 'USD');
}.bind(this)
},
splitLine: {
@ -404,8 +389,8 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
type: 'slider',
brushSelect: false,
realtime: true,
left: this.adjustedLeft,
right: this.adjustedRight,
left: this.left,
right: this.right,
selectedDataBackground: {
lineStyle: {
color: '#fff',
@ -418,7 +403,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
onChartClick(e) {
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}`);
if (e.event.event.shiftKey || e.event.event.ctrlKey || e.event.event.metaKey) {
window.open(url);
@ -435,26 +420,26 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
onLegendSelectChanged(e) {
this.selected = e.selected;
this.adjustedRight = this.selected['Fiat'] ? +this.right + 40 : +this.right;
this.adjustedLeft = this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`] ? +this.left : +this.left - 40;
this.right = this.selected['Fiat'] ? +this.initialRight + 40 : this.initialRight;
this.left = this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`] ? this.initialLeft : +this.initialLeft - 40;
this.chartOptions = {
grid: {
right: this.adjustedRight,
left: this.adjustedLeft,
right: this.right,
left: this.left,
},
legend: {
selected: this.selected,
},
dataZoom: this.allowZoom ? [{
left: this.adjustedLeft,
right: this.adjustedRight,
left: this.left,
right: this.right,
}, {
left: this.adjustedLeft,
right: this.adjustedRight,
left: this.left,
right: this.right,
}] : undefined
};
if (this.chartInstance) {
this.chartInstance.setOption(this.chartOptions);
}
@ -478,30 +463,25 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
}
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
extendedSummary.unshift({ time: Date.now() / 1000, value: 0 });
let maxTime = Date.now() / 1000;
const oneHour = 60 * 60;
extendedSummary.reverse();
let oneHour = 60 * 60;
// Fill gaps longer than interval
for (let i = 0; i < extendedSummary.length - 1; i++) {
if (extendedSummary[i].time > maxTime) {
extendedSummary[i].time = maxTime - 30;
}
maxTime = extendedSummary[i].time;
const hours = Math.floor((extendedSummary[i].time - extendedSummary[i + 1].time) / oneHour);
let hours = Math.floor((extendedSummary[i + 1].time - extendedSummary[i].time) / oneHour);
if (hours > 1) {
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 });
}
i += hours - 1;
}
}
return extendedSummary;
return extendedSummary.reverse();
}
}

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