diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b2d34bb03..51ddb7855 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -9,7 +9,7 @@ jobs:
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
strategy:
matrix:
- node: ["18", "20"]
+ node: ["20", "21"]
flavor: ["dev", "prod"]
fail-fast: false
runs-on: "ubuntu-latest"
@@ -160,7 +160,7 @@ jobs:
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
strategy:
matrix:
- node: ["18", "20"]
+ node: ["20", "21"]
flavor: ["dev", "prod"]
fail-fast: false
runs-on: "ubuntu-latest"
diff --git a/.github/workflows/get_backend_block_height.yml b/.github/workflows/get_backend_block_height.yml
new file mode 100644
index 000000000..52f3b038c
--- /dev/null
+++ b/.github/workflows/get_backend_block_height.yml
@@ -0,0 +1,19 @@
+name: 'Check if servers are in sync'
+
+on: [workflow_dispatch]
+
+jobs:
+ print-backend-sha:
+ runs-on: 'ubuntu-latest'
+ name: Get block height
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ path: repo
+
+ - name: Run script
+ working-directory: repo
+ run: |
+ chmod +x ./scripts/get_block_tip_height.sh
+ sh ./scripts/get_block_tip_height.sh
diff --git a/Cargo.lock b/Cargo.lock
index 30a0d97ab..0b51ea544 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -57,9 +57,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1586fa608b1dab41f667475b4a41faec5ba680aee428bfa5de4ea520fdc6e901"
dependencies = [
"quote",
- "syn 2.0.20",
+ "syn",
]
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
[[package]]
name = "gbt"
version = "1.0.0"
@@ -71,15 +77,15 @@ dependencies = [
"napi-derive",
"priority-queue",
"tracing",
- "tracing-log",
+ "tracing-log 0.2.0",
"tracing-subscriber",
]
[[package]]
name = "hashbrown"
-version = "0.12.3"
+version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
[[package]]
name = "hermit-abi"
@@ -92,11 +98,11 @@ dependencies = [
[[package]]
name = "indexmap"
-version = "1.9.3"
+version = "2.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4"
dependencies = [
- "autocfg",
+ "equivalent",
"hashbrown",
]
@@ -114,12 +120,12 @@ checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b"
[[package]]
name = "libloading"
-version = "0.7.4"
+version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
+checksum = "2caa5afb8bf9f3a2652760ce7d4f62d21c4d5a423e68466fca30df82f2330164"
dependencies = [
"cfg-if",
- "winapi",
+ "windows-targets",
]
[[package]]
@@ -145,9 +151,9 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "napi"
-version = "2.13.2"
+version = "2.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ede2d12cd6fce44da537a4be1f5510c73be2506c2e32dfaaafd1f36968f3a0e"
+checksum = "54a63d0570e4c3e0daf7a8d380563610e159f538e20448d6c911337246f40e84"
dependencies = [
"bitflags",
"ctor",
@@ -159,29 +165,29 @@ dependencies = [
[[package]]
name = "napi-build"
-version = "2.0.1"
+version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "882a73d9ef23e8dc2ebbffb6a6ae2ef467c0f18ac10711e4cc59c5485d41df0e"
+checksum = "2f9130fccc5f763cf2069b34a089a18f0d0883c66aceb81f2fad541a3d823c43"
[[package]]
name = "napi-derive"
-version = "2.13.0"
+version = "2.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da1c6a8fa84d549aa8708fcd062372bf8ec6e849de39016ab921067d21bde367"
+checksum = "05bb7c37e3c1dda9312fdbe4a9fc7507fca72288ba154ec093e2d49114e727ce"
dependencies = [
"cfg-if",
"convert_case",
"napi-derive-backend",
"proc-macro2",
"quote",
- "syn 1.0.109",
+ "syn",
]
[[package]]
name = "napi-derive-backend"
-version = "1.0.52"
+version = "1.0.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "20bbc7c69168d06a848f925ec5f0e0997f98e8c8d4f2cc30157f0da51c009e17"
+checksum = "f785a8b8d7b83e925f5aa6d2ae3c159d17fe137ac368dc185bef410e7acdaeb4"
dependencies = [
"convert_case",
"once_cell",
@@ -189,14 +195,14 @@ dependencies = [
"quote",
"regex",
"semver",
- "syn 1.0.109",
+ "syn",
]
[[package]]
name = "napi-sys"
-version = "2.2.3"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "166b5ef52a3ab5575047a9fe8d4a030cdd0f63c96f071cd6907674453b07bae3"
+checksum = "2503fa6af34dc83fb74888df8b22afe933b58d37daf7d80424b1c60c68196b8b"
dependencies = [
"libloading",
]
@@ -223,9 +229,9 @@ dependencies = [
[[package]]
name = "once_cell"
-version = "1.18.0"
+version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "overload"
@@ -241,11 +247,12 @@ checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
[[package]]
name = "priority-queue"
-version = "1.3.2"
+version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fff39edfcaec0d64e8d0da38564fad195d2d51b680940295fcc307366e101e61"
+checksum = "509354d8a769e8d0b567d6821b84495c60213162761a732d68ce87c964bd347f"
dependencies = [
"autocfg",
+ "equivalent",
"indexmap",
]
@@ -320,17 +327,6 @@ version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
-[[package]]
-name = "syn"
-version = "1.0.109"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
-]
-
[[package]]
name = "syn"
version = "2.0.20"
@@ -384,7 +380,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.20",
+ "syn",
]
[[package]]
@@ -408,6 +404,17 @@ dependencies = [
"tracing-core",
]
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
[[package]]
name = "tracing-subscriber"
version = "0.3.17"
@@ -423,7 +430,7 @@ dependencies = [
"thread_local",
"tracing",
"tracing-core",
- "tracing-log",
+ "tracing-log 0.1.3",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 59562297c..2f70699f4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,4 +1,5 @@
[workspace]
+resolver = "2"
members = [
"./backend/rust-gbt",
]
diff --git a/backend/mempool-config.sample.json b/backend/mempool-config.sample.json
index 3c2fccfb7..5d2cf1fba 100644
--- a/backend/mempool-config.sample.json
+++ b/backend/mempool-config.sample.json
@@ -1,5 +1,6 @@
{
"MEMPOOL": {
+ "OFFICIAL": false,
"NETWORK": "mainnet",
"BACKEND": "electrum",
"ENABLED": true,
diff --git a/backend/package-lock.json b/backend/package-lock.json
index db71881cc..95a949ef5 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -9,7 +9,7 @@
"version": "3.0.0-dev",
"license": "GNU Affero General Public License v3.0",
"dependencies": {
- "@babel/core": "^7.23.2",
+ "@babel/core": "^7.24.0",
"@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3",
"axios": "~1.6.1",
@@ -26,7 +26,7 @@
},
"devDependencies": {
"@babel/code-frame": "^7.18.6",
- "@babel/core": "^7.23.2",
+ "@babel/core": "^7.24.0",
"@types/compression": "^1.7.2",
"@types/crypto-js": "^4.1.1",
"@types/express": "^4.17.17",
@@ -65,12 +65,12 @@
}
},
"node_modules/@babel/code-frame": {
- "version": "7.22.13",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
- "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
+ "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
"dev": true,
"dependencies": {
- "@babel/highlight": "^7.22.13",
+ "@babel/highlight": "^7.23.4",
"chalk": "^2.4.2"
},
"engines": {
@@ -78,30 +78,30 @@
}
},
"node_modules/@babel/compat-data": {
- "version": "7.23.2",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.2.tgz",
- "integrity": "sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz",
+ "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core": {
- "version": "7.23.2",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz",
- "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==",
+ "version": "7.24.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz",
+ "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==",
"dev": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
- "@babel/code-frame": "^7.22.13",
- "@babel/generator": "^7.23.0",
- "@babel/helper-compilation-targets": "^7.22.15",
- "@babel/helper-module-transforms": "^7.23.0",
- "@babel/helpers": "^7.23.2",
- "@babel/parser": "^7.23.0",
- "@babel/template": "^7.22.15",
- "@babel/traverse": "^7.23.2",
- "@babel/types": "^7.23.0",
+ "@babel/code-frame": "^7.23.5",
+ "@babel/generator": "^7.23.6",
+ "@babel/helper-compilation-targets": "^7.23.6",
+ "@babel/helper-module-transforms": "^7.23.3",
+ "@babel/helpers": "^7.24.0",
+ "@babel/parser": "^7.24.0",
+ "@babel/template": "^7.24.0",
+ "@babel/traverse": "^7.24.0",
+ "@babel/types": "^7.24.0",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -123,12 +123,12 @@
"dev": true
},
"node_modules/@babel/generator": {
- "version": "7.23.0",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz",
- "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==",
+ "version": "7.23.6",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz",
+ "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==",
"dev": true,
"dependencies": {
- "@babel/types": "^7.23.0",
+ "@babel/types": "^7.23.6",
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
@@ -152,14 +152,14 @@
}
},
"node_modules/@babel/helper-compilation-targets": {
- "version": "7.22.15",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz",
- "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==",
+ "version": "7.23.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz",
+ "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==",
"dev": true,
"dependencies": {
- "@babel/compat-data": "^7.22.9",
- "@babel/helper-validator-option": "^7.22.15",
- "browserslist": "^4.21.9",
+ "@babel/compat-data": "^7.23.5",
+ "@babel/helper-validator-option": "^7.23.5",
+ "browserslist": "^4.22.2",
"lru-cache": "^5.1.1",
"semver": "^6.3.1"
},
@@ -214,9 +214,9 @@
}
},
"node_modules/@babel/helper-module-transforms": {
- "version": "7.23.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz",
- "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz",
+ "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==",
"dev": true,
"dependencies": {
"@babel/helper-environment-visitor": "^7.22.20",
@@ -266,9 +266,9 @@
}
},
"node_modules/@babel/helper-string-parser": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
- "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
+ "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==",
"dev": true,
"engines": {
"node": ">=6.9.0"
@@ -284,32 +284,32 @@
}
},
"node_modules/@babel/helper-validator-option": {
- "version": "7.22.15",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz",
- "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz",
+ "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helpers": {
- "version": "7.23.2",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz",
- "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==",
+ "version": "7.24.0",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.0.tgz",
+ "integrity": "sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==",
"dev": true,
"dependencies": {
- "@babel/template": "^7.22.15",
- "@babel/traverse": "^7.23.2",
- "@babel/types": "^7.23.0"
+ "@babel/template": "^7.24.0",
+ "@babel/traverse": "^7.24.0",
+ "@babel/types": "^7.24.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight": {
- "version": "7.22.20",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
- "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
+ "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
"dev": true,
"dependencies": {
"@babel/helper-validator-identifier": "^7.22.20",
@@ -321,9 +321,9 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.23.0",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
- "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
+ "version": "7.24.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz",
+ "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==",
"dev": true,
"bin": {
"parser": "bin/babel-parser.js"
@@ -510,34 +510,34 @@
}
},
"node_modules/@babel/template": {
- "version": "7.22.15",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
- "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
+ "version": "7.24.0",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz",
+ "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==",
"dev": true,
"dependencies": {
- "@babel/code-frame": "^7.22.13",
- "@babel/parser": "^7.22.15",
- "@babel/types": "^7.22.15"
+ "@babel/code-frame": "^7.23.5",
+ "@babel/parser": "^7.24.0",
+ "@babel/types": "^7.24.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
- "version": "7.23.2",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
- "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
+ "version": "7.24.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.0.tgz",
+ "integrity": "sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==",
"dev": true,
"dependencies": {
- "@babel/code-frame": "^7.22.13",
- "@babel/generator": "^7.23.0",
+ "@babel/code-frame": "^7.23.5",
+ "@babel/generator": "^7.23.6",
"@babel/helper-environment-visitor": "^7.22.20",
"@babel/helper-function-name": "^7.23.0",
"@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
- "@babel/parser": "^7.23.0",
- "@babel/types": "^7.23.0",
- "debug": "^4.1.0",
+ "@babel/parser": "^7.24.0",
+ "@babel/types": "^7.24.0",
+ "debug": "^4.3.1",
"globals": "^11.1.0"
},
"engines": {
@@ -545,12 +545,12 @@
}
},
"node_modules/@babel/types": {
- "version": "7.23.0",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
- "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
+ "version": "7.24.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz",
+ "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==",
"dev": true,
"dependencies": {
- "@babel/helper-string-parser": "^7.22.5",
+ "@babel/helper-string-parser": "^7.23.4",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
},
@@ -1500,9 +1500,9 @@
}
},
"node_modules/@napi-rs/cli": {
- "version": "2.16.1",
- "resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.16.1.tgz",
- "integrity": "sha512-L0Gr5iEQIDEbvWdDr1HUaBOxBSHL1VZhWSk1oryawoT8qJIY+KGfLFelU+Qma64ivCPbxYpkfPoKYVG3rcoGIA==",
+ "version": "2.18.0",
+ "resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.18.0.tgz",
+ "integrity": "sha512-lfSRT7cs3iC4L+kv9suGYQEezn5Nii7Kpu+THsYVI0tA1Vh59LH45p4QADaD7hvIkmOz79eEGtoKQ9nAkAPkzA==",
"bin": {
"napi": "scripts/index.js"
},
@@ -2594,9 +2594,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.22.1",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz",
- "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==",
+ "version": "4.23.0",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
+ "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
"dev": true,
"funding": [
{
@@ -2613,9 +2613,9 @@
}
],
"dependencies": {
- "caniuse-lite": "^1.0.30001541",
- "electron-to-chromium": "^1.4.535",
- "node-releases": "^2.0.13",
+ "caniuse-lite": "^1.0.30001587",
+ "electron-to-chromium": "^1.4.668",
+ "node-releases": "^2.0.14",
"update-browserslist-db": "^1.0.13"
},
"bin": {
@@ -2708,9 +2708,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001547",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001547.tgz",
- "integrity": "sha512-W7CrtIModMAxobGhz8iXmDfuJiiKg1WADMO/9x7/CLNin5cpSbuBjooyoIUVB5eyCc36QuTVlkVa1iB2S5+/eA==",
+ "version": "1.0.30001591",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz",
+ "integrity": "sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==",
"dev": true,
"funding": [
{
@@ -3031,9 +3031,9 @@
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
"node_modules/electron-to-chromium": {
- "version": "1.4.551",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.551.tgz",
- "integrity": "sha512-/Ng/W/kFv7wdEHYzxdK7Cv0BHEGSkSB3M0Ssl8Ndr1eMiYeas/+Mv4cNaDqamqWx6nd2uQZfPz6g25z25M/sdw==",
+ "version": "1.4.686",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.686.tgz",
+ "integrity": "sha512-3avY1B+vUzNxEgkBDpKOP8WarvUAEwpRaiCL0He5OKWEFxzaOFiq4WoZEZe7qh0ReS7DiWoHMnYoQCKxNZNzSg==",
"dev": true
},
"node_modules/emittery": {
@@ -6192,9 +6192,9 @@
"dev": true
},
"node_modules/node-releases": {
- "version": "2.0.13",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
- "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==",
+ "version": "2.0.14",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
+ "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
"dev": true
},
"node_modules/normalize-path": {
@@ -7670,7 +7670,7 @@
"version": "3.0.1",
"hasInstallScript": true,
"dependencies": {
- "@napi-rs/cli": "2.16.1"
+ "@napi-rs/cli": "2.18.0"
},
"engines": {
"node": ">= 12"
@@ -7695,37 +7695,37 @@
}
},
"@babel/code-frame": {
- "version": "7.22.13",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
- "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
+ "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
"dev": true,
"requires": {
- "@babel/highlight": "^7.22.13",
+ "@babel/highlight": "^7.23.4",
"chalk": "^2.4.2"
}
},
"@babel/compat-data": {
- "version": "7.23.2",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.2.tgz",
- "integrity": "sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz",
+ "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==",
"dev": true
},
"@babel/core": {
- "version": "7.23.2",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz",
- "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==",
+ "version": "7.24.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz",
+ "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==",
"dev": true,
"requires": {
"@ampproject/remapping": "^2.2.0",
- "@babel/code-frame": "^7.22.13",
- "@babel/generator": "^7.23.0",
- "@babel/helper-compilation-targets": "^7.22.15",
- "@babel/helper-module-transforms": "^7.23.0",
- "@babel/helpers": "^7.23.2",
- "@babel/parser": "^7.23.0",
- "@babel/template": "^7.22.15",
- "@babel/traverse": "^7.23.2",
- "@babel/types": "^7.23.0",
+ "@babel/code-frame": "^7.23.5",
+ "@babel/generator": "^7.23.6",
+ "@babel/helper-compilation-targets": "^7.23.6",
+ "@babel/helper-module-transforms": "^7.23.3",
+ "@babel/helpers": "^7.24.0",
+ "@babel/parser": "^7.24.0",
+ "@babel/template": "^7.24.0",
+ "@babel/traverse": "^7.24.0",
+ "@babel/types": "^7.24.0",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -7742,12 +7742,12 @@
}
},
"@babel/generator": {
- "version": "7.23.0",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz",
- "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==",
+ "version": "7.23.6",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz",
+ "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==",
"dev": true,
"requires": {
- "@babel/types": "^7.23.0",
+ "@babel/types": "^7.23.6",
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
@@ -7767,14 +7767,14 @@
}
},
"@babel/helper-compilation-targets": {
- "version": "7.22.15",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz",
- "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==",
+ "version": "7.23.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz",
+ "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==",
"dev": true,
"requires": {
- "@babel/compat-data": "^7.22.9",
- "@babel/helper-validator-option": "^7.22.15",
- "browserslist": "^4.21.9",
+ "@babel/compat-data": "^7.23.5",
+ "@babel/helper-validator-option": "^7.23.5",
+ "browserslist": "^4.22.2",
"lru-cache": "^5.1.1",
"semver": "^6.3.1"
}
@@ -7814,9 +7814,9 @@
}
},
"@babel/helper-module-transforms": {
- "version": "7.23.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz",
- "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz",
+ "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==",
"dev": true,
"requires": {
"@babel/helper-environment-visitor": "^7.22.20",
@@ -7851,9 +7851,9 @@
}
},
"@babel/helper-string-parser": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
- "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
+ "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==",
"dev": true
},
"@babel/helper-validator-identifier": {
@@ -7863,26 +7863,26 @@
"dev": true
},
"@babel/helper-validator-option": {
- "version": "7.22.15",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz",
- "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz",
+ "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==",
"dev": true
},
"@babel/helpers": {
- "version": "7.23.2",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz",
- "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==",
+ "version": "7.24.0",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.0.tgz",
+ "integrity": "sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==",
"dev": true,
"requires": {
- "@babel/template": "^7.22.15",
- "@babel/traverse": "^7.23.2",
- "@babel/types": "^7.23.0"
+ "@babel/template": "^7.24.0",
+ "@babel/traverse": "^7.24.0",
+ "@babel/types": "^7.24.0"
}
},
"@babel/highlight": {
- "version": "7.22.20",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
- "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
+ "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.22.20",
@@ -7891,9 +7891,9 @@
}
},
"@babel/parser": {
- "version": "7.23.0",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
- "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
+ "version": "7.24.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz",
+ "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==",
"dev": true
},
"@babel/plugin-syntax-async-generators": {
@@ -8023,41 +8023,41 @@
}
},
"@babel/template": {
- "version": "7.22.15",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
- "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
+ "version": "7.24.0",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz",
+ "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==",
"dev": true,
"requires": {
- "@babel/code-frame": "^7.22.13",
- "@babel/parser": "^7.22.15",
- "@babel/types": "^7.22.15"
+ "@babel/code-frame": "^7.23.5",
+ "@babel/parser": "^7.24.0",
+ "@babel/types": "^7.24.0"
}
},
"@babel/traverse": {
- "version": "7.23.2",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
- "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
+ "version": "7.24.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.0.tgz",
+ "integrity": "sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==",
"dev": true,
"requires": {
- "@babel/code-frame": "^7.22.13",
- "@babel/generator": "^7.23.0",
+ "@babel/code-frame": "^7.23.5",
+ "@babel/generator": "^7.23.6",
"@babel/helper-environment-visitor": "^7.22.20",
"@babel/helper-function-name": "^7.23.0",
"@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
- "@babel/parser": "^7.23.0",
- "@babel/types": "^7.23.0",
- "debug": "^4.1.0",
+ "@babel/parser": "^7.24.0",
+ "@babel/types": "^7.24.0",
+ "debug": "^4.3.1",
"globals": "^11.1.0"
}
},
"@babel/types": {
- "version": "7.23.0",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
- "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
+ "version": "7.24.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz",
+ "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==",
"dev": true,
"requires": {
- "@babel/helper-string-parser": "^7.22.5",
+ "@babel/helper-string-parser": "^7.23.4",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
}
@@ -8775,9 +8775,9 @@
"integrity": "sha512-mlvPiCzUlaETpYW3i6V87A24jjMYgsebaXtUo3WQyyLnYUuxs0KiXQ2mnKh3h15j8Xg/hfxeGIi+5OC9u0nftQ=="
},
"@napi-rs/cli": {
- "version": "2.16.1",
- "resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.16.1.tgz",
- "integrity": "sha512-L0Gr5iEQIDEbvWdDr1HUaBOxBSHL1VZhWSk1oryawoT8qJIY+KGfLFelU+Qma64ivCPbxYpkfPoKYVG3rcoGIA=="
+ "version": "2.18.0",
+ "resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.18.0.tgz",
+ "integrity": "sha512-lfSRT7cs3iC4L+kv9suGYQEezn5Nii7Kpu+THsYVI0tA1Vh59LH45p4QADaD7hvIkmOz79eEGtoKQ9nAkAPkzA=="
},
"@noble/hashes": {
"version": "1.3.0",
@@ -9633,14 +9633,14 @@
}
},
"browserslist": {
- "version": "4.22.1",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz",
- "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==",
+ "version": "4.23.0",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
+ "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
"dev": true,
"requires": {
- "caniuse-lite": "^1.0.30001541",
- "electron-to-chromium": "^1.4.535",
- "node-releases": "^2.0.13",
+ "caniuse-lite": "^1.0.30001587",
+ "electron-to-chromium": "^1.4.668",
+ "node-releases": "^2.0.14",
"update-browserslist-db": "^1.0.13"
}
},
@@ -9712,9 +9712,9 @@
"dev": true
},
"caniuse-lite": {
- "version": "1.0.30001547",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001547.tgz",
- "integrity": "sha512-W7CrtIModMAxobGhz8iXmDfuJiiKg1WADMO/9x7/CLNin5cpSbuBjooyoIUVB5eyCc36QuTVlkVa1iB2S5+/eA==",
+ "version": "1.0.30001591",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz",
+ "integrity": "sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==",
"dev": true
},
"chalk": {
@@ -9942,9 +9942,9 @@
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
"electron-to-chromium": {
- "version": "1.4.551",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.551.tgz",
- "integrity": "sha512-/Ng/W/kFv7wdEHYzxdK7Cv0BHEGSkSB3M0Ssl8Ndr1eMiYeas/+Mv4cNaDqamqWx6nd2uQZfPz6g25z25M/sdw==",
+ "version": "1.4.686",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.686.tgz",
+ "integrity": "sha512-3avY1B+vUzNxEgkBDpKOP8WarvUAEwpRaiCL0He5OKWEFxzaOFiq4WoZEZe7qh0ReS7DiWoHMnYoQCKxNZNzSg==",
"dev": true
},
"emittery": {
@@ -12298,9 +12298,9 @@
"dev": true
},
"node-releases": {
- "version": "2.0.13",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
- "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==",
+ "version": "2.0.14",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
+ "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
"dev": true
},
"normalize-path": {
@@ -12703,7 +12703,7 @@
"rust-gbt": {
"version": "file:rust-gbt",
"requires": {
- "@napi-rs/cli": "2.16.1"
+ "@napi-rs/cli": "2.18.0"
}
},
"safe-buffer": {
diff --git a/backend/package.json b/backend/package.json
index c42f455e5..640250a1f 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -39,7 +39,7 @@
"rust-build": "npm run rust-clean && cd rust-gbt && npm run build-release"
},
"dependencies": {
- "@babel/core": "^7.23.2",
+ "@babel/core": "^7.24.0",
"@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3",
"axios": "~1.6.1",
@@ -56,7 +56,7 @@
},
"devDependencies": {
"@babel/code-frame": "^7.18.6",
- "@babel/core": "^7.23.2",
+ "@babel/core": "^7.24.0",
"@types/compression": "^1.7.2",
"@types/crypto-js": "^4.1.1",
"@types/express": "^4.17.17",
diff --git a/backend/rust-gbt/Cargo.toml b/backend/rust-gbt/Cargo.toml
index 4d0a5b45d..10c572bf9 100644
--- a/backend/rust-gbt/Cargo.toml
+++ b/backend/rust-gbt/Cargo.toml
@@ -12,14 +12,14 @@ crate-type = ["cdylib"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-priority-queue = "1.3.2"
+priority-queue = "2.0.2"
bytes = "1.4.0"
-napi = { version = "2.13.2", features = ["napi8", "tokio_rt"] }
-napi-derive = "2.13.0"
+napi = { version = "2.16.0", features = ["napi8", "tokio_rt"] }
+napi-derive = "2.16.0"
bytemuck = "1.13.1"
tracing = "0.1.36"
-tracing-log = "0.1.3"
+tracing-log = "0.2.0"
tracing-subscriber = { version = "0.3.15", features = ["env-filter"]}
[build-dependencies]
-napi-build = "2.0.1"
+napi-build = "2.1.2"
diff --git a/backend/rust-gbt/index.js b/backend/rust-gbt/index.js
index 8680501d1..dd58a8b76 100644
--- a/backend/rust-gbt/index.js
+++ b/backend/rust-gbt/index.js
@@ -237,6 +237,49 @@ switch (platform) {
loadError = e
}
break
+ case 'riscv64':
+ if (isMusl()) {
+ localFileExisted = existsSync(
+ join(__dirname, 'gbt.linux-riscv64-musl.node')
+ )
+ try {
+ if (localFileExisted) {
+ nativeBinding = require('./gbt.linux-riscv64-musl.node')
+ } else {
+ nativeBinding = require('gbt-linux-riscv64-musl')
+ }
+ } catch (e) {
+ loadError = e
+ }
+ } else {
+ localFileExisted = existsSync(
+ join(__dirname, 'gbt.linux-riscv64-gnu.node')
+ )
+ try {
+ if (localFileExisted) {
+ nativeBinding = require('./gbt.linux-riscv64-gnu.node')
+ } else {
+ nativeBinding = require('gbt-linux-riscv64-gnu')
+ }
+ } catch (e) {
+ loadError = e
+ }
+ }
+ break
+ case 's390x':
+ localFileExisted = existsSync(
+ join(__dirname, 'gbt.linux-s390x-gnu.node')
+ )
+ try {
+ if (localFileExisted) {
+ nativeBinding = require('./gbt.linux-s390x-gnu.node')
+ } else {
+ nativeBinding = require('gbt-linux-s390x-gnu')
+ }
+ } catch (e) {
+ loadError = e
+ }
+ break
default:
throw new Error(`Unsupported architecture on Linux: ${arch}`)
}
diff --git a/backend/rust-gbt/package-lock.json b/backend/rust-gbt/package-lock.json
index ab3d72e52..e351c82f8 100644
--- a/backend/rust-gbt/package-lock.json
+++ b/backend/rust-gbt/package-lock.json
@@ -9,16 +9,16 @@
"version": "3.0.1",
"hasInstallScript": true,
"dependencies": {
- "@napi-rs/cli": "2.16.1"
+ "@napi-rs/cli": "2.18.0"
},
"engines": {
"node": ">= 12"
}
},
"node_modules/@napi-rs/cli": {
- "version": "2.16.1",
- "resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.16.1.tgz",
- "integrity": "sha512-L0Gr5iEQIDEbvWdDr1HUaBOxBSHL1VZhWSk1oryawoT8qJIY+KGfLFelU+Qma64ivCPbxYpkfPoKYVG3rcoGIA==",
+ "version": "2.18.0",
+ "resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.18.0.tgz",
+ "integrity": "sha512-lfSRT7cs3iC4L+kv9suGYQEezn5Nii7Kpu+THsYVI0tA1Vh59LH45p4QADaD7hvIkmOz79eEGtoKQ9nAkAPkzA==",
"bin": {
"napi": "scripts/index.js"
},
diff --git a/backend/rust-gbt/package.json b/backend/rust-gbt/package.json
index aa98313ed..b0dd96698 100644
--- a/backend/rust-gbt/package.json
+++ b/backend/rust-gbt/package.json
@@ -25,7 +25,7 @@
}
},
"dependencies": {
- "@napi-rs/cli": "2.16.1"
+ "@napi-rs/cli": "2.18.0"
},
"engines": {
"node": ">= 12"
diff --git a/backend/src/__fixtures__/mempool-config.template.json b/backend/src/__fixtures__/mempool-config.template.json
index 9ee2bd0bc..26ae6fb28 100644
--- a/backend/src/__fixtures__/mempool-config.template.json
+++ b/backend/src/__fixtures__/mempool-config.template.json
@@ -1,6 +1,7 @@
{
"MEMPOOL": {
"ENABLED": true,
+ "OFFICIAL": false,
"NETWORK": "__MEMPOOL_NETWORK__",
"BACKEND": "__MEMPOOL_BACKEND__",
"BLOCKS_SUMMARIES_INDEXING": true,
@@ -79,7 +80,8 @@
"USERNAME": "__DATABASE_USERNAME__",
"PASSWORD": "__DATABASE_PASSWORD__",
"PID_DIR": "__DATABASE_PID_FILE__",
- "TIMEOUT": 3000
+ "TIMEOUT": 3000,
+ "POOL_SIZE": 100
},
"SYSLOG": {
"ENABLED": false,
diff --git a/backend/src/__tests__/config.test.ts b/backend/src/__tests__/config.test.ts
index 6af0ce32f..5066e0ef7 100644
--- a/backend/src/__tests__/config.test.ts
+++ b/backend/src/__tests__/config.test.ts
@@ -14,6 +14,7 @@ describe('Mempool Backend Config', () => {
expect(config.MEMPOOL).toStrictEqual({
ENABLED: true,
+ OFFICIAL: false,
NETWORK: 'mainnet',
BACKEND: 'none',
BLOCKS_SUMMARIES_INDEXING: false,
@@ -93,7 +94,8 @@ describe('Mempool Backend Config', () => {
USERNAME: 'mempool',
PASSWORD: 'mempool',
TIMEOUT: 180000,
- PID_DIR: ''
+ PID_DIR: '',
+ POOL_SIZE: 100,
});
expect(config.SYSLOG).toStrictEqual({
diff --git a/backend/src/api/acceleration.ts b/backend/src/api/acceleration.ts
new file mode 100644
index 000000000..2b032d09f
--- /dev/null
+++ b/backend/src/api/acceleration.ts
@@ -0,0 +1,738 @@
+import logger from '../logger';
+import { MempoolTransactionExtended } from '../mempool.interfaces';
+import { IEsploraApi } from './bitcoin/esplora-api.interface';
+
+const BLOCK_WEIGHT_UNITS = 4_000_000;
+const BLOCK_SIGOPS = 80_000;
+const MAX_RELATIVE_GRAPH_SIZE = 200;
+const BID_BOOST_WINDOW = 40_000;
+const BID_BOOST_MIN_OFFSET = 10_000;
+const BID_BOOST_MAX_OFFSET = 400_000;
+
+type Acceleration = {
+ txid: string;
+ max_bid: number;
+};
+
+interface TxSummary {
+ txid: string; // txid of the current transaction
+ effectiveVsize: number; // Total vsize of the dependency tree
+ effectiveFee: number; // Total fee of the dependency tree in sats
+ ancestorCount: number; // Number of ancestors
+}
+
+export interface AccelerationInfo {
+ txSummary: TxSummary;
+ targetFeeRate: number; // target fee rate (recommended next block fee, or median fee for mined block)
+ nextBlockFee: number; // fee in sats required to be in the next block (using recommended next block fee, or median fee for mined block)
+ cost: number; // additional cost to accelerate ((cost + txSummary.effectiveFee) / txSummary.effectiveVsize) >= targetFeeRate
+}
+
+interface GraphTx {
+ txid: string;
+ vsize: number;
+ weight: number;
+ fees: {
+ base: number;
+ };
+ depends: string[];
+ spentby: string[];
+}
+
+interface MempoolTx extends GraphTx {
+ ancestorcount: number;
+ ancestorsize: number;
+ fees: {
+ base: number;
+ ancestor: number;
+ };
+
+ ancestors: Map,
+ ancestorRate: number;
+ individualRate: number;
+ score: number;
+}
+
+class AccelerationCosts {
+ /**
+ * Takes a list of accelerations and verbose block data
+ * Returns the "fair" boost rate to charge accelerations
+ *
+ * @param accelerationsx
+ * @param verboseBlock
+ */
+ public calculateBoostRate(accelerations: Acceleration[], blockTxs: IEsploraApi.Transaction[]): number {
+ // Run GBT ourselves to calculate accurate effective fee rates
+ // the list of transactions comes from a mined block, so we already know everything fits within consensus limits
+ const template = makeBlockTemplate(blockTxs, accelerations, 1, Infinity, Infinity);
+
+ // initialize working maps for fast tx lookups
+ const accMap = {};
+ const txMap = {};
+ for (const acceleration of accelerations) {
+ accMap[acceleration.txid] = acceleration;
+ }
+ for (const tx of template) {
+ txMap[tx.txid] = tx;
+ }
+
+ // Identify and exclude accelerated and otherwise prioritized transactions
+ const excludeMap = {};
+ let totalWeight = 0;
+ let minAcceleratedPackage = Infinity;
+ let lastEffectiveRate = 0;
+ // Iterate over the mined template from bottom to top.
+ // Transactions should appear in ascending order of mining priority.
+ for (const blockTx of [...blockTxs].reverse()) {
+ const txid = blockTx.txid;
+ const tx = txMap[txid];
+ totalWeight += tx.weight;
+ const isAccelerated = accMap[txid] != null;
+ // If a cluster has a in-band effective fee rate than the previous cluster,
+ // it must have been prioritized out-of-band (in order to have a higher mining priority)
+ // so exclude from the analysis.
+ const isPrioritized = tx.effectiveFeePerVsize < lastEffectiveRate;
+ if (isPrioritized || isAccelerated) {
+ let packageWeight = 0;
+ // exclude this whole CPFP cluster
+ for (const clusterTxid of tx.cluster) {
+ packageWeight += txMap[clusterTxid].weight;
+ if (!excludeMap[clusterTxid]) {
+ excludeMap[clusterTxid] = true;
+ }
+ }
+ // keep track of the smallest accelerated CPFP cluster for later
+ if (isAccelerated) {
+ minAcceleratedPackage = Math.min(minAcceleratedPackage, packageWeight);
+ }
+ }
+ if (!isPrioritized) {
+ if (!isAccelerated || !lastEffectiveRate) {
+ lastEffectiveRate = tx.effectiveFeePerVsize;
+ }
+ }
+ }
+
+ // The Bid Boost Rate is calculated by disregarding the bottom X weight units of the block,
+ // where X is the larger of BID_BOOST_MIN_OFFSET or the smallest accelerated package weight (the "offset"),
+ // then taking the average fee rate of the following BID_BOOST_WINDOW weight units
+ // (ignoring accelerated transactions and their ancestors).
+ //
+ // Transactions within the offset might pay less than the fair rate due to bin-packing effects
+ // But the average rate paid by the next chunk of non-accelerated transactions provides a good
+ // upper bound on the "next best rate" of alternatives to including the accelerated transactions
+ // (since, if there were any better options, they would have been included instead)
+ const spareWeight = BLOCK_WEIGHT_UNITS - totalWeight;
+ const windowOffset = Math.min(Math.max(minAcceleratedPackage, BID_BOOST_MIN_OFFSET, spareWeight), BID_BOOST_MAX_OFFSET);
+ const leftBound = windowOffset;
+ const rightBound = windowOffset + BID_BOOST_WINDOW;
+ let totalFeeInWindow = 0;
+ let totalWeightInWindow = Math.max(0, spareWeight - leftBound);
+ let txIndex = blockTxs.length - 1;
+ for (let offset = spareWeight; offset < BLOCK_WEIGHT_UNITS && txIndex >= 0; txIndex--) {
+ const txid = blockTxs[txIndex].txid;
+ const tx = txMap[txid];
+ if (excludeMap[txid]) {
+ // skip prioritized transactions and their ancestors
+ continue;
+ }
+
+ const left = offset;
+ const right = offset + tx.weight;
+ offset += tx.weight;
+ if (right < leftBound) {
+ // not within window yet
+ continue;
+ }
+ if (left > rightBound) {
+ // past window
+ break;
+ }
+ // count fees for weight units within the window
+ const overlapLeft = Math.max(leftBound, left);
+ const overlapRight = Math.min(rightBound, right);
+ const overlapUnits = overlapRight - overlapLeft;
+ totalFeeInWindow += (tx.effectiveFeePerVsize * (overlapUnits / 4));
+ totalWeightInWindow += overlapUnits;
+ }
+
+ if (totalWeightInWindow < BID_BOOST_WINDOW) {
+ // not enough un-prioritized transactions to calculate a fair rate
+ // just charge everyone their max bids
+ return Infinity;
+ }
+ // Divide the total fee by the size of the BID_BOOST_WINDOW in vbytes
+ const averageRate = totalFeeInWindow / (BID_BOOST_WINDOW / 4);
+ return averageRate;
+ }
+
+
+ /**
+ * Takes an accelerated mined txid and a target rate
+ * Returns the total vsize, fees and acceleration cost (in sats) of the tx and all same-block ancestors
+ *
+ * @param txid
+ * @param medianFeeRate
+ */
+ public getAccelerationInfo(tx: MempoolTransactionExtended, targetFeeRate: number, transactions: MempoolTransactionExtended[]): AccelerationInfo {
+ // Get same-block transaction ancestors
+ const allRelatives = this.getSameBlockRelatives(tx, transactions);
+ const relativesMap = this.initializeRelatives(allRelatives);
+ const rootTx = relativesMap.get(tx.txid) as MempoolTx;
+
+ // Calculate cost to boost
+ return this.calculateAccelerationAncestors(rootTx, relativesMap, targetFeeRate);
+ }
+
+ /**
+ * Takes a raw transaction, and builds a graph of same-block relatives,
+ * and returns as a MempoolTx
+ *
+ * @param tx
+ */
+ private getSameBlockRelatives(tx: MempoolTransactionExtended, transactions: MempoolTransactionExtended[]): Map {
+ const blockTxs = new Map(); // map of txs in this block
+ const spendMap = new Map(); // map of outpoints to spending txids
+ for (const tx of transactions) {
+ blockTxs.set(tx.txid, tx);
+ for (const vin of tx.vin) {
+ spendMap.set(`${vin.txid}:${vin.vout}`, tx.txid);
+ }
+ }
+
+ const relatives: Map = new Map();
+ const stack: string[] = [tx.txid];
+
+ // build set of same-block ancestors
+ while (stack.length > 0) {
+ const nextTxid = stack.pop();
+ const nextTx = nextTxid ? blockTxs.get(nextTxid) : null;
+ if (!nextTx || relatives.has(nextTx.txid)) {
+ continue;
+ }
+
+ const mempoolTx = this.convertToGraphTx(nextTx);
+
+ mempoolTx.fees.base = nextTx.fee || 0;
+ mempoolTx.depends = nextTx.vin.map(vin => vin.txid).filter(inTxid => inTxid && blockTxs.has(inTxid)) as string[];
+ mempoolTx.spentby = nextTx.vout.map((vout, index) => spendMap.get(`${nextTx.txid}:${index}`)).filter(outTxid => outTxid && blockTxs.has(outTxid)) as string[];
+
+ for (const txid of [...mempoolTx.depends, ...mempoolTx.spentby]) {
+ if (txid) {
+ stack.push(txid);
+ }
+ }
+
+ relatives.set(mempoolTx.txid, mempoolTx);
+ }
+
+ return relatives;
+ }
+
+ /**
+ * Takes a raw transaction and converts it to MempoolTx format
+ * fee and ancestor data is initialized with dummy/null values
+ *
+ * @param tx
+ */
+ private convertToGraphTx(tx: MempoolTransactionExtended): GraphTx {
+ return {
+ txid: tx.txid,
+ vsize: tx.vsize,
+ weight: tx.weight,
+ fees: {
+ base: 0, // dummy
+ },
+ depends: [], // dummy
+ spentby: [], //dummy
+ };
+ }
+
+ private convertGraphToMempoolTx(tx: GraphTx): MempoolTx {
+ return {
+ ...tx,
+ fees: {
+ base: tx.fees.base,
+ ancestor: tx.fees.base,
+ },
+ ancestorcount: 1,
+ ancestorsize: tx.vsize,
+ ancestors: new Map(),
+ ancestorRate: 0,
+ individualRate: 0,
+ score: 0,
+ };
+ }
+
+ /**
+ * Given a root transaction, a list of in-mempool ancestors, and a target fee rate,
+ * Calculate the minimum set of transactions to fee-bump, their total vsize + fees
+ *
+ * @param tx
+ * @param ancestors
+ */
+ private calculateAccelerationAncestors(tx: MempoolTx, relatives: Map, targetFeeRate: number): AccelerationInfo {
+ // add root tx to the ancestor map
+ relatives.set(tx.txid, tx);
+
+ // Check for high-sigop transactions (not supported)
+ relatives.forEach(entry => {
+ if (entry.vsize > Math.ceil(entry.weight / 4)) {
+ throw new Error(`high_sigop_tx`);
+ }
+ });
+
+ // Initialize individual & ancestor fee rates
+ relatives.forEach(entry => this.setAncestorScores(entry));
+
+ // Sort by descending ancestor score
+ let sortedRelatives = Array.from(relatives.values()).sort(this.mempoolComparator);
+
+ let includedInCluster: Map | null = null;
+
+ // While highest score >= targetFeeRate
+ let maxIterations = MAX_RELATIVE_GRAPH_SIZE;
+ while (sortedRelatives.length && sortedRelatives[0].score && sortedRelatives[0].score >= targetFeeRate && maxIterations > 0) {
+ maxIterations--;
+ // Grab the highest scoring entry
+ const best = sortedRelatives.shift();
+ if (best) {
+ const cluster = new Map(best.ancestors?.entries() || []);
+ if (best.ancestors.has(tx.txid)) {
+ includedInCluster = cluster;
+ }
+ cluster.set(best.txid, best);
+ // Remove this cluster (it already pays over the target rate, so doesn't need to be boosted)
+ // and update scores, ancestor totals and dependencies for the survivors
+ this.removeAncestors(cluster, relatives);
+
+ // re-sort
+ sortedRelatives = Array.from(relatives.values()).sort(this.mempoolComparator);
+ }
+ }
+
+ // sanity check for infinite loops / too many ancestors (should never happen)
+ if (maxIterations <= 0) {
+ logger.warn(`acceleration dependency calculation failed: calculateAccelerationAncestors loop exceeded ${MAX_RELATIVE_GRAPH_SIZE} iterations, unable to proceed`);
+ throw new Error('invalid_tx_dependencies');
+ }
+
+ let totalFee = Math.round(tx.fees.ancestor * 100_000_000);
+
+ // transaction is already CPFP-d above the target rate by some descendant
+ if (includedInCluster) {
+ let clusterSize = 0;
+ let clusterFee = 0;
+ includedInCluster.forEach(entry => {
+ clusterSize += entry.vsize;
+ clusterFee += (entry.fees.base * 100_000_000);
+ });
+ const clusterRate = clusterFee / clusterSize;
+ totalFee = Math.ceil(tx.ancestorsize * clusterRate);
+ }
+
+ // Whatever remains in the accelerated tx's dependencies needs to be boosted to the targetFeeRate
+ // Cost = (totalVsize * targetFeeRate) - totalFee
+ return {
+ txSummary: {
+ txid: tx.txid,
+ effectiveVsize: tx.ancestorsize,
+ effectiveFee: totalFee,
+ ancestorCount: tx.ancestorcount,
+ },
+ cost: Math.max(0, Math.ceil(tx.ancestorsize * targetFeeRate) - totalFee),
+ targetFeeRate,
+ nextBlockFee: Math.ceil(tx.ancestorsize * targetFeeRate),
+ };
+ }
+
+ /**
+ * Recursively traverses an in-mempool dependency graph, and sets a Map of in-mempool ancestors
+ * for each transaction.
+ *
+ * @param tx
+ * @param all
+ */
+ private setAncestors(tx: MempoolTx, all: Map, visited: Map>, depth: number = 0): Map {
+ // sanity check for infinite recursion / too many ancestors (should never happen)
+ if (depth >= 100) {
+ logger.warn('acceleration dependency calculation failed: setAncestors reached depth of 100, unable to proceed', `Accelerator`);
+ throw new Error('invalid_tx_dependencies');
+ }
+
+ // initialize the ancestor map for this tx
+ tx.ancestors = new Map();
+ tx.depends.forEach(parentId => {
+ const parent = all.get(parentId);
+ if (parent) {
+ // add the parent
+ tx.ancestors?.set(parentId, parent);
+ // check for a cached copy of this parent's ancestors
+ let ancestors = visited.get(parent.txid);
+ if (!ancestors) {
+ // recursively fetch the parent's ancestors
+ ancestors = this.setAncestors(parent, all, visited, depth + 1);
+ }
+ // and add to this tx's map
+ ancestors.forEach((ancestor, ancestorId) => {
+ tx.ancestors?.set(ancestorId, ancestor);
+ });
+ }
+ });
+ visited.set(tx.txid, tx.ancestors);
+
+ return tx.ancestors;
+ }
+
+ /**
+ * Efficiently sets a Map of in-mempool ancestors for each member of an expanded relative graph
+ * by running setAncestors on each leaf, and caching intermediate results.
+ * then initializes ancestor data for each transaction
+ *
+ * @param all
+ */
+ private initializeRelatives(all: Map): Map {
+ const mempoolTxs = new Map();
+ all.forEach(entry => {
+ mempoolTxs.set(entry.txid, this.convertGraphToMempoolTx(entry));
+ });
+ const visited: Map> = new Map();
+ const leaves: MempoolTx[] = Array.from(mempoolTxs.values()).filter(entry => entry.spentby.length === 0);
+ for (const leaf of leaves) {
+ this.setAncestors(leaf, mempoolTxs, visited);
+ }
+ mempoolTxs.forEach(entry => {
+ entry.ancestors?.forEach(ancestor => {
+ entry.ancestorcount++;
+ entry.ancestorsize += ancestor.vsize;
+ entry.fees.ancestor += ancestor.fees.base;
+ });
+ this.setAncestorScores(entry);
+ });
+ return mempoolTxs;
+ }
+
+ /**
+ * Remove a cluster of transactions from an in-mempool dependency graph
+ * and update the survivors' scores and ancestors
+ *
+ * @param cluster
+ * @param ancestors
+ */
+ private removeAncestors(cluster: Map, all: Map): void {
+ // remove
+ cluster.forEach(tx => {
+ all.delete(tx.txid);
+ });
+
+ // update survivors
+ all.forEach(tx => {
+ cluster.forEach(remove => {
+ if (tx.ancestors?.has(remove.txid)) {
+ // remove as dependency
+ tx.ancestors.delete(remove.txid);
+ tx.depends = tx.depends.filter(parent => parent !== remove.txid);
+ // update ancestor sizes and fees
+ tx.ancestorsize -= remove.vsize;
+ tx.fees.ancestor -= remove.fees.base;
+ }
+ });
+ // recalculate fee rates
+ this.setAncestorScores(tx);
+ });
+ }
+
+ /**
+ * Take a mempool transaction, and set the fee rates and ancestor score
+ *
+ * @param tx
+ */
+ private setAncestorScores(tx: MempoolTx): void {
+ tx.individualRate = (tx.fees.base * 100_000_000) / tx.vsize;
+ tx.ancestorRate = (tx.fees.ancestor * 100_000_000) / tx.ancestorsize;
+ tx.score = Math.min(tx.individualRate, tx.ancestorRate);
+ }
+
+ // Sort by descending score
+ private mempoolComparator(a, b): number {
+ return b.score - a.score;
+ }
+}
+
+export default new AccelerationCosts;
+
+interface TemplateTransaction {
+ txid: string;
+ order: number;
+ weight: number;
+ adjustedVsize: number; // sigop-adjusted vsize, rounded up to the nearest integer
+ sigops: number;
+ fee: number;
+ feeDelta: number;
+ ancestors: string[];
+ cluster: string[];
+ effectiveFeePerVsize: number;
+}
+
+interface MinerTransaction extends TemplateTransaction {
+ inputs: string[];
+ feePerVsize: number;
+ relativesSet: boolean;
+ ancestorMap: Map;
+ children: Set;
+ ancestorFee: number;
+ ancestorVsize: number;
+ ancestorSigops: number;
+ score: number;
+ used: boolean;
+ modified: boolean;
+ dependencyRate: number;
+}
+
+/*
+* Build a block using an approximation of the transaction selection algorithm from Bitcoin Core
+* (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp)
+*/
+function makeBlockTemplate(candidates: IEsploraApi.Transaction[], accelerations: Acceleration[], maxBlocks: number = 8, weightLimit: number = BLOCK_WEIGHT_UNITS, sigopLimit: number = BLOCK_SIGOPS): TemplateTransaction[] {
+ const auditPool: Map = new Map();
+ const mempoolArray: MinerTransaction[] = [];
+
+ candidates.forEach(tx => {
+ // initializing everything up front helps V8 optimize property access later
+ const adjustedVsize = Math.ceil(Math.max(tx.weight / 4, 5 * (tx.sigops || 0)));
+ const feePerVsize = (tx.fee / adjustedVsize);
+ auditPool.set(tx.txid, {
+ txid: tx.txid,
+ order: txidToOrdering(tx.txid),
+ fee: tx.fee,
+ feeDelta: 0,
+ weight: tx.weight,
+ adjustedVsize,
+ feePerVsize: feePerVsize,
+ effectiveFeePerVsize: feePerVsize,
+ dependencyRate: feePerVsize,
+ sigops: tx.sigops || 0,
+ inputs: (tx.vin?.map(vin => vin.txid) || []) as string[],
+ relativesSet: false,
+ ancestors: [],
+ cluster: [],
+ ancestorMap: new Map(),
+ children: new Set(),
+ ancestorFee: 0,
+ ancestorVsize: 0,
+ ancestorSigops: 0,
+ score: 0,
+ used: false,
+ modified: false,
+ });
+ mempoolArray.push(auditPool.get(tx.txid) as MinerTransaction);
+ });
+
+ // set accelerated effective fee
+ for (const acceleration of accelerations) {
+ const tx = auditPool.get(acceleration.txid);
+ if (tx) {
+ tx.feeDelta = acceleration.max_bid;
+ tx.feePerVsize = ((tx.fee + tx.feeDelta) / tx.adjustedVsize);
+ tx.effectiveFeePerVsize = tx.feePerVsize;
+ tx.dependencyRate = tx.feePerVsize;
+ }
+ }
+
+ // Build relatives graph & calculate ancestor scores
+ for (const tx of mempoolArray) {
+ if (!tx.relativesSet) {
+ setRelatives(tx, auditPool);
+ }
+ }
+
+ // Sort by descending ancestor score
+ mempoolArray.sort(priorityComparator);
+
+ // Build blocks by greedily choosing the highest feerate package
+ // (i.e. the package rooted in the transaction with the best ancestor score)
+ const blocks: number[][] = [];
+ let blockWeight = 0;
+ let blockSigops = 0;
+ const transactions: MinerTransaction[] = [];
+ let modified: MinerTransaction[] = [];
+ const overflow: MinerTransaction[] = [];
+ let failures = 0;
+ while (mempoolArray.length || modified.length) {
+ // skip invalid transactions
+ while (mempoolArray[0].used || mempoolArray[0].modified) {
+ mempoolArray.shift();
+ }
+
+ // Select best next package
+ let nextTx;
+ const nextPoolTx = mempoolArray[0];
+ const nextModifiedTx = modified[0];
+ if (nextPoolTx && (!nextModifiedTx || (nextPoolTx.score || 0) > (nextModifiedTx.score || 0))) {
+ nextTx = nextPoolTx;
+ mempoolArray.shift();
+ } else {
+ modified.shift();
+ if (nextModifiedTx) {
+ nextTx = nextModifiedTx;
+ }
+ }
+
+ if (nextTx && !nextTx?.used) {
+ // Check if the package fits into this block
+ if (blocks.length >= (maxBlocks - 1) || ((blockWeight + (4 * nextTx.ancestorVsize) < weightLimit) && (blockSigops + nextTx.ancestorSigops <= sigopLimit))) {
+ const ancestors: MinerTransaction[] = Array.from(nextTx.ancestorMap.values());
+ // sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
+ const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx];
+ const clusterTxids = sortedTxSet.map(tx => tx.txid);
+ const effectiveFeeRate = Math.min(nextTx.dependencyRate || Infinity, nextTx.ancestorFee / nextTx.ancestorVsize);
+ const used: MinerTransaction[] = [];
+ while (sortedTxSet.length) {
+ const ancestor = sortedTxSet.pop();
+ if (!ancestor) {
+ continue;
+ }
+ ancestor.used = true;
+ ancestor.usedBy = nextTx.txid;
+ // update this tx with effective fee rate & relatives data
+ if (ancestor.effectiveFeePerVsize !== effectiveFeeRate) {
+ ancestor.effectiveFeePerVsize = effectiveFeeRate;
+ }
+ ancestor.cluster = clusterTxids;
+ transactions.push(ancestor);
+ blockWeight += ancestor.weight;
+ blockSigops += ancestor.sigops;
+ used.push(ancestor);
+ }
+
+ // remove these as valid package ancestors for any descendants remaining in the mempool
+ if (used.length) {
+ used.forEach(tx => {
+ modified = updateDescendants(tx, auditPool, modified, effectiveFeeRate);
+ });
+ }
+
+ failures = 0;
+ } else {
+ // hold this package in an overflow list while we check for smaller options
+ overflow.push(nextTx);
+ failures++;
+ }
+ }
+
+ // this block is full
+ const exceededPackageTries = failures > 1000 && blockWeight > (weightLimit - 4000);
+ const queueEmpty = !mempoolArray.length && !modified.length;
+
+ if (exceededPackageTries || queueEmpty) {
+ break;
+ }
+ }
+
+ for (const tx of transactions) {
+ tx.ancestors = Object.values(tx.ancestorMap);
+ }
+
+ return transactions;
+}
+
+// traverse in-mempool ancestors
+// recursion unavoidable, but should be limited to depth < 25 by mempool policy
+function setRelatives(
+ tx: MinerTransaction,
+ mempool: Map,
+): void {
+ for (const parent of tx.inputs) {
+ const parentTx = mempool.get(parent);
+ if (parentTx && !tx.ancestorMap?.has(parent)) {
+ tx.ancestorMap.set(parent, parentTx);
+ parentTx.children.add(tx);
+ // visit each node only once
+ if (!parentTx.relativesSet) {
+ setRelatives(parentTx, mempool);
+ }
+ parentTx.ancestorMap.forEach((ancestor) => {
+ tx.ancestorMap.set(ancestor.txid, ancestor);
+ });
+ }
+ };
+ tx.ancestorFee = (tx.fee + tx.feeDelta);
+ tx.ancestorVsize = tx.adjustedVsize || 0;
+ tx.ancestorSigops = tx.sigops || 0;
+ tx.ancestorMap.forEach((ancestor) => {
+ tx.ancestorFee += (ancestor.fee + ancestor.feeDelta);
+ tx.ancestorVsize += ancestor.adjustedVsize;
+ tx.ancestorSigops += ancestor.sigops;
+ });
+ tx.score = tx.ancestorFee / tx.ancestorVsize;
+ tx.relativesSet = true;
+}
+
+// iterate over remaining descendants, removing the root as a valid ancestor & updating the ancestor score
+// avoids recursion to limit call stack depth
+function updateDescendants(
+ rootTx: MinerTransaction,
+ mempool: Map,
+ modified: MinerTransaction[],
+ clusterRate: number,
+): MinerTransaction[] {
+ const descendantSet: Set = new Set();
+ // stack of nodes left to visit
+ const descendants: MinerTransaction[] = [];
+ let descendantTx: MinerTransaction | undefined;
+ rootTx.children.forEach(childTx => {
+ if (!descendantSet.has(childTx)) {
+ descendants.push(childTx);
+ descendantSet.add(childTx);
+ }
+ });
+ while (descendants.length) {
+ descendantTx = descendants.pop();
+ if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.txid)) {
+ // remove tx as ancestor
+ descendantTx.ancestorMap.delete(rootTx.txid);
+ descendantTx.ancestorFee -= (rootTx.fee + rootTx.feeDelta);
+ descendantTx.ancestorVsize -= rootTx.adjustedVsize;
+ descendantTx.ancestorSigops -= rootTx.sigops;
+ descendantTx.score = descendantTx.ancestorFee / descendantTx.ancestorVsize;
+ descendantTx.dependencyRate = descendantTx.dependencyRate ? Math.min(descendantTx.dependencyRate, clusterRate) : clusterRate;
+
+ if (!descendantTx.modified) {
+ descendantTx.modified = true;
+ modified.push(descendantTx);
+ }
+
+ // add this node's children to the stack
+ descendantTx.children.forEach(childTx => {
+ // visit each node only once
+ if (!descendantSet.has(childTx)) {
+ descendants.push(childTx);
+ descendantSet.add(childTx);
+ }
+ });
+ }
+ }
+ // return new, resorted modified list
+ return modified.sort(priorityComparator);
+}
+
+// Used to sort an array of MinerTransactions by descending ancestor score
+function priorityComparator(a: MinerTransaction, b: MinerTransaction): number {
+ if (b.score === a.score) {
+ // tie-break by txid for stability
+ return a.order - b.order;
+ } else {
+ return b.score - a.score;
+ }
+}
+
+// returns the most significant 4 bytes of the txid as an integer
+function txidToOrdering(txid: string): number {
+ return parseInt(
+ txid.substring(62, 64) +
+ txid.substring(60, 62) +
+ txid.substring(58, 60) +
+ txid.substring(56, 58),
+ 16
+ );
+}
diff --git a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts
index 02640efc0..abd4c47a5 100644
--- a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts
+++ b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts
@@ -29,6 +29,7 @@ export interface AbstractBitcoinApi {
$getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise;
startHealthChecks(): void;
+ getHealthStatus(): HealthCheckHost[];
}
export interface BitcoinRpcCredentials {
host: string;
@@ -38,3 +39,14 @@ export interface BitcoinRpcCredentials {
timeout: number;
cookie?: string;
}
+
+export interface HealthCheckHost {
+ host: string;
+ active: boolean;
+ rtt: number;
+ latestHeight: number;
+ socket: boolean;
+ outOfSync: boolean;
+ unreachable: boolean;
+ checked: boolean;
+}
diff --git a/backend/src/api/bitcoin/bitcoin-api.ts b/backend/src/api/bitcoin/bitcoin-api.ts
index f54c836f8..d19eb06ac 100644
--- a/backend/src/api/bitcoin/bitcoin-api.ts
+++ b/backend/src/api/bitcoin/bitcoin-api.ts
@@ -1,5 +1,5 @@
import * as bitcoinjs from 'bitcoinjs-lib';
-import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
+import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
import { IBitcoinApi } from './bitcoin-api.interface';
import { IEsploraApi } from './esplora-api.interface';
import blocks from '../blocks';
@@ -382,6 +382,10 @@ class BitcoinApi implements AbstractBitcoinApi {
}
public startHealthChecks(): void {};
+
+ public getHealthStatus() {
+ return [];
+ }
}
export default BitcoinApi;
diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts
index 2f4bcee85..c80a9e18f 100644
--- a/backend/src/api/bitcoin/esplora-api.ts
+++ b/backend/src/api/bitcoin/esplora-api.ts
@@ -1,7 +1,7 @@
import config from '../../config';
-import axios, { AxiosResponse } from 'axios';
+import axios from 'axios';
import http from 'http';
-import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
+import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
import { IEsploraApi } from './esplora-api.interface';
import logger from '../../logger';
import { Common } from '../common';
@@ -157,7 +157,7 @@ class FailoverRouter {
}
// sort hosts by connection quality, and update default fallback
- private sortHosts(): FailoverHost[] {
+ public sortHosts(): FailoverHost[] {
// sort by connection quality
return this.hosts.slice().sort((a, b) => {
if ((a.unreachable || a.outOfSync) === (b.unreachable || b.outOfSync)) {
@@ -342,6 +342,23 @@ class ElectrsApi implements AbstractBitcoinApi {
public startHealthChecks(): void {
this.failoverRouter.startHealthChecks();
}
+
+ public getHealthStatus(): HealthCheckHost[] {
+ if (config.MEMPOOL.OFFICIAL) {
+ return this.failoverRouter.sortHosts().map(host => ({
+ host: host.host,
+ active: host === this.failoverRouter.activeHost,
+ rtt: host.rtt,
+ latestHeight: host.latestHeight || 0,
+ socket: !!host.socket,
+ outOfSync: !!host.outOfSync,
+ unreachable: !!host.unreachable,
+ checked: !!host.checked,
+ }));
+ } else {
+ return [];
+ }
+ }
}
export default ElectrsApi;
diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts
index 4ccf58645..28ca38152 100644
--- a/backend/src/api/blocks.ts
+++ b/backend/src/api/blocks.ts
@@ -40,6 +40,7 @@ class Blocks {
private quarterEpochBlockTime: number | null = null;
private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
private newAsyncBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: MempoolTransactionExtended[]) => Promise)[] = [];
+ private classifyingBlocks: boolean = false;
private mainLoopTimeout: number = 120000;
@@ -568,6 +569,11 @@ class Blocks {
* [INDEXING] Index transaction classification flags for Goggles
*/
public async $classifyBlocks(): Promise {
+ if (this.classifyingBlocks) {
+ return;
+ }
+ this.classifyingBlocks = true;
+
// classification requires an esplora backend
if (!Common.gogglesIndexingEnabled() || config.MEMPOOL.BACKEND !== 'esplora') {
return;
@@ -679,6 +685,8 @@ class Blocks {
indexedThisRun = 0;
}
}
+
+ this.classifyingBlocks = false;
}
/**
diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts
index 4ca0e50d1..72178df3e 100644
--- a/backend/src/api/common.ts
+++ b/backend/src/api/common.ts
@@ -7,6 +7,24 @@ import { isIP } from 'net';
import transactionUtils from './transaction-utils';
import { isPoint } from '../utils/secp256k1';
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);
+const MIN_STANDARD_TX_NONWITNESS_SIZE = 65;
+const MAX_P2SH_SIGOPS = 15;
+const MAX_STANDARD_P2WSH_STACK_ITEMS = 100;
+const MAX_STANDARD_P2WSH_STACK_ITEM_SIZE = 80;
+const MAX_STANDARD_TAPSCRIPT_STACK_ITEM_SIZE = 80;
+const MAX_STANDARD_P2WSH_SCRIPT_SIZE = 3600;
+const MAX_STANDARD_SCRIPTSIG_SIZE = 1650;
+const DUST_RELAY_TX_FEE = 3;
+const MAX_OP_RETURN_RELAY = 83;
+const DEFAULT_PERMIT_BAREMULTISIG = true;
+
export class Common {
static nativeAssetId = config.MEMPOOL.NETWORK === 'liquidtestnet' ?
'144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49'
@@ -177,6 +195,141 @@ export class Common {
);
}
+ /**
+ * Validates most standardness rules
+ *
+ * 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)
+ */
+ static isNonStandard(tx: TransactionExtended): boolean {
+ // version
+ if (tx.version > TX_MAX_STANDARD_VERSION) {
+ return true;
+ }
+
+ // tx-size
+ if (tx.weight > MAX_STANDARD_TX_WEIGHT) {
+ return true;
+ }
+
+ // tx-size-small
+ if (this.getNonWitnessSize(tx) < MIN_STANDARD_TX_NONWITNESS_SIZE) {
+ return true;
+ }
+
+ // bad-txns-too-many-sigops
+ if (tx.sigops && tx.sigops > MAX_STANDARD_TX_SIGOPS_COST) {
+ return true;
+ }
+
+ // input validation
+ for (const vin of tx.vin) {
+ if (vin.is_coinbase) {
+ // standardness rules don't apply to coinbase transactions
+ return false;
+ }
+ // scriptsig-size
+ if ((vin.scriptsig.length / 2) > MAX_STANDARD_SCRIPTSIG_SIZE) {
+ return true;
+ }
+ // scriptsig-not-pushonly
+ if (vin.scriptsig_asm) {
+ for (const op of vin.scriptsig_asm.split(' ')) {
+ if (opcodes[op] && opcodes[op] > opcodes['OP_16']) {
+ return true;
+ }
+ }
+ }
+ // bad-txns-nonstandard-inputs
+ if (vin.prevout?.scriptpubkey_type === 'p2sh') {
+ // TODO: evaluate script (https://github.com/bitcoin/bitcoin/blob/1ac627c485a43e50a9a49baddce186ee3ad4daad/src/policy/policy.cpp#L177)
+ // countScriptSigops returns the witness-scaled sigops, so divide by 4 before comparison with MAX_P2SH_SIGOPS
+ const sigops = (transactionUtils.countScriptSigops(vin.inner_redeemscript_asm) / 4);
+ if (sigops > MAX_P2SH_SIGOPS) {
+ return true;
+ }
+ } else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
+ return true;
+ }
+ // TODO: bad-witness-nonstandard
+ }
+
+ // output validation
+ let opreturnCount = 0;
+ for (const vout of tx.vout) {
+ // scriptpubkey
+ if (['unknown', 'provably_unspendable', 'empty'].includes(vout.scriptpubkey_type)) {
+ // (non-standard output type)
+ return true;
+ } else if (vout.scriptpubkey_type === 'multisig') {
+ if (!DEFAULT_PERMIT_BAREMULTISIG) {
+ // bare-multisig
+ return true;
+ }
+ const mOfN = parseMultisigScript(vout.scriptpubkey_asm);
+ if (!mOfN || mOfN.n < 1 || mOfN.n > 3 || mOfN.m < 1 || mOfN.m > mOfN.n) {
+ // (non-standard bare multisig threshold)
+ return true;
+ }
+ } else if (vout.scriptpubkey_type === 'op_return') {
+ opreturnCount++;
+ if ((vout.scriptpubkey.length / 2) > MAX_OP_RETURN_RELAY) {
+ // over default datacarrier limit
+ return true;
+ }
+ }
+ // dust
+ // (we could probably hardcode this for the different output types...)
+ if (vout.scriptpubkey_type !== 'op_return') {
+ let dustSize = (vout.scriptpubkey.length / 2);
+ // add varint length overhead
+ dustSize += getVarIntLength(dustSize);
+ // add value size
+ dustSize += 8;
+ if (['v0_p2wpkh', 'v0_p2wsh', 'v1_p2tr'].includes(vout.scriptpubkey_type)) {
+ dustSize += 67;
+ } else {
+ dustSize += 148;
+ }
+ if (vout.value < (dustSize * DUST_RELAY_TX_FEE)) {
+ // under minimum output size
+ console.log(`NON-STANDARD | dust | ${vout.value} | ${dustSize} ${dustSize * DUST_RELAY_TX_FEE} `, tx.txid);
+ return true;
+ }
+ }
+ }
+
+ // multi-op-return
+ if (opreturnCount > 1) {
+ return true;
+ }
+
+ // TODO: non-mandatory-script-verify-flag
+
+ return false;
+ }
+
+ static getNonWitnessSize(tx: TransactionExtended): number {
+ let weight = tx.weight;
+ let hasWitness = false;
+ for (const vin of tx.vin) {
+ if (vin.witness?.length) {
+ hasWitness = true;
+ // witness count
+ weight -= getVarIntLength(vin.witness.length);
+ for (const witness of vin.witness) {
+ // witness item size + content
+ weight -= getVarIntLength(witness.length / 2) + (witness.length / 2);
+ }
+ }
+ }
+ if (hasWitness) {
+ // marker & segwit flag
+ weight -= 2;
+ }
+ return Math.ceil(weight / 4);
+ }
+
static setSegwitSighashFlags(flags: bigint, witness: string[]): bigint {
for (const w of witness) {
if (this.isDERSig(w)) {
@@ -245,6 +398,8 @@ export class Common {
flags |= TransactionFlags.v1;
} else if (tx.version === 2) {
flags |= TransactionFlags.v2;
+ } else if (tx.version === 3) {
+ flags |= TransactionFlags.v3;
}
const reusedInputAddresses: { [address: string ]: number } = {};
const reusedOutputAddresses: { [address: string ]: number } = {};
@@ -349,6 +504,10 @@ export class Common {
flags |= TransactionFlags.batch_payout;
}
+ if (this.isNonStandard(tx)) {
+ flags |= TransactionFlags.nonstandard;
+ }
+
return Number(flags);
}
diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts
index 9a5eb310a..861830226 100644
--- a/backend/src/api/database-migration.ts
+++ b/backend/src/api/database-migration.ts
@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2';
class DatabaseMigration {
- private static currentVersion = 68;
+ private static currentVersion = 70;
private queryTimeout = 3600_000;
private statisticsAddedIndexed = false;
private uniqueLogs: string[] = [];
@@ -580,6 +580,16 @@ class DatabaseMigration {
await this.$executeQuery(`INSERT INTO state VALUES('last_bitcoin_block_audit', 0, NULL);`);
await this.updateToSchemaVersion(68);
}
+
+ if (databaseSchemaVersion < 69 && config.MEMPOOL.NETWORK === 'mainnet') {
+ await this.$executeQuery(this.getCreateAccelerationsTableQuery(), await this.$checkIfTableExists('accelerations'));
+ await this.updateToSchemaVersion(69);
+ }
+
+ if (databaseSchemaVersion < 70 && config.MEMPOOL.NETWORK === 'mainnet') {
+ await this.$executeQuery('ALTER TABLE accelerations MODIFY COLUMN added DATETIME;');
+ await this.updateToSchemaVersion(70);
+ }
}
/**
@@ -1123,6 +1133,23 @@ class DatabaseMigration {
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
}
+ private getCreateAccelerationsTableQuery(): string {
+ return `CREATE TABLE IF NOT EXISTS accelerations (
+ txid varchar(65) NOT NULL,
+ added datetime NOT NULL,
+ height int(10) NOT NULL,
+ pool smallint unsigned NULL,
+ effective_vsize int(10) NOT NULL,
+ effective_fee bigint(20) unsigned NOT NULL,
+ boost_rate float unsigned,
+ boost_cost bigint(20) unsigned NOT NULL,
+ PRIMARY KEY (txid),
+ INDEX (added),
+ INDEX (height),
+ INDEX (pool)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
+ }
+
public async $blocksReindexingTruncate(): Promise {
logger.warn(`Truncating pools, blocks, hashrates and difficulty_adjustments tables for re-indexing (using '--reindex-blocks'). You can cancel this command within 5 seconds`);
await Common.sleep$(5000);
diff --git a/backend/src/api/mining/mining-routes.ts b/backend/src/api/mining/mining-routes.ts
index bb78de44a..6b9ef7b9f 100644
--- a/backend/src/api/mining/mining-routes.ts
+++ b/backend/src/api/mining/mining-routes.ts
@@ -8,6 +8,7 @@ import HashratesRepository from '../../repositories/HashratesRepository';
import bitcoinClient from '../bitcoin/bitcoin-client';
import mining from "./mining";
import PricesRepository from '../../repositories/PricesRepository';
+import AccelerationRepository from '../../repositories/AccelerationRepository';
class MiningRoutes {
public initRoutes(app: Application) {
@@ -34,6 +35,10 @@ class MiningRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/:hash', this.$getBlockAudit)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/timestamp/:timestamp', this.$getHeightFromTimestamp)
.get(config.MEMPOOL.API_URL_PREFIX + 'historical-price', this.$getHistoricalPrice)
+
+ .get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/pool/:slug', this.$getAccelerationsByPool)
+ .get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/block/:height', this.$getAccelerationsByHeight)
+ .get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/recent/:interval', this.$getRecentAccelerations)
;
}
@@ -352,6 +357,52 @@ class MiningRoutes {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
+
+ private async $getAccelerationsByPool(req: Request, res: Response): Promise {
+ try {
+ res.header('Pragma', 'public');
+ 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)) {
+ res.status(400).send('Acceleration data is not available.');
+ return;
+ }
+ res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug));
+ } catch (e) {
+ res.status(500).send(e instanceof Error ? e.message : e);
+ }
+ }
+
+ private async $getAccelerationsByHeight(req: Request, res: Response): Promise {
+ try {
+ res.header('Pragma', 'public');
+ 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)) {
+ 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) {
+ res.status(500).send(e instanceof Error ? e.message : e);
+ }
+ }
+
+ private async $getRecentAccelerations(req: Request, res: Response): Promise {
+ try {
+ res.header('Pragma', 'public');
+ 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)) {
+ res.status(400).send('Acceleration data is not available.');
+ return;
+ }
+ res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval));
+ } catch (e) {
+ res.status(500).send(e instanceof Error ? e.message : e);
+ }
+ }
}
export default new MiningRoutes();
diff --git a/backend/src/api/redis-cache.ts b/backend/src/api/redis-cache.ts
index edfd2142b..d19d73a7f 100644
--- a/backend/src/api/redis-cache.ts
+++ b/backend/src/api/redis-cache.ts
@@ -19,45 +19,90 @@ class RedisCache {
private client;
private connected = false;
private schemaVersion = 1;
+ private redisConfig: any;
+ private pauseFlush: boolean = false;
private cacheQueue: MempoolTransactionExtended[] = [];
+ private removeQueue: string[] = [];
+ private rbfCacheQueue: { type: string, txid: string, value: any }[] = [];
+ private rbfRemoveQueue: { type: string, txid: string }[] = [];
private txFlushLimit: number = 10000;
constructor() {
if (config.REDIS.ENABLED) {
- const redisConfig = {
+ this.redisConfig = {
socket: {
path: config.REDIS.UNIX_SOCKET_PATH
},
database: NetworkDB[config.MEMPOOL.NETWORK],
};
- this.client = createClient(redisConfig);
- this.client.on('error', (e) => {
- logger.err(`Error in Redis client: ${e instanceof Error ? e.message : e}`);
- });
this.$ensureConnected();
+ setInterval(() => { this.$ensureConnected(); }, 10000);
}
}
- private async $ensureConnected(): Promise {
+ private async $ensureConnected(): Promise {
if (!this.connected && config.REDIS.ENABLED) {
- return this.client.connect().then(async () => {
- this.connected = true;
- logger.info(`Redis client connected`);
- const version = await this.client.get('schema_version');
- if (version !== this.schemaVersion) {
- // schema changed
- // perform migrations or flush DB if necessary
- logger.info(`Redis schema version changed from ${version} to ${this.schemaVersion}`);
- await this.client.set('schema_version', this.schemaVersion);
- }
- });
+ try {
+ this.client = createClient(this.redisConfig);
+ this.client.on('error', async (e) => {
+ logger.err(`Error in Redis client: ${e instanceof Error ? e.message : e}`);
+ this.connected = false;
+ await this.client.disconnect();
+ });
+ await this.client.connect().then(async () => {
+ try {
+ const version = await this.client.get('schema_version');
+ this.connected = true;
+ if (version !== this.schemaVersion) {
+ // schema changed
+ // perform migrations or flush DB if necessary
+ logger.info(`Redis schema version changed from ${version} to ${this.schemaVersion}`);
+ await this.client.set('schema_version', this.schemaVersion);
+ }
+ logger.info(`Redis client connected`);
+ return true;
+ } catch (e) {
+ this.connected = false;
+ logger.warn('Failed to connect to Redis');
+ return false;
+ }
+ });
+ await this.$onConnected();
+ return true;
+ } catch (e) {
+ logger.warn('Error connecting to Redis: ' + (e instanceof Error ? e.message : e));
+ return false;
+ }
+ } else {
+ try {
+ // test connection
+ await this.client.get('schema_version');
+ return true;
+ } catch (e) {
+ logger.warn('Lost connection to Redis: ' + (e instanceof Error ? e.message : e));
+ logger.warn('Attempting to reconnect in 10 seconds');
+ this.connected = false;
+ return false;
+ }
}
}
- async $updateBlocks(blocks: BlockExtended[]) {
+ private async $onConnected(): Promise {
+ await this.$flushTransactions();
+ await this.$removeTransactions([]);
+ await this.$flushRbfQueues();
+ }
+
+ async $updateBlocks(blocks: BlockExtended[]): Promise {
+ if (!config.REDIS.ENABLED) {
+ return;
+ }
+ if (!this.connected) {
+ logger.warn(`Failed to update blocks in Redis cache: Redis is not connected`);
+ return;
+ }
try {
- await this.$ensureConnected();
await this.client.set('blocks', JSON.stringify(blocks));
logger.debug(`Saved latest blocks to Redis cache`);
} catch (e) {
@@ -65,9 +110,15 @@ class RedisCache {
}
}
- async $updateBlockSummaries(summaries: BlockSummary[]) {
+ async $updateBlockSummaries(summaries: BlockSummary[]): Promise {
+ if (!config.REDIS.ENABLED) {
+ return;
+ }
+ if (!this.connected) {
+ logger.warn(`Failed to update block summaries in Redis cache: Redis is not connected`);
+ return;
+ }
try {
- await this.$ensureConnected();
await this.client.set('block-summaries', JSON.stringify(summaries));
logger.debug(`Saved latest block summaries to Redis cache`);
} catch (e) {
@@ -75,30 +126,35 @@ class RedisCache {
}
}
- async $addTransaction(tx: MempoolTransactionExtended) {
+ async $addTransaction(tx: MempoolTransactionExtended): Promise {
+ if (!config.REDIS.ENABLED) {
+ return;
+ }
this.cacheQueue.push(tx);
if (this.cacheQueue.length >= this.txFlushLimit) {
- await this.$flushTransactions();
+ if (!this.pauseFlush) {
+ await this.$flushTransactions();
+ }
}
}
- async $flushTransactions() {
- const success = await this.$addTransactions(this.cacheQueue);
- if (success) {
- logger.debug(`Saved ${this.cacheQueue.length} transactions to Redis cache`);
- this.cacheQueue = [];
- } else {
- logger.err(`Failed to save ${this.cacheQueue.length} transactions to Redis cache`);
+ async $flushTransactions(): Promise {
+ if (!config.REDIS.ENABLED) {
+ return;
+ }
+ if (!this.cacheQueue.length) {
+ return;
+ }
+ if (!this.connected) {
+ logger.warn(`Failed to add ${this.cacheQueue.length} transactions to Redis cache: Redis not connected`);
+ return;
}
- }
- private async $addTransactions(newTransactions: MempoolTransactionExtended[]): Promise {
- if (!newTransactions.length) {
- return true;
- }
+ this.pauseFlush = false;
+
+ const toAdd = this.cacheQueue.slice(0, this.txFlushLimit);
try {
- await this.$ensureConnected();
- const msetData = newTransactions.map(tx => {
+ const msetData = toAdd.map(tx => {
const minified: any = { ...tx };
delete minified.hex;
for (const vin of minified.vin) {
@@ -112,30 +168,53 @@ class RedisCache {
return [`mempool:tx:${tx.txid}`, JSON.stringify(minified)];
});
await this.client.MSET(msetData);
- return true;
+ // successful, remove transactions from cache queue
+ this.cacheQueue = this.cacheQueue.slice(toAdd.length);
+ logger.debug(`Saved ${toAdd.length} transactions to Redis cache, ${this.cacheQueue.length} left in queue`);
} catch (e) {
- logger.warn(`Failed to add ${newTransactions.length} transactions to Redis cache: ${e instanceof Error ? e.message : e}`);
- return false;
+ logger.warn(`Failed to add ${toAdd.length} transactions to Redis cache: ${e instanceof Error ? e.message : e}`);
+ this.pauseFlush = true;
}
}
- async $removeTransactions(transactions: string[]) {
- try {
- await this.$ensureConnected();
+ async $removeTransactions(transactions: string[]): Promise {
+ if (!config.REDIS.ENABLED) {
+ return;
+ }
+ const toRemove = this.removeQueue.concat(transactions);
+ this.removeQueue = [];
+ let failed: string[] = [];
+ let numRemoved = 0;
+ if (this.connected) {
const sliceLength = config.REDIS.BATCH_QUERY_BASE_SIZE;
- for (let i = 0; i < Math.ceil(transactions.length / sliceLength); i++) {
- const slice = transactions.slice(i * sliceLength, (i + 1) * sliceLength);
- await this.client.unlink(slice.map(txid => `mempool:tx:${txid}`));
- logger.debug(`Deleted ${slice.length} transactions from the Redis cache`);
+ for (let i = 0; i < Math.ceil(toRemove.length / sliceLength); i++) {
+ const slice = toRemove.slice(i * sliceLength, (i + 1) * sliceLength);
+ try {
+ await this.client.unlink(slice.map(txid => `mempool:tx:${txid}`));
+ numRemoved+= sliceLength;
+ logger.debug(`Deleted ${slice.length} transactions from the Redis cache`);
+ } catch (e) {
+ logger.warn(`Failed to remove ${slice.length} transactions from Redis cache: ${e instanceof Error ? e.message : e}`);
+ failed = failed.concat(slice);
+ }
}
- } catch (e) {
- logger.warn(`Failed to remove ${transactions.length} transactions from Redis cache: ${e instanceof Error ? e.message : e}`);
+ // concat instead of replace, in case more txs have been added in the meantime
+ this.removeQueue = this.removeQueue.concat(failed);
+ } else {
+ this.removeQueue = this.removeQueue.concat(toRemove);
}
}
async $setRbfEntry(type: string, txid: string, value: any): Promise {
+ if (!config.REDIS.ENABLED) {
+ return;
+ }
+ if (!this.connected) {
+ this.rbfCacheQueue.push({ type, txid, value });
+ logger.warn(`Failed to set RBF ${type} in Redis cache: Redis is not connected`);
+ return;
+ }
try {
- await this.$ensureConnected();
await this.client.set(`rbf:${type}:${txid}`, JSON.stringify(value));
} catch (e) {
logger.warn(`Failed to set RBF ${type} in Redis cache: ${e instanceof Error ? e.message : e}`);
@@ -143,17 +222,55 @@ class RedisCache {
}
async $removeRbfEntry(type: string, txid: string): Promise {
+ if (!config.REDIS.ENABLED) {
+ return;
+ }
+ if (!this.connected) {
+ this.rbfRemoveQueue.push({ type, txid });
+ logger.warn(`Failed to remove RBF ${type} from Redis cache: Redis is not connected`);
+ return;
+ }
try {
- await this.$ensureConnected();
await this.client.unlink(`rbf:${type}:${txid}`);
} catch (e) {
logger.warn(`Failed to remove RBF ${type} from Redis cache: ${e instanceof Error ? e.message : e}`);
}
}
- async $getBlocks(): Promise {
+ private async $flushRbfQueues(): Promise {
+ if (!config.REDIS.ENABLED) {
+ return;
+ }
+ if (!this.connected) {
+ return;
+ }
+ try {
+ const toAdd = this.rbfCacheQueue;
+ this.rbfCacheQueue = [];
+ for (const { type, txid, value } of toAdd) {
+ await this.$setRbfEntry(type, txid, value);
+ }
+ logger.debug(`Saved ${toAdd.length} queued RBF entries to the Redis cache`);
+ const toRemove = this.rbfRemoveQueue;
+ this.rbfRemoveQueue = [];
+ for (const { type, txid } of toRemove) {
+ await this.$removeRbfEntry(type, txid);
+ }
+ logger.debug(`Removed ${toRemove.length} queued RBF entries from the Redis cache`);
+ } catch (e) {
+ logger.warn(`Failed to flush RBF cache event queues after reconnecting to Redis: ${e instanceof Error ? e.message : e}`);
+ }
+ }
+
+ async $getBlocks(): Promise {
+ if (!config.REDIS.ENABLED) {
+ return [];
+ }
+ if (!this.connected) {
+ logger.warn(`Failed to retrieve blocks from Redis cache: Redis is not connected`);
+ return [];
+ }
try {
- await this.$ensureConnected();
const json = await this.client.get('blocks');
return JSON.parse(json);
} catch (e) {
@@ -163,8 +280,14 @@ class RedisCache {
}
async $getBlockSummaries(): Promise {
+ if (!config.REDIS.ENABLED) {
+ return [];
+ }
+ if (!this.connected) {
+ logger.warn(`Failed to retrieve blocks from Redis cache: Redis is not connected`);
+ return [];
+ }
try {
- await this.$ensureConnected();
const json = await this.client.get('block-summaries');
return JSON.parse(json);
} catch (e) {
@@ -174,10 +297,16 @@ class RedisCache {
}
async $getMempool(): Promise<{ [txid: string]: MempoolTransactionExtended }> {
+ if (!config.REDIS.ENABLED) {
+ return {};
+ }
+ if (!this.connected) {
+ logger.warn(`Failed to retrieve mempool from Redis cache: Redis is not connected`);
+ return {};
+ }
const start = Date.now();
const mempool = {};
try {
- await this.$ensureConnected();
const mempoolList = await this.scanKeys('mempool:tx:*');
for (const tx of mempoolList) {
mempool[tx.key] = tx.value;
@@ -191,8 +320,14 @@ class RedisCache {
}
async $getRbfEntries(type: string): Promise {
+ if (!config.REDIS.ENABLED) {
+ return [];
+ }
+ if (!this.connected) {
+ logger.warn(`Failed to retrieve Rbf ${type}s from Redis cache: Redis is not connected`);
+ return [];
+ }
try {
- await this.$ensureConnected();
const rbfEntries = await this.scanKeys(`rbf:${type}:*`);
return rbfEntries;
} catch (e) {
@@ -201,7 +336,10 @@ class RedisCache {
}
}
- async $loadCache() {
+ async $loadCache(): Promise {
+ if (!config.REDIS.ENABLED) {
+ return;
+ }
logger.info('Restoring mempool and blocks data from Redis cache');
// Load block data
const loadedBlocks = await this.$getBlocks();
@@ -226,7 +364,7 @@ class RedisCache {
});
}
- private inflateLoadedTxs(mempool: { [txid: string]: MempoolTransactionExtended }) {
+ private inflateLoadedTxs(mempool: { [txid: string]: MempoolTransactionExtended }): void {
for (const tx of Object.values(mempool)) {
for (const vin of tx.vin) {
if (vin.scriptsig) {
diff --git a/backend/src/api/transaction-utils.ts b/backend/src/api/transaction-utils.ts
index 6ff1c10b7..9107f2ae7 100644
--- a/backend/src/api/transaction-utils.ts
+++ b/backend/src/api/transaction-utils.ts
@@ -145,6 +145,10 @@ class TransactionUtils {
}
public countScriptSigops(script: string, isRawScript: boolean = false, witness: boolean = false): number {
+ if (!script?.length) {
+ return 0;
+ }
+
let sigops = 0;
// count OP_CHECKSIG and OP_CHECKSIGVERIFY
sigops += (script.match(/OP_CHECKSIG/g)?.length || 0);
diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts
index b78389b64..6711c88fb 100644
--- a/backend/src/api/websocket-handler.ts
+++ b/backend/src/api/websocket-handler.ts
@@ -24,6 +24,9 @@ import { ApiPrice } from '../repositories/PricesRepository';
import accelerationApi from './services/acceleration';
import mempool from './mempool';
import statistics from './statistics/statistics';
+import accelerationCosts from './acceleration';
+import accelerationRepository from '../repositories/AccelerationRepository';
+import bitcoinApi from './bitcoin/bitcoin-api-factory';
interface AddressTransactions {
mempool: MempoolTransactionExtended[],
@@ -37,6 +40,7 @@ const wantable = [
'mempool-blocks',
'live-2h-chart',
'stats',
+ 'tomahawk',
];
class WebsocketHandler {
@@ -121,7 +125,7 @@ class WebsocketHandler {
for (const sub of wantable) {
const key = `want-${sub}`;
const wants = parsedMessage.data.includes(sub);
- if (wants && client['wants'] && !client[key]) {
+ if (wants && !client[key]) {
wantNow[key] = true;
}
client[key] = wants;
@@ -145,6 +149,10 @@ class WebsocketHandler {
response['da'] = this.socketData['da'];
}
+ if (wantNow['want-tomahawk']) {
+ response['tomahawk'] = JSON.stringify(bitcoinApi.getHealthStatus());
+ }
+
if (parsedMessage && parsedMessage['track-tx']) {
if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-tx'])) {
client['track-tx'] = parsedMessage['track-tx'];
@@ -544,6 +552,10 @@ class WebsocketHandler {
response['mempool-blocks'] = getCachedResponse('mempool-blocks', mBlocks);
}
+ if (client['want-tomahawk']) {
+ response['tomahawk'] = getCachedResponse('tomahawk', bitcoinApi.getHealthStatus());
+ }
+
if (client['track-mempool-tx']) {
const tx = newTransactions.find((t) => t.txid === client['track-mempool-tx']);
if (tx) {
@@ -728,6 +740,28 @@ class WebsocketHandler {
const _memPool = memPool.getMempool();
+ const isAccelerated = config.MEMPOOL_SERVICES.ACCELERATIONS && accelerationApi.isAcceleratedBlock(block, Object.values(mempool.getAccelerations()));
+
+
+ if (isAccelerated) {
+ const blockTxs: { [txid: string]: MempoolTransactionExtended } = {};
+ for (const tx of transactions) {
+ blockTxs[tx.txid] = tx;
+ }
+ const accelerations = Object.values(mempool.getAccelerations());
+ const boostRate = accelerationCosts.calculateBoostRate(
+ accelerations.map(acc => ({ txid: acc.txid, max_bid: acc.feeDelta })),
+ transactions
+ );
+ for (const acc of accelerations) {
+ if (blockTxs[acc.txid]) {
+ const tx = blockTxs[acc.txid];
+ const accelerationInfo = accelerationCosts.getAccelerationInfo(tx, boostRate, transactions);
+ accelerationRepository.$saveAcceleration(accelerationInfo, block, block.extras.pool.id);
+ }
+ }
+ }
+
const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
memPool.handleMinedRbfTransactions(rbfTransactions);
memPool.removeFromSpendMap(transactions);
@@ -735,7 +769,6 @@ class WebsocketHandler {
if (config.MEMPOOL.AUDIT && memPool.isInSync()) {
let projectedBlocks;
let auditMempool = _memPool;
- const isAccelerated = config.MEMPOOL_SERVICES.ACCELERATIONS && accelerationApi.isAcceleratedBlock(block, Object.values(mempool.getAccelerations()));
// template calculation functions have mempool side effects, so calculate audits using
// a cloned copy of the mempool if we're running a different algorithm for mempool updates
const separateAudit = config.MEMPOOL.ADVANCED_GBT_AUDIT !== config.MEMPOOL.ADVANCED_GBT_MEMPOOL;
@@ -886,6 +919,10 @@ class WebsocketHandler {
response['mempool-blocks'] = getCachedResponse('mempool-blocks', mBlocks);
}
+ if (client['want-tomahawk']) {
+ response['tomahawk'] = getCachedResponse('tomahawk', bitcoinApi.getHealthStatus());
+ }
+
if (client['track-tx']) {
const trackTxid = client['track-tx'];
if (trackTxid && confirmedTxids[trackTxid]) {
diff --git a/backend/src/config.ts b/backend/src/config.ts
index 32a7af3df..3330adca0 100644
--- a/backend/src/config.ts
+++ b/backend/src/config.ts
@@ -5,6 +5,7 @@ const configFromFile = require(
interface IConfig {
MEMPOOL: {
ENABLED: boolean;
+ OFFICIAL: boolean;
NETWORK: 'mainnet' | 'testnet' | 'signet' | 'liquid' | 'liquidtestnet';
BACKEND: 'esplora' | 'electrum' | 'none';
HTTP_PORT: number;
@@ -103,6 +104,7 @@ interface IConfig {
PASSWORD: string;
TIMEOUT: number;
PID_DIR: string;
+ POOL_SIZE: number;
};
SYSLOG: {
ENABLED: boolean;
@@ -161,6 +163,7 @@ interface IConfig {
const defaults: IConfig = {
'MEMPOOL': {
'ENABLED': true,
+ 'OFFICIAL': false,
'NETWORK': 'mainnet',
'BACKEND': 'none',
'HTTP_PORT': 8999,
@@ -240,6 +243,7 @@ const defaults: IConfig = {
'PASSWORD': 'mempool',
'TIMEOUT': 180000,
'PID_DIR': '',
+ 'POOL_SIZE': 100,
},
'SYSLOG': {
'ENABLED': true,
diff --git a/backend/src/database.ts b/backend/src/database.ts
index dc543bbbc..05f624ff4 100644
--- a/backend/src/database.ts
+++ b/backend/src/database.ts
@@ -21,7 +21,7 @@ import { execSync } from 'child_process';
database: config.DATABASE.DATABASE,
user: config.DATABASE.USERNAME,
password: config.DATABASE.PASSWORD,
- connectionLimit: 10,
+ connectionLimit: config.DATABASE.POOL_SIZE,
supportBigNumbers: true,
timezone: '+00:00',
};
diff --git a/backend/src/indexer.ts b/backend/src/indexer.ts
index 90b4a59e6..dcb91d010 100644
--- a/backend/src/indexer.ts
+++ b/backend/src/indexer.ts
@@ -185,7 +185,8 @@ class Indexer {
await blocks.$generateCPFPDatabase();
await blocks.$generateAuditStats();
await auditReplicator.$sync();
- await blocks.$classifyBlocks();
+ // do not wait for classify blocks to finish
+ blocks.$classifyBlocks();
} catch (e) {
this.indexerRunning = false;
logger.err(`Indexer failed, trying again in 10 seconds. Reason: ` + (e instanceof Error ? e.message : e));
diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts
index 71612f25f..b68b137bb 100644
--- a/backend/src/mempool.interfaces.ts
+++ b/backend/src/mempool.interfaces.ts
@@ -208,6 +208,8 @@ export const TransactionFlags = {
no_rbf: 0b00000010n,
v1: 0b00000100n,
v2: 0b00001000n,
+ v3: 0b00010000n,
+ nonstandard: 0b00100000n,
// address types
p2pk: 0b00000001_00000000n,
p2ms: 0b00000010_00000000n,
diff --git a/backend/src/repositories/AccelerationRepository.ts b/backend/src/repositories/AccelerationRepository.ts
new file mode 100644
index 000000000..868f8526f
--- /dev/null
+++ b/backend/src/repositories/AccelerationRepository.ts
@@ -0,0 +1,109 @@
+import { AccelerationInfo } from '../api/acceleration';
+import { ResultSetHeader, RowDataPacket } from 'mysql2';
+import DB from '../database';
+import logger from '../logger';
+import { IEsploraApi } from '../api/bitcoin/esplora-api.interface';
+import { Common } from '../api/common';
+import config from '../config';
+
+export interface PublicAcceleration {
+ txid: string,
+ height: number,
+ pool: {
+ id: number,
+ slug: string,
+ name: string,
+ },
+ effective_vsize: number,
+ effective_fee: number,
+ boost_rate: number,
+ boost_cost: number,
+}
+
+class AccelerationRepository {
+ public async $saveAcceleration(acceleration: AccelerationInfo, block: IEsploraApi.Block, pool_id: number): Promise {
+ try {
+ await DB.query(`
+ INSERT INTO accelerations(txid, added, height, pool, effective_vsize, effective_fee, boost_rate, boost_cost)
+ VALUE (?, FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?)
+ ON DUPLICATE KEY UPDATE
+ added = FROM_UNIXTIME(?),
+ height = ?,
+ pool = ?,
+ effective_vsize = ?,
+ effective_fee = ?,
+ boost_rate = ?,
+ boost_cost = ?
+ `, [
+ acceleration.txSummary.txid,
+ block.timestamp,
+ block.height,
+ pool_id,
+ acceleration.txSummary.effectiveVsize,
+ acceleration.txSummary.effectiveFee,
+ acceleration.targetFeeRate, acceleration.cost,
+ block.timestamp,
+ block.height,
+ pool_id,
+ acceleration.txSummary.effectiveVsize,
+ acceleration.txSummary.effectiveFee,
+ acceleration.targetFeeRate, acceleration.cost,
+ ]);
+ } catch (e: any) {
+ logger.err(`Cannot save acceleration (${acceleration.txSummary.txid}) into db. Reason: ` + (e instanceof Error ? e.message : e));
+ // We don't throw, not a critical issue if we miss some accelerations
+ }
+ }
+
+ public async $getAccelerationInfo(poolSlug: string | null = null, height: number | null = null, interval: string | null = null): Promise {
+ interval = Common.getSqlInterval(interval);
+
+ if (!config.MEMPOOL_SERVICES.ACCELERATIONS || (interval == null && poolSlug == null && height == null)) {
+ return [];
+ }
+
+ let query = `
+ SELECT * FROM accelerations
+ JOIN pools on pools.unique_id = accelerations.pool
+ `;
+ let params: any[] = [];
+
+ if (interval) {
+ query += ` WHERE accelerations.added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW() `;
+ } else if (height != null) {
+ query += ` WHERE accelerations.height = ? `;
+ params.push(height);
+ } else if (poolSlug != null) {
+ query += ` WHERE pools.slug = ? `;
+ params.push(poolSlug);
+ }
+
+ query += ` ORDER BY accelerations.added DESC `;
+
+ try {
+ const [rows] = await DB.query(query, params) as RowDataPacket[][];
+ if (rows?.length) {
+ return rows.map(row => ({
+ txid: row.txid,
+ height: row.height,
+ pool: {
+ id: row.id,
+ slug: row.slug,
+ name: row.name,
+ },
+ effective_vsize: row.effective_vsize,
+ effective_fee: row.effective_fee,
+ boost_rate: row.boost_rate,
+ boost_cost: row.boost_cost,
+ }));
+ } else {
+ return [];
+ }
+ } catch (e) {
+ logger.err(`Cannot query acceleration info. Reason: ` + (e instanceof Error ? e.message : e));
+ throw e;
+ }
+ }
+}
+
+export default new AccelerationRepository();
diff --git a/backend/src/utils/bitcoin-script.ts b/backend/src/utils/bitcoin-script.ts
new file mode 100644
index 000000000..3414e8269
--- /dev/null
+++ b/backend/src/utils/bitcoin-script.ts
@@ -0,0 +1,203 @@
+const opcodes = {
+ OP_FALSE: 0,
+ OP_0: 0,
+ OP_PUSHDATA1: 76,
+ OP_PUSHDATA2: 77,
+ OP_PUSHDATA4: 78,
+ OP_1NEGATE: 79,
+ OP_PUSHNUM_NEG1: 79,
+ OP_RESERVED: 80,
+ OP_TRUE: 81,
+ OP_1: 81,
+ OP_2: 82,
+ OP_3: 83,
+ OP_4: 84,
+ OP_5: 85,
+ OP_6: 86,
+ OP_7: 87,
+ OP_8: 88,
+ OP_9: 89,
+ OP_10: 90,
+ OP_11: 91,
+ OP_12: 92,
+ OP_13: 93,
+ OP_14: 94,
+ OP_15: 95,
+ OP_16: 96,
+ OP_PUSHNUM_1: 81,
+ OP_PUSHNUM_2: 82,
+ OP_PUSHNUM_3: 83,
+ OP_PUSHNUM_4: 84,
+ OP_PUSHNUM_5: 85,
+ OP_PUSHNUM_6: 86,
+ OP_PUSHNUM_7: 87,
+ OP_PUSHNUM_8: 88,
+ OP_PUSHNUM_9: 89,
+ OP_PUSHNUM_10: 90,
+ OP_PUSHNUM_11: 91,
+ OP_PUSHNUM_12: 92,
+ OP_PUSHNUM_13: 93,
+ OP_PUSHNUM_14: 94,
+ OP_PUSHNUM_15: 95,
+ OP_PUSHNUM_16: 96,
+ OP_NOP: 97,
+ OP_VER: 98,
+ OP_IF: 99,
+ OP_NOTIF: 100,
+ OP_VERIF: 101,
+ OP_VERNOTIF: 102,
+ OP_ELSE: 103,
+ OP_ENDIF: 104,
+ OP_VERIFY: 105,
+ OP_RETURN: 106,
+ OP_TOALTSTACK: 107,
+ OP_FROMALTSTACK: 108,
+ OP_2DROP: 109,
+ OP_2DUP: 110,
+ OP_3DUP: 111,
+ OP_2OVER: 112,
+ OP_2ROT: 113,
+ OP_2SWAP: 114,
+ OP_IFDUP: 115,
+ OP_DEPTH: 116,
+ OP_DROP: 117,
+ OP_DUP: 118,
+ OP_NIP: 119,
+ OP_OVER: 120,
+ OP_PICK: 121,
+ OP_ROLL: 122,
+ OP_ROT: 123,
+ OP_SWAP: 124,
+ OP_TUCK: 125,
+ OP_CAT: 126,
+ OP_SUBSTR: 127,
+ OP_LEFT: 128,
+ OP_RIGHT: 129,
+ OP_SIZE: 130,
+ OP_INVERT: 131,
+ OP_AND: 132,
+ OP_OR: 133,
+ OP_XOR: 134,
+ OP_EQUAL: 135,
+ OP_EQUALVERIFY: 136,
+ OP_RESERVED1: 137,
+ OP_RESERVED2: 138,
+ OP_1ADD: 139,
+ OP_1SUB: 140,
+ OP_2MUL: 141,
+ OP_2DIV: 142,
+ OP_NEGATE: 143,
+ OP_ABS: 144,
+ OP_NOT: 145,
+ OP_0NOTEQUAL: 146,
+ OP_ADD: 147,
+ OP_SUB: 148,
+ OP_MUL: 149,
+ OP_DIV: 150,
+ OP_MOD: 151,
+ OP_LSHIFT: 152,
+ OP_RSHIFT: 153,
+ OP_BOOLAND: 154,
+ OP_BOOLOR: 155,
+ OP_NUMEQUAL: 156,
+ OP_NUMEQUALVERIFY: 157,
+ OP_NUMNOTEQUAL: 158,
+ OP_LESSTHAN: 159,
+ OP_GREATERTHAN: 160,
+ OP_LESSTHANOREQUAL: 161,
+ OP_GREATERTHANOREQUAL: 162,
+ OP_MIN: 163,
+ OP_MAX: 164,
+ OP_WITHIN: 165,
+ OP_RIPEMD160: 166,
+ OP_SHA1: 167,
+ OP_SHA256: 168,
+ OP_HASH160: 169,
+ OP_HASH256: 170,
+ OP_CODESEPARATOR: 171,
+ OP_CHECKSIG: 172,
+ OP_CHECKSIGVERIFY: 173,
+ OP_CHECKMULTISIG: 174,
+ OP_CHECKMULTISIGVERIFY: 175,
+ OP_NOP1: 176,
+ OP_NOP2: 177,
+ OP_CHECKLOCKTIMEVERIFY: 177,
+ OP_CLTV: 177,
+ OP_NOP3: 178,
+ OP_CHECKSEQUENCEVERIFY: 178,
+ OP_CSV: 178,
+ OP_NOP4: 179,
+ OP_NOP5: 180,
+ OP_NOP6: 181,
+ OP_NOP7: 182,
+ OP_NOP8: 183,
+ OP_NOP9: 184,
+ OP_NOP10: 185,
+ OP_CHECKSIGADD: 186,
+ OP_PUBKEYHASH: 253,
+ OP_PUBKEY: 254,
+ OP_INVALIDOPCODE: 255,
+};
+// add unused opcodes
+for (let i = 187; i <= 255; i++) {
+ opcodes[`OP_RETURN_${i}`] = i;
+}
+
+export { opcodes };
+
+/** extracts m and n from a multisig script (asm), returns nothing if it is not a multisig script */
+export function parseMultisigScript(script: string): void | { m: number, n: number } {
+ if (!script) {
+ return;
+ }
+ const ops = script.split(' ');
+ if (ops.length < 3 || ops.pop() !== 'OP_CHECKMULTISIG') {
+ return;
+ }
+ const opN = ops.pop();
+ if (!opN) {
+ return;
+ }
+ if (!opN.startsWith('OP_PUSHNUM_')) {
+ return;
+ }
+ const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10);
+ if (ops.length < n * 2 + 1) {
+ return;
+ }
+ // pop n public keys
+ for (let i = 0; i < n; i++) {
+ if (!/^0((2|3)\w{64}|4\w{128})$/.test(ops.pop() || '')) {
+ return;
+ }
+ if (!/^OP_PUSHBYTES_(33|65)$/.test(ops.pop() || '')) {
+ return;
+ }
+ }
+ const opM = ops.pop();
+ if (!opM) {
+ return;
+ }
+ if (!opM.startsWith('OP_PUSHNUM_')) {
+ return;
+ }
+ const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10);
+
+ if (ops.length) {
+ return;
+ }
+
+ return { m, n };
+}
+
+export function getVarIntLength(n: number): number {
+ if (n < 0xfd) {
+ return 1;
+ } else if (n <= 0xffff) {
+ return 3;
+ } else if (n <= 0xffffffff) {
+ return 5;
+ } else {
+ return 9;
+ }
+}
\ No newline at end of file
diff --git a/docker/backend/mempool-config.json b/docker/backend/mempool-config.json
index 8f69fd0c1..eca4cf14c 100644
--- a/docker/backend/mempool-config.json
+++ b/docker/backend/mempool-config.json
@@ -3,6 +3,7 @@
"NETWORK": "__MEMPOOL_NETWORK__",
"BACKEND": "__MEMPOOL_BACKEND__",
"ENABLED": __MEMPOOL_ENABLED__,
+ "OFFICIAL": __MEMPOOL_OFFICIAL__,
"HTTP_PORT": __MEMPOOL_HTTP_PORT__,
"SPAWN_CLUSTER_PROCS": __MEMPOOL_SPAWN_CLUSTER_PROCS__,
"API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
@@ -79,7 +80,8 @@
"USERNAME": "__DATABASE_USERNAME__",
"PASSWORD": "__DATABASE_PASSWORD__",
"TIMEOUT": __DATABASE_TIMEOUT__,
- "PID_DIR": "__DATABASE_PID_DIR__"
+ "PID_DIR": "__DATABASE_PID_DIR__",
+ "POOL_SIZE": __DATABASE_POOL_SIZE__
},
"SYSLOG": {
"ENABLED": __SYSLOG_ENABLED__,
diff --git a/docker/backend/start.sh b/docker/backend/start.sh
index ba9b99233..b700bba32 100755
--- a/docker/backend/start.sh
+++ b/docker/backend/start.sh
@@ -4,6 +4,7 @@
__MEMPOOL_NETWORK__=${MEMPOOL_NETWORK:=mainnet}
__MEMPOOL_BACKEND__=${MEMPOOL_BACKEND:=electrum}
__MEMPOOL_ENABLED__=${MEMPOOL_ENABLED:=true}
+__MEMPOOL_OFFICIAL__=${MEMPOOL_OFFICIAL:=false}
__MEMPOOL_HTTP_PORT__=${BACKEND_HTTP_PORT:=8999}
__MEMPOOL_SPAWN_CLUSTER_PROCS__=${MEMPOOL_SPAWN_CLUSTER_PROCS:=0}
__MEMPOOL_API_URL_PREFIX__=${MEMPOOL_API_URL_PREFIX:=/api/v1/}
@@ -81,6 +82,7 @@ __DATABASE_USERNAME__=${DATABASE_USERNAME:=mempool}
__DATABASE_PASSWORD__=${DATABASE_PASSWORD:=mempool}
__DATABASE_TIMEOUT__=${DATABASE_TIMEOUT:=180000}
__DATABASE_PID_DIR__=${DATABASE_PID_DIR:=""}
+__DATABASE_POOL_SIZE__=${DATABASE_POOL_SIZE:=100}
# SYSLOG
__SYSLOG_ENABLED__=${SYSLOG_ENABLED:=false}
@@ -158,6 +160,7 @@ mkdir -p "${__MEMPOOL_CACHE_DIR__}"
sed -i "s!__MEMPOOL_NETWORK__!${__MEMPOOL_NETWORK__}!g" mempool-config.json
sed -i "s!__MEMPOOL_BACKEND__!${__MEMPOOL_BACKEND__}!g" mempool-config.json
sed -i "s!__MEMPOOL_ENABLED__!${__MEMPOOL_ENABLED__}!g" mempool-config.json
+sed -i "s!__MEMPOOL_OFFICIAL__!${__MEMPOOL_OFFICIAL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_HTTP_PORT__!${__MEMPOOL_HTTP_PORT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_SPAWN_CLUSTER_PROCS__!${__MEMPOOL_SPAWN_CLUSTER_PROCS__}!g" mempool-config.json
sed -i "s!__MEMPOOL_API_URL_PREFIX__!${__MEMPOOL_API_URL_PREFIX__}!g" mempool-config.json
@@ -230,6 +233,7 @@ sed -i "s!__DATABASE_USERNAME__!${__DATABASE_USERNAME__}!g" mempool-config.json
sed -i "s!__DATABASE_PASSWORD__!${__DATABASE_PASSWORD__}!g" mempool-config.json
sed -i "s!__DATABASE_TIMEOUT__!${__DATABASE_TIMEOUT__}!g" mempool-config.json
sed -i "s!__DATABASE_PID_DIR__!${__DATABASE_PID_DIR__}!g" mempool-config.json
+sed -i "s!__DATABASE_POOL_SIZE__!${__DATABASE_POOL_SIZE__}!g" mempool-config.json
sed -i "s!__SYSLOG_ENABLED__!${__SYSLOG_ENABLED__}!g" mempool-config.json
sed -i "s!__SYSLOG_HOST__!${__SYSLOG_HOST__}!g" mempool-config.json
diff --git a/docker/init.sh b/docker/init.sh
index ee9ac9542..3c5ec6aa3 100755
--- a/docker/init.sh
+++ b/docker/init.sh
@@ -1,7 +1,7 @@
#!/bin/sh
#backend
-cp ./docker/backend/* ./backend/
+cp -r ./docker/backend/* ./backend/
#geoip-data
mkdir -p ./backend/GeoIP/
@@ -13,8 +13,8 @@ localhostIP="127.0.0.1"
cp ./docker/frontend/* ./frontend
cp ./nginx.conf ./frontend/
cp ./nginx-mempool.conf ./frontend/
-sed -i "s/${localhostIP}:80/0.0.0.0:__MEMPOOL_FRONTEND_HTTP_PORT__/g" ./frontend/nginx.conf
-sed -i "s/${localhostIP}/0.0.0.0/g" ./frontend/nginx.conf
-sed -i "s/user nobody;//g" ./frontend/nginx.conf
-sed -i "s!/etc/nginx/nginx-mempool.conf!/etc/nginx/conf.d/nginx-mempool.conf!g" ./frontend/nginx.conf
-sed -i "s/${localhostIP}:8999/__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__:__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__/g" ./frontend/nginx-mempool.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
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index f49fd614a..0fab7e887 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -32,7 +32,7 @@
"browserify": "^17.0.0",
"clipboard": "^2.0.11",
"domino": "^2.1.6",
- "echarts": "~5.4.3",
+ "echarts": "~5.5.0",
"lightweight-charts": "~3.8.0",
"ngx-echarts": "~16.2.0",
"ngx-infinite-scroll": "^16.0.0",
@@ -7783,12 +7783,12 @@
}
},
"node_modules/echarts": {
- "version": "5.4.3",
- "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.4.3.tgz",
- "integrity": "sha512-mYKxLxhzy6zyTi/FaEbJMOZU1ULGEQHaeIeuMR5L+JnJTpz+YR03mnnpBhbR4+UYJAgiXgpyTVLffPAjOTLkZA==",
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.5.0.tgz",
+ "integrity": "sha512-rNYnNCzqDAPCr4m/fqyUFv7fD9qIsd50S6GDFgO1DxZhncCsNsG7IfUlAlvZe5oSEQxtsjnHiUuppzccry93Xw==",
"dependencies": {
"tslib": "2.3.0",
- "zrender": "5.4.4"
+ "zrender": "5.5.0"
}
},
"node_modules/echarts/node_modules/tslib": {
@@ -17319,9 +17319,9 @@
}
},
"node_modules/zrender": {
- "version": "5.4.4",
- "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.4.4.tgz",
- "integrity": "sha512-0VxCNJ7AGOMCWeHVyTrGzUgrK4asT4ml9PEkeGirAkKNYXYzoPJCLvmyfdoOXcjTHPs10OZVMfD1Rwg16AZyYw==",
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.5.0.tgz",
+ "integrity": "sha512-O3MilSi/9mwoovx77m6ROZM7sXShR/O/JIanvzTwjN3FORfLSr81PsUGd7jlaYOeds9d8tw82oP44+3YucVo+w==",
"dependencies": {
"tslib": "2.3.0"
}
@@ -22822,12 +22822,12 @@
}
},
"echarts": {
- "version": "5.4.3",
- "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.4.3.tgz",
- "integrity": "sha512-mYKxLxhzy6zyTi/FaEbJMOZU1ULGEQHaeIeuMR5L+JnJTpz+YR03mnnpBhbR4+UYJAgiXgpyTVLffPAjOTLkZA==",
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.5.0.tgz",
+ "integrity": "sha512-rNYnNCzqDAPCr4m/fqyUFv7fD9qIsd50S6GDFgO1DxZhncCsNsG7IfUlAlvZe5oSEQxtsjnHiUuppzccry93Xw==",
"requires": {
"tslib": "2.3.0",
- "zrender": "5.4.4"
+ "zrender": "5.5.0"
},
"dependencies": {
"tslib": {
@@ -29869,9 +29869,9 @@
}
},
"zrender": {
- "version": "5.4.4",
- "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.4.4.tgz",
- "integrity": "sha512-0VxCNJ7AGOMCWeHVyTrGzUgrK4asT4ml9PEkeGirAkKNYXYzoPJCLvmyfdoOXcjTHPs10OZVMfD1Rwg16AZyYw==",
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.5.0.tgz",
+ "integrity": "sha512-O3MilSi/9mwoovx77m6ROZM7sXShR/O/JIanvzTwjN3FORfLSr81PsUGd7jlaYOeds9d8tw82oP44+3YucVo+w==",
"requires": {
"tslib": "2.3.0"
},
diff --git a/frontend/package.json b/frontend/package.json
index 330250871..2a0de0977 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -84,7 +84,7 @@
"browserify": "^17.0.0",
"clipboard": "^2.0.11",
"domino": "^2.1.6",
- "echarts": "~5.4.3",
+ "echarts": "~5.5.0",
"lightweight-charts": "~3.8.0",
"ngx-echarts": "~16.2.0",
"ngx-infinite-scroll": "^16.0.0",
diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html
index 054f26fe6..009040889 100644
--- a/frontend/src/app/components/about/about.component.html
+++ b/frontend/src/app/components/about/about.component.html
@@ -416,7 +416,7 @@
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the full license terms for more details.
- This program incorporates software and other components licensed from third parties. See the full list of Third-Party Licenses for legal notices from those projects.
+ This program incorporates software and other components licensed from third parties. See the full list of Third-Party Licenses for legal notices from those projects.
Trademark Notice
@@ -429,10 +429,6 @@
-
-
diff --git a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.html b/frontend/src/app/components/accelerate-preview/accelerate-preview.component.html
index 2d2c9c3f3..b4488d33d 100644
--- a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.html
+++ b/frontend/src/app/components/accelerate-preview/accelerate-preview.component.html
@@ -129,7 +129,7 @@
- mempool.space fee
+ Accelerator Service Fee
+{{ estimate.mempoolBaseFee | number }}
@@ -141,7 +141,7 @@
- Transaction vsize fee
+ Transaction Size Surcharge
+{{ estimate.vsizeFee | number }}
diff --git a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.html b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.html
index 3698a3060..faf48eac7 100644
--- a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.html
+++ b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.html
@@ -23,6 +23,9 @@
1M
+
+ 3M
+
diff --git a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.scss b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.scss
index e01beb350..11b468a24 100644
--- a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.scss
+++ b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.scss
@@ -25,7 +25,8 @@
flex-direction: column;
padding: 0px 15px;
width: 100%;
- height: calc(100vh - 250px);
+ height: calc(100vh - 225px);
+ min-height: 400px;
@media (min-width: 992px) {
height: calc(100vh - 150px);
}
@@ -35,6 +36,7 @@
display: flex;
flex: 1;
width: 100%;
+ height: 100%;
padding-bottom: 20px;
padding-right: 10px;
@media (max-width: 992px) {
diff --git a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts
index e786635ba..001f005a1 100644
--- a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts
+++ b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts
@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnDestroy, OnInit } from '@angular/core';
-import { EChartsOption, graphic } from 'echarts';
+import { EChartsOption } from 'echarts';
import { Observable, Subscription, combineLatest, fromEvent } from 'rxjs';
-import { map, max, startWith, switchMap, tap } from 'rxjs/operators';
+import { startWith, switchMap, tap } from 'rxjs/operators';
import { SeoService } from '../../../services/seo.service';
import { formatNumber } from '@angular/common';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
@@ -11,7 +11,6 @@ import { MiningService } from '../../../services/mining.service';
import { ActivatedRoute } from '@angular/router';
import { Acceleration } from '../../../interfaces/node-api.interface';
import { ServicesApiServices } from '../../../services/services-api.service';
-import { ApiService } from '../../../services/api.service';
@Component({
selector: 'app-acceleration-fees-graph',
@@ -29,7 +28,7 @@ import { ApiService } from '../../../services/api.service';
})
export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
@Input() widget: boolean = false;
- @Input() height: number | string = '200';
+ @Input() height: number = 300;
@Input() right: number | string = 45;
@Input() left: number | string = 75;
@Input() accelerations$: Observable;
@@ -55,7 +54,6 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
constructor(
@Inject(LOCALE_ID) public locale: string,
private seoService: SeoService,
- private apiService: ApiService,
private servicesApiService: ServicesApiServices,
private formBuilder: UntypedFormBuilder,
private storageService: StorageService,
@@ -69,104 +67,56 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
}
ngOnInit(): void {
- this.isLoading = true;
if (this.widget) {
- this.miningWindowPreference = '1m';
- this.timespan = this.miningWindowPreference;
-
- this.statsObservable$ = combineLatest([
- (this.accelerations$ || this.servicesApiService.getAccelerationHistory$({ timeframe: this.miningWindowPreference })),
- this.apiService.getHistoricalBlockFees$(this.miningWindowPreference),
- fromEvent(window, 'resize').pipe(startWith(null)),
- ]).pipe(
- tap(([accelerations, blockFeesResponse]) => {
- this.prepareChartOptions(accelerations, blockFeesResponse.body);
- }),
- map(([accelerations, blockFeesResponse]) => {
- return {
- avgFeesPaid: accelerations.filter(acc => acc.status === 'completed').reduce((total, acc) => total + (acc.feePaid - acc.baseFee - acc.vsizeFee), 0) / accelerations.length
- };
- }),
- );
+ this.miningWindowPreference = '3m';
} else {
this.seoService.setTitle($localize`:@@bcf34abc2d9ed8f45a2f65dd464c46694e9a181e:Acceleration Fees`);
- this.miningWindowPreference = this.miningService.getDefaultTimespan('1w');
- this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
- this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
- this.route.fragment.subscribe((fragment) => {
- if (['24h', '3d', '1w', '1m'].indexOf(fragment) > -1) {
- this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
- }
- });
- this.statsObservable$ = combineLatest([
- this.radioGroupForm.get('dateSpan').valueChanges.pipe(
- startWith(this.radioGroupForm.controls.dateSpan.value),
- switchMap((timespan) => {
- this.isLoading = true;
- this.storageService.setValue('miningWindowPreference', timespan);
- this.timespan = timespan;
- return this.servicesApiService.getAccelerationHistory$({});
- })
- ),
- this.radioGroupForm.get('dateSpan').valueChanges.pipe(
- startWith(this.radioGroupForm.controls.dateSpan.value),
- switchMap((timespan) => {
- return this.apiService.getHistoricalBlockFees$(timespan);
- })
- )
- ]).pipe(
- tap(([accelerations, blockFeesResponse]) => {
- this.prepareChartOptions(accelerations, blockFeesResponse.body);
- })
- );
+ this.miningWindowPreference = this.miningService.getDefaultTimespan('3m');
}
- this.statsSubscription = this.statsObservable$.subscribe(() => {
- this.isLoading = false;
- this.cd.markForCheck();
+ this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
+ this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
+
+ this.route.fragment.subscribe((fragment) => {
+ if (['24h', '3d', '1w', '1m', '3m'].indexOf(fragment) > -1) {
+ this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
+ }
});
+ this.statsObservable$ = combineLatest([
+ this.radioGroupForm.get('dateSpan').valueChanges.pipe(
+ startWith(this.radioGroupForm.controls.dateSpan.value),
+ switchMap((timespan) => {
+ if (!this.widget) {
+ this.storageService.setValue('miningWindowPreference', timespan);
+ }
+ this.isLoading = true;
+ this.timespan = timespan;
+ return this.servicesApiService.getAggregatedAccelerationHistory$({timeframe: this.timespan});
+ })
+ ),
+ fromEvent(window, 'resize').pipe(startWith(null)),
+ ]).pipe(
+ tap(([history]) => {
+ this.isLoading = false;
+ this.prepareChartOptions(history);
+ this.cd.markForCheck();
+ })
+ );
+
+ this.statsObservable$.subscribe();
}
- prepareChartOptions(accelerations, blockFees) {
+ prepareChartOptions(data) {
let title: object;
-
- const blockAccelerations = {};
-
- for (const acceleration of accelerations) {
- if (acceleration.status === 'completed') {
- if (!blockAccelerations[acceleration.blockHeight]) {
- blockAccelerations[acceleration.blockHeight] = [];
- }
- blockAccelerations[acceleration.blockHeight].push(acceleration);
- }
- }
-
- let last = null;
- let minValue = Infinity;
- let maxValue = 0;
- const data = [];
- for (const val of blockFees) {
- if (last == null) {
- last = val.avgHeight;
- }
- let totalFeeDelta = 0;
- let totalFeePaid = 0;
- let totalCount = 0;
- let blockCount = 0;
- while (last <= val.avgHeight) {
- blockCount++;
- totalFeeDelta += (blockAccelerations[last] || []).reduce((total, acc) => total + acc.feeDelta, 0);
- totalFeePaid += (blockAccelerations[last] || []).reduce((total, acc) => total + (acc.feePaid - acc.baseFee - acc.vsizeFee), 0);
- totalCount += (blockAccelerations[last] || []).length;
- last++;
- }
- minValue = Math.min(minValue, val.avgFees);
- maxValue = Math.max(maxValue, val.avgFees);
- data.push({
- ...val,
- feeDelta: totalFeeDelta,
- avgFeePaid: (totalFeePaid / blockCount),
- accelerations: totalCount / blockCount,
- });
+ if (data.length === 0) {
+ title = {
+ textStyle: {
+ color: 'grey',
+ fontSize: 15
+ },
+ text: $localize`No accelerated transaction for this timeframe`,
+ left: 'center',
+ top: 'center'
+ };
}
this.chartOptions = {
@@ -177,11 +127,11 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
],
animation: false,
grid: {
- height: this.height,
+ height: (this.widget && this.height) ? this.height - 30 : undefined,
+ top: this.widget ? 20 : 40,
+ bottom: this.widget ? 30 : 80,
right: this.right,
left: this.left,
- bottom: this.widget ? 30 : 80,
- top: this.widget ? 20 : (this.isMobile() ? 10 : 50),
},
tooltip: {
show: !this.isMobile(),
@@ -197,29 +147,23 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
align: 'left',
},
borderColor: '#000',
- formatter: function (data) {
- if (data.length <= 0) {
- return '';
- }
- let tooltip = `
- ${formatterXAxis(this.locale, this.timespan, parseInt(data[0].axisValue, 10))} `;
+ formatter: (ticks) => {
+ let tooltip = `${formatterXAxis(this.locale, this.timespan, parseInt(ticks[0].axisValue, 10))} `;
- for (const tick of data.reverse()) {
- if (tick.data[1] >= 1_000_000) {
- tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1] / 100_000_000, this.locale, '1.0-3')} BTC `;
- } else {
- tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')} sats `;
- }
+ if (ticks[0].data[1] > 10_000_000) {
+ tooltip += `${ticks[0].marker} ${ticks[0].seriesName}: ${formatNumber(ticks[0].data[1] / 100_000_000, this.locale, '1.0-0')} BTC `;
+ } else {
+ tooltip += `${ticks[0].marker} ${ticks[0].seriesName}: ${formatNumber(ticks[0].data[1], this.locale, '1.0-0')} sats `;
}
if (['24h', '3d'].includes(this.timespan)) {
- tooltip += `` + $localize`At block: ${data[0].data[2]}` + ` `;
+ tooltip += `` + $localize`At block: ${ticks[0].data[2]}` + ` `;
} else {
- tooltip += `` + $localize`Around block: ${data[0].data[2]}` + ` `;
+ tooltip += `` + $localize`Around block: ${ticks[0].data[2]}` + ` `;
}
return tooltip;
- }.bind(this)
+ }
},
xAxis: data.length === 0 ? undefined :
{
@@ -228,7 +172,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
nameTextStyle: {
padding: [10, 0, 0, 0],
},
- type: 'category',
+ type: 'time',
boundaryGap: false,
axisLine: { onZero: true },
axisLabel: {
@@ -243,15 +187,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
legend: {
data: [
{
- name: 'In-band fees per block',
- inactiveColor: 'rgb(110, 112, 121)',
- textStyle: {
- color: 'white',
- },
- icon: 'roundRect',
- },
- {
- name: 'Total bid boost per block',
+ name: 'Total bid boost',
inactiveColor: 'rgb(110, 112, 121)',
textStyle: {
color: 'white',
@@ -260,8 +196,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
},
],
selected: {
- 'In-band fees per block': false,
- 'Total bid boost per block': true,
+ 'Total bid boost': true,
},
show: !this.widget,
},
@@ -304,21 +239,13 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
{
legendHoverLink: false,
zlevel: 1,
- name: 'Total bid boost per block',
- data: data.map(block => [block.timestamp * 1000, block.avgFeePaid, block.avgHeight]),
+ name: 'Total bid boost',
+ data: data.map(h => {
+ return [h.timestamp * 1000, h.sumBidBoost, h.avgHeight]
+ }),
stack: 'Total',
type: 'bar',
- barWidth: '100%',
- large: true,
- },
- {
- legendHoverLink: false,
- zlevel: 0,
- name: 'In-band fees per block',
- data: data.map(block => [block.timestamp * 1000, block.avgFees, block.avgHeight]),
- stack: 'Total',
- type: 'bar',
- barWidth: '100%',
+ barWidth: '90%',
large: true,
},
],
@@ -347,17 +274,6 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
}
},
}],
- visualMap: {
- type: 'continuous',
- min: minValue,
- max: maxValue,
- dimension: 1,
- seriesIndex: 1,
- show: false,
- inRange: {
- color: ['#F4511E7f', '#FB8C007f', '#FFB3007f', '#FDD8357f', '#7CB3427f'].reverse() // Gradient color range
- }
- },
};
}
diff --git a/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.html b/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.html
index 5e8aa729a..fef91acc0 100644
--- a/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.html
+++ b/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.html
@@ -3,16 +3,16 @@
Requests
-
{{ stats.count }}
+
{{ stats.totalRequested }}
accelerated
Total Bid Boost
-
{{ stats.totalFeesPaid / 100_000_000 | amountShortener: 4 }} BTC
+
{{ stats.totalBidBoost / 100_000_000 | amountShortener: 4 }} BTC
-
+
diff --git a/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.ts b/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.ts
index 0a6ef065c..29909ff39 100644
--- a/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.ts
+++ b/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.ts
@@ -1,9 +1,12 @@
-import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
-import { Observable, of } from 'rxjs';
-import { switchMap } from 'rxjs/operators';
-import { ApiService } from '../../../services/api.service';
-import { StateService } from '../../../services/state.service';
-import { Acceleration } from '../../../interfaces/node-api.interface';
+import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
+import { Observable } from 'rxjs';
+import { ServicesApiServices } from '../../../services/services-api.service';
+
+export type AccelerationStats = {
+ totalRequested: number;
+ totalBidBoost: number;
+ successRate: number;
+}
@Component({
selector: 'app-acceleration-stats',
@@ -12,35 +15,13 @@ import { Acceleration } from '../../../interfaces/node-api.interface';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AccelerationStatsComponent implements OnInit {
- @Input() timespan: '24h' | '1w' | '1m' = '24h';
- @Input() accelerations$: Observable;
- public accelerationStats$: Observable;
+ accelerationStats$: Observable;
constructor(
- private apiService: ApiService,
- private stateService: StateService,
+ private servicesApiService: ServicesApiServices
) { }
ngOnInit(): void {
- this.accelerationStats$ = this.accelerations$.pipe(
- switchMap(accelerations => {
- let totalFeesPaid = 0;
- let totalSucceeded = 0;
- let totalCanceled = 0;
- for (const acc of accelerations) {
- if (acc.status === 'completed') {
- totalSucceeded++;
- totalFeesPaid += (acc.feePaid - acc.baseFee - acc.vsizeFee) || 0;
- } else if (acc.status === 'failed') {
- totalCanceled++;
- }
- }
- return of({
- count: totalSucceeded,
- totalFeesPaid,
- successRate: (totalSucceeded + totalCanceled > 0) ? ((totalSucceeded / (totalSucceeded + totalCanceled)) * 100) : 0.0,
- });
- })
- );
+ this.accelerationStats$ = this.servicesApiService.getAccelerationStats$();
}
}
diff --git a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html
index 9a919ca54..177eee973 100644
--- a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html
+++ b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html
@@ -1,4 +1,4 @@
-
diff --git a/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.ts b/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.ts
index a2abc657a..ba9240d1b 100644
--- a/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.ts
+++ b/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.ts
@@ -60,7 +60,7 @@ export class AcceleratorDashboardComponent implements OnInit {
this.accelerations$ = this.stateService.chainTip$.pipe(
distinctUntilChanged(),
switchMap(() => {
- return this.serviceApiServices.getAccelerationHistory$({ timeframe: '1m' }).pipe(
+ return this.serviceApiServices.getAccelerationHistory$({ timeframe: '3m', page: 1, pageLength: 100}).pipe(
catchError(() => {
return of([]);
}),
@@ -71,7 +71,7 @@ export class AcceleratorDashboardComponent implements OnInit {
this.minedAccelerations$ = this.accelerations$.pipe(
map(accelerations => {
- return accelerations.filter(acc => ['mined', 'completed', 'failed'].includes(acc.status));
+ return accelerations.filter(acc => ['mined', 'completed'].includes(acc.status));
})
);
@@ -128,11 +128,11 @@ export class AcceleratorDashboardComponent implements OnInit {
@HostListener('window:resize', ['$event'])
onResize(): void {
if (window.innerWidth >= 992) {
- this.graphHeight = 330;
+ this.graphHeight = 380;
} else if (window.innerWidth >= 768) {
- this.graphHeight = 245;
+ this.graphHeight = 300;
} else {
- this.graphHeight = 210;
+ this.graphHeight = 270;
}
}
}
diff --git a/frontend/src/app/components/block-overview-graph/tx-view.ts b/frontend/src/app/components/block-overview-graph/tx-view.ts
index da36b9880..b6008ef1d 100644
--- a/frontend/src/app/components/block-overview-graph/tx-view.ts
+++ b/frontend/src/app/components/block-overview-graph/tx-view.ts
@@ -4,6 +4,7 @@ import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from './sprite-ty
import { hexToColor } from './utils';
import BlockScene from './block-scene';
import { TransactionStripped } from '../../interfaces/node-api.interface';
+import { TransactionFlags } from '../../shared/filters.utils';
const hoverTransitionTime = 300;
const defaultHoverColor = hexToColor('1bd8f4');
@@ -58,7 +59,7 @@ export default class TxView implements TransactionStripped {
this.acc = tx.acc;
this.rate = tx.rate;
this.status = tx.status;
- this.bigintFlags = tx.flags ? BigInt(tx.flags) : 0n;
+ this.bigintFlags = tx.flags ? (BigInt(tx.flags) | (this.acc ? TransactionFlags.acceleration : 0n)): 0n;
this.initialised = false;
this.vertexArray = scene.vertexArray;
diff --git a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html
index 9e3e94111..8fb687ebd 100644
--- a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html
+++ b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html
@@ -28,7 +28,7 @@
-
+
Effective fee rate
Accelerated fee rate
diff --git a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.scss b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.scss
index 9ad50fc30..81a79eb67 100644
--- a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.scss
+++ b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.scss
@@ -11,6 +11,7 @@
text-align: left;
min-width: 320px;
pointer-events: none;
+ z-index: 11;
&.clickable {
pointer-events: all;
diff --git a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.ts b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.ts
index a6e2a2697..6b23276c8 100644
--- a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.ts
+++ b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.ts
@@ -2,6 +2,7 @@ import { Component, ElementRef, ViewChild, Input, OnChanges, ChangeDetectionStra
import { Position } from '../../components/block-overview-graph/sprite-types.js';
import { Price } from '../../services/price.service';
import { TransactionStripped } from '../../interfaces/node-api.interface.js';
+import { TransactionFlags } from '../../shared/filters.utils';
@Component({
selector: 'app-block-overview-tooltip',
@@ -22,6 +23,7 @@ export class BlockOverviewTooltipComponent implements OnChanges {
feeRate = 0;
effectiveRate;
acceleration;
+ hasEffectiveRate: boolean = false;
tooltipPosition: Position = { x: 0, y: 0 };
@@ -55,6 +57,8 @@ export class BlockOverviewTooltipComponent implements OnChanges {
this.feeRate = this.fee / this.vsize;
this.effectiveRate = tx.rate;
this.acceleration = tx.acc;
+ this.hasEffectiveRate = Math.abs((this.fee / this.vsize) - this.effectiveRate) > 0.05
+ || (tx.bigintFlags && (tx.bigintFlags & (TransactionFlags.cpfp_child | TransactionFlags.cpfp_parent)) > 0n);
}
}
}
diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html
index 443fc1946..e9f64b9b8 100644
--- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html
+++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html
@@ -56,7 +56,7 @@
-
+
{{ block.extras.pool.name}}
diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss
index 795e1f4df..c1cc6809d 100644
--- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss
+++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss
@@ -166,7 +166,7 @@
opacity: 1;
}
.hide {
- opacity: 0;
+ opacity: 0.4;
pointer-events : none;
}
diff --git a/frontend/src/app/components/difficulty-mining/difficulty-mining.component.html b/frontend/src/app/components/difficulty-mining/difficulty-mining.component.html
index 215b5c68a..1461b0c59 100644
--- a/frontend/src/app/components/difficulty-mining/difficulty-mining.component.html
+++ b/frontend/src/app/components/difficulty-mining/difficulty-mining.component.html
@@ -46,15 +46,13 @@
Next Halving
-
-
{{ i }} blocks
-
{{ i }} block
+
{{ timeUntilHalving | date }}
-
{{ timeUntilHalving | date }}
+
diff --git a/frontend/src/app/components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component.html b/frontend/src/app/components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component.html
index 0a37b1d13..61de92e5d 100644
--- a/frontend/src/app/components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component.html
+++ b/frontend/src/app/components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component.html
@@ -1,4 +1,4 @@
-
+
diff --git a/frontend/src/app/components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component.scss b/frontend/src/app/components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component.scss
index ebe52e6d1..b711f659b 100644
--- a/frontend/src/app/components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component.scss
+++ b/frontend/src/app/components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component.scss
@@ -4,6 +4,12 @@
margin-top: 13px;
}
+.extra-margin-right {
+ @media (max-width: 380px) {
+ margin-left: -10px;
+ }
+}
+
tr, td, th {
border: 0px;
padding-top: 0.65rem;
diff --git a/frontend/src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.scss b/frontend/src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.scss
index e754234c3..7a3d9e49f 100644
--- a/frontend/src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.scss
+++ b/frontend/src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.scss
@@ -31,7 +31,7 @@ tr, td, th {
}
.transaction {
- width: 20%;
+ width: 65%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -57,7 +57,7 @@ tr, td, th {
}
.output {
- width: 20%;
+ width: 50%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
diff --git a/frontend/src/app/components/master-page/master-page.component.html b/frontend/src/app/components/master-page/master-page.component.html
index 66d8627a8..828ece1e5 100644
--- a/frontend/src/app/components/master-page/master-page.component.html
+++ b/frontend/src/app/components/master-page/master-page.component.html
@@ -52,7 +52,7 @@
-
+
beta
diff --git a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html
index 7cc458e60..6fb8dd4d6 100644
--- a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html
+++ b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html
@@ -8,6 +8,7 @@
[showFilters]="showFilters"
[filterFlags]="filterFlags"
[filterMode]="filterMode"
+ [excludeFilters]="['nonstandard']"
[overrideColors]="overrideColors"
(txClickEvent)="onTxClick($event)"
>
diff --git a/frontend/src/app/components/pool-ranking/pool-ranking.component.ts b/frontend/src/app/components/pool-ranking/pool-ranking.component.ts
index f7b91e151..a6f43909a 100644
--- a/frontend/src/app/components/pool-ranking/pool-ranking.component.ts
+++ b/frontend/src/app/components/pool-ranking/pool-ranking.component.ts
@@ -163,7 +163,7 @@ export class PoolRankingComponent implements OnInit {
const i = pool.blockCount.toString();
if (this.miningWindowPreference === '24h') {
return `${pool.name} (${pool.share}%) ` +
- pool.lastEstimatedHashrate.toString() + ' PH/s' +
+ pool.lastEstimatedHashrate.toString() + ' ' + miningStats.miningUnits.hashrateUnit +
` ` + $localize`${ i }:INTERPOLATION: blocks`;
} else {
return `${pool.name} (${pool.share}%) ` +
@@ -201,7 +201,7 @@ export class PoolRankingComponent implements OnInit {
const i = totalBlockOther.toString();
if (this.miningWindowPreference === '24h') {
return `` + $localize`Other (${percentage})` + ` ` +
- totalEstimatedHashrateOther.toString() + ' PH/s' +
+ totalEstimatedHashrateOther.toString() + ' ' + miningStats.miningUnits.hashrateUnit +
` ` + $localize`${ i }:INTERPOLATION: blocks`;
} else {
return `` + $localize`Other (${percentage})` + ` ` +
diff --git a/frontend/src/app/components/server-health/server-health.component.html b/frontend/src/app/components/server-health/server-health.component.html
new file mode 100644
index 000000000..0350997e1
--- /dev/null
+++ b/frontend/src/app/components/server-health/server-health.component.html
@@ -0,0 +1,32 @@
+
+
+
Node Status
+
+
+
+
+
+
+
+
+ Host
+ RTT
+ RTT
+ Height
+
+
+ {{ i + 1 }}
+ {{ host.active ? 'βοΈ' : host.flag }}
+ {{ host.link }}
+ {{ (host.rtt / 1000) | number : '1.1-1' }} {{ host.rtt == null ? '' : 's'}} {{ !host.checked ? 'β³' : (host.unreachable ? 'π₯' : 'β
') }}
+ {{ host.rtt | number : '1.0-0' }} {{ host.rtt == null ? '' : 'ms'}} {{ !host.checked ? 'β³' : (host.unreachable ? 'π₯' : 'β
') }}
+ {{ host.latestHeight }} {{ !host.checked ? 'β³' : (host.outOfSync ? 'π«' : (host.latestHeight && host.latestHeight < tip ? 'π§' : 'β
')) }}
+
+
+
+
+
+
diff --git a/frontend/src/app/components/server-health/server-health.component.scss b/frontend/src/app/components/server-health/server-health.component.scss
new file mode 100644
index 000000000..572628a9d
--- /dev/null
+++ b/frontend/src/app/components/server-health/server-health.component.scss
@@ -0,0 +1,72 @@
+.tomahawk {
+ .links {
+ float: right;
+ text-align: right;
+ margin-top: 1em;
+
+ a, span {
+ margin-left: 1em;
+ }
+ }
+
+ .dashboard-title {
+ text-align: left;
+ }
+
+ .status-panel {
+ max-width: 720px;
+ margin: auto;
+ margin-top: 2em;
+ padding: 1em;
+ background: #24273e;
+ }
+
+ .status-table {
+ width: 100%;
+
+ td, th {
+ padding: 0.25em;
+
+ &.rank, &.flag {
+ width: 28px;
+ text-align: right;
+ }
+ &.rtt, &.height {
+ width: 92px;
+ text-align: right;
+ }
+ &.only-small {
+ display: table-cell;
+ &.rtt {
+ width: 60px;
+ }
+ }
+ &.only-large {
+ display: none;
+ }
+ &.height {
+ padding-right: 0.5em;
+ }
+ &.host {
+ width: auto;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ @media (min-width: 576px) {
+ &.rank, &.flag {
+ width: 32px;
+ }
+ &.rtt, &.height {
+ width: 96px;
+ }
+ &.only-small {
+ display: none;
+ }
+ &.only-large {
+ display: table-cell;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/app/components/server-health/server-health.component.ts b/frontend/src/app/components/server-health/server-health.component.ts
new file mode 100644
index 000000000..363b28111
--- /dev/null
+++ b/frontend/src/app/components/server-health/server-health.component.ts
@@ -0,0 +1,67 @@
+import { Component, OnInit, ChangeDetectionStrategy, SecurityContext } from '@angular/core';
+import { WebsocketService } from '../../services/websocket.service';
+import { Observable, Subject, map } from 'rxjs';
+import { StateService } from '../../services/state.service';
+import { HealthCheckHost } from '../../interfaces/websocket.interface';
+import { DomSanitizer } from '@angular/platform-browser';
+
+@Component({
+ selector: 'app-server-health',
+ templateUrl: './server-health.component.html',
+ styleUrls: ['./server-health.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class ServerHealthComponent implements OnInit {
+ hosts$: Observable;
+ tip$: Subject;
+
+ constructor(
+ private websocketService: WebsocketService,
+ private stateService: StateService,
+ public sanitizer: DomSanitizer,
+ ) {}
+
+ ngOnInit(): void {
+ this.hosts$ = this.stateService.serverHealth$.pipe(
+ map((hosts) => {
+ const subpath = window.location.pathname.slice(0, -6);
+ for (const host of hosts) {
+ let statusUrl = '';
+ let linkHost = '';
+ if (host.socket) {
+ statusUrl = 'https://' + window.location.hostname + subpath + '/status';
+ linkHost = window.location.hostname + subpath;
+ } else {
+ const hostUrl = new URL(host.host);
+ statusUrl = 'https://' + hostUrl.hostname + subpath + '/status';
+ linkHost = hostUrl.hostname + subpath;
+ }
+ host.statusPage = this.sanitizer.bypassSecurityTrustResourceUrl(this.sanitizer.sanitize(SecurityContext.URL, statusUrl));
+ host.link = linkHost;
+ host.flag = this.parseFlag(host.host);
+ }
+ return hosts;
+ })
+ );
+ this.tip$ = this.stateService.chainTip$;
+ this.websocketService.want(['blocks', 'tomahawk']);
+ }
+
+ trackByFn(index: number, host: HealthCheckHost): string {
+ return host.host;
+ }
+
+ private parseFlag(host: string): string {
+ if (host.includes('.fra.')) {
+ return 'π©πͺ';
+ } else if (host.includes('.tk7.')) {
+ return 'π―π΅';
+ } else if (host.includes('.fmt.')) {
+ return 'πΊπΈ';
+ } else if (host.includes('.va1.')) {
+ return 'πΊπΈ';
+ } else {
+ return '';
+ }
+ }
+}
diff --git a/frontend/src/app/components/server-health/server-status.component.html b/frontend/src/app/components/server-health/server-status.component.html
new file mode 100644
index 000000000..23ca16a61
--- /dev/null
+++ b/frontend/src/app/components/server-health/server-status.component.html
@@ -0,0 +1,16 @@
+
diff --git a/frontend/src/app/components/server-health/server-status.component.scss b/frontend/src/app/components/server-health/server-status.component.scss
new file mode 100644
index 000000000..09bebe040
--- /dev/null
+++ b/frontend/src/app/components/server-health/server-status.component.scss
@@ -0,0 +1,26 @@
+.tomahawk {
+ .links {
+ float: right;
+ text-align: right;
+ margin-top: 1em;
+
+ a, span {
+ margin-left: 1em;
+ }
+ }
+
+ .dashboard-title {
+ text-align: left;
+ }
+
+ .mempoolStatus {
+ width: 100%;
+ height: 270px;
+ }
+
+ .hostLink {
+ text-align: center;
+ margin: auto;
+ margin-top: 1em;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/app/components/server-health/server-status.component.ts b/frontend/src/app/components/server-health/server-status.component.ts
new file mode 100644
index 000000000..2e522b805
--- /dev/null
+++ b/frontend/src/app/components/server-health/server-status.component.ts
@@ -0,0 +1,80 @@
+import { Component, OnInit, ChangeDetectionStrategy, SecurityContext, OnDestroy, ChangeDetectorRef } from '@angular/core';
+import { WebsocketService } from '../../services/websocket.service';
+import { Observable, Subject, Subscription, map, tap } from 'rxjs';
+import { StateService } from '../../services/state.service';
+import { HealthCheckHost } from '../../interfaces/websocket.interface';
+import { DomSanitizer } from '@angular/platform-browser';
+
+@Component({
+ selector: 'app-server-status',
+ templateUrl: './server-status.component.html',
+ styleUrls: ['./server-status.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class ServerStatusComponent implements OnInit, OnDestroy {
+ tip$: Subject;
+ hosts: HealthCheckHost[] = [];
+ hostSubscription: Subscription;
+
+ constructor(
+ private websocketService: WebsocketService,
+ private stateService: StateService,
+ private cd: ChangeDetectorRef,
+ public sanitizer: DomSanitizer,
+ ) {}
+
+ ngOnInit(): void {
+ this.hostSubscription = this.stateService.serverHealth$.pipe(
+ map((hosts) => {
+ const subpath = window.location.pathname.slice(0, -8);
+ for (const host of hosts) {
+ let statusUrl = '';
+ let linkHost = '';
+ if (host.socket) {
+ statusUrl = 'https://' + window.location.hostname + subpath + '/status';
+ linkHost = window.location.hostname + subpath;
+ } else {
+ const hostUrl = new URL(host.host);
+ statusUrl = 'https://' + hostUrl.hostname + subpath + '/status';
+ linkHost = hostUrl.hostname + subpath;
+ }
+ host.statusPage = this.sanitizer.bypassSecurityTrustResourceUrl(this.sanitizer.sanitize(SecurityContext.URL, statusUrl));
+ host.link = linkHost;
+ }
+ return hosts;
+ }),
+ tap((hosts) => {
+ if (this.hosts.length !== hosts.length) {
+ this.hosts = hosts.sort((a,b) => {
+ const aParts = (a.host?.split('.') || []).reverse();
+ const bParts = (b.host?.split('.') || []).reverse();
+ let i = 0;
+ while (i < Math.max(aParts.length, bParts.length)) {
+ if (aParts[i] && !bParts[i]) {
+ return 1;
+ } else if (bParts[i] && !aParts[i]) {
+ return -1;
+ } else if (aParts[i] !== bParts[i]) {
+ return aParts[i].localeCompare(bParts[i]);
+ }
+ i++;
+ }
+ return 0;
+ });
+ }
+ this.cd.markForCheck();
+ })
+ ).subscribe();
+ this.tip$ = this.stateService.chainTip$;
+ this.websocketService.want(['blocks', 'tomahawk']);
+ }
+
+ trackByFn(index: number, host: HealthCheckHost): string {
+ return host.host;
+ }
+
+ ngOnDestroy(): void {
+ this.hosts = [];
+ this.hostSubscription.unsubscribe();
+ }
+}
diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts
index 589b48869..60797a9a1 100644
--- a/frontend/src/app/components/transaction/transaction.component.ts
+++ b/frontend/src/app/components/transaction/transaction.component.ts
@@ -26,6 +26,7 @@ import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pi
import { Price, PriceService } from '../../services/price.service';
import { isFeatureActive } from '../../bitcoin.utils';
import { ServicesApiServices } from '../../services/services-api.service';
+import { EnterpriseService } from '../../services/enterprise.service';
@Component({
selector: 'app-transaction',
@@ -116,12 +117,15 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
private servicesApiService: ServicesApiServices,
private seoService: SeoService,
private priceService: PriceService,
- private storageService: StorageService
+ private storageService: StorageService,
+ private enterpriseService: EnterpriseService,
) {}
ngOnInit() {
this.acceleratorAvailable = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === '';
+ this.enterpriseService.page();
+
this.websocketService.want(['blocks', 'mempool-blocks']);
this.stateService.networkChanged$.subscribe(
(network) => {
@@ -527,6 +531,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
if (!this.txId) {
return;
}
+ this.enterpriseService.goal(8);
this.showAccelerationSummary = true && this.acceleratorAvailable;
this.scrollIntoAccelPreview = !this.scrollIntoAccelPreview;
return false;
diff --git a/frontend/src/app/dashboard/dashboard.component.scss b/frontend/src/app/dashboard/dashboard.component.scss
index 49d848c20..b039e36f8 100644
--- a/frontend/src/app/dashboard/dashboard.component.scss
+++ b/frontend/src/app/dashboard/dashboard.component.scss
@@ -427,6 +427,7 @@
.card-title-liquid {
padding-top: 20px;
+ margin-left: 10px;
}
.in-progress-message {
diff --git a/frontend/src/app/docs/api-docs/api-docs-data.ts b/frontend/src/app/docs/api-docs/api-docs-data.ts
index 5ce7c491f..d79c23e4a 100644
--- a/frontend/src/app/docs/api-docs/api-docs-data.ts
+++ b/frontend/src/app/docs/api-docs/api-docs-data.ts
@@ -1017,7 +1017,7 @@ export const restApiDocsData = [
fragment: "get-address-transactions",
title: "GET Address Transactions",
description: {
- default: "Get transaction history for the specified address/scripthash, sorted with newest first. Returns up to 50 mempool transactions plus the first 25 confirmed transactions. You can request more confirmed transactions using :last_seen_txid (see below)."
+ default: "Get transaction history for the specified address/scripthash, sorted with newest first. Returns up to 50 mempool transactions plus the first 25 confirmed transactions. You can request more confirmed transactions using an after_txid query parameter."
},
urlString: "/address/:address/txs",
showConditions: bitcoinNetworks.concat(liquidNetworks),
@@ -10070,8 +10070,7 @@ export const restApiDocsData = [
"id": 89,
"user_id": 1,
"txid": "ae2639469ec000ed1d14e2550cbb01794e1cd288a00cdc7cce18398ba3cc2ffe",
- "status": "failed",
- "estimated_fee": 247,
+ "status": "failed"
"fee_paid": 0,
"added": 1706378712,
"last_updated": 1706378712,
@@ -10100,8 +10099,7 @@ export const restApiDocsData = [
"id": 88,
"user_id": 1,
"txid": "c5840e89173331760e959a190b24e2a289121277ed7f8a095fe289b37cee9fde",
- "status": "completed",
- "estimated_fee": 223,
+ "status": "completed"
"fee_paid": 140019,
"added": 1706378704,
"last_updated": 1706380231,
@@ -10130,8 +10128,7 @@ export const restApiDocsData = [
"id": 87,
"user_id": 1,
"txid": "178b5b9b310f0d667d7ea563a2cdcc17bc8cd15261b58b1653860a724ca83458",
- "status": "completed",
- "estimated_fee": 115,
+ "status": "completed"
"fee_paid": 90062,
"added": 1706378684,
"last_updated": 1706380231,
diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts
index 6ef650e32..e5764e785 100644
--- a/frontend/src/app/interfaces/node-api.interface.ts
+++ b/frontend/src/app/interfaces/node-api.interface.ts
@@ -393,8 +393,11 @@ export interface Acceleration {
}
export interface AccelerationHistoryParams {
- timeframe?: string,
- status?: string,
- pool?: string,
- blockHash?: string,
+ status?: string;
+ timeframe?: string;
+ poolUniqueId?: number;
+ blockHash?: string;
+ blockHeight?: number;
+ page?: number;
+ pageLength?: number;
}
\ No newline at end of file
diff --git a/frontend/src/app/interfaces/websocket.interface.ts b/frontend/src/app/interfaces/websocket.interface.ts
index ff5977332..d085c22fa 100644
--- a/frontend/src/app/interfaces/websocket.interface.ts
+++ b/frontend/src/app/interfaces/websocket.interface.ts
@@ -1,3 +1,4 @@
+import { SafeResourceUrl } from '@angular/platform-browser';
import { ILoadingIndicators } from '../services/state.service';
import { Transaction } from './electrs.interface';
import { BlockExtended, DifficultyAdjustment, RbfTree } from './node-api.interface';
@@ -120,4 +121,18 @@ export interface Recommendedfees {
hourFee: number;
minimumFee: number;
economyFee: number;
+}
+
+export interface HealthCheckHost {
+ host: string;
+ active: boolean;
+ rtt: number;
+ latestHeight: number;
+ socket: boolean;
+ outOfSync: boolean;
+ unreachable: boolean;
+ checked: boolean;
+ link?: string;
+ statusPage?: SafeResourceUrl;
+ flag?: string;
}
\ No newline at end of file
diff --git a/frontend/src/app/lightning/nodes-map/nodes-map.component.ts b/frontend/src/app/lightning/nodes-map/nodes-map.component.ts
index 5e655b584..bb9e21c4b 100644
--- a/frontend/src/app/lightning/nodes-map/nodes-map.component.ts
+++ b/frontend/src/app/lightning/nodes-map/nodes-map.component.ts
@@ -88,7 +88,7 @@ export class NodesMap implements OnInit, OnChanges {
node.public_key,
node.alias,
node.capacity,
- node.active_channel_count,
+ node.channels,
node.country,
node.iso_code,
]);
diff --git a/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.html b/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.html
index 9318e925b..be7737894 100644
--- a/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.html
+++ b/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.html
@@ -64,8 +64,8 @@
Channels
Location
-
-
+
+
{{ node.alias }}
@@ -116,5 +116,10 @@
+
+
diff --git a/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.scss b/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.scss
index 25e4cf7f3..97d42298c 100644
--- a/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.scss
+++ b/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.scss
@@ -22,14 +22,14 @@
.timestamp-first {
width: 20%;
- @media (max-width: 576px) {
+ @media (max-width: 1060px) {
display: none
}
}
.timestamp-update {
width: 16%;
- @media (max-width: 576px) {
+ @media (max-width: 1060px) {
display: none
}
}
@@ -50,7 +50,7 @@
.city {
max-width: 150px;
- @media (max-width: 576px) {
+ @media (max-width: 675px) {
display: none
}
}
\ No newline at end of file
diff --git a/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.ts b/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.ts
index 19dd999ee..4035b62d4 100644
--- a/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.ts
+++ b/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.ts
@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
-import { map, Observable, share } from 'rxjs';
+import { BehaviorSubject, combineLatest, map, Observable, share, tap } from 'rxjs';
import { ApiService } from '../../services/api.service';
import { SeoService } from '../../services/seo.service';
import { getFlagEmoji } from '../../shared/common.utils';
@@ -15,6 +15,12 @@ import { GeolocationData } from '../../shared/components/geolocation/geolocation
export class NodesPerCountry implements OnInit {
nodes$: Observable;
country: {name: string, flag: string};
+ nodesPagination$: Observable;
+ startingIndexSubject: BehaviorSubject = new BehaviorSubject(0);
+ page = 1;
+ pageSize = 15;
+ maxSize = window.innerWidth <= 767.98 ? 3 : 5;
+ isLoading = true;
skeletonLines: number[] = [];
@@ -23,7 +29,7 @@ export class NodesPerCountry implements OnInit {
private seoService: SeoService,
private route: ActivatedRoute,
) {
- for (let i = 0; i < 20; ++i) {
+ for (let i = 0; i < this.pageSize; ++i) {
this.skeletonLines.push(i);
}
}
@@ -31,6 +37,7 @@ export class NodesPerCountry implements OnInit {
ngOnInit(): void {
this.nodes$ = this.apiService.getNodeForCountry$(this.route.snapshot.params.country)
.pipe(
+ tap(() => this.isLoading = true),
map(response => {
this.seoService.setTitle($localize`Lightning nodes in ${response.country.en}`);
this.seoService.setDescription($localize`:@@meta.description.lightning.nodes-country:Explore all the Lightning nodes hosted in ${response.country.en} and see an overview of each node's capacity, number of open channels, and more.`);
@@ -87,11 +94,21 @@ export class NodesPerCountry implements OnInit {
ispCount: Object.keys(isps).length
};
}),
+ tap(() => this.isLoading = false),
share()
);
+
+ this.nodesPagination$ = combineLatest([this.nodes$, this.startingIndexSubject]).pipe(
+ map(([response, startingIndex]) => response.nodes.slice(startingIndex, startingIndex + this.pageSize))
+ );
}
trackByPublicKey(index: number, node: any): string {
return node.public_key;
}
+
+ pageChange(page: number): void {
+ this.startingIndexSubject.next((page - 1) * this.pageSize);
+ this.page = page;
+ }
}
diff --git a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.html b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.html
index 3daafe4db..865d2d2dd 100644
--- a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.html
+++ b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.html
@@ -61,8 +61,8 @@
Channels
Location
-
-
+
+
{{ node.alias }}
@@ -113,5 +113,10 @@
+
+
diff --git a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.scss b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.scss
index b829c5b59..b043d36f8 100644
--- a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.scss
+++ b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.scss
@@ -24,7 +24,7 @@
.timestamp-first {
width: 20%;
- @media (max-width: 576px) {
+ @media (max-width: 1060px) {
display: none
}
}
@@ -32,7 +32,7 @@
.timestamp-update {
width: 16%;
- @media (max-width: 576px) {
+ @media (max-width: 1060px) {
display: none
}
}
@@ -56,7 +56,7 @@
.city {
max-width: 150px;
- @media (max-width: 576px) {
+ @media (max-width: 675px) {
display: none
}
}
diff --git a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.ts b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.ts
index d4f27975c..f6c61a9f6 100644
--- a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.ts
+++ b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.ts
@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
-import { map, Observable, share } from 'rxjs';
+import { BehaviorSubject, combineLatest, map, Observable, share, tap } from 'rxjs';
import { ApiService } from '../../services/api.service';
import { SeoService } from '../../services/seo.service';
import { getFlagEmoji } from '../../shared/common.utils';
@@ -15,6 +15,12 @@ import { GeolocationData } from '../../shared/components/geolocation/geolocation
export class NodesPerISP implements OnInit {
nodes$: Observable;
isp: {name: string, id: number};
+ nodesPagination$: Observable;
+ startingIndexSubject: BehaviorSubject = new BehaviorSubject(0);
+ page = 1;
+ pageSize = 15;
+ maxSize = window.innerWidth <= 767.98 ? 3 : 5;
+ isLoading = true;
skeletonLines: number[] = [];
@@ -23,7 +29,7 @@ export class NodesPerISP implements OnInit {
private seoService: SeoService,
private route: ActivatedRoute,
) {
- for (let i = 0; i < 20; ++i) {
+ for (let i = 0; i < this.pageSize; ++i) {
this.skeletonLines.push(i);
}
}
@@ -31,6 +37,7 @@ export class NodesPerISP implements OnInit {
ngOnInit(): void {
this.nodes$ = this.apiService.getNodeForISP$(this.route.snapshot.params.isp)
.pipe(
+ tap(() => this.isLoading = true),
map(response => {
this.isp = {
name: response.isp,
@@ -77,11 +84,21 @@ export class NodesPerISP implements OnInit {
topCountry: topCountry,
};
}),
+ tap(() => this.isLoading = false),
share()
);
+
+ this.nodesPagination$ = combineLatest([this.nodes$, this.startingIndexSubject]).pipe(
+ map(([response, startingIndex]) => response.nodes.slice(startingIndex, startingIndex + this.pageSize))
+ );
}
trackByPublicKey(index: number, node: any): string {
return node.public_key;
}
+
+ pageChange(page: number): void {
+ this.startingIndexSubject.next((page - 1) * this.pageSize);
+ this.page = page;
+ }
}
diff --git a/frontend/src/app/liquid/liquid-master-page.module.ts b/frontend/src/app/liquid/liquid-master-page.module.ts
index 8988cb05c..4b8364ad5 100644
--- a/frontend/src/app/liquid/liquid-master-page.module.ts
+++ b/frontend/src/app/liquid/liquid-master-page.module.ts
@@ -19,6 +19,8 @@ import { RecentPegsListComponent } from '../components/liquid-reserves-audit/rec
import { FederationWalletComponent } from '../components/liquid-reserves-audit/federation-wallet/federation-wallet.component';
import { FederationUtxosListComponent } from '../components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component';
import { FederationAddressesListComponent } from '../components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component';
+import { ServerHealthComponent } from '../components/server-health/server-health.component';
+import { ServerStatusComponent } from '../components/server-health/server-status.component';
const routes: Routes = [
{
@@ -140,6 +142,19 @@ const routes: Routes = [
},
];
+if (window['__env']?.OFFICIAL_MEMPOOL_SPACE) {
+ routes[0].children.push({
+ path: 'nodes',
+ data: { networks: ['bitcoin', 'liquid'] },
+ component: ServerHealthComponent
+ });
+ routes[0].children.push({
+ path: 'network',
+ data: { networks: ['bitcoin', 'liquid'] },
+ component: ServerStatusComponent
+ });
+}
+
@NgModule({
imports: [
RouterModule.forChild(routes)
diff --git a/frontend/src/app/master-page.module.ts b/frontend/src/app/master-page.module.ts
index e19e86518..0e4b26606 100644
--- a/frontend/src/app/master-page.module.ts
+++ b/frontend/src/app/master-page.module.ts
@@ -11,6 +11,8 @@ import { PushTransactionComponent } from './components/push-transaction/push-tra
import { CalculatorComponent } from './components/calculator/calculator.component';
import { BlocksList } from './components/blocks-list/blocks-list.component';
import { RbfList } from './components/rbf-list/rbf-list.component';
+import { ServerHealthComponent } from './components/server-health/server-health.component';
+import { ServerStatusComponent } from './components/server-health/server-status.component';
const browserWindow = window || {};
// @ts-ignore
@@ -97,6 +99,19 @@ const routes: Routes = [
}
];
+if (window['__env']?.OFFICIAL_MEMPOOL_SPACE) {
+ routes[0].children.push({
+ path: 'nodes',
+ data: { networks: ['bitcoin', 'liquid'] },
+ component: ServerHealthComponent
+ });
+ routes[0].children.push({
+ path: 'network',
+ data: { networks: ['bitcoin', 'liquid'] },
+ component: ServerStatusComponent
+ });
+}
+
@NgModule({
imports: [
RouterModule.forChild(routes)
diff --git a/frontend/src/app/services/enterprise.service.ts b/frontend/src/app/services/enterprise.service.ts
index 7e69af223..d1e3624f9 100644
--- a/frontend/src/app/services/enterprise.service.ts
+++ b/frontend/src/app/services/enterprise.service.ts
@@ -139,6 +139,14 @@ export class EnterpriseService {
this.getMatomo()?.trackGoal(id);
}
+ page() {
+ const matomo = this.getMatomo();
+ if (matomo) {
+ matomo.setCustomUrl(this.getCustomUrl());
+ matomo.trackPageView();
+ }
+ }
+
private getCustomUrl(): string {
let url = window.location.origin + '/';
let route = this.activatedRoute;
diff --git a/frontend/src/app/services/services-api.service.ts b/frontend/src/app/services/services-api.service.ts
index f11b3460c..f41c5b42c 100644
--- a/frontend/src/app/services/services-api.service.ts
+++ b/frontend/src/app/services/services-api.service.ts
@@ -7,6 +7,7 @@ import { MenuGroup } from '../interfaces/services.interface';
import { Observable, of, ReplaySubject, tap, catchError, share, filter, switchMap } from 'rxjs';
import { IBackendInfo } from '../interfaces/websocket.interface';
import { Acceleration, AccelerationHistoryParams } from '../interfaces/node-api.interface';
+import { AccelerationStats } from '../components/acceleration/acceleration-stats/acceleration-stats.component';
export type ProductType = 'enterprise' | 'community' | 'mining_pool' | 'custom';
export interface IUser {
@@ -144,7 +145,19 @@ export class ServicesApiServices {
return this.httpClient.get(`${SERVICES_API_PREFIX}/accelerator/accelerations`);
}
+ getAggregatedAccelerationHistory$(params: AccelerationHistoryParams): Observable {
+ return this.httpClient.get(`${SERVICES_API_PREFIX}/accelerator/accelerations/history/aggregated`, { params: { ...params } });
+ }
+
getAccelerationHistory$(params: AccelerationHistoryParams): Observable {
return this.httpClient.get(`${SERVICES_API_PREFIX}/accelerator/accelerations/history`, { params: { ...params } });
}
+
+ getAccelerationHistoryObserveResponse$(params: AccelerationHistoryParams): Observable {
+ return this.httpClient.get(`${SERVICES_API_PREFIX}/accelerator/accelerations/history`, { params: { ...params }, observe: 'response'});
+ }
+
+ getAccelerationStats$(): Observable {
+ return this.httpClient.get(`${SERVICES_API_PREFIX}/accelerator/accelerations/stats`);
+ }
}
diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts
index d0dec76f5..e54d89403 100644
--- a/frontend/src/app/services/state.service.ts
+++ b/frontend/src/app/services/state.service.ts
@@ -1,14 +1,13 @@
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs';
import { Transaction } from '../interfaces/electrs.interface';
-import { IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, TransactionCompressed, TransactionStripped } from '../interfaces/websocket.interface';
+import { HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, TransactionStripped } from '../interfaces/websocket.interface';
import { BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
import { Router, NavigationStart } from '@angular/router';
import { isPlatformBrowser } from '@angular/common';
import { filter, map, scan, shareReplay } from 'rxjs/operators';
import { StorageService } from './storage.service';
import { hasTouchScreen } from '../shared/pipes/bytes-pipe/utils';
-import { ApiService } from './api.service';
import { ActiveFilter } from '../shared/filters.utils';
export interface MarkBlockState {
@@ -130,6 +129,7 @@ export class StateService {
loadingIndicators$ = new ReplaySubject(1);
recommendedFees$ = new ReplaySubject(1);
chainTip$ = new ReplaySubject(-1);
+ serverHealth$ = new Subject();
live2Chart$ = new Subject();
diff --git a/frontend/src/app/services/websocket.service.ts b/frontend/src/app/services/websocket.service.ts
index 90ddb6599..ab51a5bbc 100644
--- a/frontend/src/app/services/websocket.service.ts
+++ b/frontend/src/app/services/websocket.service.ts
@@ -433,6 +433,10 @@ export class WebsocketService {
this.stateService.previousRetarget$.next(response.previousRetarget);
}
+ if (response['tomahawk']) {
+ this.stateService.serverHealth$.next(response['tomahawk']);
+ }
+
if (response['git-commit']) {
this.stateService.backendInfo$.next(response['git-commit']);
}
diff --git a/frontend/src/app/shared/components/global-footer/global-footer.component.html b/frontend/src/app/shared/components/global-footer/global-footer.component.html
index 47b5d9835..271279589 100644
--- a/frontend/src/app/shared/components/global-footer/global-footer.component.html
+++ b/frontend/src/app/shared/components/global-footer/global-footer.component.html
@@ -77,6 +77,7 @@
Terms of Service
Privacy Policy
Trademark Policy
+ Third-party Licenses
diff --git a/frontend/src/app/shared/filters.utils.ts b/frontend/src/app/shared/filters.utils.ts
index 3930dc8ca..da22efb66 100644
--- a/frontend/src/app/shared/filters.utils.ts
+++ b/frontend/src/app/shared/filters.utils.ts
@@ -21,7 +21,8 @@ export const TransactionFlags = {
no_rbf: 0b00000010n,
v1: 0b00000100n,
v2: 0b00001000n,
- multisig: 0b00010000n,
+ v3: 0b00010000n,
+ nonstandard: 0b00100000n,
// address types
p2pk: 0b00000001_00000000n,
p2ms: 0b00000010_00000000n,
@@ -34,6 +35,7 @@ export const TransactionFlags = {
cpfp_parent: 0b00000001_00000000_00000000n,
cpfp_child: 0b00000010_00000000_00000000n,
replacement: 0b00000100_00000000_00000000n,
+ acceleration: 0b00001000_00000000_00000000n,
// data
op_return: 0b00000001_00000000_00000000_00000000n,
fake_pubkey: 0b00000010_00000000_00000000_00000000n,
@@ -64,7 +66,8 @@ export const TransactionFilters: { [key: string]: Filter } = {
no_rbf: { key: 'no_rbf', label: 'RBF disabled', flag: TransactionFlags.no_rbf, toggle: 'rbf', important: true },
v1: { key: 'v1', label: 'Version 1', flag: TransactionFlags.v1, toggle: 'version' },
v2: { key: 'v2', label: 'Version 2', flag: TransactionFlags.v2, toggle: 'version' },
- // multisig: { key: 'multisig', label: 'Multisig', flag: TransactionFlags.multisig },
+ v3: { key: 'v3', label: 'Version 3', flag: TransactionFlags.v3, toggle: 'version' },
+ nonstandard: { key: 'nonstandard', label: 'Non-Standard', flag: TransactionFlags.nonstandard, important: true },
/* address types */
p2pk: { key: 'p2pk', label: 'P2PK', flag: TransactionFlags.p2pk, important: true },
p2ms: { key: 'p2ms', label: 'Bare multisig', flag: TransactionFlags.p2ms, important: true },
@@ -77,6 +80,7 @@ export const TransactionFilters: { [key: string]: Filter } = {
cpfp_parent: { key: 'cpfp_parent', label: 'Paid for by child', flag: TransactionFlags.cpfp_parent, important: true },
cpfp_child: { key: 'cpfp_child', label: 'Pays for parent', flag: TransactionFlags.cpfp_child, important: true },
replacement: { key: 'replacement', label: 'Replacement', flag: TransactionFlags.replacement, important: true },
+ acceleration: window?.['__env']?.ACCELERATOR ? { key: 'acceleration', label: 'Accelerated', flag: TransactionFlags.acceleration, important: false } : undefined,
/* data */
op_return: { key: 'op_return', label: 'OP_RETURN', flag: TransactionFlags.op_return, important: true },
fake_pubkey: { key: 'fake_pubkey', label: 'Fake pubkey', flag: TransactionFlags.fake_pubkey },
@@ -94,9 +98,9 @@ export const TransactionFilters: { [key: string]: Filter } = {
};
export const FilterGroups: { label: string, filters: Filter[]}[] = [
- { label: 'Features', filters: ['rbf', 'no_rbf', 'v1', 'v2', 'multisig'] },
+ { label: 'Features', filters: ['rbf', 'no_rbf', 'v1', 'v2', 'v3', 'nonstandard'] },
{ label: 'Address Types', filters: ['p2pk', 'p2ms', 'p2pkh', 'p2sh', 'p2wpkh', 'p2wsh', 'p2tr'] },
- { label: 'Behavior', filters: ['cpfp_parent', 'cpfp_child', 'replacement'] },
+ { label: 'Behavior', filters: ['cpfp_parent', 'cpfp_child', 'replacement', 'acceleration'] },
{ label: 'Data', filters: ['op_return', 'fake_pubkey', 'inscription'] },
{ label: 'Heuristics', filters: ['coinjoin', 'consolidation', 'batch_payout'] },
{ label: 'Sighash Flags', filters: ['sighash_all', 'sighash_none', 'sighash_single', 'sighash_default', 'sighash_acp'] },
diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts
index b3c942ccf..07ce39167 100644
--- a/frontend/src/app/shared/shared.module.ts
+++ b/frontend/src/app/shared/shared.module.ts
@@ -54,6 +54,8 @@ import { AssetComponent } from '../components/asset/asset.component';
import { AssetsComponent } from '../components/assets/assets.component';
import { AssetsNavComponent } from '../components/assets/assets-nav/assets-nav.component';
import { StatusViewComponent } from '../components/status-view/status-view.component';
+import { ServerHealthComponent } from '../components/server-health/server-health.component';
+import { ServerStatusComponent } from '../components/server-health/server-status.component';
import { FeesBoxComponent } from '../components/fees-box/fees-box.component';
import { DifficultyComponent } from '../components/difficulty/difficulty.component';
import { DifficultyTooltipComponent } from '../components/difficulty/difficulty-tooltip.component';
@@ -153,6 +155,8 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
AssetComponent,
AssetsComponent,
StatusViewComponent,
+ ServerHealthComponent,
+ ServerStatusComponent,
FeesBoxComponent,
DifficultyComponent,
DifficultyMiningComponent,
@@ -280,6 +284,8 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
AssetComponent,
AssetsComponent,
StatusViewComponent,
+ ServerHealthComponent,
+ ServerStatusComponent,
FeesBoxComponent,
DifficultyComponent,
DifficultyMiningComponent,
diff --git a/production/bitcoin.conf b/production/bitcoin.conf
index cdfb5715c..3ec72e2e2 100644
--- a/production/bitcoin.conf
+++ b/production/bitcoin.conf
@@ -7,7 +7,6 @@ discover=1
par=16
dbcache=8192
maxmempool=4096
-mempoolexpiry=999999
mempoolfullrbf=1
maxconnections=100
onion=127.0.0.1:9050
@@ -20,6 +19,7 @@ whitelist=2401:b140::/32
#uacomment=@wiz
[main]
+mempoolexpiry=999999
rpcbind=127.0.0.1:8332
rpcbind=[::1]:8332
bind=0.0.0.0:8333
diff --git a/production/check b/production/check
new file mode 100755
index 000000000..bbae2824b
--- /dev/null
+++ b/production/check
@@ -0,0 +1,23 @@
+#!/usr/bin/env zsh
+
+check_frontend_git_commit_hash() {
+ echo -n $(curl -s https://node$1.$2.mempool.space/resources/config.js|grep GIT_COMMIT_HASH|cut -d "'" -f2|cut -c1-8)
+}
+
+check_html_md5_hash() {
+ echo -n $(curl -s https://node$1.$2.mempool.space|md5|cut -c1-8)
+}
+
+for site in fmt va1 fra tk7;do
+ echo "${site}"
+ for node in 201 202 203 204 205 206 207 208 209 210 211 212 213 214;do
+ [ "${site}" = "fmt" ] && [ "${node}" -gt 206 ] && continue
+ [ "${site}" = "tk7" ] && [ "${node}" -gt 206 ] && continue
+ echo -n "node${node}.${site}: "
+ check_frontend_git_commit_hash $node $site
+ echo -n " "
+ check_html_md5_hash $node $site
+ echo
+ done
+done
+
diff --git a/production/mempool-config.bisq.json b/production/mempool-config.bisq.json
index 26024f8a3..4913cb986 100644
--- a/production/mempool-config.bisq.json
+++ b/production/mempool-config.bisq.json
@@ -1,5 +1,6 @@
{
"MEMPOOL": {
+ "OFFICIAL": true,
"NETWORK": "bisq",
"BACKEND": "esplora",
"HTTP_PORT": 8996,
diff --git a/production/mempool-config.liquid.json b/production/mempool-config.liquid.json
index 459476688..9051bba74 100644
--- a/production/mempool-config.liquid.json
+++ b/production/mempool-config.liquid.json
@@ -1,5 +1,6 @@
{
"MEMPOOL": {
+ "OFFICIAL": true,
"NETWORK": "liquid",
"BACKEND": "esplora",
"HTTP_PORT": 8998,
@@ -64,15 +65,7 @@
"http://node203.tk7.mempool.space:3001",
"http://node204.tk7.mempool.space:3001",
"http://node205.tk7.mempool.space:3001",
- "http://node206.tk7.mempool.space:3001",
- "http://node207.tk7.mempool.space:3001",
- "http://node208.tk7.mempool.space:3001",
- "http://node209.tk7.mempool.space:3001",
- "http://node210.tk7.mempool.space:3001",
- "http://node211.tk7.mempool.space:3001",
- "http://node212.tk7.mempool.space:3001",
- "http://node213.tk7.mempool.space:3001",
- "http://node214.tk7.mempool.space:3001"
+ "http://node206.tk7.mempool.space:3001"
]
},
"DATABASE": {
diff --git a/production/mempool-config.liquidtestnet.json b/production/mempool-config.liquidtestnet.json
index d77148341..ae6d7b1ac 100644
--- a/production/mempool-config.liquidtestnet.json
+++ b/production/mempool-config.liquidtestnet.json
@@ -1,5 +1,6 @@
{
"MEMPOOL": {
+ "OFFICIAL": true,
"NETWORK": "liquid",
"BACKEND": "esplora",
"HTTP_PORT": 8994,
@@ -64,15 +65,7 @@
"http://node203.tk7.mempool.space:3004",
"http://node204.tk7.mempool.space:3004",
"http://node205.tk7.mempool.space:3004",
- "http://node206.tk7.mempool.space:3004",
- "http://node207.tk7.mempool.space:3004",
- "http://node208.tk7.mempool.space:3004",
- "http://node209.tk7.mempool.space:3004",
- "http://node210.tk7.mempool.space:3004",
- "http://node211.tk7.mempool.space:3004",
- "http://node212.tk7.mempool.space:3004",
- "http://node213.tk7.mempool.space:3004",
- "http://node214.tk7.mempool.space:3004"
+ "http://node206.tk7.mempool.space:3004"
]
},
"DATABASE": {
diff --git a/production/mempool-config.mainnet-lightning.json b/production/mempool-config.mainnet-lightning.json
index 48454ef9f..8dea10b4a 100644
--- a/production/mempool-config.mainnet-lightning.json
+++ b/production/mempool-config.mainnet-lightning.json
@@ -1,5 +1,6 @@
{
"MEMPOOL": {
+ "OFFICIAL": true,
"ENABLED": false,
"NETWORK": "mainnet",
"BACKEND": "esplora",
@@ -57,15 +58,7 @@
"http://node203.tk7.mempool.space:3000",
"http://node204.tk7.mempool.space:3000",
"http://node205.tk7.mempool.space:3000",
- "http://node206.tk7.mempool.space:3000",
- "http://node207.tk7.mempool.space:3000",
- "http://node208.tk7.mempool.space:3000",
- "http://node209.tk7.mempool.space:3000",
- "http://node210.tk7.mempool.space:3000",
- "http://node211.tk7.mempool.space:3000",
- "http://node212.tk7.mempool.space:3000",
- "http://node213.tk7.mempool.space:3000",
- "http://node214.tk7.mempool.space:3000"
+ "http://node206.tk7.mempool.space:3000"
]
},
"LIGHTNING": {
diff --git a/production/mempool-config.mainnet.json b/production/mempool-config.mainnet.json
index 37adae32f..5038d9bfb 100644
--- a/production/mempool-config.mainnet.json
+++ b/production/mempool-config.mainnet.json
@@ -1,5 +1,6 @@
{
"MEMPOOL": {
+ "OFFICIAL": true,
"NETWORK": "mainnet",
"BACKEND": "esplora",
"HTTP_PORT": 8999,
@@ -21,7 +22,8 @@
"DISK_CACHE_BLOCK_INTERVAL": 1,
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
"ALLOW_UNREACHABLE": true,
- "PRICE_UPDATES_PER_HOUR": 12
+ "PRICE_UPDATES_PER_HOUR": 12,
+ "MAX_TRACKED_ADDRESSES": 10
},
"SYSLOG" : {
"MIN_PRIORITY": "debug"
@@ -78,15 +80,7 @@
"http://node203.tk7.mempool.space:3000",
"http://node204.tk7.mempool.space:3000",
"http://node205.tk7.mempool.space:3000",
- "http://node206.tk7.mempool.space:3000",
- "http://node207.tk7.mempool.space:3000",
- "http://node208.tk7.mempool.space:3000",
- "http://node209.tk7.mempool.space:3000",
- "http://node210.tk7.mempool.space:3000",
- "http://node211.tk7.mempool.space:3000",
- "http://node212.tk7.mempool.space:3000",
- "http://node213.tk7.mempool.space:3000",
- "http://node214.tk7.mempool.space:3000"
+ "http://node206.tk7.mempool.space:3000"
]
},
"DATABASE": {
@@ -145,15 +139,7 @@
"node203.tk7.mempool.space",
"node204.tk7.mempool.space",
"node205.tk7.mempool.space",
- "node206.tk7.mempool.space",
- "node207.tk7.mempool.space",
- "node208.tk7.mempool.space",
- "node209.tk7.mempool.space",
- "node210.tk7.mempool.space",
- "node211.tk7.mempool.space",
- "node212.tk7.mempool.space",
- "node213.tk7.mempool.space",
- "node214.tk7.mempool.space"
+ "node206.tk7.mempool.space"
]
},
"REDIS": {
diff --git a/production/mempool-config.signet-lightning.json b/production/mempool-config.signet-lightning.json
index fdc96d256..f90b18f50 100644
--- a/production/mempool-config.signet-lightning.json
+++ b/production/mempool-config.signet-lightning.json
@@ -1,5 +1,6 @@
{
"MEMPOOL": {
+ "OFFICIAL": true,
"ENABLED": false,
"NETWORK": "signet",
"BACKEND": "esplora",
@@ -57,15 +58,7 @@
"http://node203.tk7.mempool.space:3003",
"http://node204.tk7.mempool.space:3003",
"http://node205.tk7.mempool.space:3003",
- "http://node206.tk7.mempool.space:3003",
- "http://node207.tk7.mempool.space:3003",
- "http://node208.tk7.mempool.space:3003",
- "http://node209.tk7.mempool.space:3003",
- "http://node210.tk7.mempool.space:3003",
- "http://node211.tk7.mempool.space:3003",
- "http://node212.tk7.mempool.space:3003",
- "http://node213.tk7.mempool.space:3003",
- "http://node214.tk7.mempool.space:3003"
+ "http://node206.tk7.mempool.space:3003"
]
},
"LIGHTNING": {
diff --git a/production/mempool-config.signet.json b/production/mempool-config.signet.json
index 9151679bd..0a711d16f 100644
--- a/production/mempool-config.signet.json
+++ b/production/mempool-config.signet.json
@@ -1,5 +1,6 @@
{
"MEMPOOL": {
+ "OFFICIAL": true,
"NETWORK": "signet",
"BACKEND": "esplora",
"HTTP_PORT": 8995,
@@ -14,7 +15,8 @@
"POLL_RATE_MS": 1000,
"DISK_CACHE_BLOCK_INTERVAL": 1,
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
- "ALLOW_UNREACHABLE": true
+ "ALLOW_UNREACHABLE": true,
+ "MAX_TRACKED_ADDRESSES": 10
},
"SYSLOG" : {
"MIN_PRIORITY": "debug"
@@ -66,15 +68,7 @@
"http://node203.tk7.mempool.space:3003",
"http://node204.tk7.mempool.space:3003",
"http://node205.tk7.mempool.space:3003",
- "http://node206.tk7.mempool.space:3003",
- "http://node207.tk7.mempool.space:3003",
- "http://node208.tk7.mempool.space:3003",
- "http://node209.tk7.mempool.space:3003",
- "http://node210.tk7.mempool.space:3003",
- "http://node211.tk7.mempool.space:3003",
- "http://node212.tk7.mempool.space:3003",
- "http://node213.tk7.mempool.space:3003",
- "http://node214.tk7.mempool.space:3003"
+ "http://node206.tk7.mempool.space:3003"
]
},
"DATABASE": {
diff --git a/production/mempool-config.testnet-lightning.json b/production/mempool-config.testnet-lightning.json
index 2ab3c6f19..59a858cbf 100644
--- a/production/mempool-config.testnet-lightning.json
+++ b/production/mempool-config.testnet-lightning.json
@@ -1,5 +1,6 @@
{
"MEMPOOL": {
+ "OFFICIAL": true,
"ENABLED": false,
"NETWORK": "testnet",
"BACKEND": "esplora",
@@ -57,15 +58,7 @@
"http://node203.tk7.mempool.space:3002",
"http://node204.tk7.mempool.space:3002",
"http://node205.tk7.mempool.space:3002",
- "http://node206.tk7.mempool.space:3002",
- "http://node207.tk7.mempool.space:3002",
- "http://node208.tk7.mempool.space:3002",
- "http://node209.tk7.mempool.space:3002",
- "http://node210.tk7.mempool.space:3002",
- "http://node211.tk7.mempool.space:3002",
- "http://node212.tk7.mempool.space:3002",
- "http://node213.tk7.mempool.space:3002",
- "http://node214.tk7.mempool.space:3002"
+ "http://node206.tk7.mempool.space:3002"
]
},
"LIGHTNING": {
diff --git a/production/mempool-config.testnet.json b/production/mempool-config.testnet.json
index 4a4b550c1..adc93c0e9 100644
--- a/production/mempool-config.testnet.json
+++ b/production/mempool-config.testnet.json
@@ -1,5 +1,6 @@
{
"MEMPOOL": {
+ "OFFICIAL": true,
"NETWORK": "testnet",
"BACKEND": "esplora",
"HTTP_PORT": 8997,
@@ -14,7 +15,8 @@
"POLL_RATE_MS": 1000,
"DISK_CACHE_BLOCK_INTERVAL": 1,
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
- "ALLOW_UNREACHABLE": true
+ "ALLOW_UNREACHABLE": true,
+ "MAX_TRACKED_ADDRESSES": 10
},
"SYSLOG" : {
"MIN_PRIORITY": "debug"
@@ -66,15 +68,7 @@
"http://node203.tk7.mempool.space:3002",
"http://node204.tk7.mempool.space:3002",
"http://node205.tk7.mempool.space:3002",
- "http://node206.tk7.mempool.space:3002",
- "http://node207.tk7.mempool.space:3002",
- "http://node208.tk7.mempool.space:3002",
- "http://node209.tk7.mempool.space:3002",
- "http://node210.tk7.mempool.space:3002",
- "http://node211.tk7.mempool.space:3002",
- "http://node212.tk7.mempool.space:3002",
- "http://node213.tk7.mempool.space:3002",
- "http://node214.tk7.mempool.space:3002"
+ "http://node206.tk7.mempool.space:3002"
]
},
"DATABASE": {
diff --git a/production/mempool-kill-all b/production/mempool-kill-all
index b3f88c6da..adb56915b 100755
--- a/production/mempool-kill-all
+++ b/production/mempool-kill-all
@@ -20,5 +20,10 @@ for pid in `ps uaxww|grep warmer|grep zsh|awk '{print $2}'`;do
kill $pid
done
+# kill nginx cache heater scripts
+for pid in `ps uaxww|grep heater|grep zsh|awk '{print $2}'`;do
+ kill $pid
+done
+
# always exit successfully despite above errors
exit 0
diff --git a/production/nginx/http-acl.conf b/production/nginx/http-acl.conf
new file mode 100644
index 000000000..9fa2d6493
--- /dev/null
+++ b/production/nginx/http-acl.conf
@@ -0,0 +1,6 @@
+# used for "internal" API restriction
+geo $remote_addr $mempool_external {
+ 127.0.0.1 '';
+ ::1 '';
+ default 1;
+}
diff --git a/production/nginx/http-proxy-cache.conf b/production/nginx/http-proxy-cache.conf
index 0024a3b30..dcc17208b 100644
--- a/production/nginx/http-proxy-cache.conf
+++ b/production/nginx/http-proxy-cache.conf
@@ -8,5 +8,9 @@ proxy_cache_path /var/cache/nginx/apicold keys_zone=apicold:200m levels=1:2 inac
proxy_cache_path /var/cache/nginx/unfurler keys_zone=unfurler:200m levels=1:2 inactive=30d max_size=2000m;
proxy_cache_path /var/cache/nginx/slurper keys_zone=slurper:500m levels=1:2 inactive=365d max_size=5000m;
proxy_cache_path /var/cache/nginx/markets keys_zone=markets:20m levels=1:2 inactive=365d max_size=100m;
-types_hash_max_size 4096;
-proxy_buffer_size 8k;
+
+types_hash_max_size 8192;
+
+proxy_busy_buffers_size 256k;
+proxy_buffer_size 128k;
+proxy_buffers 4 256k;
diff --git a/production/nginx/location-api.conf b/production/nginx/location-api.conf
index bee4ce50d..49bda31a4 100644
--- a/production/nginx/location-api.conf
+++ b/production/nginx/location-api.conf
@@ -4,10 +4,17 @@
# Block the internal APIs of esplora
location /api/internal/ {
- return 403;
+ if ($mempool_external) {
+ return 403;
+ }
+ rewrite ^/api/(.*) /$1 break;
+ try_files /dev/null @esplora-api-cache-disabled;
}
location /api/v1/internal/ {
- return 403;
+ if ($mempool_external) {
+ return 403;
+ }
+ try_files /dev/null @mempool-api-v1-cache-normal;
}
# websocket has special HTTP headers
diff --git a/production/nginx/nginx.conf b/production/nginx/nginx.conf
index c08005d14..3646dd316 100644
--- a/production/nginx/nginx.conf
+++ b/production/nginx/nginx.conf
@@ -19,6 +19,7 @@ http {
# HTTP basic configuration
include mempool/production/nginx/http-basic.conf;
+ include mempool/production/nginx/http-acl.conf;
include mempool/production/nginx/http-proxy-cache.conf;
include mempool/production/nginx/http-language.conf;
diff --git a/scripts/get_block_tip_height.sh b/scripts/get_block_tip_height.sh
new file mode 100644
index 000000000..603193bb8
--- /dev/null
+++ b/scripts/get_block_tip_height.sh
@@ -0,0 +1,25 @@
+BASE_HEIGHT=$(curl -sk https://node202.tk7.mempool.space/api/v1/blocks/tip/height)
+IN_SYNC=true
+echo "Base height (node202.tk7): $BASE_HEIGHT"
+
+for LOCATION in fmt va1 fra tk7
+do
+ for NODE in 201 202 203 204 205 206
+ do
+ NODE_HEIGHT=$(curl -sk https://node$NODE.$LOCATION.mempool.space/api/v1/blocks/tip/height)
+ echo $(echo node$NODE.$LOCATION.mempool.space) - $NODE_HEIGHT
+ if [ "$NODE_HEIGHT" -ne "$BASE_HEIGHT" ]; then
+ COUNT=$((BASE_HEIGHT-NODE_HEIGHT))
+ echo $(echo node$NODE.$LOCATION.mempool.space) is not in sync. delta: $COUNT
+ IN_SYNC=false
+ fi
+ done
+done
+
+if [ "$IN_SYNC" = false ]; then
+ echo "One or more servers are out of sync. Check the logs."
+ exit -1
+else
+ echo "All servers are in sync."
+fi
+
diff --git a/unfurler/src/index.ts b/unfurler/src/index.ts
index aaa72b163..162525dca 100644
--- a/unfurler/src/index.ts
+++ b/unfurler/src/index.ts
@@ -251,7 +251,8 @@ class Server {
if (!img) {
// send local fallback image file
- res.sendFile(nodejsPath.join(__dirname, matchedRoute.fallbackFile));
+ res.set('Cache-control', 'no-cache');
+ res.sendFile(nodejsPath.join(__dirname, matchedRoute.fallbackImg));
} else {
res.contentType('image/png');
res.send(img);
diff --git a/unfurler/src/resources b/unfurler/src/resources
new file mode 120000
index 000000000..aadffa9d9
--- /dev/null
+++ b/unfurler/src/resources
@@ -0,0 +1 @@
+../../frontend/src/resources
\ No newline at end of file
diff --git a/unfurler/src/resources/img/bisq.png b/unfurler/src/resources/img/bisq.png
deleted file mode 100644
index 2b5e1250b..000000000
Binary files a/unfurler/src/resources/img/bisq.png and /dev/null differ
diff --git a/unfurler/src/resources/img/dashboard.png b/unfurler/src/resources/img/dashboard.png
deleted file mode 100644
index 60c8bcc6a..000000000
Binary files a/unfurler/src/resources/img/dashboard.png and /dev/null differ
diff --git a/unfurler/src/resources/img/lightning.png b/unfurler/src/resources/img/lightning.png
deleted file mode 100644
index 4686b0ef0..000000000
Binary files a/unfurler/src/resources/img/lightning.png and /dev/null differ
diff --git a/unfurler/src/resources/img/liquid.png b/unfurler/src/resources/img/liquid.png
deleted file mode 100644
index 72942110c..000000000
Binary files a/unfurler/src/resources/img/liquid.png and /dev/null differ
diff --git a/unfurler/src/resources/img/mempool.png b/unfurler/src/resources/img/mempool.png
deleted file mode 100644
index 60c8bcc6a..000000000
Binary files a/unfurler/src/resources/img/mempool.png and /dev/null differ
diff --git a/unfurler/src/resources/img/mining.png b/unfurler/src/resources/img/mining.png
deleted file mode 100644
index 6a2aa1b41..000000000
Binary files a/unfurler/src/resources/img/mining.png and /dev/null differ
diff --git a/unfurler/src/routes.ts b/unfurler/src/routes.ts
index 0c626c0c9..f9280369c 100644
--- a/unfurler/src/routes.ts
+++ b/unfurler/src/routes.ts
@@ -2,7 +2,6 @@ interface Match {
render: boolean;
title: string;
fallbackImg: string;
- fallbackFile: string;
staticImg?: string;
networkMode: string;
}
@@ -32,7 +31,6 @@ const routes = {
lightning: {
title: "Lightning",
fallbackImg: '/resources/previews/lightning.png',
- fallbackFile: '/resources/img/lightning.png',
routes: {
node: {
render: true,
@@ -71,7 +69,6 @@ const routes = {
mining: {
title: "Mining",
fallbackImg: '/resources/previews/mining.png',
- fallbackFile: '/resources/img/mining.png',
routes: {
pool: {
render: true,
@@ -87,14 +84,12 @@ const routes = {
const networks = {
bitcoin: {
fallbackImg: '/resources/previews/dashboard.png',
- fallbackFile: '/resources/img/dashboard.png',
routes: {
...routes // all routes supported
}
},
liquid: {
fallbackImg: '/resources/liquid/liquid-network-preview.png',
- fallbackFile: '/resources/img/liquid',
routes: { // only block, address & tx routes supported
block: routes.block,
address: routes.address,
@@ -103,7 +98,6 @@ const networks = {
},
bisq: {
fallbackImg: '/resources/bisq/bisq-markets-preview.png',
- fallbackFile: '/resources/img/bisq.png',
routes: {} // no routes supported
}
};
@@ -113,7 +107,6 @@ export function matchRoute(network: string, path: string): Match {
render: false,
title: '',
fallbackImg: '',
- fallbackFile: '',
networkMode: 'mainnet'
}
@@ -128,7 +121,6 @@ export function matchRoute(network: string, path: string): Match {
let route = networks[network] || networks.bitcoin;
match.fallbackImg = route.fallbackImg;
- match.fallbackFile = route.fallbackFile;
// traverse the route tree until we run out of route or tree, or hit a renderable match
while (!route.render && route.routes && parts.length && route.routes[parts[0]]) {
@@ -136,7 +128,6 @@ export function matchRoute(network: string, path: string): Match {
parts.shift();
if (route.fallbackImg) {
match.fallbackImg = route.fallbackImg;
- match.fallbackFile = route.fallbackFile;
}
}