Merge branch 'master' into mononaut/optimize-gbt-process-blocks

This commit is contained in:
softsimon 2024-09-22 12:41:51 +08:00 committed by GitHub
commit 367ee68fe0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
115 changed files with 2827 additions and 721 deletions

12
LICENSE
View File

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

View File

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

View File

@ -1,21 +1,22 @@
{ {
"name": "mempool-backend", "name": "mempool-backend",
"version": "3.0.0-beta", "version": "3.1.0-dev",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mempool-backend", "name": "mempool-backend",
"version": "3.0.0-beta", "version": "3.1.0-dev",
"hasInstallScript": true, "hasInstallScript": true,
"license": "GNU Affero General Public License v3.0", "license": "GNU Affero General Public License v3.0",
"dependencies": { "dependencies": {
"@babel/core": "^7.25.2",
"@mempool/electrum-client": "1.1.9", "@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3", "@types/node": "^18.15.3",
"axios": "~1.7.2", "axios": "1.7.2",
"bitcoinjs-lib": "~6.1.3", "bitcoinjs-lib": "~6.1.3",
"crypto-js": "~4.2.0", "crypto-js": "~4.2.0",
"express": "~4.19.2", "express": "~4.21.0",
"maxmind": "~4.3.11", "maxmind": "~4.3.11",
"mysql2": "~3.11.0", "mysql2": "~3.11.0",
"redis": "^4.7.0", "redis": "^4.7.0",
@ -2280,6 +2281,7 @@
"version": "1.7.2", "version": "1.7.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
"license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",
"form-data": "^4.0.0", "form-data": "^4.0.0",
@ -2488,9 +2490,9 @@
} }
}, },
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "1.20.2", "version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"dependencies": { "dependencies": {
"bytes": "3.1.2", "bytes": "3.1.2",
"content-type": "~1.0.5", "content-type": "~1.0.5",
@ -2500,7 +2502,7 @@
"http-errors": "2.0.0", "http-errors": "2.0.0",
"iconv-lite": "0.4.24", "iconv-lite": "0.4.24",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"qs": "6.11.0", "qs": "6.13.0",
"raw-body": "2.5.2", "raw-body": "2.5.2",
"type-is": "~1.6.18", "type-is": "~1.6.18",
"unpipe": "1.0.0" "unpipe": "1.0.0"
@ -3029,9 +3031,9 @@
"dev": true "dev": true
}, },
"node_modules/encodeurl": { "node_modules/encodeurl": {
"version": "1.0.2", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
} }
@ -3459,36 +3461,36 @@
} }
}, },
"node_modules/express": { "node_modules/express": {
"version": "4.19.2", "version": "4.21.0",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
"dependencies": { "dependencies": {
"accepts": "~1.3.8", "accepts": "~1.3.8",
"array-flatten": "1.1.1", "array-flatten": "1.1.1",
"body-parser": "1.20.2", "body-parser": "1.20.3",
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"content-type": "~1.0.4", "content-type": "~1.0.4",
"cookie": "0.6.0", "cookie": "0.6.0",
"cookie-signature": "1.0.6", "cookie-signature": "1.0.6",
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
"encodeurl": "~1.0.2", "encodeurl": "~2.0.0",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"etag": "~1.8.1", "etag": "~1.8.1",
"finalhandler": "1.2.0", "finalhandler": "1.3.1",
"fresh": "0.5.2", "fresh": "0.5.2",
"http-errors": "2.0.0", "http-errors": "2.0.0",
"merge-descriptors": "1.0.1", "merge-descriptors": "1.0.3",
"methods": "~1.1.2", "methods": "~1.1.2",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"path-to-regexp": "0.1.7", "path-to-regexp": "0.1.10",
"proxy-addr": "~2.0.7", "proxy-addr": "~2.0.7",
"qs": "6.11.0", "qs": "6.13.0",
"range-parser": "~1.2.1", "range-parser": "~1.2.1",
"safe-buffer": "5.2.1", "safe-buffer": "5.2.1",
"send": "0.18.0", "send": "0.19.0",
"serve-static": "1.15.0", "serve-static": "1.16.2",
"setprototypeof": "1.2.0", "setprototypeof": "1.2.0",
"statuses": "2.0.1", "statuses": "2.0.1",
"type-is": "~1.6.18", "type-is": "~1.6.18",
@ -3601,12 +3603,12 @@
} }
}, },
"node_modules/finalhandler": { "node_modules/finalhandler": {
"version": "1.2.0", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"dependencies": { "dependencies": {
"debug": "2.6.9", "debug": "2.6.9",
"encodeurl": "~1.0.2", "encodeurl": "~2.0.0",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
@ -6050,9 +6052,12 @@
} }
}, },
"node_modules/merge-descriptors": { "node_modules/merge-descriptors": {
"version": "1.0.1", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
}, },
"node_modules/merge-stream": { "node_modules/merge-stream": {
"version": "2.0.0", "version": "2.0.0",
@ -6266,9 +6271,12 @@
} }
}, },
"node_modules/object-inspect": { "node_modules/object-inspect": {
"version": "1.13.1", "version": "1.13.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
"engines": {
"node": ">= 0.4"
},
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
@ -6436,9 +6444,9 @@
"dev": true "dev": true
}, },
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "0.1.7", "version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
}, },
"node_modules/path-type": { "node_modules/path-type": {
"version": "4.0.0", "version": "4.0.0",
@ -6646,11 +6654,11 @@
] ]
}, },
"node_modules/qs": { "node_modules/qs": {
"version": "6.11.0", "version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"dependencies": { "dependencies": {
"side-channel": "^1.0.4" "side-channel": "^1.0.6"
}, },
"engines": { "engines": {
"node": ">=0.6" "node": ">=0.6"
@ -6871,9 +6879,9 @@
} }
}, },
"node_modules/send": { "node_modules/send": {
"version": "0.18.0", "version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"dependencies": { "dependencies": {
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
@ -6906,6 +6914,14 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
}, },
"node_modules/send/node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/send/node_modules/ms": { "node_modules/send/node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -6917,14 +6933,14 @@
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
}, },
"node_modules/serve-static": { "node_modules/serve-static": {
"version": "1.15.0", "version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"dependencies": { "dependencies": {
"encodeurl": "~1.0.2", "encodeurl": "~2.0.0",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"send": "0.18.0" "send": "0.19.0"
}, },
"engines": { "engines": {
"node": ">= 0.8.0" "node": ">= 0.8.0"
@ -9603,9 +9619,9 @@
} }
}, },
"body-parser": { "body-parser": {
"version": "1.20.2", "version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"requires": { "requires": {
"bytes": "3.1.2", "bytes": "3.1.2",
"content-type": "~1.0.5", "content-type": "~1.0.5",
@ -9615,7 +9631,7 @@
"http-errors": "2.0.0", "http-errors": "2.0.0",
"iconv-lite": "0.4.24", "iconv-lite": "0.4.24",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"qs": "6.11.0", "qs": "6.13.0",
"raw-body": "2.5.2", "raw-body": "2.5.2",
"type-is": "~1.6.18", "type-is": "~1.6.18",
"unpipe": "1.0.0" "unpipe": "1.0.0"
@ -9996,9 +10012,9 @@
"dev": true "dev": true
}, },
"encodeurl": { "encodeurl": {
"version": "1.0.2", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="
}, },
"error-ex": { "error-ex": {
"version": "1.3.2", "version": "1.3.2",
@ -10303,36 +10319,36 @@
} }
}, },
"express": { "express": {
"version": "4.19.2", "version": "4.21.0",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
"requires": { "requires": {
"accepts": "~1.3.8", "accepts": "~1.3.8",
"array-flatten": "1.1.1", "array-flatten": "1.1.1",
"body-parser": "1.20.2", "body-parser": "1.20.3",
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"content-type": "~1.0.4", "content-type": "~1.0.4",
"cookie": "0.6.0", "cookie": "0.6.0",
"cookie-signature": "1.0.6", "cookie-signature": "1.0.6",
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
"encodeurl": "~1.0.2", "encodeurl": "~2.0.0",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"etag": "~1.8.1", "etag": "~1.8.1",
"finalhandler": "1.2.0", "finalhandler": "1.3.1",
"fresh": "0.5.2", "fresh": "0.5.2",
"http-errors": "2.0.0", "http-errors": "2.0.0",
"merge-descriptors": "1.0.1", "merge-descriptors": "1.0.3",
"methods": "~1.1.2", "methods": "~1.1.2",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"path-to-regexp": "0.1.7", "path-to-regexp": "0.1.10",
"proxy-addr": "~2.0.7", "proxy-addr": "~2.0.7",
"qs": "6.11.0", "qs": "6.13.0",
"range-parser": "~1.2.1", "range-parser": "~1.2.1",
"safe-buffer": "5.2.1", "safe-buffer": "5.2.1",
"send": "0.18.0", "send": "0.19.0",
"serve-static": "1.15.0", "serve-static": "1.16.2",
"setprototypeof": "1.2.0", "setprototypeof": "1.2.0",
"statuses": "2.0.1", "statuses": "2.0.1",
"type-is": "~1.6.18", "type-is": "~1.6.18",
@ -10434,12 +10450,12 @@
} }
}, },
"finalhandler": { "finalhandler": {
"version": "1.2.0", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"requires": { "requires": {
"debug": "2.6.9", "debug": "2.6.9",
"encodeurl": "~1.0.2", "encodeurl": "~2.0.0",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
@ -12236,9 +12252,9 @@
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="
}, },
"merge-descriptors": { "merge-descriptors": {
"version": "1.0.1", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="
}, },
"merge-stream": { "merge-stream": {
"version": "2.0.0", "version": "2.0.0",
@ -12401,9 +12417,9 @@
} }
}, },
"object-inspect": { "object-inspect": {
"version": "1.13.1", "version": "1.13.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==" "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g=="
}, },
"on-finished": { "on-finished": {
"version": "2.4.1", "version": "2.4.1",
@ -12520,9 +12536,9 @@
"dev": true "dev": true
}, },
"path-to-regexp": { "path-to-regexp": {
"version": "0.1.7", "version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
}, },
"path-type": { "path-type": {
"version": "4.0.0", "version": "4.0.0",
@ -12664,11 +12680,11 @@
"dev": true "dev": true
}, },
"qs": { "qs": {
"version": "6.11.0", "version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"requires": { "requires": {
"side-channel": "^1.0.4" "side-channel": "^1.0.6"
} }
}, },
"queue-microtask": { "queue-microtask": {
@ -12802,9 +12818,9 @@
"dev": true "dev": true
}, },
"send": { "send": {
"version": "0.18.0", "version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"requires": { "requires": {
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
@ -12836,6 +12852,11 @@
} }
} }
}, },
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
},
"ms": { "ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -12849,14 +12870,14 @@
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
}, },
"serve-static": { "serve-static": {
"version": "1.15.0", "version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"requires": { "requires": {
"encodeurl": "~1.0.2", "encodeurl": "~2.0.0",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"send": "0.18.0" "send": "0.19.0"
} }
}, },
"set-function-length": { "set-function-length": {

View File

@ -1,6 +1,6 @@
{ {
"name": "mempool-backend", "name": "mempool-backend",
"version": "3.0.0-beta", "version": "3.1.0-dev",
"description": "Bitcoin mempool visualizer and blockchain explorer backend", "description": "Bitcoin mempool visualizer and blockchain explorer backend",
"license": "GNU Affero General Public License v3.0", "license": "GNU Affero General Public License v3.0",
"homepage": "https://mempool.space", "homepage": "https://mempool.space",
@ -42,10 +42,10 @@
"@babel/core": "^7.25.2", "@babel/core": "^7.25.2",
"@mempool/electrum-client": "1.1.9", "@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3", "@types/node": "^18.15.3",
"axios": "~1.7.2", "axios": "1.7.2",
"bitcoinjs-lib": "~6.1.3", "bitcoinjs-lib": "~6.1.3",
"crypto-js": "~4.2.0", "crypto-js": "~4.2.0",
"express": "~4.19.2", "express": "~4.21.0",
"maxmind": "~4.3.11", "maxmind": "~4.3.11",
"mysql2": "~3.11.0", "mysql2": "~3.11.0",
"rust-gbt": "file:./rust-gbt", "rust-gbt": "file:./rust-gbt",

View File

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

View File

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

View File

@ -323,6 +323,7 @@ class BitcoinApi implements AbstractBitcoinApi {
'witness_v1_taproot': 'v1_p2tr', 'witness_v1_taproot': 'v1_p2tr',
'nonstandard': 'nonstandard', 'nonstandard': 'nonstandard',
'multisig': 'multisig', 'multisig': 'multisig',
'anchor': 'anchor',
'nulldata': 'op_return' 'nulldata': 'op_return'
}; };

View File

@ -20,6 +20,7 @@ import difficultyAdjustment from '../difficulty-adjustment';
import transactionRepository from '../../repositories/TransactionRepository'; import transactionRepository from '../../repositories/TransactionRepository';
import rbfCache from '../rbf-cache'; import rbfCache from '../rbf-cache';
import { calculateMempoolTxCpfp } from '../cpfp'; import { calculateMempoolTxCpfp } from '../cpfp';
import { handleError } from '../../utils/api';
class BitcoinRoutes { class BitcoinRoutes {
public initRoutes(app: Application) { public initRoutes(app: Application) {
@ -86,7 +87,7 @@ class BitcoinRoutes {
res.set('Content-Type', 'application/json'); res.set('Content-Type', 'application/json');
res.send(result); res.send(result);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -105,13 +106,13 @@ class BitcoinRoutes {
const result = mempoolBlocks.getMempoolBlocks(); const result = mempoolBlocks.getMempoolBlocks();
res.json(result); res.json(result);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
private getTransactionTimes(req: Request, res: Response) { private getTransactionTimes(req: Request, res: Response) {
if (!Array.isArray(req.query.txId)) { if (!Array.isArray(req.query.txId)) {
res.status(500).send('Not an array'); handleError(req, res, 500, 'Not an array');
return; return;
} }
const txIds: string[] = []; const txIds: string[] = [];
@ -128,12 +129,12 @@ class BitcoinRoutes {
private async $getBatchedOutspends(req: Request, res: Response): Promise<IEsploraApi.Outspend[][] | void> { private async $getBatchedOutspends(req: Request, res: Response): Promise<IEsploraApi.Outspend[][] | void> {
const txids_csv = req.query.txids; const txids_csv = req.query.txids;
if (!txids_csv || typeof txids_csv !== 'string') { if (!txids_csv || typeof txids_csv !== 'string') {
res.status(500).send('Invalid txids format'); handleError(req, res, 500, 'Invalid txids format');
return; return;
} }
const txids = txids_csv.split(','); const txids = txids_csv.split(',');
if (txids.length > 50) { if (txids.length > 50) {
res.status(400).send('Too many txids requested'); handleError(req, res, 400, 'Too many txids requested');
return; return;
} }
@ -141,13 +142,13 @@ class BitcoinRoutes {
const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids); const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids);
res.json(batchedOutspends); res.json(batchedOutspends);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
private async $getCpfpInfo(req: Request, res: Response) { private async $getCpfpInfo(req: Request, res: Response) {
if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) { if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
res.status(501).send(`Invalid transaction ID.`); handleError(req, res, 501, `Invalid transaction ID.`);
return; return;
} }
@ -180,7 +181,7 @@ class BitcoinRoutes {
try { try {
cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId); cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
} catch (e) { } catch (e) {
res.status(500).send('failed to get CPFP info'); handleError(req, res, 500, 'failed to get CPFP info');
return; return;
} }
} }
@ -209,7 +210,7 @@ class BitcoinRoutes {
if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404; statusCode = 404;
} }
res.status(statusCode).send(e instanceof Error ? e.message : e); handleError(req, res, statusCode, e instanceof Error ? e.message : e);
} }
} }
@ -223,7 +224,7 @@ class BitcoinRoutes {
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404; statusCode = 404;
} }
res.status(statusCode).send(e instanceof Error ? e.message : e); handleError(req, res, statusCode, e instanceof Error ? e.message : e);
} }
} }
@ -284,13 +285,13 @@ class BitcoinRoutes {
// Not modified // Not modified
// 422 Unprocessable Entity // 422 Unprocessable Entity
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422 // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422
res.status(422).send(`Psbt had no missing nonWitnessUtxos.`); handleError(req, res, 422, `Psbt had no missing nonWitnessUtxos.`);
} }
} catch (e: any) { } catch (e: any) {
if (e instanceof Error && new RegExp(notFoundError).test(e.message)) { if (e instanceof Error && new RegExp(notFoundError).test(e.message)) {
res.status(404).send(e.message); handleError(req, res, 404, e.message);
} else { } else {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
} }
@ -304,7 +305,7 @@ class BitcoinRoutes {
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404; statusCode = 404;
} }
res.status(statusCode).send(e instanceof Error ? e.message : e); handleError(req, res, statusCode, e instanceof Error ? e.message : e);
} }
} }
@ -314,7 +315,7 @@ class BitcoinRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(transactions); res.json(transactions);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -336,7 +337,7 @@ class BitcoinRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString());
res.json(block); res.json(block);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -346,7 +347,7 @@ class BitcoinRoutes {
res.setHeader('content-type', 'text/plain'); res.setHeader('content-type', 'text/plain');
res.send(blockHeader); res.send(blockHeader);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -357,10 +358,11 @@ class BitcoinRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(auditSummary); res.json(auditSummary);
} else { } else {
return res.status(404).send(`audit not available`); handleError(req, res, 404, `audit not available`);
return;
} }
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -371,7 +373,8 @@ class BitcoinRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(auditSummary); res.json(auditSummary);
} else { } else {
return res.status(404).send(`transaction audit not available`); handleError(req, res, 404, `transaction audit not available`);
return;
} }
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); res.status(500).send(e instanceof Error ? e.message : e);
@ -388,42 +391,49 @@ class BitcoinRoutes {
return await this.getLegacyBlocks(req, res); return await this.getLegacyBlocks(req, res);
} }
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
private async getBlocksByBulk(req: Request, res: Response) { private async getBlocksByBulk(req: Request, res: Response) {
try { try {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { // Liquid - Not implemented if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { // Liquid - Not implemented
return res.status(404).send(`This API is only available for Bitcoin networks`); handleError(req, res, 404, `This API is only available for Bitcoin networks`);
return;
} }
if (config.MEMPOOL.MAX_BLOCKS_BULK_QUERY <= 0) { if (config.MEMPOOL.MAX_BLOCKS_BULK_QUERY <= 0) {
return res.status(404).send(`This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`); handleError(req, res, 404, `This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`);
return;
} }
if (!Common.indexingEnabled()) { if (!Common.indexingEnabled()) {
return res.status(404).send(`Indexing is required for this API`); handleError(req, res, 404, `Indexing is required for this API`);
return;
} }
const from = parseInt(req.params.from, 10); const from = parseInt(req.params.from, 10);
if (!req.params.from || from < 0) { if (!req.params.from || from < 0) {
return res.status(400).send(`Parameter 'from' must be a block height (integer)`); handleError(req, res, 400, `Parameter 'from' must be a block height (integer)`);
return;
} }
const to = req.params.to === undefined ? await bitcoinApi.$getBlockHeightTip() : parseInt(req.params.to, 10); const to = req.params.to === undefined ? await bitcoinApi.$getBlockHeightTip() : parseInt(req.params.to, 10);
if (to < 0) { if (to < 0) {
return res.status(400).send(`Parameter 'to' must be a block height (integer)`); handleError(req, res, 400, `Parameter 'to' must be a block height (integer)`);
return;
} }
if (from > to) { if (from > to) {
return res.status(400).send(`Parameter 'to' must be a higher block height than 'from'`); handleError(req, res, 400, `Parameter 'to' must be a higher block height than 'from'`);
return;
} }
if ((to - from + 1) > config.MEMPOOL.MAX_BLOCKS_BULK_QUERY) { if ((to - from + 1) > config.MEMPOOL.MAX_BLOCKS_BULK_QUERY) {
return res.status(400).send(`You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`); handleError(req, res, 400, `You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`);
return;
} }
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(await blocks.$getBlocksBetweenHeight(from, to)); res.json(await blocks.$getBlocksBetweenHeight(from, to));
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -458,7 +468,7 @@ class BitcoinRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(returnBlocks); res.json(returnBlocks);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -483,7 +493,7 @@ class BitcoinRoutes {
res.json(transactions); res.json(transactions);
} catch (e) { } catch (e) {
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100); loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100);
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -492,13 +502,13 @@ class BitcoinRoutes {
const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10)); const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10));
res.send(blockHash); res.send(blockHash);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
private async getAddress(req: Request, res: Response) { private async getAddress(req: Request, res: Response) {
if (config.MEMPOOL.BACKEND === 'none') { if (config.MEMPOOL.BACKEND === 'none') {
res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return; return;
} }
@ -507,15 +517,16 @@ class BitcoinRoutes {
res.json(addressData); res.json(addressData);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
return res.status(413).send(e instanceof Error ? e.message : e); handleError(req, res, 413, e instanceof Error ? e.message : e);
return;
} }
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
private async getAddressTransactions(req: Request, res: Response): Promise<void> { private async getAddressTransactions(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND === 'none') { if (config.MEMPOOL.BACKEND === 'none') {
res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return; return;
} }
@ -528,23 +539,23 @@ class BitcoinRoutes {
res.json(transactions); res.json(transactions);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
res.status(413).send(e instanceof Error ? e.message : e); handleError(req, res, 413, e instanceof Error ? e.message : e);
return; return;
} }
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
private async getAddressTransactionSummary(req: Request, res: Response): Promise<void> { private async getAddressTransactionSummary(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND !== 'esplora') { if (config.MEMPOOL.BACKEND !== 'esplora') {
res.status(405).send('Address summary lookups require mempool/electrs backend.'); handleError(req, res, 405, 'Address summary lookups require mempool/electrs backend.');
return; return;
} }
} }
private async getScriptHash(req: Request, res: Response) { private async getScriptHash(req: Request, res: Response) {
if (config.MEMPOOL.BACKEND === 'none') { if (config.MEMPOOL.BACKEND === 'none') {
res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return; return;
} }
@ -555,15 +566,16 @@ class BitcoinRoutes {
res.json(addressData); res.json(addressData);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
return res.status(413).send(e instanceof Error ? e.message : e); handleError(req, res, 413, e instanceof Error ? e.message : e);
return;
} }
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
private async getScriptHashTransactions(req: Request, res: Response): Promise<void> { private async getScriptHashTransactions(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND === 'none') { if (config.MEMPOOL.BACKEND === 'none') {
res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return; return;
} }
@ -578,16 +590,16 @@ class BitcoinRoutes {
res.json(transactions); res.json(transactions);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
res.status(413).send(e instanceof Error ? e.message : e); handleError(req, res, 413, e instanceof Error ? e.message : e);
return; return;
} }
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
private async getScriptHashTransactionSummary(req: Request, res: Response): Promise<void> { private async getScriptHashTransactionSummary(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND !== 'esplora') { if (config.MEMPOOL.BACKEND !== 'esplora') {
res.status(405).send('Scripthash summary lookups require mempool/electrs backend.'); handleError(req, res, 405, 'Scripthash summary lookups require mempool/electrs backend.');
return; return;
} }
} }
@ -597,7 +609,7 @@ class BitcoinRoutes {
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix); const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
res.send(blockHash); res.send(blockHash);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -624,7 +636,7 @@ class BitcoinRoutes {
const rawMempool = await bitcoinApi.$getRawMempool(); const rawMempool = await bitcoinApi.$getRawMempool();
res.send(rawMempool); res.send(rawMempool);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -632,12 +644,13 @@ class BitcoinRoutes {
try { try {
const result = blocks.getCurrentBlockHeight(); const result = blocks.getCurrentBlockHeight();
if (!result) { if (!result) {
return res.status(503).send(`Service Temporarily Unavailable`); handleError(req, res, 503, `Service Temporarily Unavailable`);
return;
} }
res.setHeader('content-type', 'text/plain'); res.setHeader('content-type', 'text/plain');
res.send(result.toString()); res.send(result.toString());
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -647,7 +660,7 @@ class BitcoinRoutes {
res.setHeader('content-type', 'text/plain'); res.setHeader('content-type', 'text/plain');
res.send(result); res.send(result);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -657,7 +670,7 @@ class BitcoinRoutes {
res.setHeader('content-type', 'application/octet-stream'); res.setHeader('content-type', 'application/octet-stream');
res.send(result); res.send(result);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -666,7 +679,7 @@ class BitcoinRoutes {
const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash); const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
res.json(result); res.json(result);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -675,7 +688,7 @@ class BitcoinRoutes {
const result = await bitcoinClient.validateAddress(req.params.address); const result = await bitcoinClient.validateAddress(req.params.address);
res.json(result); res.json(result);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -688,7 +701,7 @@ class BitcoinRoutes {
replaces replaces
}); });
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -697,7 +710,7 @@ class BitcoinRoutes {
const result = rbfCache.getRbfTrees(false); const result = rbfCache.getRbfTrees(false);
res.json(result); res.json(result);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -706,7 +719,7 @@ class BitcoinRoutes {
const result = rbfCache.getRbfTrees(true); const result = rbfCache.getRbfTrees(true);
res.json(result); res.json(result);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -719,7 +732,7 @@ class BitcoinRoutes {
res.status(204).send(); res.status(204).send();
} }
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -728,7 +741,7 @@ class BitcoinRoutes {
const result = await bitcoinApi.$getOutspends(req.params.txId); const result = await bitcoinApi.$getOutspends(req.params.txId);
res.json(result); res.json(result);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -738,10 +751,10 @@ class BitcoinRoutes {
if (da) { if (da) {
res.json(da); res.json(da);
} else { } else {
res.status(503).send(`Service Temporarily Unavailable`); handleError(req, res, 503, `Service Temporarily Unavailable`);
} }
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -752,7 +765,7 @@ class BitcoinRoutes {
const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx); const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx);
res.send(txIdResult); res.send(txIdResult);
} catch (e: any) { } catch (e: any) {
res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) handleError(req, res, 400, e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error')); : (e.message || 'Error'));
} }
} }
@ -764,7 +777,7 @@ class BitcoinRoutes {
const txIdResult = await bitcoinClient.sendRawTransaction(txHex); const txIdResult = await bitcoinClient.sendRawTransaction(txHex);
res.send(txIdResult); res.send(txIdResult);
} catch (e: any) { } catch (e: any) {
res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) handleError(req, res, 400, e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error')); : (e.message || 'Error'));
} }
} }
@ -776,8 +789,7 @@ class BitcoinRoutes {
const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate); const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate);
res.send(result); res.send(result);
} catch (e: any) { } catch (e: any) {
res.setHeader('content-type', 'text/plain'); handleError(req, res, 400, e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
res.status(400).send(e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error')); : (e.message || 'Error'));
} }
} }

View File

@ -219,10 +219,10 @@ class Blocks {
}; };
} }
public summarizeBlockTransactions(hash: string, transactions: TransactionExtended[]): BlockSummary { public summarizeBlockTransactions(hash: string, height: number, transactions: TransactionExtended[]): BlockSummary {
return { return {
id: hash, id: hash,
transactions: Common.classifyTransactions(transactions), transactions: Common.classifyTransactions(transactions, height),
}; };
} }
@ -616,7 +616,7 @@ class Blocks {
// add CPFP // add CPFP
const cpfpSummary = calculateGoodBlockCpfp(height, txs, []); const cpfpSummary = calculateGoodBlockCpfp(height, txs, []);
// classify // classify
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions); const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions);
await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 2); await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 2);
if (unclassifiedBlocks[height].version < 2 && targetSummaryVersion === 2) { if (unclassifiedBlocks[height].version < 2 && targetSummaryVersion === 2) {
const cpfpClusters = await CpfpRepository.$getClustersAt(height); const cpfpClusters = await CpfpRepository.$getClustersAt(height);
@ -653,7 +653,7 @@ class Blocks {
} }
const cpfpSummary = calculateGoodBlockCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as MempoolTransactionExtended[], []); const cpfpSummary = calculateGoodBlockCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as MempoolTransactionExtended[], []);
// classify // classify
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions); const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions);
const classifiedTxMap: { [txid: string]: TransactionClassified } = {}; const classifiedTxMap: { [txid: string]: TransactionClassified } = {};
for (const tx of classifiedTxs) { for (const tx of classifiedTxs) {
classifiedTxMap[tx.txid] = tx; classifiedTxMap[tx.txid] = tx;
@ -912,7 +912,7 @@ class Blocks {
} }
const cpfpSummary: CpfpSummary = calculateGoodBlockCpfp(block.height, transactions, accelerations.map(a => ({ txid: a.txid, max_bid: a.feeDelta }))); const cpfpSummary: CpfpSummary = calculateGoodBlockCpfp(block.height, transactions, accelerations.map(a => ({ txid: a.txid, max_bid: a.feeDelta })));
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions); const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions); const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, block.height, cpfpSummary.transactions);
this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`); this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`);
if (Common.indexingEnabled()) { if (Common.indexingEnabled()) {
@ -1169,7 +1169,7 @@ class Blocks {
transactions: cpfpSummary.transactions.map(tx => { transactions: cpfpSummary.transactions.map(tx => {
let flags: number = 0; let flags: number = 0;
try { try {
flags = Common.getTransactionFlags(tx); flags = Common.getTransactionFlags(tx, height);
} catch (e) { } catch (e) {
logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e)); logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e));
} }
@ -1188,7 +1188,7 @@ class Blocks {
} else { } else {
if (config.MEMPOOL.BACKEND === 'esplora') { if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx)); const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
summary = this.summarizeBlockTransactions(hash, txs); summary = this.summarizeBlockTransactions(hash, height || 0, txs);
summaryVersion = 1; summaryVersion = 1;
} else { } else {
// Call Core RPC // Call Core RPC
@ -1324,7 +1324,7 @@ class Blocks {
let summaryVersion = 0; let summaryVersion = 0;
if (config.MEMPOOL.BACKEND === 'esplora') { if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx)); const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx));
summary = this.summarizeBlockTransactions(cleanBlock.hash, txs); summary = this.summarizeBlockTransactions(cleanBlock.hash, cleanBlock.height, txs);
summaryVersion = 1; summaryVersion = 1;
} else { } else {
// Call Core RPC // Call Core RPC

View File

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

View File

@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2'; import { RowDataPacket } from 'mysql2';
class DatabaseMigration { class DatabaseMigration {
private static currentVersion = 81; private static currentVersion = 82;
private queryTimeout = 3600_000; private queryTimeout = 3600_000;
private statisticsAddedIndexed = false; private statisticsAddedIndexed = false;
private uniqueLogs: string[] = []; private uniqueLogs: string[] = [];
@ -700,6 +700,11 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"'); await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"');
await this.updateToSchemaVersion(81); await this.updateToSchemaVersion(81);
} }
if (databaseSchemaVersion < 82 && isBitcoin === true && config.MEMPOOL.NETWORK === 'mainnet') {
await this.$fixBadV1AuditBlocks();
await this.updateToSchemaVersion(82);
}
} }
/** /**
@ -1314,6 +1319,28 @@ class DatabaseMigration {
logger.warn(`Failed to migrate cpfp transaction data`); logger.warn(`Failed to migrate cpfp transaction data`);
} }
} }
private async $fixBadV1AuditBlocks(): Promise<void> {
const badBlocks = [
'000000000000000000011ad49227fc8c9ba0ca96ad2ebce41a862f9a244478dc',
'000000000000000000010ac1f68b3080153f2826ffddc87ceffdd68ed97d6960',
'000000000000000000024cbdafeb2660ae8bd2947d166e7fe15d1689e86b2cf7',
'00000000000000000002e1dbfbf6ae057f331992a058b822644b368034f87286',
'0000000000000000000019973b2778f08ad6d21e083302ff0833d17066921ebb',
];
for (const hash of badBlocks) {
try {
await this.$executeQuery(`
UPDATE blocks_audits
SET prioritized_txs = '[]'
WHERE hash = '${hash}'
`, true);
} catch (e) {
continue;
}
}
}
} }
export default new DatabaseMigration(); export default new DatabaseMigration();

View File

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

View File

@ -1,6 +1,7 @@
import config from '../../config'; import config from '../../config';
import { Application, Request, Response } from 'express'; import { Application, Request, Response } from 'express';
import channelsApi from './channels.api'; import channelsApi from './channels.api';
import { handleError } from '../../utils/api';
class ChannelsRoutes { class ChannelsRoutes {
constructor() { } constructor() { }
@ -22,7 +23,7 @@ class ChannelsRoutes {
const channels = await channelsApi.$searchChannelsById(req.params.search); const channels = await channelsApi.$searchChannelsById(req.params.search);
res.json(channels); res.json(channels);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -38,7 +39,7 @@ class ChannelsRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channel); res.json(channel);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -53,11 +54,11 @@ class ChannelsRoutes {
const status: string = typeof req.query.status === 'string' ? req.query.status : ''; const status: string = typeof req.query.status === 'string' ? req.query.status : '';
if (index < -1) { if (index < -1) {
res.status(400).send('Invalid index'); handleError(req, res, 400, 'Invalid index');
return; return;
} }
if (['open', 'active', 'closed'].includes(status) === false) { if (['open', 'active', 'closed'].includes(status) === false) {
res.status(400).send('Invalid status'); handleError(req, res, 400, 'Invalid status');
return; return;
} }
@ -69,14 +70,14 @@ class ChannelsRoutes {
res.header('X-Total-Count', channelsCount.toString()); res.header('X-Total-Count', channelsCount.toString());
res.json(channels); res.json(channels);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
private async $getChannelsByTransactionIds(req: Request, res: Response): Promise<void> { private async $getChannelsByTransactionIds(req: Request, res: Response): Promise<void> {
try { try {
if (!Array.isArray(req.query.txId)) { if (!Array.isArray(req.query.txId)) {
res.status(400).send('Not an array'); handleError(req, res, 400, 'Not an array');
return; return;
} }
const txIds: string[] = []; const txIds: string[] = [];
@ -107,7 +108,7 @@ class ChannelsRoutes {
res.json(result); res.json(result);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -119,7 +120,7 @@ class ChannelsRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channels); res.json(channels);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -132,7 +133,7 @@ class ChannelsRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channels); res.json(channels);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }

View File

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

View File

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

View File

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

View File

@ -19,12 +19,13 @@ class Mempool {
private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {}; private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {};
private mempoolCandidates: { [txid: string ]: boolean } = {}; private mempoolCandidates: { [txid: string ]: boolean } = {};
private spendMap = new Map<string, MempoolTransactionExtended>(); private spendMap = new Map<string, MempoolTransactionExtended>();
private recentlyDeleted: MempoolTransactionExtended[][] = []; // buffer of transactions deleted in recent mempool updates
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0, private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
maxmempool: 300000000, mempoolminfee: Common.isLiquid() ? 0.00000100 : 0.00001000, minrelaytxfee: Common.isLiquid() ? 0.00000100 : 0.00001000 }; maxmempool: 300000000, mempoolminfee: Common.isLiquid() ? 0.00000100 : 0.00001000, minrelaytxfee: Common.isLiquid() ? 0.00000100 : 0.00001000 };
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[], private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void) | undefined; deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void) | undefined;
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[], private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[],
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], candidates?: GbtCandidates) => Promise<void>) | undefined; deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[], candidates?: GbtCandidates) => Promise<void>) | undefined;
private accelerations: { [txId: string]: Acceleration } = {}; private accelerations: { [txId: string]: Acceleration } = {};
private accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {}; private accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {};
@ -74,12 +75,12 @@ class Mempool {
} }
public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void): void { newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void): void {
this.mempoolChangedCallback = fn; this.mempoolChangedCallback = fn;
} }
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number, public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number,
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[],
candidates?: GbtCandidates) => Promise<void>): void { candidates?: GbtCandidates) => Promise<void>): void {
this.$asyncMempoolChangedCallback = fn; this.$asyncMempoolChangedCallback = fn;
} }
@ -362,12 +363,15 @@ class Mempool {
const candidatesChanged = candidates?.added?.length || candidates?.removed?.length; const candidatesChanged = candidates?.added?.length || candidates?.removed?.length;
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) { this.recentlyDeleted.unshift(deletedTransactions);
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta); this.recentlyDeleted.length = Math.min(this.recentlyDeleted.length, 10); // truncate to the last 10 mempool updates
if (this.mempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length)) {
this.mempoolChangedCallback(this.mempoolCache, newTransactions, this.recentlyDeleted, accelerationDelta);
} }
if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length || candidatesChanged)) { if (this.$asyncMempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length || candidatesChanged)) {
this.updateTimerProgress(timer, 'running async mempool callback'); this.updateTimerProgress(timer, 'running async mempool callback');
await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, deletedTransactions, accelerationDelta, candidates); await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, this.recentlyDeleted, accelerationDelta, candidates);
this.updateTimerProgress(timer, 'completed async mempool callback'); this.updateTimerProgress(timer, 'completed async mempool callback');
} }
@ -541,16 +545,7 @@ class Mempool {
} }
} }
public handleRbfTransactions(rbfTransactions: { [txid: string]: MempoolTransactionExtended[]; }): void { public handleRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void {
for (const rbfTransaction in rbfTransactions) {
if (this.mempoolCache[rbfTransaction] && rbfTransactions[rbfTransaction]?.length) {
// Store replaced transactions
rbfCache.add(rbfTransactions[rbfTransaction], this.mempoolCache[rbfTransaction]);
}
}
}
public handleMinedRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void {
for (const rbfTransaction in rbfTransactions) { for (const rbfTransaction in rbfTransactions) {
if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) { if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) {
// Store replaced transactions // Store replaced transactions

View File

@ -10,6 +10,7 @@ import mining from "./mining";
import PricesRepository from '../../repositories/PricesRepository'; import PricesRepository from '../../repositories/PricesRepository';
import AccelerationRepository from '../../repositories/AccelerationRepository'; import AccelerationRepository from '../../repositories/AccelerationRepository';
import accelerationApi from '../services/acceleration'; import accelerationApi from '../services/acceleration';
import { handleError } from '../../utils/api';
class MiningRoutes { class MiningRoutes {
public initRoutes(app: Application) { public initRoutes(app: Application) {
@ -53,7 +54,7 @@ class MiningRoutes {
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
if (['testnet', 'signet', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) { if (['testnet', 'signet', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) {
res.status(400).send('Prices are not available on testnets.'); handleError(req, res, 400, 'Prices are not available on testnets.');
return; return;
} }
const timestamp = parseInt(req.query.timestamp as string, 10) || 0; const timestamp = parseInt(req.query.timestamp as string, 10) || 0;
@ -71,7 +72,7 @@ class MiningRoutes {
} }
res.status(200).send(response); res.status(200).send(response);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -84,9 +85,9 @@ class MiningRoutes {
res.json(stats); res.json(stats);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
res.status(404).send(e.message); handleError(req, res, 404, e.message);
} else { } else {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
} }
@ -103,9 +104,9 @@ class MiningRoutes {
res.json(poolBlocks); res.json(poolBlocks);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
res.status(404).send(e.message); handleError(req, res, 404, e.message);
} else { } else {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
} }
@ -129,7 +130,7 @@ class MiningRoutes {
res.json(pools); res.json(pools);
} }
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -143,7 +144,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(stats); res.json(stats);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -157,7 +158,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(hashrates); res.json(hashrates);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -172,9 +173,9 @@ class MiningRoutes {
res.json(hashrates); res.json(hashrates);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
res.status(404).send(e.message); handleError(req, res, 404, e.message);
} else { } else {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
} }
@ -203,7 +204,7 @@ class MiningRoutes {
currentDifficulty: currentDifficulty, currentDifficulty: currentDifficulty,
}); });
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -217,7 +218,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFees); res.json(blockFees);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -235,7 +236,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFees); res.json(blockFees);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -249,7 +250,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockRewards); res.json(blockRewards);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -263,7 +264,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFeeRates); res.json(blockFeeRates);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -281,7 +282,7 @@ class MiningRoutes {
weights: blockWeights weights: blockWeights
}); });
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -293,7 +294,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment])); res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment]));
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -317,7 +318,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate])); res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate]));
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -326,7 +327,7 @@ class MiningRoutes {
const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash); const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash);
if (!audit) { if (!audit) {
res.status(204).send(`This block has not been audited.`); handleError(req, res, 204, `This block has not been audited.`);
return; return;
} }
@ -335,7 +336,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
res.json(audit); res.json(audit);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -358,7 +359,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(result); res.json(result);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -371,7 +372,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15)); res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15));
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -384,7 +385,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
res.json(audit || 'null'); res.json(audit || 'null');
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -394,12 +395,12 @@ class MiningRoutes {
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
res.status(400).send('Acceleration data is not available.'); handleError(req, res, 400, 'Acceleration data is not available.');
return; return;
} }
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug)); res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug));
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -409,13 +410,13 @@ class MiningRoutes {
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
res.status(400).send('Acceleration data is not available.'); handleError(req, res, 400, 'Acceleration data is not available.');
return; return;
} }
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10); const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height)); res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height));
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -425,12 +426,12 @@ class MiningRoutes {
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
res.status(400).send('Acceleration data is not available.'); handleError(req, res, 400, 'Acceleration data is not available.');
return; return;
} }
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval)); res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval));
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -440,12 +441,12 @@ class MiningRoutes {
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
res.status(400).send('Acceleration data is not available.'); handleError(req, res, 400, 'Acceleration data is not available.');
return; return;
} }
res.status(200).send(await AccelerationRepository.$getAccelerationTotals(<string>req.query.pool, <string>req.query.interval)); res.status(200).send(await AccelerationRepository.$getAccelerationTotals(<string>req.query.pool, <string>req.query.interval));
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -455,12 +456,12 @@ class MiningRoutes {
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
res.status(400).send('Acceleration data is not available.'); handleError(req, res, 400, 'Acceleration data is not available.');
return; return;
} }
res.status(200).send(accelerationApi.accelerations || []); res.status(200).send(accelerationApi.accelerations || []);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -472,7 +473,7 @@ class MiningRoutes {
accelerationApi.accelerationRequested(req.params.txid); accelerationApi.accelerationRequested(req.params.txid);
res.status(200).send(); res.status(200).send();
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
} }

View File

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

View File

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

View File

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

View File

@ -520,8 +520,17 @@ class WebsocketHandler {
} }
} }
/**
*
* @param newMempool
* @param mempoolSize
* @param newTransactions array of transactions added this mempool update.
* @param recentlyDeletedTransactions array of arrays of transactions removed in the last N mempool updates, most recent first.
* @param accelerationDelta
* @param candidates
*/
async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number, async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number,
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], newTransactions: MempoolTransactionExtended[], recentlyDeletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[],
candidates?: GbtCandidates): Promise<void> { candidates?: GbtCandidates): Promise<void> {
if (!this.webSocketServers.length) { if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server have been set'); throw new Error('No WebSocket.Server have been set');
@ -529,6 +538,8 @@ class WebsocketHandler {
this.printLogs(); this.printLogs();
const deletedTransactions = recentlyDeletedTransactions.length ? recentlyDeletedTransactions[0] : [];
const transactionIds = (memPool.limitGBT && candidates) ? Object.keys(candidates?.txs || {}) : Object.keys(newMempool); const transactionIds = (memPool.limitGBT && candidates) ? Object.keys(candidates?.txs || {}) : Object.keys(newMempool);
let added = newTransactions; let added = newTransactions;
let removed = deletedTransactions; let removed = deletedTransactions;
@ -547,7 +558,7 @@ class WebsocketHandler {
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas(); const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
const mempoolInfo = memPool.getMempoolInfo(); const mempoolInfo = memPool.getMempoolInfo();
const vBytesPerSecond = memPool.getVBytesPerSecond(); const vBytesPerSecond = memPool.getVBytesPerSecond();
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions); const rbfTransactions = Common.findRbfTransactions(newTransactions, recentlyDeletedTransactions.flat());
const da = difficultyAdjustment.getDifficultyAdjustment(); const da = difficultyAdjustment.getDifficultyAdjustment();
const accelerations = memPool.getAccelerations(); const accelerations = memPool.getAccelerations();
memPool.handleRbfTransactions(rbfTransactions); memPool.handleRbfTransactions(rbfTransactions);
@ -578,7 +589,7 @@ class WebsocketHandler {
const replacedTransactions: { replaced: string, by: TransactionExtended }[] = []; const replacedTransactions: { replaced: string, by: TransactionExtended }[] = [];
for (const tx of newTransactions) { for (const tx of newTransactions) {
if (rbfTransactions[tx.txid]) { if (rbfTransactions[tx.txid]) {
for (const replaced of rbfTransactions[tx.txid]) { for (const replaced of rbfTransactions[tx.txid].replaced) {
replacedTransactions.push({ replaced: replaced.txid, by: tx }); replacedTransactions.push({ replaced: replaced.txid, by: tx });
} }
} }
@ -947,7 +958,7 @@ class WebsocketHandler {
await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions)); await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions));
const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap()); const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
memPool.handleMinedRbfTransactions(rbfTransactions); memPool.handleRbfTransactions(rbfTransactions);
memPool.removeFromSpendMap(transactions); memPool.removeFromSpendMap(transactions);
if (config.MEMPOOL.AUDIT && memPool.isInSync()) { if (config.MEMPOOL.AUDIT && memPool.isInSync()) {

View File

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

View File

@ -1106,7 +1106,7 @@ class BlocksRepository {
let summaryVersion = 0; let summaryVersion = 0;
if (config.MEMPOOL.BACKEND === 'esplora') { if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(dbBlk.id)).map(tx => transactionUtils.extendTransaction(tx)); const txs = (await bitcoinApi.$getTxsForBlock(dbBlk.id)).map(tx => transactionUtils.extendTransaction(tx));
summary = blocks.summarizeBlockTransactions(dbBlk.id, txs); summary = blocks.summarizeBlockTransactions(dbBlk.id, dbBlk.height, txs);
summaryVersion = 1; summaryVersion = 1;
} else { } else {
// Call Core RPC // Call Core RPC

9
backend/src/utils/api.ts Normal file
View File

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

View File

@ -158,7 +158,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
if (!opN) { if (!opN) {
return; return;
} }
if (!opN.startsWith('OP_PUSHNUM_')) { if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
return; return;
} }
const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10); const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10);
@ -178,7 +178,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
if (!opM) { if (!opM) {
return; return;
} }
if (!opM.startsWith('OP_PUSHNUM_')) { if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
return; return;
} }
const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10); const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10);

View File

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

View File

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

View File

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

View File

@ -1,12 +1,12 @@
{ {
"name": "mempool-frontend", "name": "mempool-frontend",
"version": "3.0.0-beta", "version": "3.1.0-dev",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mempool-frontend", "name": "mempool-frontend",
"version": "3.0.0-beta", "version": "3.1.0-dev",
"license": "GNU Affero General Public License v3.0", "license": "GNU Affero General Public License v3.0",
"dependencies": { "dependencies": {
"@angular-devkit/build-angular": "^17.3.1", "@angular-devkit/build-angular": "^17.3.1",
@ -42,7 +42,7 @@
"rxjs": "~7.8.1", "rxjs": "~7.8.1",
"tinyify": "^4.0.0", "tinyify": "^4.0.0",
"tlite": "^0.1.9", "tlite": "^0.1.9",
"tslib": "~2.6.0", "tslib": "~2.7.0",
"zone.js": "~0.14.4" "zone.js": "~0.14.4"
}, },
"devDependencies": { "devDependencies": {
@ -62,7 +62,7 @@
"optionalDependencies": { "optionalDependencies": {
"@cypress/schematic": "^2.5.0", "@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3", "@types/cypress": "^1.1.3",
"cypress": "^13.13.0", "cypress": "^13.14.0",
"cypress-fail-on-console-error": "~5.1.0", "cypress-fail-on-console-error": "~5.1.0",
"cypress-wait-until": "^2.0.1", "cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1", "mock-socket": "~9.3.1",
@ -699,6 +699,11 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/@angular-devkit/build-angular/node_modules/tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
},
"node_modules/@angular-devkit/build-webpack": { "node_modules/@angular-devkit/build-webpack": {
"version": "0.1703.1", "version": "0.1703.1",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1703.1.tgz", "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1703.1.tgz",
@ -6014,9 +6019,9 @@
"integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ=="
}, },
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "1.20.2", "version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"dependencies": { "dependencies": {
"bytes": "3.1.2", "bytes": "3.1.2",
"content-type": "~1.0.5", "content-type": "~1.0.5",
@ -6026,7 +6031,7 @@
"http-errors": "2.0.0", "http-errors": "2.0.0",
"iconv-lite": "0.4.24", "iconv-lite": "0.4.24",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"qs": "6.11.0", "qs": "6.13.0",
"raw-body": "2.5.2", "raw-body": "2.5.2",
"type-is": "~1.6.18", "type-is": "~1.6.18",
"unpipe": "1.0.0" "unpipe": "1.0.0"
@ -6061,11 +6066,11 @@
} }
}, },
"node_modules/body-parser/node_modules/qs": { "node_modules/body-parser/node_modules/qs": {
"version": "6.11.0", "version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"dependencies": { "dependencies": {
"side-channel": "^1.0.4" "side-channel": "^1.0.6"
}, },
"engines": { "engines": {
"node": ">=0.6" "node": ">=0.6"
@ -8040,13 +8045,13 @@
"peer": true "peer": true
}, },
"node_modules/cypress": { "node_modules/cypress": {
"version": "13.13.0", "version": "13.14.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.13.0.tgz", "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.14.0.tgz",
"integrity": "sha512-ou/MQUDq4tcDJI2FsPaod2FZpex4kpIK43JJlcBgWrX8WX7R/05ZxGTuxedOuZBfxjZxja+fbijZGyxiLP6CFA==", "integrity": "sha512-r0+nhd033x883YL6068futewUsl02Q7rWiinyAAIBDW/OOTn+UMILWgNuCiY3vtJjd53efOqq5R9dctQk/rKiw==",
"hasInstallScript": true, "hasInstallScript": true,
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"@cypress/request": "^3.0.0", "@cypress/request": "^3.0.1",
"@cypress/xvfb": "^1.2.4", "@cypress/xvfb": "^1.2.4",
"@types/sinonjs__fake-timers": "8.1.1", "@types/sinonjs__fake-timers": "8.1.1",
"@types/sizzle": "^2.3.2", "@types/sizzle": "^2.3.2",
@ -8805,9 +8810,9 @@
"integrity": "sha512-XzWNH4ZSa9BwVUQSDorPWAUQ5WGuYz7zJUNpNif40zFCiCl20t8zgylmreNmn26h5kiyw2lg7RfTmeMBsDklqg==" "integrity": "sha512-XzWNH4ZSa9BwVUQSDorPWAUQ5WGuYz7zJUNpNif40zFCiCl20t8zgylmreNmn26h5kiyw2lg7RfTmeMBsDklqg=="
}, },
"node_modules/elliptic": { "node_modules/elliptic": {
"version": "6.5.4", "version": "6.5.7",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz",
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", "integrity": "sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==",
"dependencies": { "dependencies": {
"bn.js": "^4.11.9", "bn.js": "^4.11.9",
"brorand": "^1.1.0", "brorand": "^1.1.0",
@ -9870,36 +9875,36 @@
"integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==" "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw=="
}, },
"node_modules/express": { "node_modules/express": {
"version": "4.19.2", "version": "4.21.0",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
"dependencies": { "dependencies": {
"accepts": "~1.3.8", "accepts": "~1.3.8",
"array-flatten": "1.1.1", "array-flatten": "1.1.1",
"body-parser": "1.20.2", "body-parser": "1.20.3",
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"content-type": "~1.0.4", "content-type": "~1.0.4",
"cookie": "0.6.0", "cookie": "0.6.0",
"cookie-signature": "1.0.6", "cookie-signature": "1.0.6",
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
"encodeurl": "~1.0.2", "encodeurl": "~2.0.0",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"etag": "~1.8.1", "etag": "~1.8.1",
"finalhandler": "1.2.0", "finalhandler": "1.3.1",
"fresh": "0.5.2", "fresh": "0.5.2",
"http-errors": "2.0.0", "http-errors": "2.0.0",
"merge-descriptors": "1.0.1", "merge-descriptors": "1.0.3",
"methods": "~1.1.2", "methods": "~1.1.2",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"path-to-regexp": "0.1.7", "path-to-regexp": "0.1.10",
"proxy-addr": "~2.0.7", "proxy-addr": "~2.0.7",
"qs": "6.11.0", "qs": "6.13.0",
"range-parser": "~1.2.1", "range-parser": "~1.2.1",
"safe-buffer": "5.2.1", "safe-buffer": "5.2.1",
"send": "0.18.0", "send": "0.19.0",
"serve-static": "1.15.0", "serve-static": "1.16.2",
"setprototypeof": "1.2.0", "setprototypeof": "1.2.0",
"statuses": "2.0.1", "statuses": "2.0.1",
"type-is": "~1.6.18", "type-is": "~1.6.18",
@ -9918,6 +9923,14 @@
"ms": "2.0.0" "ms": "2.0.0"
} }
}, },
"node_modules/express/node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/express/node_modules/ms": { "node_modules/express/node_modules/ms": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@ -9935,11 +9948,11 @@
} }
}, },
"node_modules/express/node_modules/qs": { "node_modules/express/node_modules/qs": {
"version": "6.11.0", "version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"dependencies": { "dependencies": {
"side-channel": "^1.0.4" "side-channel": "^1.0.6"
}, },
"engines": { "engines": {
"node": ">=0.6" "node": ">=0.6"
@ -10172,12 +10185,12 @@
} }
}, },
"node_modules/finalhandler": { "node_modules/finalhandler": {
"version": "1.2.0", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"dependencies": { "dependencies": {
"debug": "2.6.9", "debug": "2.6.9",
"encodeurl": "~1.0.2", "encodeurl": "~2.0.0",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
@ -10196,6 +10209,14 @@
"ms": "2.0.0" "ms": "2.0.0"
} }
}, },
"node_modules/finalhandler/node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/finalhandler/node_modules/ms": { "node_modules/finalhandler/node_modules/ms": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@ -12662,9 +12683,12 @@
} }
}, },
"node_modules/merge-descriptors": { "node_modules/merge-descriptors": {
"version": "1.0.1", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
}, },
"node_modules/merge-stream": { "node_modules/merge-stream": {
"version": "2.0.0", "version": "2.0.0",
@ -12688,12 +12712,12 @@
} }
}, },
"node_modules/micromatch": { "node_modules/micromatch": {
"version": "4.0.4", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dependencies": { "dependencies": {
"braces": "^3.0.1", "braces": "^3.0.3",
"picomatch": "^2.2.3" "picomatch": "^2.3.1"
}, },
"engines": { "engines": {
"node": ">=8.6" "node": ">=8.6"
@ -13669,9 +13693,12 @@
} }
}, },
"node_modules/object-inspect": { "node_modules/object-inspect": {
"version": "1.9.0", "version": "1.13.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
"integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==", "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
"engines": {
"node": ">= 0.4"
},
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
@ -14185,9 +14212,9 @@
} }
}, },
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "0.1.7", "version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
}, },
"node_modules/path-type": { "node_modules/path-type": {
"version": "4.0.0", "version": "4.0.0",
@ -15472,9 +15499,9 @@
} }
}, },
"node_modules/send": { "node_modules/send": {
"version": "0.18.0", "version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"dependencies": { "dependencies": {
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
@ -15613,19 +15640,27 @@
"integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
}, },
"node_modules/serve-static": { "node_modules/serve-static": {
"version": "1.15.0", "version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"dependencies": { "dependencies": {
"encodeurl": "~1.0.2", "encodeurl": "~2.0.0",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"send": "0.18.0" "send": "0.19.0"
}, },
"engines": { "engines": {
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/serve-static/node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/server-destroy": { "node_modules/server-destroy": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz",
@ -15717,13 +15752,17 @@
} }
}, },
"node_modules/side-channel": { "node_modules/side-channel": {
"version": "1.0.4", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
"dependencies": { "dependencies": {
"call-bind": "^1.0.0", "call-bind": "^1.0.7",
"get-intrinsic": "^1.0.2", "es-errors": "^1.3.0",
"object-inspect": "^1.9.0" "get-intrinsic": "^1.2.4",
"object-inspect": "^1.13.1"
},
"engines": {
"node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
@ -16925,9 +16964,9 @@
} }
}, },
"node_modules/tslib": { "node_modules/tslib": {
"version": "2.6.2", "version": "2.7.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="
}, },
"node_modules/tuf-js": { "node_modules/tuf-js": {
"version": "2.2.0", "version": "2.2.0",
@ -18849,6 +18888,11 @@
"requires": { "requires": {
"lru-cache": "^6.0.0" "lru-cache": "^6.0.0"
} }
},
"tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
} }
} }
}, },
@ -22572,9 +22616,9 @@
"integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ=="
}, },
"body-parser": { "body-parser": {
"version": "1.20.2", "version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"requires": { "requires": {
"bytes": "3.1.2", "bytes": "3.1.2",
"content-type": "~1.0.5", "content-type": "~1.0.5",
@ -22584,7 +22628,7 @@
"http-errors": "2.0.0", "http-errors": "2.0.0",
"iconv-lite": "0.4.24", "iconv-lite": "0.4.24",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"qs": "6.11.0", "qs": "6.13.0",
"raw-body": "2.5.2", "raw-body": "2.5.2",
"type-is": "~1.6.18", "type-is": "~1.6.18",
"unpipe": "1.0.0" "unpipe": "1.0.0"
@ -22612,11 +22656,11 @@
} }
}, },
"qs": { "qs": {
"version": "6.11.0", "version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"requires": { "requires": {
"side-channel": "^1.0.4" "side-channel": "^1.0.6"
} }
} }
} }
@ -24127,12 +24171,12 @@
"peer": true "peer": true
}, },
"cypress": { "cypress": {
"version": "13.13.0", "version": "13.14.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.13.0.tgz", "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.14.0.tgz",
"integrity": "sha512-ou/MQUDq4tcDJI2FsPaod2FZpex4kpIK43JJlcBgWrX8WX7R/05ZxGTuxedOuZBfxjZxja+fbijZGyxiLP6CFA==", "integrity": "sha512-r0+nhd033x883YL6068futewUsl02Q7rWiinyAAIBDW/OOTn+UMILWgNuCiY3vtJjd53efOqq5R9dctQk/rKiw==",
"optional": true, "optional": true,
"requires": { "requires": {
"@cypress/request": "^3.0.0", "@cypress/request": "^3.0.1",
"@cypress/xvfb": "^1.2.4", "@cypress/xvfb": "^1.2.4",
"@types/sinonjs__fake-timers": "8.1.1", "@types/sinonjs__fake-timers": "8.1.1",
"@types/sizzle": "^2.3.2", "@types/sizzle": "^2.3.2",
@ -24723,9 +24767,9 @@
"integrity": "sha512-XzWNH4ZSa9BwVUQSDorPWAUQ5WGuYz7zJUNpNif40zFCiCl20t8zgylmreNmn26h5kiyw2lg7RfTmeMBsDklqg==" "integrity": "sha512-XzWNH4ZSa9BwVUQSDorPWAUQ5WGuYz7zJUNpNif40zFCiCl20t8zgylmreNmn26h5kiyw2lg7RfTmeMBsDklqg=="
}, },
"elliptic": { "elliptic": {
"version": "6.5.4", "version": "6.5.7",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz",
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", "integrity": "sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==",
"requires": { "requires": {
"bn.js": "^4.11.9", "bn.js": "^4.11.9",
"brorand": "^1.1.0", "brorand": "^1.1.0",
@ -25540,36 +25584,36 @@
"integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==" "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw=="
}, },
"express": { "express": {
"version": "4.19.2", "version": "4.21.0",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
"requires": { "requires": {
"accepts": "~1.3.8", "accepts": "~1.3.8",
"array-flatten": "1.1.1", "array-flatten": "1.1.1",
"body-parser": "1.20.2", "body-parser": "1.20.3",
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"content-type": "~1.0.4", "content-type": "~1.0.4",
"cookie": "0.6.0", "cookie": "0.6.0",
"cookie-signature": "1.0.6", "cookie-signature": "1.0.6",
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
"encodeurl": "~1.0.2", "encodeurl": "~2.0.0",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"etag": "~1.8.1", "etag": "~1.8.1",
"finalhandler": "1.2.0", "finalhandler": "1.3.1",
"fresh": "0.5.2", "fresh": "0.5.2",
"http-errors": "2.0.0", "http-errors": "2.0.0",
"merge-descriptors": "1.0.1", "merge-descriptors": "1.0.3",
"methods": "~1.1.2", "methods": "~1.1.2",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"path-to-regexp": "0.1.7", "path-to-regexp": "0.1.10",
"proxy-addr": "~2.0.7", "proxy-addr": "~2.0.7",
"qs": "6.11.0", "qs": "6.13.0",
"range-parser": "~1.2.1", "range-parser": "~1.2.1",
"safe-buffer": "5.2.1", "safe-buffer": "5.2.1",
"send": "0.18.0", "send": "0.19.0",
"serve-static": "1.15.0", "serve-static": "1.16.2",
"setprototypeof": "1.2.0", "setprototypeof": "1.2.0",
"statuses": "2.0.1", "statuses": "2.0.1",
"type-is": "~1.6.18", "type-is": "~1.6.18",
@ -25585,6 +25629,11 @@
"ms": "2.0.0" "ms": "2.0.0"
} }
}, },
"encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="
},
"ms": { "ms": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@ -25599,11 +25648,11 @@
} }
}, },
"qs": { "qs": {
"version": "6.11.0", "version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"requires": { "requires": {
"side-channel": "^1.0.4" "side-channel": "^1.0.6"
} }
}, },
"safe-buffer": { "safe-buffer": {
@ -25778,12 +25827,12 @@
} }
}, },
"finalhandler": { "finalhandler": {
"version": "1.2.0", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"requires": { "requires": {
"debug": "2.6.9", "debug": "2.6.9",
"encodeurl": "~1.0.2", "encodeurl": "~2.0.0",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
@ -25799,6 +25848,11 @@
"ms": "2.0.0" "ms": "2.0.0"
} }
}, },
"encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="
},
"ms": { "ms": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@ -27591,9 +27645,9 @@
} }
}, },
"merge-descriptors": { "merge-descriptors": {
"version": "1.0.1", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="
}, },
"merge-stream": { "merge-stream": {
"version": "2.0.0", "version": "2.0.0",
@ -27611,12 +27665,12 @@
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="
}, },
"micromatch": { "micromatch": {
"version": "4.0.4", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"requires": { "requires": {
"braces": "^3.0.1", "braces": "^3.0.3",
"picomatch": "^2.2.3" "picomatch": "^2.3.1"
} }
}, },
"miller-rabin": { "miller-rabin": {
@ -28364,9 +28418,9 @@
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
}, },
"object-inspect": { "object-inspect": {
"version": "1.9.0", "version": "1.13.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
"integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==" "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g=="
}, },
"object-keys": { "object-keys": {
"version": "1.1.1", "version": "1.1.1",
@ -28740,9 +28794,9 @@
} }
}, },
"path-to-regexp": { "path-to-regexp": {
"version": "0.1.7", "version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
}, },
"path-type": { "path-type": {
"version": "4.0.0", "version": "4.0.0",
@ -29663,9 +29717,9 @@
} }
}, },
"send": { "send": {
"version": "0.18.0", "version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"requires": { "requires": {
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
@ -29786,14 +29840,21 @@
} }
}, },
"serve-static": { "serve-static": {
"version": "1.15.0", "version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"requires": { "requires": {
"encodeurl": "~1.0.2", "encodeurl": "~2.0.0",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"send": "0.18.0" "send": "0.19.0"
},
"dependencies": {
"encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="
}
} }
}, },
"server-destroy": { "server-destroy": {
@ -29869,13 +29930,14 @@
"integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==" "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA=="
}, },
"side-channel": { "side-channel": {
"version": "1.0.4", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
"requires": { "requires": {
"call-bind": "^1.0.0", "call-bind": "^1.0.7",
"get-intrinsic": "^1.0.2", "es-errors": "^1.3.0",
"object-inspect": "^1.9.0" "get-intrinsic": "^1.2.4",
"object-inspect": "^1.13.1"
} }
}, },
"signal-exit": { "signal-exit": {
@ -30763,9 +30825,9 @@
} }
}, },
"tslib": { "tslib": {
"version": "2.6.2", "version": "2.7.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="
}, },
"tuf-js": { "tuf-js": {
"version": "2.2.0", "version": "2.2.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "mempool-frontend", "name": "mempool-frontend",
"version": "3.0.0-beta", "version": "3.1.0-dev",
"description": "Bitcoin mempool visualizer and blockchain explorer backend", "description": "Bitcoin mempool visualizer and blockchain explorer backend",
"license": "GNU Affero General Public License v3.0", "license": "GNU Affero General Public License v3.0",
"homepage": "https://mempool.space", "homepage": "https://mempool.space",
@ -95,7 +95,7 @@
"esbuild": "^0.23.0", "esbuild": "^0.23.0",
"tinyify": "^4.0.0", "tinyify": "^4.0.0",
"tlite": "^0.1.9", "tlite": "^0.1.9",
"tslib": "~2.6.0", "tslib": "~2.7.0",
"zone.js": "~0.14.4" "zone.js": "~0.14.4"
}, },
"devDependencies": { "devDependencies": {
@ -115,7 +115,7 @@
"optionalDependencies": { "optionalDependencies": {
"@cypress/schematic": "^2.5.0", "@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3", "@types/cypress": "^1.1.3",
"cypress": "^13.13.0", "cypress": "^13.14.0",
"cypress-fail-on-console-error": "~5.1.0", "cypress-fail-on-console-error": "~5.1.0",
"cypress-wait-until": "^2.0.1", "cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1", "mock-socket": "~9.3.1",

View File

@ -151,7 +151,7 @@ export const languages: Language[] = [
{ code: 'fr', name: 'Français' }, // French { code: 'fr', name: 'Français' }, // French
// { code: 'gl', name: 'Galego' }, // Galician // { code: 'gl', name: 'Galego' }, // Galician
{ code: 'ko', name: '한국어' }, // Korean { code: 'ko', name: '한국어' }, // Korean
// { code: 'hr', name: 'Hrvatski' }, // Croatian { code: 'hr', name: 'Hrvatski' }, // Croatian
// { code: 'id', name: 'Bahasa Indonesia' },// Indonesian // { code: 'id', name: 'Bahasa Indonesia' },// Indonesian
{ code: 'hi', name: 'हिन्दी' }, // Hindi { code: 'hi', name: 'हिन्दी' }, // Hindi
{ code: 'ne', name: 'नेपाली' }, // Nepalese { code: 'ne', name: 'नेपाली' }, // Nepalese

View File

@ -135,7 +135,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
return; return;
} }
const opN = ops.pop(); const opN = ops.pop();
if (!opN.startsWith('OP_PUSHNUM_')) { if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
return; return;
} }
const n = parseInt(opN.match(/[0-9]+/)[0], 10); const n = parseInt(opN.match(/[0-9]+/)[0], 10);
@ -152,7 +152,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
} }
} }
const opM = ops.pop(); const opM = ops.pop();
if (!opM.startsWith('OP_PUSHNUM_')) { if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
return; return;
} }
const m = parseInt(opM.match(/[0-9]+/)[0], 10); const m = parseInt(opM.match(/[0-9]+/)[0], 10);

View File

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

View File

@ -13,8 +13,6 @@
.image.not-rounded { .image.not-rounded {
border-radius: 0; border-radius: 0;
width: 60px;
height: 60px;
} }
.intro { .intro {
@ -158,9 +156,8 @@
margin: 40px 29px 10px; margin: 40px 29px 10px;
&.image.coldcard { &.image.coldcard {
border-radius: 0; border-radius: 0;
width: auto; height: auto;
max-height: 50px; margin: 20px 29px 20px;
margin: 40px 29px 14px 29px;
} }
} }
} }
@ -254,3 +251,12 @@
width: 64px; width: 64px;
height: 64px; height: 64px;
} }
.enterprise-sponsor {
.wrapper {
display: flex;
flex-wrap: wrap;
justify-content: center;
max-width: 800px;
}
}

View File

@ -75,6 +75,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
@Output() changeMode = new EventEmitter<boolean>(); @Output() changeMode = new EventEmitter<boolean>();
calculating = true; calculating = true;
processing = false;
selectedOption: 'wait' | 'accel'; selectedOption: 'wait' | 'accel';
cantPayReason = ''; cantPayReason = '';
quoteError = ''; // error fetching estimate or initial data quoteError = ''; // error fetching estimate or initial data
@ -196,9 +197,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
if (changes.scrollEvent && this.scrollEvent) { if (changes.scrollEvent && this.scrollEvent) {
this.scrollToElement('acceleratePreviewAnchor', 'start'); this.scrollToElement('acceleratePreviewAnchor', 'start');
} }
if (changes.accelerating) { if (changes.accelerating && this.accelerating) {
if ((this.step === 'processing' || this.step === 'paid') && this.accelerating) { if (this.step === 'processing' || this.step === 'paid') {
this.moveToStep('success'); this.moveToStep('success');
} else { // Edge case where the transaction gets accelerated by someone else or on another session
this.closeModal();
} }
} }
} }
@ -378,9 +381,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
* Account-based acceleration request * Account-based acceleration request
*/ */
accelerateWithMempoolAccount(): void { accelerateWithMempoolAccount(): void {
if (!this.canPay || this.calculating) { if (!this.canPay || this.calculating || this.processing) {
return; return;
} }
this.processing = true;
if (this.accelerationSubscription) { if (this.accelerationSubscription) {
this.accelerationSubscription.unsubscribe(); this.accelerationSubscription.unsubscribe();
} }
@ -390,6 +394,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.accelerationUUID this.accelerationUUID
).subscribe({ ).subscribe({
next: () => { next: () => {
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon'); this.audioService.playSound('ascend-chime-cartoon');
this.showSuccess = true; this.showSuccess = true;
@ -397,6 +402,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.moveToStep('paid'); this.moveToStep('paid');
}, },
error: (response) => { error: (response) => {
this.processing = false;
this.accelerateError = response.error; this.accelerateError = response.error;
} }
}); });
@ -466,10 +472,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
* APPLE PAY * APPLE PAY
*/ */
async requestApplePayPayment(): Promise<void> { async requestApplePayPayment(): Promise<void> {
if (this.processing) {
return;
}
if (this.conversionsSubscription) { if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe(); this.conversionsSubscription.unsubscribe();
} }
this.processing = true;
this.conversionsSubscription = this.stateService.conversions$.subscribe( this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => { async (conversions) => {
this.conversions = conversions; this.conversions = conversions;
@ -494,6 +504,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
console.error(`Unable to find apple pay button id='apple-pay-button'`); console.error(`Unable to find apple pay button id='apple-pay-button'`);
// Try again // Try again
setTimeout(this.requestApplePayPayment.bind(this), 500); setTimeout(this.requestApplePayPayment.bind(this), 500);
this.processing = false;
return; return;
} }
this.loadingApplePay = false; this.loadingApplePay = false;
@ -505,6 +516,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) { if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
console.error(`Cannot retreive payment card details`); console.error(`Cannot retreive payment card details`);
this.accelerateError = 'apple_pay_no_card_details'; this.accelerateError = 'apple_pay_no_card_details';
this.processing = false;
return; return;
} }
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase()); const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
@ -516,6 +528,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.accelerationUUID this.accelerationUUID
).subscribe({ ).subscribe({
next: () => { next: () => {
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon'); this.audioService.playSound('ascend-chime-cartoon');
if (this.applePay) { if (this.applePay) {
@ -526,6 +539,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}, 1000); }, 1000);
}, },
error: (response) => { error: (response) => {
this.processing = false;
this.accelerateError = response.error; this.accelerateError = response.error;
if (!(response.status === 403 && response.error === 'not_available')) { if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => { setTimeout(() => {
@ -537,6 +551,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
} }
}); });
} else { } else {
this.processing = false;
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`; let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
if (tokenResult.errors) { if (tokenResult.errors) {
errorMessage += ` and errors: ${JSON.stringify( errorMessage += ` and errors: ${JSON.stringify(
@ -547,6 +562,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
} }
}); });
} catch (e) { } catch (e) {
this.processing = false;
console.error(e); console.error(e);
} }
} }
@ -557,10 +573,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
* GOOGLE PAY * GOOGLE PAY
*/ */
async requestGooglePayPayment(): Promise<void> { async requestGooglePayPayment(): Promise<void> {
if (this.processing) {
return;
}
if (this.conversionsSubscription) { if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe(); this.conversionsSubscription.unsubscribe();
} }
this.processing = true;
this.conversionsSubscription = this.stateService.conversions$.subscribe( this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => { async (conversions) => {
this.conversions = conversions; this.conversions = conversions;
@ -595,6 +615,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) { if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
console.error(`Cannot retreive payment card details`); console.error(`Cannot retreive payment card details`);
this.accelerateError = 'apple_pay_no_card_details'; this.accelerateError = 'apple_pay_no_card_details';
this.processing = false;
return; return;
} }
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase()); const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
@ -606,6 +627,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.accelerationUUID this.accelerationUUID
).subscribe({ ).subscribe({
next: () => { next: () => {
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon'); this.audioService.playSound('ascend-chime-cartoon');
if (this.googlePay) { if (this.googlePay) {
@ -616,6 +638,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}, 1000); }, 1000);
}, },
error: (response) => { error: (response) => {
this.processing = false;
this.accelerateError = response.error; this.accelerateError = response.error;
if (!(response.status === 403 && response.error === 'not_available')) { if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => { setTimeout(() => {
@ -627,6 +650,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
} }
}); });
} else { } else {
this.processing = false;
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`; let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
if (tokenResult.errors) { if (tokenResult.errors) {
errorMessage += ` and errors: ${JSON.stringify( errorMessage += ` and errors: ${JSON.stringify(
@ -644,10 +668,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
* CASHAPP * CASHAPP
*/ */
async requestCashAppPayment(): Promise<void> { async requestCashAppPayment(): Promise<void> {
if (this.processing) {
return;
}
if (this.conversionsSubscription) { if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe(); this.conversionsSubscription.unsubscribe();
} }
this.processing = true;
this.conversionsSubscription = this.stateService.conversions$.subscribe( this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => { async (conversions) => {
this.conversions = conversions; this.conversions = conversions;
@ -678,6 +706,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.cashAppPay.addEventListener('ontokenization', event => { this.cashAppPay.addEventListener('ontokenization', event => {
const { tokenResult, error } = event.detail; const { tokenResult, error } = event.detail;
if (error) { if (error) {
this.processing = false;
this.accelerateError = error; this.accelerateError = error;
} else if (tokenResult.status === 'OK') { } else if (tokenResult.status === 'OK') {
this.servicesApiService.accelerateWithCashApp$( this.servicesApiService.accelerateWithCashApp$(
@ -688,6 +717,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.accelerationUUID this.accelerationUUID
).subscribe({ ).subscribe({
next: () => { next: () => {
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon'); this.audioService.playSound('ascend-chime-cartoon');
if (this.cashAppPay) { if (this.cashAppPay) {
@ -702,6 +732,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}, 1000); }, 1000);
}, },
error: (response) => { error: (response) => {
this.processing = false;
this.accelerateError = response.error; this.accelerateError = response.error;
if (!(response.status === 403 && response.error === 'not_available')) { if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => { setTimeout(() => {

View File

@ -47,13 +47,14 @@
<tr *ngIf="['accelerated', 'mined'].includes(accelerationInfo.status) && hasPoolsData()"> <tr *ngIf="['accelerated', 'mined'].includes(accelerationInfo.status) && hasPoolsData()">
<td class="label" i18n="transaction.accelerated-by-hashrate|Accelerated to hashrate">Accelerated by</td> <td class="label" i18n="transaction.accelerated-by-hashrate|Accelerated to hashrate">Accelerated by</td>
<td class="value" *ngIf="accelerationInfo.pools"> <td class="value" *ngIf="accelerationInfo.pools">
<ng-container *ngFor="let pool of accelerationInfo.pools"> <ng-container *ngFor="let pool of accelerationInfo.pools; let i = index;">
<img *ngIf="accelerationInfo.poolsData[pool]" <img *ngIf="accelerationInfo.poolsData[pool]"
class="pool-logo" class="pool-logo"
[style.opacity]="accelerationInfo?.minedByPoolUniqueId && pool !== accelerationInfo?.minedByPoolUniqueId ? '0.3' : '1'" [style.opacity]="accelerationInfo?.minedByPoolUniqueId && pool !== accelerationInfo?.minedByPoolUniqueId ? '0.3' : '1'"
[src]="'/resources/mining-pools/' + accelerationInfo.poolsData[pool].slug + '.svg'" [src]="'/resources/mining-pools/' + accelerationInfo.poolsData[pool].slug + '.svg'"
onError="this.src = '/resources/mining-pools/default.svg'" onError="this.src = '/resources/mining-pools/default.svg'"
[alt]="'Logo of ' + pool.name + ' mining pool'"> [alt]="'Logo of ' + pool.name + ' mining pool'">
<br *ngIf="i % 6 === 5">
</ng-container> </ng-container>
</td> </td>
</tr> </tr>

View File

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

View File

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

View File

@ -67,13 +67,17 @@ export class ActiveAccelerationBox implements OnChanges {
const acceleratingPools = (poolList || []).filter(id => pools[id]).sort((a,b) => pools[a].lastEstimatedHashrate - pools[b].lastEstimatedHashrate); const acceleratingPools = (poolList || []).filter(id => pools[id]).sort((a,b) => pools[a].lastEstimatedHashrate - pools[b].lastEstimatedHashrate);
const totalAcceleratedHashrate = acceleratingPools.reduce((total, pool) => total + pools[pool].lastEstimatedHashrate, 0); const totalAcceleratedHashrate = acceleratingPools.reduce((total, pool) => total + pools[pool].lastEstimatedHashrate, 0);
const lightenStep = acceleratingPools.length ? (0.48 / acceleratingPools.length) : 0; // Find the first pool with at least 1% of the total network hashrate
const firstSignificantPool = acceleratingPools.findIndex(pool => pools[pool].lastEstimatedHashrate > this.miningStats.lastEstimatedHashrate / 100);
const numSignificantPools = acceleratingPools.length - firstSignificantPool;
acceleratingPools.forEach((poolId, index) => { acceleratingPools.forEach((poolId, index) => {
const pool = pools[poolId]; const pool = pools[poolId];
const poolShare = ((pool.lastEstimatedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1); const poolShare = ((pool.lastEstimatedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1);
data.push(getDataItem( data.push(getDataItem(
pool.lastEstimatedHashrate, pool.lastEstimatedHashrate,
toRGB(lighten({ r: 147, g: 57, b: 244 }, index * lightenStep)), index >= firstSignificantPool
? toRGB(lighten({ r: 147, g: 57, b: 244 }, 1 - (index - firstSignificantPool) / (numSignificantPools - 1)))
: 'white',
`<b style="color: white">${pool.name} (${poolShare}%)</b>`, `<b style="color: white">${pool.name} (${poolShare}%)</b>`,
true, true,
) as PieSeriesOption); ) as PieSeriesOption);

View File

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

View File

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

View File

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

View File

@ -55,7 +55,7 @@ export class AddressLabelsComponent implements OnChanges {
} }
handleVin() { handleVin() {
const address = new AddressTypeInfo(this.network || 'mainnet', this.vin.prevout?.scriptpubkey_address, this.vin.prevout?.scriptpubkey_type as AddressType, [this.vin]) const address = new AddressTypeInfo(this.network || 'mainnet', this.vin.prevout?.scriptpubkey_address, this.vin.prevout?.scriptpubkey_type as AddressType, [this.vin]);
if (address?.scripts.size) { if (address?.scripts.size) {
const script = address?.scripts.values().next().value; const script = address?.scripts.values().next().value;
if (script.template?.label) { if (script.template?.label) {

View File

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

View File

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

View File

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

View File

@ -0,0 +1,36 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { StorageService } from '../../services/storage.service';
import { StateService } from '../../services/state.service';
@Component({
selector: 'app-amount-selector',
templateUrl: './amount-selector.component.html',
styleUrls: ['./amount-selector.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AmountSelectorComponent implements OnInit {
amountForm: UntypedFormGroup;
modes = ['btc', 'sats', 'fiat'];
constructor(
private formBuilder: UntypedFormBuilder,
private stateService: StateService,
private storageService: StorageService,
) { }
ngOnInit() {
this.amountForm = this.formBuilder.group({
mode: ['btc']
});
this.stateService.viewAmountMode$.subscribe((mode) => {
this.amountForm.get('mode')?.setValue(mode);
});
}
changeMode() {
const newMode = this.amountForm.get('mode')?.value;
this.storageService.setValue('view-amount-mode', newMode);
this.stateService.viewAmountMode$.next(newMode);
}
}

View File

@ -198,7 +198,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
} }
// initialize the scene without any entry transition // initialize the scene without any entry transition
setup(transactions: TransactionStripped[]): void { setup(transactions: TransactionStripped[], sort: boolean = false): void {
const filtersAvailable = transactions.reduce((flagSet, tx) => flagSet || tx.flags > 0, false); const filtersAvailable = transactions.reduce((flagSet, tx) => flagSet || tx.flags > 0, false);
if (filtersAvailable !== this.filtersAvailable) { if (filtersAvailable !== this.filtersAvailable) {
this.setFilterFlags(); this.setFilterFlags();
@ -206,7 +206,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
this.filtersAvailable = filtersAvailable; this.filtersAvailable = filtersAvailable;
if (this.scene) { if (this.scene) {
this.clearUpdateQueue(); this.clearUpdateQueue();
this.scene.setup(transactions); this.scene.setup(transactions, sort);
this.readyNextFrame = true; this.readyNextFrame = true;
this.start(); this.start();
this.updateSearchHighlight(); this.updateSearchHighlight();

View File

@ -88,16 +88,19 @@ export default class BlockScene {
} }
// set up the scene with an initial set of transactions, without any transition animation // set up the scene with an initial set of transactions, without any transition animation
setup(txs: TransactionStripped[]) { setup(txs: TransactionStripped[], sort: boolean = false) {
// clean up any old transactions // clean up any old transactions
Object.values(this.txs).forEach(tx => { Object.values(this.txs).forEach(tx => {
tx.destroy(); tx.destroy();
delete this.txs[tx.txid]; delete this.txs[tx.txid];
}); });
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight }); this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
txs.forEach(tx => { let txViews = txs.map(tx => new TxView(tx, this));
const txView = new TxView(tx, this); if (sort) {
this.txs[tx.txid] = txView; txViews = txViews.sort(feeRateDescending);
}
txViews.forEach(txView => {
this.txs[txView.txid] = txView;
this.place(txView); this.place(txView);
this.saveGridToScreenPosition(txView); this.saveGridToScreenPosition(txView);
this.applyTxUpdate(txView, { this.applyTxUpdate(txView, {

View File

@ -33,7 +33,7 @@ export default class TxView implements TransactionStripped {
flags: number; flags: number;
bigintFlags?: bigint | null = 0b00000100_00000000_00000000_00000000n; bigintFlags?: bigint | null = 0b00000100_00000000_00000000_00000000n;
time?: number; time?: number;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated'; status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'added_deprioritized' | 'deprioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
context?: 'projected' | 'actual'; context?: 'projected' | 'actual';
scene?: BlockScene; scene?: BlockScene;

View File

@ -142,6 +142,10 @@ export function defaultColorFunction(
return auditColors.added_prioritized; return auditColors.added_prioritized;
case 'prioritized': case 'prioritized':
return auditColors.prioritized; return auditColors.prioritized;
case 'added_deprioritized':
return auditColors.added_prioritized;
case 'deprioritized':
return auditColors.prioritized;
case 'selected': case 'selected':
return colors.marginal[levelIndex] || colors.marginal[defaultMempoolFeeColors.length - 1]; return colors.marginal[levelIndex] || colors.marginal[defaultMempoolFeeColors.length - 1];
case 'accelerated': case 'accelerated':

View File

@ -79,6 +79,11 @@
<span class="badge badge-warning" i18n="tx-features.tag.added|Added">Added</span> <span class="badge badge-warning" i18n="tx-features.tag.added|Added">Added</span>
<span class="badge badge-warning ml-1" i18n="tx-features.tag.prioritized|Prioritized">Prioritized</span> <span class="badge badge-warning ml-1" i18n="tx-features.tag.prioritized|Prioritized">Prioritized</span>
</ng-container> </ng-container>
<span *ngSwitchCase="'deprioritized'" class="badge badge-warning" i18n="tx-features.tag.prioritized|Deprioritized">Deprioritized</span>
<ng-container *ngSwitchCase="'added_deprioritized'">
<span class="badge badge-warning" i18n="tx-features.tag.added|Added">Added</span>
<span class="badge badge-warning ml-1" i18n="tx-features.tag.prioritized|Deprioritized">Deprioritized</span>
</ng-container>
<span *ngSwitchCase="'selected'" class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span> <span *ngSwitchCase="'selected'" class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span>
<span *ngSwitchCase="'rbf'" class="badge badge-warning" i18n="tx-features.tag.conflict|Conflict">Conflict</span> <span *ngSwitchCase="'rbf'" class="badge badge-warning" i18n="tx-features.tag.conflict|Conflict">Conflict</span>
<span *ngSwitchCase="'accelerated'" class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span> <span *ngSwitchCase="'accelerated'" class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span>

View File

@ -137,7 +137,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
}) })
), ),
this.stateService.env.ACCELERATOR === true && block.height > 819500 this.stateService.env.ACCELERATOR === true && block.height > 819500
? this.servicesApiService.getAccelerationHistory$({ blockHeight: block.height }) ? this.servicesApiService.getAllAccelerationHistory$({ blockHeight: block.height })
.pipe(catchError(() => { .pipe(catchError(() => {
return of([]); return of([]);
})) }))

View File

@ -17,6 +17,7 @@ import { PriceService, Price } from '../../services/price.service';
import { CacheService } from '../../services/cache.service'; import { CacheService } from '../../services/cache.service';
import { ServicesApiServices } from '../../services/services-api.service'; import { ServicesApiServices } from '../../services/services-api.service';
import { PreloadService } from '../../services/preload.service'; import { PreloadService } from '../../services/preload.service';
import { identifyPrioritizedTransactions } from '../../shared/transaction.utils';
@Component({ @Component({
selector: 'app-block', selector: 'app-block',
@ -318,7 +319,7 @@ export class BlockComponent implements OnInit, OnDestroy {
this.accelerationsSubscription = this.block$.pipe( this.accelerationsSubscription = this.block$.pipe(
switchMap((block) => { switchMap((block) => {
return this.stateService.env.ACCELERATOR === true && block.height > 819500 return this.stateService.env.ACCELERATOR === true && block.height > 819500
? this.servicesApiService.getAccelerationHistory$({ blockHeight: block.height }) ? this.servicesApiService.getAllAccelerationHistory$({ blockHeight: block.height })
.pipe(catchError(() => { .pipe(catchError(() => {
return of([]); return of([]);
})) }))
@ -326,7 +327,7 @@ export class BlockComponent implements OnInit, OnDestroy {
}) })
).subscribe((accelerations) => { ).subscribe((accelerations) => {
this.accelerations = accelerations; this.accelerations = accelerations;
if (accelerations.length) { if (accelerations.length && this.strippedTransactions) { // Don't call setupBlockAudit if we don't have transactions yet; it will be called later in overviewSubscription
this.setupBlockAudit(); this.setupBlockAudit();
} }
}); });
@ -524,6 +525,7 @@ export class BlockComponent implements OnInit, OnDestroy {
const isUnseen = {}; const isUnseen = {};
const isAdded = {}; const isAdded = {};
const isPrioritized = {}; const isPrioritized = {};
const isDeprioritized = {};
const isCensored = {}; const isCensored = {};
const isMissing = {}; const isMissing = {};
const isSelected = {}; const isSelected = {};
@ -535,6 +537,17 @@ export class BlockComponent implements OnInit, OnDestroy {
this.numUnexpected = 0; this.numUnexpected = 0;
if (blockAudit?.template) { if (blockAudit?.template) {
// augment with locally calculated *de*prioritized transactions if possible
const { prioritized, deprioritized } = identifyPrioritizedTransactions(transactions);
// but if the local calculation produces returns unexpected results, don't use it
let useLocalDeprioritized = deprioritized.length < (transactions.length * 0.1);
for (const tx of prioritized) {
if (!isPrioritized[tx] && !isAccelerated[tx]) {
useLocalDeprioritized = false;
break;
}
}
for (const tx of blockAudit.template) { for (const tx of blockAudit.template) {
inTemplate[tx.txid] = true; inTemplate[tx.txid] = true;
if (tx.acc) { if (tx.acc) {
@ -550,9 +563,14 @@ export class BlockComponent implements OnInit, OnDestroy {
for (const txid of blockAudit.addedTxs) { for (const txid of blockAudit.addedTxs) {
isAdded[txid] = true; isAdded[txid] = true;
} }
for (const txid of blockAudit.prioritizedTxs || []) { for (const txid of blockAudit.prioritizedTxs) {
isPrioritized[txid] = true; isPrioritized[txid] = true;
} }
if (useLocalDeprioritized) {
for (const txid of deprioritized || []) {
isDeprioritized[txid] = true;
}
}
for (const txid of blockAudit.missingTxs) { for (const txid of blockAudit.missingTxs) {
isCensored[txid] = true; isCensored[txid] = true;
} }
@ -608,6 +626,12 @@ export class BlockComponent implements OnInit, OnDestroy {
} else { } else {
tx.status = 'prioritized'; tx.status = 'prioritized';
} }
} else if (isDeprioritized[tx.txid]) {
if (isAdded[tx.txid] || (blockAudit.version > 0 && isUnseen[tx.txid])) {
tx.status = 'added_deprioritized';
} else {
tx.status = 'deprioritized';
}
} else if (isAdded[tx.txid] && (blockAudit.version === 0 || isUnseen[tx.txid])) { } else if (isAdded[tx.txid] && (blockAudit.version === 0 || isUnseen[tx.txid])) {
tx.status = 'added'; tx.status = 'added';
} else if (inTemplate[tx.txid]) { } else if (inTemplate[tx.txid]) {

View File

@ -1,7 +1,10 @@
<app-indexing-progress *ngIf="!widget"></app-indexing-progress> <app-indexing-progress *ngIf="!widget"></app-indexing-progress>
<div class="container-xl" style="min-height: 335px" [ngClass]="{'widget': widget, 'full-height': !widget, 'legacy': !isMempoolModule}"> <div class="container-xl" style="min-height: 335px" [ngClass]="{'widget': widget, 'full-height': !widget, 'legacy': !isMempoolModule}">
<h1 *ngIf="!widget" class="float-left" i18n="master-page.blocks">Blocks</h1> <div *ngIf="!widget" class="float-left" style="display: flex; width: 100%; align-items: center;">
<h1 i18n="master-page.blocks">Blocks</h1>
<app-svg-images name="blocks-2-3" style="width: 275px; max-width: 90%; margin-top: -10px"></app-svg-images>
</div>
<div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div> <div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div>
<div class="clearfix"></div> <div class="clearfix"></div>

View File

@ -12,7 +12,7 @@
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text">{{ currency$ | async }}</span> <span class="input-group-text">{{ currency$ | async }}</span>
</div> </div>
<input type="text" class="form-control" formControlName="fiat" (input)="transformInput('fiat')" (click)="selectAll($event)"> <input type="text" inputmode="numeric" class="form-control" formControlName="fiat" (input)="transformInput('fiat')" (click)="selectAll($event)">
<app-clipboard [button]="true" [text]="form.get('fiat').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard> <app-clipboard [button]="true" [text]="form.get('fiat').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
</div> </div>
@ -20,7 +20,7 @@
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text">BTC</span> <span class="input-group-text">BTC</span>
</div> </div>
<input type="text" class="form-control" formControlName="bitcoin" (input)="transformInput('bitcoin')" (click)="selectAll($event)"> <input type="text" inputmode="numeric" class="form-control" formControlName="bitcoin" (input)="transformInput('bitcoin')" (click)="selectAll($event)">
<app-clipboard [button]="true" [text]="form.get('bitcoin').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard> <app-clipboard [button]="true" [text]="form.get('bitcoin').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
</div> </div>
@ -28,7 +28,7 @@
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text" i18n="shared.sats">sats</span> <span class="input-group-text" i18n="shared.sats">sats</span>
</div> </div>
<input type="text" class="form-control" formControlName="satoshis" (input)="transformInput('satoshis')" (click)="selectAll($event)"> <input type="text" inputmode="numeric" class="form-control" formControlName="satoshis" (input)="transformInput('satoshis')" (click)="selectAll($event)">
<app-clipboard [button]="true" [text]="form.get('satoshis').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard> <app-clipboard [button]="true" [text]="form.get('satoshis').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
</div> </div>
</form> </form>

View File

@ -77,7 +77,7 @@ export class DifficultyMiningComponent implements OnInit {
base: `${da.progressPercent.toFixed(2)}%`, base: `${da.progressPercent.toFixed(2)}%`,
change: da.difficultyChange, change: da.difficultyChange,
progress: da.progressPercent, progress: da.progressPercent,
remainingBlocks: da.remainingBlocks - 1, remainingBlocks: da.remainingBlocks,
colorAdjustments, colorAdjustments,
colorPreviousAdjustments, colorPreviousAdjustments,
newDifficultyHeight: da.nextRetargetHeight, newDifficultyHeight: da.nextRetargetHeight,

View File

@ -153,8 +153,8 @@ export class DifficultyComponent implements OnInit {
base: `${da.progressPercent.toFixed(2)}%`, base: `${da.progressPercent.toFixed(2)}%`,
change: da.difficultyChange, change: da.difficultyChange,
progress: da.progressPercent, progress: da.progressPercent,
minedBlocks: this.currentIndex + 1, minedBlocks: this.currentIndex,
remainingBlocks: da.remainingBlocks - 1, remainingBlocks: da.remainingBlocks,
expectedBlocks: Math.floor(da.expectedBlocks), expectedBlocks: Math.floor(da.expectedBlocks),
colorAdjustments, colorAdjustments,
colorPreviousAdjustments, colorPreviousAdjustments,

View File

@ -36,6 +36,13 @@
<app-twitter-login customClass="btn btn-sm" width="180px" redirectTo="/testnet4/faucet" buttonString="Link your Twitter"></app-twitter-login> <app-twitter-login customClass="btn btn-sm" width="180px" redirectTo="/testnet4/faucet" buttonString="Link your Twitter"></app-twitter-login>
</div> </div>
} }
@else if (error === 'account_limited') {
<div class="alert alert-mempool d-block text-center w-100">
<div class="d-inline align-middle">
<span class="mb-2 mr-2">Your Twitter account does not allow you to access the faucet</span>
</div>
</div>
}
@else if (error) { @else if (error) {
<!-- User can request --> <!-- User can request -->
<app-mempool-error class="w-100" [error]="error"></app-mempool-error> <app-mempool-error class="w-100" [error]="error"></app-mempool-error>

View File

@ -31,7 +31,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
lastBlockHeight: number; lastBlockHeight: number;
blockIndex: number; blockIndex: number;
isLoading$ = new BehaviorSubject<boolean>(true); isLoading$ = new BehaviorSubject<boolean>(false);
timeLtrSubscription: Subscription; timeLtrSubscription: Subscription;
timeLtr: boolean; timeLtr: boolean;
chainDirection: string = 'right'; chainDirection: string = 'right';
@ -95,6 +95,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
} }
} }
this.updateBlock({ this.updateBlock({
block: this.blockIndex,
removed, removed,
changed, changed,
added added
@ -110,8 +111,11 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
if (this.blockGraph) { if (this.blockGraph) {
this.blockGraph.clear(changes.index.currentValue > changes.index.previousValue ? this.chainDirection : this.poolDirection); this.blockGraph.clear(changes.index.currentValue > changes.index.previousValue ? this.chainDirection : this.poolDirection);
} }
if (!this.websocketService.startTrackMempoolBlock(changes.index.currentValue) && this.stateService.mempoolBlockState && this.stateService.mempoolBlockState.block === changes.index.currentValue) {
this.resumeBlock(Object.values(this.stateService.mempoolBlockState.transactions));
} else {
this.isLoading$.next(true); this.isLoading$.next(true);
this.websocketService.startTrackMempoolBlock(changes.index.currentValue); }
} }
} }
@ -153,6 +157,19 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
this.isLoading$.next(false); this.isLoading$.next(false);
} }
resumeBlock(transactionsStripped: TransactionStripped[]): void {
if (this.blockGraph) {
this.firstLoad = false;
this.blockGraph.setup(transactionsStripped, true);
this.blockIndex = this.index;
this.isLoading$.next(false);
} else {
requestAnimationFrame(() => {
this.resumeBlock(transactionsStripped);
});
}
}
onTxClick(event: { tx: TransactionStripped, keyModifier: boolean }): void { onTxClick(event: { tx: TransactionStripped, keyModifier: boolean }): void {
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.tx.txid}`); const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.tx.txid}`);
if (!event.keyModifier) { if (!event.keyModifier) {

View File

@ -71,7 +71,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
}) })
); );
this.mempoolBlockTransactions$ = this.stateService.liveMempoolBlockTransactions$.pipe(map(txMap => Object.values(txMap))); this.mempoolBlockTransactions$ = this.stateService.liveMempoolBlockTransactions$.pipe(map(({transactions}) => Object.values(transactions)));
this.network$ = this.stateService.networkChanged$; this.network$ = this.stateService.networkChanged$;
} }

View File

@ -51,7 +51,8 @@
</div> </div>
</ng-template> </ng-template>
</div> </div>
<div *ngIf="arrowVisible" id="arrow-up" [ngStyle]="{'right': rightPosition + (blockWidth * 0.3) + containerOffset + 'px', transition: transition }" [class.blink]="txPosition?.accelerated"></div> <app-acceleration-sparkles [style]="{ position: 'absolute', right: 0}" [arrow]="arrowElement" [run]="acceleratingArrow"></app-acceleration-sparkles>
<div *ngIf="arrowVisible" #arrowUp id="arrow-up" [ngStyle]="{'right': rightPosition + (blockWidth * 0.3) + containerOffset + 'px', transition: transition }" [class.blink]="txPosition?.accelerated"></div>
</div> </div>
</ng-container> </ng-container>

View File

@ -1,4 +1,4 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges, Output, EventEmitter } from '@angular/core'; import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core';
import { Subscription, Observable, of, combineLatest } from 'rxjs'; import { Subscription, Observable, of, combineLatest } from 'rxjs';
import { MempoolBlock } from '../../interfaces/websocket.interface'; import { MempoolBlock } from '../../interfaces/websocket.interface';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
@ -77,6 +77,9 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
maxArrowPosition = 0; maxArrowPosition = 0;
rightPosition = 0; rightPosition = 0;
transition = 'background 2s, right 2s, transform 1s'; transition = 'background 2s, right 2s, transform 1s';
@ViewChild('arrowUp')
arrowElement: ElementRef<HTMLDivElement>;
acceleratingArrow: boolean = false;
markIndex: number; markIndex: number;
txPosition: MempoolPosition; txPosition: MempoolPosition;
@ -201,6 +204,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.markBlocksSubscription = this.stateService.markBlock$ this.markBlocksSubscription = this.stateService.markBlock$
.subscribe((state) => { .subscribe((state) => {
const oldTxPosition = this.txPosition;
this.markIndex = undefined; this.markIndex = undefined;
this.txPosition = undefined; this.txPosition = undefined;
this.txFeePerVSize = undefined; this.txFeePerVSize = undefined;
@ -209,6 +213,12 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
} }
if (state.mempoolPosition) { if (state.mempoolPosition) {
this.txPosition = state.mempoolPosition; this.txPosition = state.mempoolPosition;
if (this.txPosition.accelerated && !oldTxPosition?.accelerated) {
this.acceleratingArrow = true;
setTimeout(() => {
this.acceleratingArrow = false;
}, 2000);
}
} }
if (state.txFeePerVSize) { if (state.txFeePerVSize) {
this.txFeePerVSize = state.txFeePerVSize; this.txFeePerVSize = state.txFeePerVSize;

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,8 @@
<div class="container-xl"> <div class="container-xl">
<div style="display: flex; width: 100%; align-items: center; flex-wrap: wrap;">
<h1 class="text-left" i18n="shared.test-transactions|Test Transactions">Test Transactions</h1> <h1 class="text-left" i18n="shared.test-transactions|Test Transactions">Test Transactions</h1>
<app-svg-images name="blocks-3-2" style="width: 275px; max-width: 90%; margin-top: -9px"></app-svg-images>
</div>
<form [formGroup]="testTxsForm" (submit)="testTxsForm.valid && testTxs()" novalidate> <form [formGroup]="testTxsForm" (submit)="testTxsForm.valid && testTxs()" novalidate>
<label for="maxfeerate" i18n="test.tx.raw-hex">Raw hex</label> <label for="maxfeerate" i18n="test.tx.raw-hex">Raw hex</label>

View File

@ -42,7 +42,7 @@
<div class="blockchain-wrapper" [style]="{ height: blockchainHeight * 1.16 + 'px' }"> <div class="blockchain-wrapper" [style]="{ height: blockchainHeight * 1.16 + 'px' }">
<app-clockchain [height]="blockchainHeight" [width]="blockchainWidth" mode="none"></app-clockchain> <app-clockchain [height]="blockchainHeight" [width]="blockchainWidth" mode="none"></app-clockchain>
</div> </div>
<div class="panel"> <div class="panel" *ngIf="!error || waitingForTransaction">
@if (replaced) { @if (replaced) {
<div class="alert-replaced" role="alert"> <div class="alert-replaced" role="alert">
<span i18n="transaction.rbf.replacement|RBF replacement">This transaction has been replaced by:</span> <span i18n="transaction.rbf.replacement|RBF replacement">This transaction has been replaced by:</span>
@ -65,6 +65,7 @@
} }
</div> </div>
</div> </div>
@if (!replaced) {
<div class="field narrower"> <div class="field narrower">
<div class="label" i18n="transaction.eta|Transaction ETA">ETA</div> <div class="label" i18n="transaction.eta|Transaction ETA">ETA</div>
<div class="value"> <div class="value">
@ -82,6 +83,7 @@
</ng-template> </ng-template>
</div> </div>
</div> </div>
}
} @else if (tx && tx.status?.confirmed) { } @else if (tx && tx.status?.confirmed) {
<div class="field narrower mt-2"> <div class="field narrower mt-2">
<div class="label" i18n="transaction.confirmed-at">Confirmed at</div> <div class="label" i18n="transaction.confirmed-at">Confirmed at</div>
@ -111,7 +113,7 @@
</div> </div>
</div> </div>
<div class="bottom-panel"> <div class="bottom-panel" *ngIf="!error || waitingForTransaction">
@if (isLoading) { @if (isLoading) {
<div class="progress-icon"> <div class="progress-icon">
<div class="spinner-border text-light" style="width: 1em; height: 1em"></div> <div class="spinner-border text-light" style="width: 1em; height: 1em"></div>
@ -185,6 +187,12 @@
} }
</div> </div>
<div class="bottom-panel" *ngIf="error && !waitingForTransaction">
<app-http-error [error]="error">
<span i18n="transaction.error.loading-transaction-data">Error loading transaction data.</span>
</app-http-error>
</div>
<div class="footer-link" <div class="footer-link"
[routerLink]="['/tx' | relativeUrl, tx?.txid || txId]" [routerLink]="['/tx' | relativeUrl, tx?.txid || txId]"
[queryParams]="{ mode: 'details' }" [queryParams]="{ mode: 'details' }"

View File

@ -286,14 +286,14 @@ export class TrackerComponent implements OnInit, OnDestroy {
this.accelerationInfo = null; this.accelerationInfo = null;
}), }),
switchMap((blockHash: string) => { switchMap((blockHash: string) => {
return this.servicesApiService.getAccelerationHistory$({ blockHash }); return this.servicesApiService.getAllAccelerationHistory$({ blockHash }, null, this.txId);
}), }),
catchError(() => { catchError(() => {
return of(null); return of(null);
}) })
).subscribe((accelerationHistory) => { ).subscribe((accelerationHistory) => {
for (const acceleration of accelerationHistory) { for (const acceleration of accelerationHistory) {
if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional')) { if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional') && acceleration.pools.includes(acceleration.minedByPoolUniqueId)) {
const boostCost = acceleration.boostCost || acceleration.bidBoost; const boostCost = acceleration.boostCost || acceleration.bidBoost;
acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize; acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
acceleration.boost = boostCost; acceleration.boost = boostCost;
@ -747,7 +747,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
checkAccelerationEligibility() { checkAccelerationEligibility() {
if (this.tx) { if (this.tx) {
this.tx.flags = getTransactionFlags(this.tx); this.tx.flags = getTransactionFlags(this.tx, null, null, this.tx.status?.block_time, this.stateService.network);
const replaceableInputs = (this.tx.flags & (TransactionFlags.sighash_none | TransactionFlags.sighash_acp)) > 0n; const replaceableInputs = (this.tx.flags & (TransactionFlags.sighash_none | TransactionFlags.sighash_acp)) > 0n;
const highSigop = (this.tx.sigops * 20) > this.tx.weight; const highSigop = (this.tx.sigops * 20) > this.tx.weight;
this.eligibleForAcceleration = !replaceableInputs && !highSigop; this.eligibleForAcceleration = !replaceableInputs && !highSigop;

View File

@ -8,7 +8,7 @@
<div *ngIf="officialMempoolSpace"> <div *ngIf="officialMempoolSpace">
<h2>Trademark Policy and Guidelines</h2> <h2>Trademark Policy and Guidelines</h2>
<h5>The Mempool Open Source Project &reg;</h5> <h5>The Mempool Open Source Project &reg;</h5>
<h6>Updated: July 3, 2024</h6> <h6>Updated: August 19, 2024</h6>
<br> <br>
<div class="text-left"> <div class="text-left">
@ -95,16 +95,31 @@
<p>The mempool Square Logo</p> <p>The mempool Square Logo</p>
<br><br> <br><br>
<app-svg-images name="accelerator" height="76px"></app-svg-images> <app-svg-images name="accelerator" style="width: 500px; max-width: 80%"></app-svg-images>
<br><br> <br><br>
<p>The Mempool Accelerator Logo</p> <p>The Mempool Accelerator Logo</p>
<br><br> <br><br>
<img src="/resources/mempool-research.png" style="width: 500px; max-width: 80%">
<br><br>
<p>The mempool research Logo</p>
<br><br>
<app-svg-images name="goggles" height="96px"></app-svg-images> <app-svg-images name="goggles" height="96px"></app-svg-images>
<br><br> <br><br>
<p>The Mempool Goggles Logo</p> <p>The Mempool Goggles Logo</p>
<br><br> <br><br>
<img src="/resources/mempool-transaction.png" style="width: 500px; max-width: 80%">
<br><br>
<p>The mempool transaction Logo</p>
<br><br>
<img src="/resources/mempool-block-visualization.png" style="width: 500px; max-width: 80%">
<br><br>
<p>The mempool block visualization Logo</p>
<br><br>
<img src="/resources/mempool-blocks-2-3-logo.jpeg" style="width: 500px; max-width: 80%"> <img src="/resources/mempool-blocks-2-3-logo.jpeg" style="width: 500px; max-width: 80%">
<br><br> <br><br>
<p>The mempool Blocks Logo</p> <p>The mempool Blocks Logo</p>

View File

@ -551,23 +551,23 @@
<td class="td-width align-items-center align-middle" i18n="transaction.eta|Transaction ETA">ETA</td> <td class="td-width align-items-center align-middle" i18n="transaction.eta|Transaction ETA">ETA</td>
<td> <td>
<ng-container *ngIf="(ETA$ | async) as eta; else etaSkeleton"> <ng-container *ngIf="(ETA$ | async) as eta; else etaSkeleton">
@if (eta.blocks >= 7) { @if (network === 'liquid' || network === 'liquidtestnet') {
<span [class]="(!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) ? 'etaDeepMempool justify-content-end align-items-center' : ''">
<span i18n="transaction.eta.not-any-time-soon|Transaction ETA mot any time soon">Not any time soon</span>
@if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) {
<a class="btn btn-sm accelerateDeepMempool btn-small-height float-right" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
}
</span>
} @else if (network === 'liquid' || network === 'liquidtestnet') {
<app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time> <app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time>
} @else { } @else {
<span [class]="(!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) ? 'etaDeepMempool justify-content-end align-items-center' : ''"> <span [class]="(!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && notAcceleratedOnLoad) ? 'etaDeepMempool d-flex justify-content-between' : ''">
@if (eta.blocks >= 7) {
<span i18n="transaction.eta.not-any-time-soon|Transaction ETA mot any time soon">Not any time soon</span>
} @else {
<app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time> <app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time>
@if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) {
<a class="btn btn-sm accelerateDeepMempool btn-small-height float-right" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
} }
</span> @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && notAcceleratedOnLoad) {
<span class="eta justify-content-end"> <div class="d-flex accelerate">
<a class="btn btn-sm accelerateDeepMempool btn-small-height" [class.disabled]="!eligibleForAcceleration" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
<a *ngIf="!eligibleForAcceleration" href="https://mempool.space/accelerator#why-cant-accelerate" target="_blank" class="info-badges ml-1" i18n-ngbTooltip="Mempool Accelerator&trade; tooltip" ngbTooltip="This transaction cannot be accelerated">
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon>
</a>
</div>
}
</span> </span>
} }
</ng-container> </ng-container>
@ -607,15 +607,10 @@
<tr> <tr>
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td> <td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
<td class="text-wrap">{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <td class="text-wrap">{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
@if (accelerationInfo?.bidBoost) { @if (accelerationInfo?.bidBoost ?? tx.feeDelta > 0) {
<span class="oobFees" i18n-ngbTooltip="Acceleration Fees" ngbTooltip="Acceleration fees paid out-of-band"> +{{ accelerationInfo.bidBoost | number }} </span><span class="symbol" i18n="shared.sat|sat">sat</span> <span class="oobFees" i18n-ngbTooltip="Acceleration Fees" ngbTooltip="Acceleration fees paid out-of-band"> +{{ accelerationInfo?.bidBoost ?? tx.feeDelta | number }} </span><span class="symbol" i18n="shared.sat|sat">sat</span>
<span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee + accelerationInfo.bidBoost"></app-fiat></span>
} @else if (tx.feeDelta && !accelerationInfo) {
<span class="oobFees" i18n-ngbTooltip="Acceleration Fees" ngbTooltip="Acceleration fees paid out-of-band"> +{{ tx.feeDelta | number }} </span><span class="symbol" i18n="shared.sat|sat">sat</span>
<span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee + tx.feeDelta"></app-fiat></span>
} @else {
<span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee"></app-fiat></span>
} }
<span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee + ((accelerationInfo?.bidBoost ?? tx.feeDelta) || 0)"></app-fiat></span>
</td> </td>
</tr> </tr>
} @else { } @else {

View File

@ -287,38 +287,22 @@
} }
.accelerate { .accelerate {
display: flex !important; @media (min-width: 850px) {
align-self: auto;
margin-left: auto; margin-left: auto;
background-color: var(--tertiary);
@media (max-width: 849px) {
margin-left: 5px;
} }
} }
.etaDeepMempool { .etaDeepMempool {
justify-content: flex-end;
flex-wrap: wrap; flex-wrap: wrap;
align-content: center;
@media (max-width: 995px) {
justify-content: left !important;
}
@media (max-width: 849px) { @media (max-width: 849px) {
justify-content: right !important; justify-content: right !important;
} }
} }
.accelerateDeepMempool { .accelerateDeepMempool {
align-self: auto;
margin-left: auto;
background-color: var(--tertiary); background-color: var(--tertiary);
@media (max-width: 995px) {
margin-left: 0px;
}
@media (max-width: 849px) {
margin-left: 5px; margin-left: 5px;
} }
}
.goggles-icon { .goggles-icon {
display: block; display: block;
@ -336,3 +320,8 @@
.oobFees { .oobFees {
color: #905cf4; color: #905cf4;
} }
.disabled {
opacity: 0.5;
pointer-events: none;
}

View File

@ -139,6 +139,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
firstLoad = true; firstLoad = true;
waitingForAccelerationInfo: boolean = false; waitingForAccelerationInfo: boolean = false;
isLoadingFirstSeen = false; isLoadingFirstSeen = false;
notAcceleratedOnLoad: boolean = null;
featuresEnabled: boolean; featuresEnabled: boolean;
segwitEnabled: boolean; segwitEnabled: boolean;
@ -191,7 +192,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.hideAccelerationSummary = this.stateService.isMempoolSpaceBuild ? this.storageService.getValue('hide-accelerator-pref') == 'true' : true; this.hideAccelerationSummary = this.stateService.isMempoolSpaceBuild ? this.storageService.getValue('hide-accelerator-pref') == 'true' : true;
if (!this.stateService.isLiquid()) { if (!this.stateService.isLiquid()) {
this.miningService.getMiningStats('1w').subscribe(stats => { this.miningService.getMiningStats('1m').subscribe(stats => {
this.miningStats = stats; this.miningStats = stats;
}); });
} }
@ -343,7 +344,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.setIsAccelerated(); this.setIsAccelerated();
}), }),
switchMap((blockHeight: number) => { switchMap((blockHeight: number) => {
return this.servicesApiService.getAccelerationHistory$({ blockHeight }).pipe( return this.servicesApiService.getAllAccelerationHistory$({ blockHeight }, null, this.txId).pipe(
switchMap((accelerationHistory: Acceleration[]) => { switchMap((accelerationHistory: Acceleration[]) => {
if (this.tx.acceleration && !accelerationHistory.length) { // If the just mined transaction was accelerated, but services backend did not return any acceleration data, retry if (this.tx.acceleration && !accelerationHistory.length) { // If the just mined transaction was accelerated, but services backend did not return any acceleration data, retry
return throwError('retry'); return throwError('retry');
@ -358,12 +359,18 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
}), }),
).subscribe((accelerationHistory) => { ).subscribe((accelerationHistory) => {
for (const acceleration of accelerationHistory) { for (const acceleration of accelerationHistory) {
if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional')) { if (acceleration.txid === this.txId) {
if (acceleration.status === 'completed' || acceleration.status === 'completed_provisional') {
if (acceleration.pools.includes(acceleration.minedByPoolUniqueId)) {
const boostCost = acceleration.boostCost || acceleration.bidBoost; const boostCost = acceleration.boostCost || acceleration.bidBoost;
acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize; acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
acceleration.boost = boostCost; acceleration.boost = boostCost;
this.tx.acceleratedAt = acceleration.added; this.tx.acceleratedAt = acceleration.added;
this.accelerationInfo = acceleration; this.accelerationInfo = acceleration;
} else {
this.tx.feeDelta = undefined;
}
}
this.waitingForAccelerationInfo = false; this.waitingForAccelerationInfo = false;
this.setIsAccelerated(); this.setIsAccelerated();
} }
@ -484,7 +491,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
if (this.stateService.network === '') { if (this.stateService.network === '') {
if (!this.mempoolPosition.accelerated) { if (!this.mempoolPosition.accelerated) {
if (!this.accelerationFlowCompleted && !this.hideAccelerationSummary && !this.showAccelerationSummary) { if (!this.accelerationFlowCompleted && !this.hideAccelerationSummary && !this.showAccelerationSummary) {
this.miningService.getMiningStats('1w').subscribe(stats => { this.miningService.getMiningStats('1m').subscribe(stats => {
this.miningStats = stats; this.miningStats = stats;
}); });
} }
@ -843,6 +850,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.setIsAccelerated(firstCpfp); this.setIsAccelerated(firstCpfp);
} }
if (this.notAcceleratedOnLoad === null) {
this.notAcceleratedOnLoad = !this.isAcceleration;
}
if (!this.isAcceleration && this.fragmentParams.has('accelerate')) { if (!this.isAcceleration && this.fragmentParams.has('accelerate')) {
this.forceAccelerationSummary = true; this.forceAccelerationSummary = true;
} }
@ -895,7 +906,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.segwitEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'segwit'); this.segwitEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'segwit');
this.taprootEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'taproot'); this.taprootEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'taproot');
this.rbfEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'rbf'); this.rbfEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'rbf');
this.tx.flags = getTransactionFlags(this.tx); this.tx.flags = getTransactionFlags(this.tx, null, null, this.tx.status?.block_time, this.stateService.network);
this.filters = this.tx.flags ? toFilters(this.tx.flags).filter(f => f.txPage) : []; this.filters = this.tx.flags ? toFilters(this.tx.flags).filter(f => f.txPage) : [];
this.checkAccelerationEligibility(); this.checkAccelerationEligibility();
} else { } else {
@ -960,6 +971,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.filters = []; this.filters = [];
this.showCpfpDetails = false; this.showCpfpDetails = false;
this.showAccelerationDetails = false; this.showAccelerationDetails = false;
this.accelerationFlowCompleted = false;
this.accelerationInfo = null; this.accelerationInfo = null;
this.cashappEligible = false; this.cashappEligible = false;
this.txInBlockIndex = null; this.txInBlockIndex = null;
@ -1077,6 +1089,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
(!this.hideAccelerationSummary && !this.accelerationFlowCompleted) (!this.hideAccelerationSummary && !this.accelerationFlowCompleted)
|| this.forceAccelerationSummary || this.forceAccelerationSummary
) )
&& this.notAcceleratedOnLoad // avoid briefly showing accelerator checkout on already accelerated txs
); );
} }

View File

@ -43,7 +43,7 @@
<span *ngSwitchCase="'output'" i18n="transaction.output">Output</span> <span *ngSwitchCase="'output'" i18n="transaction.output">Output</span>
<span *ngSwitchCase="'fee'" i18n="transaction.fee|Transaction fee">Fee</span> <span *ngSwitchCase="'fee'" i18n="transaction.fee|Transaction fee">Fee</span>
</ng-container> </ng-container>
<span *ngIf="line.type !== 'fee'"> #{{ line.index + 1 }}</span> <span *ngIf="line.type !== 'fee'"> #{{ line.index }}</span>
<ng-container [ngSwitch]="line.type"> <ng-container [ngSwitch]="line.type">
<span *ngSwitchCase="'input'"> <span *ngSwitchCase="'input'">
<ng-container *ngIf="line.status?.block_height"> <ng-container *ngIf="line.status?.block_height">
@ -73,7 +73,7 @@
<app-truncate [text]="line.txid"></app-truncate> <app-truncate [text]="line.txid"></app-truncate>
</p> </p>
<ng-container [ngSwitch]="line.type"> <ng-container [ngSwitch]="line.type">
<p *ngSwitchCase="'input'"><span i18n="transaction.output">Output</span>&nbsp; #{{ line.vout + 1 }} <p *ngSwitchCase="'input'"><span i18n="transaction.output">Output</span>&nbsp; #{{ line.vout }}
<ng-container *ngIf="line.status?.block_height"> <ng-container *ngIf="line.status?.block_height">
<ng-container *ngIf="line.blockHeight; else noBlockHeight"> <ng-container *ngIf="line.blockHeight; else noBlockHeight">
<ng-container *ngTemplateOutlet="nBlocksEarlier; context:{n: line.blockHeight - line?.status?.block_height, connector: true}"></ng-container> <ng-container *ngTemplateOutlet="nBlocksEarlier; context:{n: line.blockHeight - line?.status?.block_height, connector: true}"></ng-container>
@ -83,7 +83,7 @@
</ng-template> </ng-template>
</ng-container> </ng-container>
</p> </p>
<p *ngSwitchCase="'output'"><span i18n="transaction.input">Input</span>&nbsp; #{{ line.vin + 1 }} <p *ngSwitchCase="'output'"><span i18n="transaction.input">Input</span>&nbsp; #{{ line.vin }}
<ng-container *ngIf="line.blockHeight"> <ng-container *ngIf="line.blockHeight">
<ng-container *ngIf="line?.status?.block_height; else noBlockHeight"> <ng-container *ngIf="line?.status?.block_height; else noBlockHeight">
<ng-container *ngTemplateOutlet="nBlocksLater; context:{n: line?.status?.block_height - line.blockHeight, connector: true}"></ng-container> <ng-container *ngTemplateOutlet="nBlocksLater; context:{n: line?.status?.block_height - line.blockHeight, connector: true}"></ng-container>

View File

@ -0,0 +1,21 @@
<app-indexing-progress *ngIf="!widget"></app-indexing-progress>
<div [class.full-container]="!widget">
<ng-container *ngIf="!error">
<div [class]="!widget ? 'chart' : 'chart-widget'" *browserOnly [style]="{ height: widget ? ((height + 20) + 'px') : null, paddingBottom: !widget}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="spinner-border text-light"></div>
</div>
</ng-container>
<ng-container *ngIf="error">
<div class="error-wrapper">
<p class="error">{{ error }}</p>
</div>
</ng-container>
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
<div class="spinner-border text-light"></div>
</div>
</div>

View File

@ -0,0 +1,59 @@
.card-header {
border-bottom: 0;
font-size: 18px;
@media (min-width: 465px) {
font-size: 20px;
}
@media (min-width: 992px) {
height: 40px;
}
}
.main-title {
position: relative;
color: var(--fg);
opacity: var(--opacity);
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;
font-weight: 500;
text-align: center;
padding-bottom: 3px;
}
.full-container {
display: flex;
flex-direction: column;
padding: 0px;
width: 100%;
height: 400px;
}
.error-wrapper {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
font-size: 15px;
color: grey;
font-weight: bold;
}
.chart {
display: flex;
flex: 1;
width: 100%;
padding-right: 10px;
}
.chart-widget {
width: 100%;
height: 100%;
}
.disabled {
pointer-events: none;
opacity: 0.5;
}

View File

@ -0,0 +1,285 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, NgZone, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
import { EChartsOption } from '../../graphs/echarts';
import { BehaviorSubject, Subscription } from 'rxjs';
import { Utxo } from '../../interfaces/electrs.interface';
import { StateService } from '../../services/state.service';
import { Router } from '@angular/router';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { renderSats } from '../../shared/common.utils';
@Component({
selector: 'app-utxo-graph',
templateUrl: './utxo-graph.component.html',
styleUrls: ['./utxo-graph.component.scss'],
styles: [`
.loadingGraphs {
position: absolute;
top: 50%;
left: calc(50% - 15px);
z-index: 99;
}
`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UtxoGraphComponent implements OnChanges, OnDestroy {
@Input() utxos: Utxo[];
@Input() height: number = 200;
@Input() right: number | string = 10;
@Input() left: number | string = 70;
@Input() widget: boolean = false;
subscription: Subscription;
redraw$: BehaviorSubject<boolean> = new BehaviorSubject(false);
chartOptions: EChartsOption = {};
chartInitOptions = {
renderer: 'svg',
};
error: any;
isLoading = true;
chartInstance: any = undefined;
constructor(
public stateService: StateService,
private cd: ChangeDetectorRef,
private zone: NgZone,
private router: Router,
private relativeUrlPipe: RelativeUrlPipe,
) {}
ngOnChanges(changes: SimpleChanges): void {
this.isLoading = true;
if (!this.utxos) {
return;
}
if (changes.utxos) {
this.prepareChartOptions(this.utxos);
}
}
prepareChartOptions(utxos: Utxo[]) {
if (!utxos || utxos.length === 0) {
return;
}
this.isLoading = false;
// Helper functions
const distance = (x1: number, y1: number, x2: number, y2: number): number => Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
const intersectionPoints = (x1: number, y1: number, r1: number, x2: number, y2: number, r2: number): [number, number][] => {
const d = distance(x1, y1, x2, y2);
const a = (r1 * r1 - r2 * r2 + d * d) / (2 * d);
const h = Math.sqrt(r1 * r1 - a * a);
const x3 = x1 + a * (x2 - x1) / d;
const y3 = y1 + a * (y2 - y1) / d;
return [
[x3 + h * (y2 - y1) / d, y3 - h * (x2 - x1) / d],
[x3 - h * (y2 - y1) / d, y3 + h * (x2 - x1) / d]
];
};
// Naive algorithm to pack circles as tightly as possible without overlaps
const placedCircles: { x: number, y: number, r: number, utxo: Utxo, distances: number[] }[] = [];
// Pack in descending order of value, and limit to the top 500 to preserve performance
const sortedUtxos = utxos.sort((a, b) => b.value - a.value).slice(0, 500);
let centerOfMass = { x: 0, y: 0 };
let weightOfMass = 0;
sortedUtxos.forEach((utxo, index) => {
// area proportional to value
const r = Math.sqrt(utxo.value);
// special cases for the first two utxos
if (index === 0) {
placedCircles.push({ x: 0, y: 0, r, utxo, distances: [0] });
return;
}
if (index === 1) {
const c = placedCircles[0];
placedCircles.push({ x: c.r + r, y: 0, r, utxo, distances: [c.r + r, 0] });
c.distances.push(c.r + r);
return;
}
// The best position will be touching two other circles
// generate a list of candidate points by finding all such positions
// where the circle can be placed without overlapping other circles
const candidates: [number, number, number[]][] = [];
const numCircles = placedCircles.length;
for (let i = 0; i < numCircles; i++) {
for (let j = i + 1; j < numCircles; j++) {
const c1 = placedCircles[i];
const c2 = placedCircles[j];
if (c1.distances[j] > (c1.r + c2.r + r + r)) {
// too far apart for new circle to touch both
continue;
}
const points = intersectionPoints(c1.x, c1.y, c1.r + r, c2.x, c2.y, c2.r + r);
points.forEach(([x, y]) => {
const distances: number[] = [];
let valid = true;
for (let k = 0; k < numCircles; k++) {
const c = placedCircles[k];
const d = distance(x, y, c.x, c.y);
if (k !== i && k !== j && d < (r + c.r)) {
valid = false;
break;
} else {
distances.push(d);
}
}
if (valid) {
candidates.push([x, y, distances]);
}
});
}
}
// Pick the candidate closest to the center of mass
const [x, y, distances] = candidates.length ? candidates.reduce((closest, candidate) =>
distance(candidate[0], candidate[1], centerOfMass[0], centerOfMass[1]) <
distance(closest[0], closest[1], centerOfMass[0], centerOfMass[1])
? candidate
: closest
) : [0, 0, []];
placedCircles.push({ x, y, r, utxo, distances });
for (let i = 0; i < distances.length; i++) {
placedCircles[i].distances.push(distances[i]);
}
distances.push(0);
// Update center of mass
centerOfMass = {
x: (centerOfMass.x * weightOfMass + x) / (weightOfMass + r),
y: (centerOfMass.y * weightOfMass + y) / (weightOfMass + r),
};
weightOfMass += r;
});
// Precompute the bounding box of the graph
const minX = Math.min(...placedCircles.map(d => d.x - d.r));
const maxX = Math.max(...placedCircles.map(d => d.x + d.r));
const minY = Math.min(...placedCircles.map(d => d.y - d.r));
const maxY = Math.max(...placedCircles.map(d => d.y + d.r));
const width = maxX - minX;
const height = maxY - minY;
const data = placedCircles.map((circle, index) => [
circle.utxo,
index,
circle.x,
circle.y,
circle.r
]);
this.chartOptions = {
series: [{
type: 'custom',
coordinateSystem: undefined,
data,
renderItem: (params, api) => {
const idx = params.dataIndex;
const datum = data[idx];
const utxo = datum[0] as Utxo;
const chartWidth = api.getWidth();
const chartHeight = api.getHeight();
const scale = Math.min(chartWidth / width, chartHeight / height);
const scaledWidth = width * scale;
const scaledHeight = height * scale;
const offsetX = (chartWidth - scaledWidth) / 2 - minX * scale;
const offsetY = (chartHeight - scaledHeight) / 2 - minY * scale;
const x = datum[2] as number;
const y = datum[3] as number;
const r = datum[4] as number;
if (r * scale < 3) {
// skip items too small to render cleanly
return;
}
const valueStr = renderSats(utxo.value, this.stateService.network);
const elements: any[] = [
{
type: 'circle',
autoBatch: true,
shape: {
cx: (x * scale) + offsetX,
cy: (y * scale) + offsetY,
r: (r * scale) - 1,
},
style: {
fill: '#5470c6',
}
},
];
const labelFontSize = Math.min(36, r * scale * 0.25);
if (labelFontSize > 8) {
elements.push({
type: 'text',
x: (x * scale) + offsetX,
y: (y * scale) + offsetY,
style: {
text: valueStr,
fontSize: labelFontSize,
fill: '#fff',
align: 'center',
verticalAlign: 'middle',
},
});
}
return {
type: 'group',
children: elements,
};
}
}],
tooltip: {
backgroundColor: 'rgba(17, 19, 31, 1)',
borderRadius: 4,
shadowColor: 'rgba(0, 0, 0, 0.5)',
textStyle: {
color: 'var(--tooltip-grey)',
align: 'left',
},
borderColor: '#000',
formatter: (params: any): string => {
const utxo = params.data[0] as Utxo;
const valueStr = renderSats(utxo.value, this.stateService.network);
return `
<b style="color: white;">${utxo.txid.slice(0, 6)}...${utxo.txid.slice(-6)}:${utxo.vout}</b>
<br>
${valueStr}`;
},
}
};
this.cd.markForCheck();
}
onChartClick(e): void {
if (e.data?.[0]?.txid) {
this.zone.run(() => {
const url = this.relativeUrlPipe.transform(`/tx/${e.data[0].txid}`);
if (e.event.event.shiftKey || e.event.event.ctrlKey || e.event.event.metaKey) {
window.open(url + '?mode=details#vout=' + e.data[0].vout);
} else {
this.router.navigate([url], { fragment: `vout=${e.data[0].vout}` });
}
});
}
}
onChartInit(ec): void {
this.chartInstance = ec;
this.chartInstance.on('click', 'series', this.onChartClick.bind(this));
}
ngOnDestroy(): void {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
isMobile(): boolean {
return (window.innerWidth <= 767.98);
}
}

View File

@ -1,6 +1,6 @@
// Import tree-shakeable echarts // Import tree-shakeable echarts
import * as echarts from 'echarts/core'; import * as echarts from 'echarts/core';
import { LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart } from 'echarts/charts'; import { LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart, CustomChart } from 'echarts/charts';
import { TitleComponent, TooltipComponent, GridComponent, LegendComponent, GeoComponent, DataZoomComponent, VisualMapComponent, MarkLineComponent } from 'echarts/components'; import { TitleComponent, TooltipComponent, GridComponent, LegendComponent, GeoComponent, DataZoomComponent, VisualMapComponent, MarkLineComponent } from 'echarts/components';
import { SVGRenderer, CanvasRenderer } from 'echarts/renderers'; import { SVGRenderer, CanvasRenderer } from 'echarts/renderers';
// Typescript interfaces // Typescript interfaces
@ -12,6 +12,7 @@ echarts.use([
TitleComponent, TooltipComponent, GridComponent, TitleComponent, TooltipComponent, GridComponent,
LegendComponent, GeoComponent, DataZoomComponent, LegendComponent, GeoComponent, DataZoomComponent,
VisualMapComponent, MarkLineComponent, VisualMapComponent, MarkLineComponent,
LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart,
CustomChart,
]); ]);
export { echarts, EChartsOption, TreemapSeriesOption, LineSeriesOption, PieSeriesOption }; export { echarts, EChartsOption, TreemapSeriesOption, LineSeriesOption, PieSeriesOption };

View File

@ -36,6 +36,7 @@ import { HashrateChartPoolsComponent } from '../components/hashrates-chart-pools
import { BlockHealthGraphComponent } from '../components/block-health-graph/block-health-graph.component'; import { BlockHealthGraphComponent } from '../components/block-health-graph/block-health-graph.component';
import { AddressComponent } from '../components/address/address.component'; import { AddressComponent } from '../components/address/address.component';
import { AddressGraphComponent } from '../components/address-graph/address-graph.component'; import { AddressGraphComponent } from '../components/address-graph/address-graph.component';
import { UtxoGraphComponent } from '../components/utxo-graph/utxo-graph.component';
import { ActiveAccelerationBox } from '../components/acceleration/active-acceleration-box/active-acceleration-box.component'; import { ActiveAccelerationBox } from '../components/acceleration/active-acceleration-box/active-acceleration-box.component';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@ -76,6 +77,7 @@ import { CommonModule } from '@angular/common';
HashrateChartPoolsComponent, HashrateChartPoolsComponent,
BlockHealthGraphComponent, BlockHealthGraphComponent,
AddressGraphComponent, AddressGraphComponent,
UtxoGraphComponent,
ActiveAccelerationBox, ActiveAccelerationBox,
], ],
imports: [ imports: [

View File

@ -233,3 +233,10 @@ interface AssetStats {
peg_out_amount: number; peg_out_amount: number;
burn_count: number; burn_count: number;
} }
export interface Utxo {
txid: string;
vout: number;
value: number;
status: Status;
}

View File

@ -239,7 +239,7 @@ export interface TransactionStripped {
acc?: boolean; acc?: boolean;
flags?: number | null; flags?: number | null;
time?: number; time?: number;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated'; status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'added_deprioritized' | 'deprioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
context?: 'projected' | 'actual'; context?: 'projected' | 'actual';
} }

View File

@ -72,11 +72,13 @@ export interface MempoolBlockWithTransactions extends MempoolBlock {
} }
export interface MempoolBlockDelta { export interface MempoolBlockDelta {
block: number;
added: TransactionStripped[]; added: TransactionStripped[];
removed: string[]; removed: string[];
changed: { txid: string, rate: number, flags: number, acc: boolean }[]; changed: { txid: string, rate: number, flags: number, acc: boolean }[];
} }
export interface MempoolBlockState { export interface MempoolBlockState {
block: number;
transactions: TransactionStripped[]; transactions: TransactionStripped[];
} }
export type MempoolBlockUpdate = MempoolBlockDelta | MempoolBlockState; export type MempoolBlockUpdate = MempoolBlockDelta | MempoolBlockState;

View File

@ -13,7 +13,8 @@ class GuardService {
trackerGuard(route: Route, segments: UrlSegment[]): boolean { trackerGuard(route: Route, segments: UrlSegment[]): boolean {
const preferredRoute = this.router.getCurrentNavigation()?.extractedUrl.queryParams?.mode; const preferredRoute = this.router.getCurrentNavigation()?.extractedUrl.queryParams?.mode;
return (preferredRoute === 'status' || (preferredRoute !== 'details' && this.navigationService.isInitialLoad())) && window.innerWidth <= 767.98; const path = this.router.getCurrentNavigation()?.extractedUrl.root.children.primary.segments;
return (preferredRoute === 'status' || (preferredRoute !== 'details' && this.navigationService.isInitialLoad())) && window.innerWidth <= 767.98 && !(path.length === 2 && ['push', 'test'].includes(path[1].path));
} }
} }

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http'; import { HttpClient, HttpParams } from '@angular/common/http';
import { BehaviorSubject, Observable, catchError, filter, from, of, shareReplay, switchMap, take, tap } from 'rxjs'; import { BehaviorSubject, Observable, catchError, filter, from, of, shareReplay, switchMap, take, tap } from 'rxjs';
import { Transaction, Address, Outspend, Recent, Asset, ScriptHash, AddressTxSummary } from '../interfaces/electrs.interface'; import { Transaction, Address, Outspend, Recent, Asset, ScriptHash, AddressTxSummary, Utxo } from '../interfaces/electrs.interface';
import { StateService } from './state.service'; import { StateService } from './state.service';
import { BlockExtended } from '../interfaces/node-api.interface'; import { BlockExtended } from '../interfaces/node-api.interface';
import { calcScriptHash$ } from '../bitcoin.utils'; import { calcScriptHash$ } from '../bitcoin.utils';
@ -166,6 +166,16 @@ export class ElectrsApiService {
); );
} }
getAddressUtxos$(address: string): Observable<Utxo[]> {
return this.httpClient.get<Utxo[]>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/utxo');
}
getScriptHashUtxos$(script: string): Observable<Utxo[]> {
return from(calcScriptHash$(script)).pipe(
switchMap(scriptHash => this.httpClient.get<Utxo[]>(this.apiBaseUrl + this.apiBasePath + '/api/scripthash/' + scriptHash + '/utxo')),
);
}
getAsset$(assetId: string): Observable<Asset> { getAsset$(assetId: string): Observable<Asset> {
return this.httpClient.get<Asset>(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId); return this.httpClient.get<Asset>(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId);
} }

View File

@ -28,7 +28,7 @@ export class EtaService {
return combineLatest([ return combineLatest([
this.stateService.mempoolTxPosition$.pipe(map(p => p?.position)), this.stateService.mempoolTxPosition$.pipe(map(p => p?.position)),
this.stateService.difficultyAdjustment$, this.stateService.difficultyAdjustment$,
miningStats ? of(miningStats) : this.miningService.getMiningStats('1w'), miningStats ? of(miningStats) : this.miningService.getMiningStats('1m'),
]).pipe( ]).pipe(
map(([mempoolPosition, da, miningStats]) => { map(([mempoolPosition, da, miningStats]) => {
if (!mempoolPosition || !estimate?.pools?.length || !miningStats || !da) { if (!mempoolPosition || !estimate?.pools?.length || !miningStats || !da) {
@ -166,7 +166,7 @@ export class EtaService {
pools[pool.poolUniqueId] = pool; pools[pool.poolUniqueId] = pool;
} }
const unacceleratedPosition = this.mempoolPositionFromFees(getUnacceleratedFeeRate(tx, true), mempoolBlocks); const unacceleratedPosition = this.mempoolPositionFromFees(getUnacceleratedFeeRate(tx, true), mempoolBlocks);
const totalAcceleratedHashrate = accelerationPositions.reduce((total, pos) => total + (pools[pos.poolId].lastEstimatedHashrate), 0); const totalAcceleratedHashrate = accelerationPositions.reduce((total, pos) => total + (pools[pos.poolId]?.lastEstimatedHashrate || 0), 0);
const shares = [ const shares = [
{ {
block: unacceleratedPosition.block, block: unacceleratedPosition.block,
@ -174,7 +174,7 @@ export class EtaService {
}, },
...accelerationPositions.map(pos => ({ ...accelerationPositions.map(pos => ({
block: pos.block, block: pos.block,
hashrateShare: ((pools[pos.poolId].lastEstimatedHashrate) / miningStats.lastEstimatedHashrate) hashrateShare: ((pools[pos.poolId]?.lastEstimatedHashrate || 0) / miningStats.lastEstimatedHashrate)
})) }))
]; ];
return this.calculateETAFromShares(shares, da); return this.calculateETAFromShares(shares, da);
@ -204,7 +204,7 @@ export class EtaService {
let tailProb = 0; let tailProb = 0;
let Q = 0; let Q = 0;
for (let i = 0; i < max; i++) { for (let i = 0; i <= max; i++) {
// find H_i // find H_i
const H = shares.reduce((total, share) => total + (share.block <= i ? share.hashrateShare : 0), 0); const H = shares.reduce((total, share) => total + (share.block <= i ? share.hashrateShare : 0), 0);
// find S_i // find S_i
@ -215,7 +215,7 @@ export class EtaService {
tailProb += S; tailProb += S;
} }
// at max depth, the transaction is guaranteed to be mined in the next block if it hasn't already // at max depth, the transaction is guaranteed to be mined in the next block if it hasn't already
Q += (1-tailProb); Q += ((max + 1) * (1-tailProb));
const eta = da.timeAvg * Q; // T x Q const eta = da.timeAvg * Q; // T x Q
return { return {

View File

@ -4,7 +4,7 @@ import { HttpClient } from '@angular/common/http';
import { StateService } from './state.service'; import { StateService } from './state.service';
import { StorageService } from './storage.service'; import { StorageService } from './storage.service';
import { MenuGroup } from '../interfaces/services.interface'; import { MenuGroup } from '../interfaces/services.interface';
import { Observable, of, ReplaySubject, tap, catchError, share, filter, switchMap } from 'rxjs'; import { Observable, of, ReplaySubject, tap, catchError, share, filter, switchMap, map } from 'rxjs';
import { IBackendInfo } from '../interfaces/websocket.interface'; import { IBackendInfo } from '../interfaces/websocket.interface';
import { Acceleration, AccelerationHistoryParams } from '../interfaces/node-api.interface'; import { Acceleration, AccelerationHistoryParams } from '../interfaces/node-api.interface';
import { AccelerationStats } from '../components/acceleration/acceleration-stats/acceleration-stats.component'; import { AccelerationStats } from '../components/acceleration/acceleration-stats/acceleration-stats.component';
@ -160,6 +160,29 @@ export class ServicesApiServices {
return this.httpClient.get<Acceleration[]>(`${this.stateService.env.SERVICES_API}/accelerator/accelerations/history`, { params: { ...params } }); return this.httpClient.get<Acceleration[]>(`${this.stateService.env.SERVICES_API}/accelerator/accelerations/history`, { params: { ...params } });
} }
getAllAccelerationHistory$(params: AccelerationHistoryParams, limit?: number, findTxid?: string): Observable<Acceleration[]> {
const getPage$ = (page: number, accelerations: Acceleration[] = []): Observable<{ page: number, total: number, accelerations: Acceleration[] }> => {
return this.getAccelerationHistoryObserveResponse$({...params, page}).pipe(
map((response) => ({
page,
total: parseInt(response.headers.get('X-Total-Count'), 10),
accelerations: accelerations.concat(response.body || []),
})),
switchMap(({page, total, accelerations}) => {
if (accelerations.length >= Math.min(total, limit ?? Infinity) || (findTxid && accelerations.find((acc) => acc.txid === findTxid))) {
return of({ page, total, accelerations });
} else {
return getPage$(page + 1, accelerations);
}
}),
);
};
return getPage$(1).pipe(
map(({ accelerations }) => accelerations),
);
}
getAccelerationHistoryObserveResponse$(params: AccelerationHistoryParams): Observable<any> { getAccelerationHistoryObserveResponse$(params: AccelerationHistoryParams): Observable<any> {
return this.httpClient.get<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerations/history`, { params: { ...params }, observe: 'response'}); return this.httpClient.get<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerations/history`, { params: { ...params }, observe: 'response'});
} }

View File

@ -5,7 +5,7 @@ import { AccelerationDelta, HealthCheckHost, IBackendInfo, MempoolBlock, Mempool
import { Acceleration, AccelerationPosition, BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '../interfaces/node-api.interface'; import { Acceleration, AccelerationPosition, BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '../interfaces/node-api.interface';
import { Router, NavigationStart } from '@angular/router'; import { Router, NavigationStart } from '@angular/router';
import { isPlatformBrowser } from '@angular/common'; import { isPlatformBrowser } from '@angular/common';
import { filter, map, scan, shareReplay } from 'rxjs/operators'; import { filter, map, scan, share, shareReplay } from 'rxjs/operators';
import { StorageService } from './storage.service'; import { StorageService } from './storage.service';
import { hasTouchScreen } from '../shared/pipes/bytes-pipe/utils'; import { hasTouchScreen } from '../shared/pipes/bytes-pipe/utils';
import { ActiveFilter } from '../shared/filters.utils'; import { ActiveFilter } from '../shared/filters.utils';
@ -131,6 +131,7 @@ export class StateService {
latestBlockHeight = -1; latestBlockHeight = -1;
blocks: BlockExtended[] = []; blocks: BlockExtended[] = [];
mempoolSequence: number; mempoolSequence: number;
mempoolBlockState: { block: number, transactions: { [txid: string]: TransactionStripped} };
backend$ = new BehaviorSubject<'esplora' | 'electrum' | 'none'>('esplora'); backend$ = new BehaviorSubject<'esplora' | 'electrum' | 'none'>('esplora');
networkChanged$ = new ReplaySubject<string>(1); networkChanged$ = new ReplaySubject<string>(1);
@ -143,7 +144,7 @@ export class StateService {
mempoolInfo$ = new ReplaySubject<MempoolInfo>(1); mempoolInfo$ = new ReplaySubject<MempoolInfo>(1);
mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1); mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1);
mempoolBlockUpdate$ = new Subject<MempoolBlockUpdate>(); mempoolBlockUpdate$ = new Subject<MempoolBlockUpdate>();
liveMempoolBlockTransactions$: Observable<{ [txid: string]: TransactionStripped}>; liveMempoolBlockTransactions$: Observable<{ block: number, transactions: { [txid: string]: TransactionStripped} }>;
accelerations$ = new Subject<AccelerationDelta>(); accelerations$ = new Subject<AccelerationDelta>();
liveAccelerations$: Observable<Acceleration[]>; liveAccelerations$: Observable<Acceleration[]>;
txConfirmed$ = new Subject<[string, BlockExtended]>(); txConfirmed$ = new Subject<[string, BlockExtended]>();
@ -231,29 +232,40 @@ export class StateService {
} }
}); });
this.liveMempoolBlockTransactions$ = this.mempoolBlockUpdate$.pipe(scan((transactions: { [txid: string]: TransactionStripped }, change: MempoolBlockUpdate): { [txid: string]: TransactionStripped } => { this.liveMempoolBlockTransactions$ = this.mempoolBlockUpdate$.pipe(scan((acc: { block: number, transactions: { [txid: string]: TransactionStripped } }, change: MempoolBlockUpdate): { block: number, transactions: { [txid: string]: TransactionStripped } } => {
if (isMempoolState(change)) { if (isMempoolState(change)) {
const txMap = {}; const txMap = {};
change.transactions.forEach(tx => { change.transactions.forEach(tx => {
txMap[tx.txid] = tx; txMap[tx.txid] = tx;
}); });
return txMap; this.mempoolBlockState = {
block: change.block,
transactions: txMap
};
return this.mempoolBlockState;
} else { } else {
change.added.forEach(tx => { change.added.forEach(tx => {
transactions[tx.txid] = tx; acc.transactions[tx.txid] = tx;
}); });
change.removed.forEach(txid => { change.removed.forEach(txid => {
delete transactions[txid]; delete acc.transactions[txid];
}); });
change.changed.forEach(tx => { change.changed.forEach(tx => {
if (transactions[tx.txid]) { if (acc.transactions[tx.txid]) {
transactions[tx.txid].rate = tx.rate; acc.transactions[tx.txid].rate = tx.rate;
transactions[tx.txid].acc = tx.acc; acc.transactions[tx.txid].acc = tx.acc;
} }
}); });
return transactions; this.mempoolBlockState = {
block: change.block,
transactions: acc.transactions
};
return this.mempoolBlockState;
} }
}, {})); }, {}),
share()
);
this.liveMempoolBlockTransactions$.subscribe();
// Emits the full list of pending accelerations each time it changes // Emits the full list of pending accelerations each time it changes
this.liveAccelerations$ = this.accelerations$.pipe( this.liveAccelerations$ = this.accelerations$.pipe(

View File

@ -35,6 +35,7 @@ export class WebsocketService {
private isTrackingAddresses: string[] | false = false; private isTrackingAddresses: string[] | false = false;
private isTrackingAccelerations: boolean = false; private isTrackingAccelerations: boolean = false;
private trackingMempoolBlock: number; private trackingMempoolBlock: number;
private stoppingTrackMempoolBlock: any | null = null;
private latestGitCommit = ''; private latestGitCommit = '';
private onlineCheckTimeout: number; private onlineCheckTimeout: number;
private onlineCheckTimeoutTwo: number; private onlineCheckTimeoutTwo: number;
@ -203,19 +204,31 @@ export class WebsocketService {
this.websocketSubject.next({ 'track-asset': 'stop' }); this.websocketSubject.next({ 'track-asset': 'stop' });
} }
startTrackMempoolBlock(block: number, force: boolean = false) { startTrackMempoolBlock(block: number, force: boolean = false): boolean {
if (this.stoppingTrackMempoolBlock) {
clearTimeout(this.stoppingTrackMempoolBlock);
}
// skip duplicate tracking requests // skip duplicate tracking requests
if (force || this.trackingMempoolBlock !== block) { if (force || this.trackingMempoolBlock !== block) {
this.websocketSubject.next({ 'track-mempool-block': block }); this.websocketSubject.next({ 'track-mempool-block': block });
this.isTrackingMempoolBlock = true; this.isTrackingMempoolBlock = true;
this.trackingMempoolBlock = block; this.trackingMempoolBlock = block;
return true;
} }
return false;
} }
stopTrackMempoolBlock() { stopTrackMempoolBlock(): void {
this.websocketSubject.next({ 'track-mempool-block': -1 }); if (this.stoppingTrackMempoolBlock) {
clearTimeout(this.stoppingTrackMempoolBlock);
}
this.isTrackingMempoolBlock = false; this.isTrackingMempoolBlock = false;
this.stoppingTrackMempoolBlock = setTimeout(() => {
this.stoppingTrackMempoolBlock = null;
this.websocketSubject.next({ 'track-mempool-block': -1 });
this.trackingMempoolBlock = null; this.trackingMempoolBlock = null;
this.stateService.mempoolBlockState = null;
}, 2000);
} }
startTrackRbf(mode: 'all' | 'fullRbf') { startTrackRbf(mode: 'all' | 'fullRbf') {
@ -424,6 +437,7 @@ export class WebsocketService {
if (response['projected-block-transactions'].blockTransactions) { if (response['projected-block-transactions'].blockTransactions) {
this.stateService.mempoolSequence = response['projected-block-transactions'].sequence; this.stateService.mempoolSequence = response['projected-block-transactions'].sequence;
this.stateService.mempoolBlockUpdate$.next({ this.stateService.mempoolBlockUpdate$.next({
block: this.trackingMempoolBlock,
transactions: response['projected-block-transactions'].blockTransactions.map(uncompressTx), transactions: response['projected-block-transactions'].blockTransactions.map(uncompressTx),
}); });
} else if (response['projected-block-transactions'].delta) { } else if (response['projected-block-transactions'].delta) {
@ -432,7 +446,7 @@ export class WebsocketService {
this.startTrackMempoolBlock(this.trackingMempoolBlock, true); this.startTrackMempoolBlock(this.trackingMempoolBlock, true);
} else { } else {
this.stateService.mempoolSequence = response['projected-block-transactions'].sequence; this.stateService.mempoolSequence = response['projected-block-transactions'].sequence;
this.stateService.mempoolBlockUpdate$.next(uncompressDeltaChange(response['projected-block-transactions'].delta)); this.stateService.mempoolBlockUpdate$.next(uncompressDeltaChange(this.trackingMempoolBlock, response['projected-block-transactions'].delta));
} }
} }
} }

View File

@ -17,6 +17,7 @@ export type AddressType = 'fee'
| 'v0_p2wsh' | 'v0_p2wsh'
| 'v1_p2tr' | 'v1_p2tr'
| 'confidential' | 'confidential'
| 'anchor'
| 'unknown' | 'unknown'
const ADDRESS_PREFIXES = { const ADDRESS_PREFIXES = {
@ -188,6 +189,12 @@ export class AddressTypeInfo {
const v = vin[0]; const v = vin[0];
this.processScript(new ScriptInfo('scriptpubkey', v.prevout.scriptpubkey, v.prevout.scriptpubkey_asm)); this.processScript(new ScriptInfo('scriptpubkey', v.prevout.scriptpubkey, v.prevout.scriptpubkey_asm));
} }
} else if (this.type === 'unknown') {
for (const v of vin) {
if (v.prevout?.scriptpubkey === '51024e73') {
this.type = 'anchor';
}
}
} }
// and there's nothing more to learn from processing inputs for other types // and there's nothing more to learn from processing inputs for other types
} }
@ -197,6 +204,10 @@ export class AddressTypeInfo {
if (!this.scripts.size) { if (!this.scripts.size) {
this.processScript(new ScriptInfo('scriptpubkey', output.scriptpubkey, output.scriptpubkey_asm)); this.processScript(new ScriptInfo('scriptpubkey', output.scriptpubkey, output.scriptpubkey_asm));
} }
} else if (this.type === 'unknown') {
if (output.scriptpubkey === '51024e73') {
this.type = 'anchor';
}
} }
} }

View File

@ -1,5 +1,7 @@
import { MempoolBlockDelta, MempoolBlockDeltaCompressed, MempoolDeltaChange, TransactionCompressed } from "../interfaces/websocket.interface"; import { MempoolBlockDelta, MempoolBlockDeltaCompressed, MempoolDeltaChange, TransactionCompressed } from "../interfaces/websocket.interface";
import { TransactionStripped } from "../interfaces/node-api.interface"; import { TransactionStripped } from "../interfaces/node-api.interface";
import { AmountShortenerPipe } from "./pipes/amount-shortener.pipe";
const amountShortenerPipe = new AmountShortenerPipe();
export function isMobile(): boolean { export function isMobile(): boolean {
return (window.innerWidth <= 767.98); return (window.innerWidth <= 767.98);
@ -170,8 +172,9 @@ export function uncompressTx(tx: TransactionCompressed): TransactionStripped {
}; };
} }
export function uncompressDeltaChange(delta: MempoolBlockDeltaCompressed): MempoolBlockDelta { export function uncompressDeltaChange(block: number, delta: MempoolBlockDeltaCompressed): MempoolBlockDelta {
return { return {
block,
added: delta.added.map(uncompressTx), added: delta.added.map(uncompressTx),
removed: delta.removed, removed: delta.removed,
changed: delta.changed.map(tx => ({ changed: delta.changed.map(tx => ({
@ -183,6 +186,33 @@ export function uncompressDeltaChange(delta: MempoolBlockDeltaCompressed): Mempo
}; };
} }
export function renderSats(value: number, network: string, mode: 'sats' | 'btc' | 'auto' = 'auto'): string {
let prefix = '';
switch (network) {
case 'liquid':
prefix = 'L';
break;
case 'liquidtestnet':
prefix = 'tL';
break;
case 'testnet':
case 'testnet4':
prefix = 't';
break;
case 'signet':
prefix = 's';
break;
}
if (mode === 'btc' || (mode === 'auto' && value >= 1000000)) {
return `${amountShortenerPipe.transform(value / 100000000)} ${prefix}BTC`;
} else {
if (prefix.length) {
prefix += '-';
}
return `${amountShortenerPipe.transform(value)} ${prefix}sats`;
}
}
export function insecureRandomUUID(): string { export function insecureRandomUUID(): string {
const hexDigits = '0123456789abcdef'; const hexDigits = '0123456789abcdef';
const uuidLengths = [8, 4, 4, 4, 12]; const uuidLengths = [8, 4, 4, 4, 12];

View File

@ -20,6 +20,9 @@
@case ('multisig') { @case ('multisig') {
<span i18n="address.bare-multisig">bare multisig</span> <span i18n="address.bare-multisig">bare multisig</span>
} }
@case ('anchor') {
<span>anchor</span>
}
@case (null) { @case (null) {
<span>unknown</span> <span>unknown</span>
} }

View File

@ -13,8 +13,13 @@
</div> </div>
@if (!enterpriseInfo?.footer_img) { @if (!enterpriseInfo?.footer_img) {
<p class="explore-tagline-mobile"> <p class="explore-tagline-mobile">
@if (officialMempoolSpace) {
<ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container> <ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container>
<ng-template [ngIf]="locale.substr(0, 2) === 'en'">&reg;</ng-template> <ng-template [ngIf]="locale.substr(0, 2) === 'en'">&reg;</ng-template>
} @else {
<ng-container i18n="shared.be-your-own-explorer">Be your own explorer</ng-container>
<ng-template [ngIf]="locale.substr(0, 2) === 'en'">&trade;</ng-template>
}
</p> </p>
} }
<div class="site-options language-selector d-flex justify-content-center align-items-center" [class]="{'services': isServicesPage}"> <div class="site-options language-selector d-flex justify-content-center align-items-center" [class]="{'services': isServicesPage}">
@ -27,29 +32,38 @@
<div class="selector"> <div class="selector">
<app-rate-unit-selector></app-rate-unit-selector> <app-rate-unit-selector></app-rate-unit-selector>
</div> </div>
<div class="selector d-none" [ngClass]="isServicesPage ? 'd-lg-flex' : 'd-md-flex'">
<app-amount-selector></app-amount-selector>
</div>
@if (!env.customize?.theme) { @if (!env.customize?.theme) {
<div class="selector d-none d-sm-flex"> <div class="selector d-none" [ngClass]="isServicesPage ? 'd-lg-flex' : 'd-md-flex'">
<app-theme-selector></app-theme-selector> <app-theme-selector></app-theme-selector>
</div> </div>
} }
<a *ngIf="stateService.isMempoolSpaceBuild" class="btn btn-purple sponsor d-none d-sm-flex justify-content-center" [routerLink]="['/login']"> <a *ngIf="stateService.isMempoolSpaceBuild" class="btn btn-purple sponsor d-none justify-content-center" [ngClass]="isServicesPage ? 'd-lg-flex' : 'd-md-flex'" [routerLink]="['/login']">
<span *ngIf="user" i18n="shared.my-account" class="nowrap">My Account</span> <span *ngIf="user" i18n="shared.my-account" class="nowrap">My Account</span>
<span *ngIf="!user" i18n="shared.sign-in" class="nowrap">Sign In</span> <span *ngIf="!user" i18n="shared.sign-in" class="nowrap">Sign In</span>
</a> </a>
</div> </div>
@if (!env.customize?.theme) { @if (!env.customize?.theme) {
<div class="selector d-flex d-sm-none justify-content-center ml-auto mr-auto mt-0"> <div class="selector d-flex justify-content-center ml-auto mr-auto mt-0" [ngClass]="isServicesPage ? 'd-lg-none' : 'd-md-none'">
<app-theme-selector></app-theme-selector> <app-amount-selector class="add-margin"></app-amount-selector>
<app-theme-selector class="add-margin"></app-theme-selector>
</div> </div>
} }
@if (!enterpriseInfo?.footer_img) { @if (!enterpriseInfo?.footer_img) {
<a *ngIf="stateService.isMempoolSpaceBuild" class="btn btn-purple sponsor d-flex d-sm-none justify-content-center ml-auto mr-auto mt-0 mb-2" [routerLink]="['/login']"> <a *ngIf="stateService.isMempoolSpaceBuild" class="btn btn-purple sponsor d-flex justify-content-center ml-auto mr-auto mt-0 mb-2" [ngClass]="isServicesPage ? 'd-lg-none' : 'd-md-none'" [routerLink]="['/login']">
<span *ngIf="user" i18n="shared.my-account" class="nowrap">My Account</span> <span *ngIf="user" i18n="shared.my-account" class="nowrap">My Account</span>
<span *ngIf="!user" i18n="shared.sign-in" class="nowrap">Sign In</span> <span *ngIf="!user" i18n="shared.sign-in" class="nowrap">Sign In</span>
</a> </a>
<p class="explore-tagline-desktop"> <p class="explore-tagline-desktop">
@if (officialMempoolSpace) {
<ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container> <ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container>
<ng-template [ngIf]="locale.substr(0, 2) === 'en'">&reg;</ng-template> <ng-template [ngIf]="locale.substr(0, 2) === 'en'">&reg;</ng-template>
} @else {
<ng-container i18n="shared.be-your-own-explorer">Be your own explorer</ng-container>
<ng-template [ngIf]="locale.substr(0, 2) === 'en'">&trade;</ng-template>
}
</p> </p>
} }
</div> </div>

View File

@ -76,6 +76,11 @@ footer .selector {
display: inline-block; display: inline-block;
} }
footer .add-margin {
margin-left: 5px;
margin-right: 5px;
}
footer .row.link-tree { footer .row.link-tree {
max-width: 1140px; max-width: 1140px;
margin: 0 auto; margin: 0 auto;
@ -154,7 +159,7 @@ footer .nowrap {
display: block; display: block;
} }
@media (min-width: 951px) { @media (min-width: 1020px) {
:host-context(.ltr-layout) .language-selector { :host-context(.ltr-layout) .language-selector {
float: right !important; float: right !important;
} }
@ -172,7 +177,24 @@ footer .nowrap {
} }
.services { .services {
@media (min-width: 951px) and (max-width: 1147px) { @media (min-width: 1300px) {
:host-context(.ltr-layout) .language-selector {
float: right !important;
}
:host-context(.rtl-layout) .language-selector {
float: left !important;
}
.explore-tagline-desktop {
display: block;
}
.explore-tagline-mobile {
display: none;
}
}
@media (max-width: 1300px) {
:host-context(.ltr-layout) .services .language-selector { :host-context(.ltr-layout) .services .language-selector {
float: none !important; float: none !important;
} }
@ -248,7 +270,7 @@ footer .nowrap {
} }
@media (max-width: 950px) { @media (max-width: 1019px) {
.main-logo { .main-logo {
width: 220px; width: 220px;
@ -287,7 +309,7 @@ footer .nowrap {
} }
} }
@media (max-width: 1147px) { @media (max-width: 1300px) {
.services.main-logo { .services.main-logo {
width: 220px; width: 220px;

View File

@ -166,6 +166,7 @@ export const ScriptTemplates: { [type: string]: (...args: any) => ScriptTemplate
ln_anchor: () => ({ type: 'ln_anchor', label: 'Lightning Anchor' }), ln_anchor: () => ({ type: 'ln_anchor', label: 'Lightning Anchor' }),
ln_anchor_swept: () => ({ type: 'ln_anchor_swept', label: 'Swept Lightning Anchor' }), ln_anchor_swept: () => ({ type: 'ln_anchor_swept', label: 'Swept Lightning Anchor' }),
multisig: (m: number, n: number) => ({ type: 'multisig', m, n, label: $localize`:@@address-label.multisig:Multisig ${m}:multisigM: of ${n}:multisigN:` }), multisig: (m: number, n: number) => ({ type: 'multisig', m, n, label: $localize`:@@address-label.multisig:Multisig ${m}:multisigM: of ${n}:multisigN:` }),
anchor: () => ({ type: 'anchor', label: 'anchor' }),
}; };
export class ScriptInfo { export class ScriptInfo {
@ -266,7 +267,7 @@ export function parseMultisigScript(script: string): undefined | { m: number, n:
if (!opN) { if (!opN) {
return; return;
} }
if (!opN.startsWith('OP_PUSHNUM_')) { if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
return; return;
} }
const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10); const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10);
@ -286,7 +287,7 @@ export function parseMultisigScript(script: string): undefined | { m: number, n:
if (!opM) { if (!opM) {
return; return;
} }
if (!opM.startsWith('OP_PUSHNUM_')) { if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
return; return;
} }
const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10); const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10);

View File

@ -4,7 +4,7 @@ import { NgbCollapseModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstra
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome'; import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle, import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle,
faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown, faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown,
faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline, faCircleXmark} from '@fortawesome/free-solid-svg-icons'; faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline, faCircleXmark, faCalendarCheck } from '@fortawesome/free-solid-svg-icons';
import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { MenuComponent } from '../components/menu/menu.component'; import { MenuComponent } from '../components/menu/menu.component';
import { PreviewTitleComponent } from '../components/master-page-preview/preview-title.component'; import { PreviewTitleComponent } from '../components/master-page-preview/preview-title.component';
@ -35,6 +35,7 @@ import { LanguageSelectorComponent } from '../components/language-selector/langu
import { FiatSelectorComponent } from '../components/fiat-selector/fiat-selector.component'; import { FiatSelectorComponent } from '../components/fiat-selector/fiat-selector.component';
import { RateUnitSelectorComponent } from '../components/rate-unit-selector/rate-unit-selector.component'; import { RateUnitSelectorComponent } from '../components/rate-unit-selector/rate-unit-selector.component';
import { ThemeSelectorComponent } from '../components/theme-selector/theme-selector.component'; import { ThemeSelectorComponent } from '../components/theme-selector/theme-selector.component';
import { AmountSelectorComponent } from '../components/amount-selector/amount-selector.component';
import { BrowserOnlyDirective } from './directives/browser-only.directive'; import { BrowserOnlyDirective } from './directives/browser-only.directive';
import { ServerOnlyDirective } from './directives/server-only.directive'; import { ServerOnlyDirective } from './directives/server-only.directive';
import { ColoredPriceDirective } from './directives/colored-price.directive'; import { ColoredPriceDirective } from './directives/colored-price.directive';
@ -100,6 +101,7 @@ import { MempoolErrorComponent } from './components/mempool-error/mempool-error.
import { AccelerationsListComponent } from '../components/acceleration/accelerations-list/accelerations-list.component'; import { AccelerationsListComponent } from '../components/acceleration/accelerations-list/accelerations-list.component';
import { PendingStatsComponent } from '../components/acceleration/pending-stats/pending-stats.component'; import { PendingStatsComponent } from '../components/acceleration/pending-stats/pending-stats.component';
import { AccelerationStatsComponent } from '../components/acceleration/acceleration-stats/acceleration-stats.component'; import { AccelerationStatsComponent } from '../components/acceleration/acceleration-stats/acceleration-stats.component';
import { AccelerationSparklesComponent } from '../components/acceleration/sparkles/acceleration-sparkles.component';
import { BlockViewComponent } from '../components/block-view/block-view.component'; import { BlockViewComponent } from '../components/block-view/block-view.component';
import { EightBlocksComponent } from '../components/eight-blocks/eight-blocks.component'; import { EightBlocksComponent } from '../components/eight-blocks/eight-blocks.component';
@ -130,6 +132,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
FiatSelectorComponent, FiatSelectorComponent,
ThemeSelectorComponent, ThemeSelectorComponent,
RateUnitSelectorComponent, RateUnitSelectorComponent,
AmountSelectorComponent,
ScriptpubkeyTypePipe, ScriptpubkeyTypePipe,
RelativeUrlPipe, RelativeUrlPipe,
NoSanitizePipe, NoSanitizePipe,
@ -225,6 +228,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
AccelerationsListComponent, AccelerationsListComponent,
AccelerationStatsComponent, AccelerationStatsComponent,
PendingStatsComponent, PendingStatsComponent,
AccelerationSparklesComponent,
HttpErrorComponent, HttpErrorComponent,
TwitterWidgetComponent, TwitterWidgetComponent,
FaucetComponent, FaucetComponent,
@ -276,6 +280,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
FiatSelectorComponent, FiatSelectorComponent,
RateUnitSelectorComponent, RateUnitSelectorComponent,
ThemeSelectorComponent, ThemeSelectorComponent,
AmountSelectorComponent,
ScriptpubkeyTypePipe, ScriptpubkeyTypePipe,
RelativeUrlPipe, RelativeUrlPipe,
Hex2asciiPipe, Hex2asciiPipe,
@ -355,6 +360,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
AccelerationsListComponent, AccelerationsListComponent,
AccelerationStatsComponent, AccelerationStatsComponent,
PendingStatsComponent, PendingStatsComponent,
AccelerationSparklesComponent,
HttpErrorComponent, HttpErrorComponent,
TwitterWidgetComponent, TwitterWidgetComponent,
TwitterLogin, TwitterLogin,
@ -437,5 +443,6 @@ export class SharedModule {
library.addIcons(faFaucetDrip); library.addIcons(faFaucetDrip);
library.addIcons(faTimeline); library.addIcons(faTimeline);
library.addIcons(faCircleXmark); library.addIcons(faCircleXmark);
library.addIcons(faCalendarCheck);
} }
} }

View File

@ -1,10 +1,10 @@
import { TransactionFlags } from './filters.utils'; import { TransactionFlags } from './filters.utils';
import { getVarIntLength, opcodes, parseMultisigScript, isPoint } from './script.utils'; import { getVarIntLength, opcodes, parseMultisigScript, isPoint } from './script.utils';
import { Transaction } from '../interfaces/electrs.interface'; import { Transaction } from '../interfaces/electrs.interface';
import { CpfpInfo, RbfInfo } from '../interfaces/node-api.interface'; import { CpfpInfo, RbfInfo, TransactionStripped } from '../interfaces/node-api.interface';
import { StateService } from '../services/state.service';
// Bitcoin Core default policy settings // Bitcoin Core default policy settings
const TX_MAX_STANDARD_VERSION = 2;
const MAX_STANDARD_TX_WEIGHT = 400_000; const MAX_STANDARD_TX_WEIGHT = 400_000;
const MAX_BLOCK_SIGOPS_COST = 80_000; const MAX_BLOCK_SIGOPS_COST = 80_000;
const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5); const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5);
@ -89,10 +89,13 @@ export function isDERSig(w: string): boolean {
* *
* returns true early if any standardness rule is violated, otherwise false * 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) * (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced)
*
* As standardness rules change, we'll need to apply the rules in force *at the time* to older blocks.
* For now, just pull out individual rules into versioned functions where necessary.
*/ */
export function isNonStandard(tx: Transaction): boolean { export function isNonStandard(tx: Transaction, height?: number, network?: string): boolean {
// version // version
if (tx.version > TX_MAX_STANDARD_VERSION) { if (isNonStandardVersion(tx, height, network)) {
return true; return true;
} }
@ -139,6 +142,8 @@ export function isNonStandard(tx: Transaction): boolean {
} }
} else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) { } else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
return true; return true;
} else if (isNonStandardAnchor(tx, height, network)) {
return true;
} }
// TODO: bad-witness-nonstandard // TODO: bad-witness-nonstandard
} }
@ -203,6 +208,51 @@ export function isNonStandard(tx: Transaction): boolean {
return false; return false;
} }
// Individual versioned standardness rules
const V3_STANDARDNESS_ACTIVATION_HEIGHT = {
'testnet4': 42_000,
'testnet': 2_900_000,
'signet': 211_000,
'': 863_500,
};
function isNonStandardVersion(tx: Transaction, height?: number, network?: string): boolean {
let TX_MAX_STANDARD_VERSION = 3;
if (
height != null
&& network != null
&& V3_STANDARDNESS_ACTIVATION_HEIGHT[network]
&& height <= V3_STANDARDNESS_ACTIVATION_HEIGHT[network]
) {
// V3 transactions were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
TX_MAX_STANDARD_VERSION = 2;
}
if (tx.version > TX_MAX_STANDARD_VERSION) {
return true;
}
return false;
}
const ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT = {
'testnet4': 42_000,
'testnet': 2_900_000,
'signet': 211_000,
'': 863_500,
};
function isNonStandardAnchor(tx: Transaction, height?: number, network?: string): boolean {
if (
height != null
&& network != null
&& ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[network]
&& height <= ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[network]
) {
// anchor outputs were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
return true;
}
return false;
}
// A witness program is any valid scriptpubkey that consists of a 1-byte push opcode // A witness program is any valid scriptpubkey that consists of a 1-byte push opcode
// followed by a data push between 2 and 40 bytes. // followed by a data push between 2 and 40 bytes.
// https://github.com/bitcoin/bitcoin/blob/2c79abc7ad4850e9e3ba32a04c530155cda7f980/src/script/script.cpp#L224-L240 // https://github.com/bitcoin/bitcoin/blob/2c79abc7ad4850e9e3ba32a04c530155cda7f980/src/script/script.cpp#L224-L240
@ -289,7 +339,7 @@ export function isBurnKey(pubkey: string): boolean {
].includes(pubkey); ].includes(pubkey);
} }
export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replacement?: boolean): bigint { export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replacement?: boolean, height?: number, network?: string): bigint {
let flags = tx.flags ? BigInt(tx.flags) : 0n; let flags = tx.flags ? BigInt(tx.flags) : 0n;
// Update variable flags (CPFP, RBF) // Update variable flags (CPFP, RBF)
@ -439,7 +489,7 @@ export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replac
flags |= TransactionFlags.batch_payout; flags |= TransactionFlags.batch_payout;
} }
if (isNonStandard(tx)) { if (isNonStandard(tx, height, network)) {
flags |= TransactionFlags.nonstandard; flags |= TransactionFlags.nonstandard;
} }
@ -459,3 +509,82 @@ export function getUnacceleratedFeeRate(tx: Transaction, accelerated: boolean):
return tx.effectiveFeePerVsize; return tx.effectiveFeePerVsize;
} }
} }
export function identifyPrioritizedTransactions(transactions: TransactionStripped[]): { prioritized: string[], deprioritized: string[] } {
// find the longest increasing subsequence of transactions
// (adapted from https://en.wikipedia.org/wiki/Longest_increasing_subsequence#Efficient_algorithms)
// should be O(n log n)
const X = transactions.slice(1).reverse(); // standard block order is by *decreasing* effective fee rate, but we want to iterate in increasing order (and skip the coinbase)
if (X.length < 2) {
return { prioritized: [], deprioritized: [] };
}
const N = X.length;
const P: number[] = new Array(N);
const M: number[] = new Array(N + 1);
M[0] = -1; // undefined so can be set to any value
let L = 0;
for (let i = 0; i < N; i++) {
// Binary search for the smallest positive l ≤ L
// such that X[M[l]].effectiveFeePerVsize > X[i].effectiveFeePerVsize
let lo = 1;
let hi = L + 1;
while (lo < hi) {
const mid = lo + Math.floor((hi - lo) / 2); // lo <= mid < hi
if (X[M[mid]].rate > X[i].rate) {
hi = mid;
} else { // if X[M[mid]].effectiveFeePerVsize < X[i].effectiveFeePerVsize
lo = mid + 1;
}
}
// After searching, lo == hi is 1 greater than the
// length of the longest prefix of X[i]
const newL = lo;
// The predecessor of X[i] is the last index of
// the subsequence of length newL-1
P[i] = M[newL - 1];
M[newL] = i;
if (newL > L) {
// If we found a subsequence longer than any we've
// found yet, update L
L = newL;
}
}
// Reconstruct the longest increasing subsequence
// It consists of the values of X at the L indices:
// ..., P[P[M[L]]], P[M[L]], M[L]
const LIS: TransactionStripped[] = new Array(L);
let k = M[L];
for (let j = L - 1; j >= 0; j--) {
LIS[j] = X[k];
k = P[k];
}
const lisMap = new Map<string, number>();
LIS.forEach((tx, index) => lisMap.set(tx.txid, index));
const prioritized: string[] = [];
const deprioritized: string[] = [];
let lastRate = 0;
for (const tx of X) {
if (lisMap.has(tx.txid)) {
lastRate = tx.rate;
} else {
if (Math.abs(tx.rate - lastRate) < 0.1) {
// skip if the rate is almost the same as the previous transaction
} else if (tx.rate <= lastRate) {
prioritized.push(tx.txid);
} else {
deprioritized.push(tx.txid);
}
}
}
return { prioritized, deprioritized };
}

File diff suppressed because it is too large Load Diff

View File

@ -457,6 +457,7 @@
</trans-unit> </trans-unit>
<trans-unit id="bee6b649ee82d9a7cde233070b665eec7c531b1d" datatype="html"> <trans-unit id="bee6b649ee82d9a7cde233070b665eec7c531b1d" datatype="html">
<source>Plus <x id="INTERPOLATION" equiv-text="{{ estimate.txSummary.ancestorCount - 1 }}"/> unconfirmed ancestor(s)</source> <source>Plus <x id="INTERPOLATION" equiv-text="{{ estimate.txSummary.ancestorCount - 1 }}"/> unconfirmed ancestor(s)</source>
<target>컨펌되지 않은 조상(들) <x id="INTERPOLATION" equiv-text="{{ estimate.txSummary.ancestorCount - 1 }}"/></target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context> <context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context>
<context context-type="linenumber">41</context> <context context-type="linenumber">41</context>
@ -491,6 +492,7 @@
</trans-unit> </trans-unit>
<trans-unit id="df89e157bacb4ab32e6ec725bf1eb176dc15201e" datatype="html"> <trans-unit id="df89e157bacb4ab32e6ec725bf1eb176dc15201e" datatype="html">
<source>Size in vbytes of this transaction (including unconfirmed ancestors)</source> <source>Size in vbytes of this transaction (including unconfirmed ancestors)</source>
<target>트랜잭션 크기 (확인되지 않은 조상 포함)</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context> <context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context>
<context context-type="linenumber">51</context> <context context-type="linenumber">51</context>
@ -499,6 +501,7 @@
</trans-unit> </trans-unit>
<trans-unit id="adbeb446bf941afda4d4a923b5e4ce0cf4a1c1b8" datatype="html"> <trans-unit id="adbeb446bf941afda4d4a923b5e4ce0cf4a1c1b8" datatype="html">
<source>In-band fees</source> <source>In-band fees</source>
<target>대역 내 수수료</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context> <context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context>
<context context-type="linenumber">55</context> <context context-type="linenumber">55</context>
@ -624,6 +627,7 @@
</trans-unit> </trans-unit>
<trans-unit id="fad137784196a8fdc10588e27ed5d8ae95fe4e79" datatype="html"> <trans-unit id="fad137784196a8fdc10588e27ed5d8ae95fe4e79" datatype="html">
<source>Fees already paid by this transaction (including unconfirmed ancestors)</source> <source>Fees already paid by this transaction (including unconfirmed ancestors)</source>
<target>이 트랜잭션이 이미 지불한 수수료 (확인되지 않은 조상 포함)</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context> <context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context>
<context context-type="linenumber">62</context> <context context-type="linenumber">62</context>
@ -632,6 +636,7 @@
</trans-unit> </trans-unit>
<trans-unit id="4169a885bc1747a38344bae64e6926c6d7d7ec43" datatype="html"> <trans-unit id="4169a885bc1747a38344bae64e6926c6d7d7ec43" datatype="html">
<source>How much faster?</source> <source>How much faster?</source>
<target>얼마나 더 빠르게 원하시나요?</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context> <context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context>
<context context-type="linenumber">71</context> <context context-type="linenumber">71</context>
@ -640,6 +645,7 @@
</trans-unit> </trans-unit>
<trans-unit id="d1a62bdb732f1efbfdc8af6fbb4349b89015b5e5" datatype="html"> <trans-unit id="d1a62bdb732f1efbfdc8af6fbb4349b89015b5e5" datatype="html">
<source>This will reduce your expected waiting time until the first confirmation to <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="&lt;strong&gt;"/><x id="START_TAG_APP_TIME" ctype="x-app_time" equiv-text="n&quot; [time]=&quot;etaInfo.acceleratedETA&quot; [fastRender]=&quot;false&quot; [fixedRender]=&quot;true&quot;&gt;"/><x id="CLOSE_TAG_APP_TIME" ctype="x-app_time" equiv-text="&lt;/strong&gt;&lt;/s"/><x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="&lt;/small&gt;"/></source> <source>This will reduce your expected waiting time until the first confirmation to <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="&lt;strong&gt;"/><x id="START_TAG_APP_TIME" ctype="x-app_time" equiv-text="n&quot; [time]=&quot;etaInfo.acceleratedETA&quot; [fastRender]=&quot;false&quot; [fixedRender]=&quot;true&quot;&gt;"/><x id="CLOSE_TAG_APP_TIME" ctype="x-app_time" equiv-text="&lt;/strong&gt;&lt;/s"/><x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="&lt;/small&gt;"/></source>
<target>첫 번째 컨펌까지 예상 대기 시간이 <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="&lt;strong&gt;"/><x id="START_TAG_APP_TIME" ctype="x-app_time" equiv-text="n&quot; [time]=&quot;etaInfo.acceleratedETA&quot; [fastRender]=&quot;false&quot; [fixedRender]=&quot;true&quot;&gt;"/><x id="CLOSE_TAG_APP_TIME" ctype="x-app_time" equiv-text="&lt;/strong&gt;&lt;/s"/><x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="&lt;/small&gt;"/>로 단축됩니다</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context> <context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context>
<context context-type="linenumber">76,77</context> <context context-type="linenumber">76,77</context>
@ -657,6 +663,7 @@
</trans-unit> </trans-unit>
<trans-unit id="0b537472d5f7518ed2c2c2b747997b0447ec5ee8" datatype="html"> <trans-unit id="0b537472d5f7518ed2c2c2b747997b0447ec5ee8" datatype="html">
<source>Next block market rate</source> <source>Next block market rate</source>
<target>다음 블록 시장 수수료율</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context> <context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context>
<context context-type="linenumber">109</context> <context context-type="linenumber">109</context>
@ -687,6 +694,7 @@
</trans-unit> </trans-unit>
<trans-unit id="c2836a2964adf9e369ee0a1ce67f991cf2aa435d" datatype="html"> <trans-unit id="c2836a2964adf9e369ee0a1ce67f991cf2aa435d" datatype="html">
<source>Estimated extra fee required</source> <source>Estimated extra fee required</source>
<target>예측된 추가발생 수수료</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context> <context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context>
<context context-type="linenumber">117</context> <context context-type="linenumber">117</context>
@ -695,6 +703,7 @@
</trans-unit> </trans-unit>
<trans-unit id="6c37b6a6f9e5ec98367ed744afa4b36800aa79ce" datatype="html"> <trans-unit id="6c37b6a6f9e5ec98367ed744afa4b36800aa79ce" datatype="html">
<source>Target rate</source> <source>Target rate</source>
<target>목표율</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context> <context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context>
<context context-type="linenumber">131</context> <context context-type="linenumber">131</context>
@ -703,6 +712,7 @@
</trans-unit> </trans-unit>
<trans-unit id="e26d365629446e476b5d437e343b5b02b49adea2" datatype="html"> <trans-unit id="e26d365629446e476b5d437e343b5b02b49adea2" datatype="html">
<source>Extra fee required</source> <source>Extra fee required</source>
<target>필요한 추가 수수료</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context> <context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context>
<context context-type="linenumber">139</context> <context context-type="linenumber">139</context>
@ -711,6 +721,7 @@
</trans-unit> </trans-unit>
<trans-unit id="26e50fa97db4eecde26ff892d725e61ca9201c48" datatype="html"> <trans-unit id="26e50fa97db4eecde26ff892d725e61ca9201c48" datatype="html">
<source>Mempool Accelerator™ fees</source> <source>Mempool Accelerator™ fees</source>
<target>멤풀 엑셀러레이터 수수료</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context> <context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context>
<context context-type="linenumber">153</context> <context context-type="linenumber">153</context>
@ -719,6 +730,7 @@
</trans-unit> </trans-unit>
<trans-unit id="1ec82428244c76064090ea5a55827e3fada82306" datatype="html"> <trans-unit id="1ec82428244c76064090ea5a55827e3fada82306" datatype="html">
<source>Accelerator Service Fee</source> <source>Accelerator Service Fee</source>
<target>엑셀러레이터 서비스 수수료</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context> <context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context>
<context context-type="linenumber">157</context> <context context-type="linenumber">157</context>
@ -727,6 +739,7 @@
</trans-unit> </trans-unit>
<trans-unit id="7d07b80b17dfab3582807759420b8d723c9e4414" datatype="html"> <trans-unit id="7d07b80b17dfab3582807759420b8d723c9e4414" datatype="html">
<source>Transaction Size Surcharge</source> <source>Transaction Size Surcharge</source>
<target>트랜잭션 사이즈에 의한 추가 요금</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context> <context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context>
<context context-type="linenumber">169</context> <context context-type="linenumber">169</context>
@ -735,6 +748,7 @@
</trans-unit> </trans-unit>
<trans-unit id="004732b44df582a2d24e2abbd3f46bc42ae8c546" datatype="html"> <trans-unit id="004732b44df582a2d24e2abbd3f46bc42ae8c546" datatype="html">
<source>Estimated acceleration cost</source> <source>Estimated acceleration cost</source>
<target>예상 가속 비용</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context> <context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context>
<context context-type="linenumber">185</context> <context context-type="linenumber">185</context>
@ -743,6 +757,7 @@
</trans-unit> </trans-unit>
<trans-unit id="c9ec95585f57bd87212693db7cb00d9ed70d49b1" datatype="html"> <trans-unit id="c9ec95585f57bd87212693db7cb00d9ed70d49b1" datatype="html">
<source>Maximum acceleration cost</source> <source>Maximum acceleration cost</source>
<target>최대 가속 비용</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context> <context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context>
<context context-type="linenumber">204</context> <context context-type="linenumber">204</context>
@ -760,6 +775,7 @@
</trans-unit> </trans-unit>
<trans-unit id="f3ff11006f77909b9fca2e0fda0a72b097cd76de" datatype="html"> <trans-unit id="f3ff11006f77909b9fca2e0fda0a72b097cd76de" datatype="html">
<source>Available balance</source> <source>Available balance</source>
<target>잔액</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context> <context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context>
<context context-type="linenumber">226</context> <context context-type="linenumber">226</context>
@ -785,6 +801,7 @@
</trans-unit> </trans-unit>
<trans-unit id="7d89e94e98140d07d5c2bb12d6166b8b74506eb0" datatype="html"> <trans-unit id="7d89e94e98140d07d5c2bb12d6166b8b74506eb0" datatype="html">
<source>Accelerate your Bitcoin transaction?</source> <source>Accelerate your Bitcoin transaction?</source>
<target>비트코인 트랜잭션 가속하기</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context> <context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context>
<context context-type="linenumber">273</context> <context context-type="linenumber">273</context>
@ -802,6 +819,7 @@
</trans-unit> </trans-unit>
<trans-unit id="f6a46cd5ca2087712a145f2c680e2aad5f926eaf" datatype="html"> <trans-unit id="f6a46cd5ca2087712a145f2c680e2aad5f926eaf" datatype="html">
<source>Confirmation expected</source> <source>Confirmation expected</source>
<target>컨펌이 예상됩니다</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context> <context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context>
<context context-type="linenumber">287</context> <context context-type="linenumber">287</context>
@ -1587,6 +1605,7 @@
</trans-unit> </trans-unit>
<trans-unit id="cf2ec414465d65ab24b354663d94d051a67e26e9" datatype="html"> <trans-unit id="cf2ec414465d65ab24b354663d94d051a67e26e9" datatype="html">
<source>Total vSize</source> <source>Total vSize</source>
<target>총 vSize</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/acceleration/acceleration-stats/acceleration-stats.component.html</context> <context context-type="sourcefile">src/app/components/acceleration/acceleration-stats/acceleration-stats.component.html</context>
<context context-type="linenumber">20</context> <context context-type="linenumber">20</context>

View File

@ -510,7 +510,7 @@
</trans-unit> </trans-unit>
<trans-unit id="e4b2d9e6a2ab9e6ca34027ec03beaac42b7badd4" datatype="html"> <trans-unit id="e4b2d9e6a2ab9e6ca34027ec03beaac42b7badd4" datatype="html">
<source>sats</source> <source>sats</source>
<target>sats</target> <target>sat</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context> <context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context>
<context context-type="linenumber">57</context> <context context-type="linenumber">57</context>
@ -881,7 +881,7 @@
</trans-unit> </trans-unit>
<trans-unit id="65fd4251d8ddfe4017d4d83f8cec6f5a80d89289" datatype="html"> <trans-unit id="65fd4251d8ddfe4017d4d83f8cec6f5a80d89289" datatype="html">
<source>Pay</source> <source>Pay</source>
<target>Betale</target> <target>Betal</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context> <context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context>
<context context-type="linenumber">378</context> <context context-type="linenumber">378</context>
@ -4846,7 +4846,7 @@
</trans-unit> </trans-unit>
<trans-unit id="615ba6c4511a36f93c225c725935fdbf16f162a5" datatype="html"> <trans-unit id="615ba6c4511a36f93c225c725935fdbf16f162a5" datatype="html">
<source>Amount (sats)</source> <source>Amount (sats)</source>
<target>Beløp (sats)</target> <target>Beløp (sat)</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/faucet/faucet.component.html</context> <context context-type="sourcefile">src/app/components/faucet/faucet.component.html</context>
<context context-type="linenumber">51</context> <context context-type="linenumber">51</context>
@ -6442,7 +6442,7 @@
</trans-unit> </trans-unit>
<trans-unit id="31443c29cb161e8aa661eb5035f675746ef95b45" datatype="html"> <trans-unit id="31443c29cb161e8aa661eb5035f675746ef95b45" datatype="html">
<source>sats/tx</source> <source>sats/tx</source>
<target>sats/tx</target> <target>sat/tx</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/reward-stats/reward-stats.component.html</context> <context context-type="sourcefile">src/app/components/reward-stats/reward-stats.component.html</context>
<context context-type="linenumber">33</context> <context context-type="linenumber">33</context>
@ -8145,7 +8145,7 @@
</trans-unit> </trans-unit>
<trans-unit id="6acd06bd5a3af583cd46c6d9f7954d7a2b44095e" datatype="html"> <trans-unit id="6acd06bd5a3af583cd46c6d9f7954d7a2b44095e" datatype="html">
<source>mSats</source> <source>mSats</source>
<target>mSats</target> <target>mSat</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/lightning/channel/channel-box/channel-box.component.html</context> <context context-type="sourcefile">src/app/lightning/channel/channel-box/channel-box.component.html</context>
<context context-type="linenumber">35</context> <context context-type="linenumber">35</context>

View File

@ -645,6 +645,7 @@
</trans-unit> </trans-unit>
<trans-unit id="d1a62bdb732f1efbfdc8af6fbb4349b89015b5e5" datatype="html"> <trans-unit id="d1a62bdb732f1efbfdc8af6fbb4349b89015b5e5" datatype="html">
<source>This will reduce your expected waiting time until the first confirmation to <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="&lt;strong&gt;"/><x id="START_TAG_APP_TIME" ctype="x-app_time" equiv-text="n&quot; [time]=&quot;etaInfo.acceleratedETA&quot; [fastRender]=&quot;false&quot; [fixedRender]=&quot;true&quot;&gt;"/><x id="CLOSE_TAG_APP_TIME" ctype="x-app_time" equiv-text="&lt;/strong&gt;&lt;/s"/><x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="&lt;/small&gt;"/></source> <source>This will reduce your expected waiting time until the first confirmation to <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="&lt;strong&gt;"/><x id="START_TAG_APP_TIME" ctype="x-app_time" equiv-text="n&quot; [time]=&quot;etaInfo.acceleratedETA&quot; [fastRender]=&quot;false&quot; [fixedRender]=&quot;true&quot;&gt;"/><x id="CLOSE_TAG_APP_TIME" ctype="x-app_time" equiv-text="&lt;/strong&gt;&lt;/s"/><x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="&lt;/small&gt;"/></source>
<target>İlk onaya kadar geçen bekleme süresini <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="&lt;strong&gt;"/><x id="START_TAG_APP_TIME" ctype="x-app_time" equiv-text="n&quot; [time]=&quot;etaInfo.acceleratedETA&quot; [fastRender]=&quot;false&quot; [fixedRender]=&quot;true&quot;&gt;"/><x id="CLOSE_TAG_APP_TIME" ctype="x-app_time" equiv-text="&lt;/strong&gt;&lt;/s"/><x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="&lt;/small&gt;"/>kadar azaltacak.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context> <context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context>
<context context-type="linenumber">76,77</context> <context context-type="linenumber">76,77</context>
@ -1392,6 +1393,7 @@
</trans-unit> </trans-unit>
<trans-unit id="c3aaae1073e33c932a5c98f98c3520645c0e3a93" datatype="html"> <trans-unit id="c3aaae1073e33c932a5c98f98c3520645c0e3a93" datatype="html">
<source>Out-of-band fees</source> <source>Out-of-band fees</source>
<target>Bant-dışı ücretler</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.html</context> <context context-type="sourcefile">src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.html</context>
<context context-type="linenumber">27</context> <context context-type="linenumber">27</context>
@ -1791,6 +1793,7 @@
</trans-unit> </trans-unit>
<trans-unit id="a7c328c4773db932ff14a1954e15e43dca58e7b7" datatype="html"> <trans-unit id="a7c328c4773db932ff14a1954e15e43dca58e7b7" datatype="html">
<source>Completed</source> <source>Completed</source>
<target>Tamamlandı</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/acceleration/accelerations-list/accelerations-list.component.html</context> <context context-type="sourcefile">src/app/components/acceleration/accelerations-list/accelerations-list.component.html</context>
<context context-type="linenumber">65</context> <context context-type="linenumber">65</context>
@ -1799,6 +1802,7 @@
</trans-unit> </trans-unit>
<trans-unit id="64b582e0d8e3a28331a14d2a1017fa5d6ffb8d93" datatype="html"> <trans-unit id="64b582e0d8e3a28331a14d2a1017fa5d6ffb8d93" datatype="html">
<source>Failed</source> <source>Failed</source>
<target>Başarısız oldu</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/acceleration/accelerations-list/accelerations-list.component.html</context> <context context-type="sourcefile">src/app/components/acceleration/accelerations-list/accelerations-list.component.html</context>
<context context-type="linenumber">67</context> <context context-type="linenumber">67</context>
@ -2320,6 +2324,7 @@
</trans-unit> </trans-unit>
<trans-unit id="9eb81e2576ffe4e8fb0a303e203040b6ab23cc22" datatype="html"> <trans-unit id="9eb81e2576ffe4e8fb0a303e203040b6ab23cc22" datatype="html">
<source><x id="START_ITALIC_TEXT" ctype="x-i" equiv-text="There are too many transactions on this address, more than your backend can handle. See more on &lt;"/>There are too many transactions on this address, more than your backend can handle. See more on <x id="START_LINK" ctype="x-a" equiv-text="&lt;a href=&quot;/docs/faq#address-lookup-issues&quot;&gt;"/>setting up a stronger backend<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a&gt;"/>.<x id="CLOSE_ITALIC_TEXT" ctype="x-i" equiv-text="&lt;/i&gt;"/><x id="LINE_BREAK" ctype="lb"/><x id="LINE_BREAK" ctype="lb"/> Consider viewing this address on the official Mempool website instead: </source> <source><x id="START_ITALIC_TEXT" ctype="x-i" equiv-text="There are too many transactions on this address, more than your backend can handle. See more on &lt;"/>There are too many transactions on this address, more than your backend can handle. See more on <x id="START_LINK" ctype="x-a" equiv-text="&lt;a href=&quot;/docs/faq#address-lookup-issues&quot;&gt;"/>setting up a stronger backend<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a&gt;"/>.<x id="CLOSE_ITALIC_TEXT" ctype="x-i" equiv-text="&lt;/i&gt;"/><x id="LINE_BREAK" ctype="lb"/><x id="LINE_BREAK" ctype="lb"/> Consider viewing this address on the official Mempool website instead: </source>
<target><x id="START_ITALIC_TEXT" ctype="x-i" equiv-text="There are too many transactions on this address, more than your backend can handle. See more on &lt;"/> Bu adres üzerindeki işlem sayısı arka arayüzününüzün işleyemeyeceği kadar fazla. Daha kuvvetli bir arkayüz için <x id="START_LINK" ctype="x-a" equiv-text="&lt;a href=&quot;/docs/faq#address-lookup-issues&quot;&gt;"/>'ye bakın. <x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a&gt;"/>.<x id="CLOSE_ITALIC_TEXT" ctype="x-i" equiv-text="&lt;/i&gt;"/><x id="LINE_BREAK" ctype="lb"/><x id="LINE_BREAK" ctype="lb"/> Ya da bu adresi resmi Mempool sitesinde görüntüleyin: </target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/address/address.component.html</context> <context context-type="sourcefile">src/app/components/address/address.component.html</context>
<context context-type="linenumber">204,207</context> <context context-type="linenumber">204,207</context>
@ -2535,6 +2540,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.liquid.asset" datatype="html"> <trans-unit id="meta.description.liquid.asset" datatype="html">
<source>Browse an overview of the Liquid asset <x id="INTERPOLATION" equiv-text="this.assetContract[1]"/> (<x id="INTERPOLATION" equiv-text="this.assetContract[1]"/>): see issued amount, burned amount, circulating amount, related transactions, and more.</source> <source>Browse an overview of the Liquid asset <x id="INTERPOLATION" equiv-text="this.assetContract[1]"/> (<x id="INTERPOLATION" equiv-text="this.assetContract[1]"/>): see issued amount, burned amount, circulating amount, related transactions, and more.</source>
<target>Liquid varlığın genel görünümünü incele <x id="INTERPOLATION" equiv-text="this.assetContract[1]"/>(<x id="INTERPOLATION" equiv-text="this.assetContract[1]"/>): üretilen, yakılan, dolaşan miktarlır ve ilişkili işlemleri ve daha fazlasını gör. </target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/asset/asset.component.ts</context> <context context-type="sourcefile">src/app/components/asset/asset.component.ts</context>
<context context-type="linenumber">108</context> <context context-type="linenumber">108</context>
@ -2800,6 +2806,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.bitcoin.graphs.block-fee-rates" datatype="html"> <trans-unit id="meta.description.bitcoin.graphs.block-fee-rates" datatype="html">
<source>See Bitcoin feerates visualized over time, including minimum and maximum feerates per block along with feerates at various percentiles.</source> <source>See Bitcoin feerates visualized over time, including minimum and maximum feerates per block along with feerates at various percentiles.</source>
<target>Bitcoin ücret çizelgesinin zaman içindeki değişimini görüntüle. Minimum ve maksimum ücretler ve farklı yüzdelik dilimlerdeki ücretleri görüntüleyebilirsin. </target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts</context> <context context-type="sourcefile">src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts</context>
<context context-type="linenumber">73</context> <context context-type="linenumber">73</context>
@ -2824,6 +2831,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.bitcoin.graphs.block-fees" datatype="html"> <trans-unit id="meta.description.bitcoin.graphs.block-fees" datatype="html">
<source>See the average mining fees earned per Bitcoin block visualized in BTC and USD over time.</source> <source>See the average mining fees earned per Bitcoin block visualized in BTC and USD over time.</source>
<target>Bitcoin bloğu başına ortalama madencilik ücretlerinin BTC ve USD cinsi olarak değişimini gör. </target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/block-fees-graph/block-fees-graph.component.ts</context> <context context-type="sourcefile">src/app/components/block-fees-graph/block-fees-graph.component.ts</context>
<context context-type="linenumber">70</context> <context context-type="linenumber">70</context>
@ -3012,6 +3020,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.bitcoin.graphs.block-health" datatype="html"> <trans-unit id="meta.description.bitcoin.graphs.block-health" datatype="html">
<source>See Bitcoin block health visualized over time. Block health is a measure of how many expected transactions were included in an actual mined block. Expected transactions are determined using Mempool's re-implementation of Bitcoin Core's transaction selection algorithm.</source> <source>See Bitcoin block health visualized over time. Block health is a measure of how many expected transactions were included in an actual mined block. Expected transactions are determined using Mempool's re-implementation of Bitcoin Core's transaction selection algorithm.</source>
<target>Bitcoin blok sağlığını zaman içinde görüntüle. Blok sağlığı beklenen işlemlerin kaçının gerçekten bloğa dahil edildiğinin ölçüsüdür. Beklenen işlemler Mempool'un çalıştırdığı Bitcoin Core işlem seçme algoritması ile belirlenir.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/block-health-graph/block-health-graph.component.ts</context> <context context-type="sourcefile">src/app/components/block-health-graph/block-health-graph.component.ts</context>
<context context-type="linenumber">64</context> <context context-type="linenumber">64</context>
@ -3298,6 +3307,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.bitcoin.graphs.block-rewards" datatype="html"> <trans-unit id="meta.description.bitcoin.graphs.block-rewards" datatype="html">
<source>See Bitcoin block rewards in BTC and USD visualized over time. Block rewards are the total funds miners earn from the block subsidy and fees.</source> <source>See Bitcoin block rewards in BTC and USD visualized over time. Block rewards are the total funds miners earn from the block subsidy and fees.</source>
<target>Bitcoin blok ödüllerini BTC ve USD cinsinden zaman içerisinde görüntüle. Blok ödülleri yeni çıkarılan bitcoin ödülleri ve işlem ücretlerinin toplamıdır. </target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/block-rewards-graph/block-rewards-graph.component.ts</context> <context context-type="sourcefile">src/app/components/block-rewards-graph/block-rewards-graph.component.ts</context>
<context context-type="linenumber">68</context> <context context-type="linenumber">68</context>
@ -3322,6 +3332,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.bitcoin.graphs.block-sizes" datatype="html"> <trans-unit id="meta.description.bitcoin.graphs.block-sizes" datatype="html">
<source>See Bitcoin block sizes (MB) and block weights (weight units) visualized over time.</source> <source>See Bitcoin block sizes (MB) and block weights (weight units) visualized over time.</source>
<target>Bitcoin blok boyutlarını (MB) ve blok ağırlıklarını (ağırlık ünitesi) zaman içinde görselleştir.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.ts</context> <context context-type="sourcefile">src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.ts</context>
<context context-type="linenumber">65</context> <context context-type="linenumber">65</context>
@ -3445,6 +3456,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.liquid.block" datatype="html"> <trans-unit id="meta.description.liquid.block" datatype="html">
<source>See size, weight, fee range, included transactions, and more for Liquid<x id="PH" equiv-text="seoDescriptionNetwork(this.stateService.network)"/> block <x id="BLOCK_HEIGHT" equiv-text="block.height"/> (<x id="BLOCK_ID" equiv-text="block.id"/>).</source> <source>See size, weight, fee range, included transactions, and more for Liquid<x id="PH" equiv-text="seoDescriptionNetwork(this.stateService.network)"/> block <x id="BLOCK_HEIGHT" equiv-text="block.height"/> (<x id="BLOCK_ID" equiv-text="block.id"/>).</source>
<target>Liquid <x id="PH" equiv-text="seoDescriptionNetwork(this.stateService.network)"/> bloğundaki <x id="BLOCK_HEIGHT" equiv-text="block.height"/> (<x id="BLOCK_ID" equiv-text="block.id"/>) boyut, ağırlık, ücret aralığı, dahil edilen işlemler ve daha fazlasını gör.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/block-view/block-view.component.ts</context> <context context-type="sourcefile">src/app/components/block-view/block-view.component.ts</context>
<context context-type="linenumber">112</context> <context context-type="linenumber">112</context>
@ -3460,6 +3472,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.bitcoin.block" datatype="html"> <trans-unit id="meta.description.bitcoin.block" datatype="html">
<source>See size, weight, fee range, included transactions, audit (expected v actual), and more for Bitcoin<x id="PH" equiv-text="seoDescriptionNetwork(this.stateService.network)"/> block <x id="BLOCK_HEIGHT" equiv-text="block.height"/> (<x id="BLOCK_ID" equiv-text="block.id"/>).</source> <source>See size, weight, fee range, included transactions, audit (expected v actual), and more for Bitcoin<x id="PH" equiv-text="seoDescriptionNetwork(this.stateService.network)"/> block <x id="BLOCK_HEIGHT" equiv-text="block.height"/> (<x id="BLOCK_ID" equiv-text="block.id"/>).</source>
<target>Bitcoin <x id="PH" equiv-text="seoDescriptionNetwork(this.stateService.network)"/> block <x id="BLOCK_HEIGHT" equiv-text="block.height"/>(<x id="BLOCK_ID" equiv-text="block.id"/>) için boyut, ağırlıklar, ücret aralığı, dahili işlemler, denetim (beklene vs gerçek) ve daha fazlasını gör.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/block-view/block-view.component.ts</context> <context context-type="sourcefile">src/app/components/block-view/block-view.component.ts</context>
<context context-type="linenumber">114</context> <context context-type="linenumber">114</context>
@ -3651,6 +3664,7 @@
</trans-unit> </trans-unit>
<trans-unit id="e170a90ee0d3a604adf439a60c890caff9152466" datatype="html"> <trans-unit id="e170a90ee0d3a604adf439a60c890caff9152466" datatype="html">
<source>This block does not belong to the main chain, it has been replaced by:</source> <source>This block does not belong to the main chain, it has been replaced by:</source>
<target>Bu blok ana-zincire dahil değil ve şununla değiştirilebilir: </target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/block/block.component.html</context> <context context-type="sourcefile">src/app/components/block/block.component.html</context>
<context context-type="linenumber">5</context> <context context-type="linenumber">5</context>
@ -4173,6 +4187,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.liquid.blocks" datatype="html"> <trans-unit id="meta.description.liquid.blocks" datatype="html">
<source>See the most recent Liquid<x id="PH" equiv-text="seoDescriptionNetwork(this.stateService.network)"/> blocks along with basic stats such as block height, block size, and more.</source> <source>See the most recent Liquid<x id="PH" equiv-text="seoDescriptionNetwork(this.stateService.network)"/> blocks along with basic stats such as block height, block size, and more.</source>
<target>En güncel Liquid <x id="PH" equiv-text="seoDescriptionNetwork(this.stateService.network)"/> blokları için blok yüksekliği, blok büyüklüğü vb temel dataları gör.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/blocks-list/blocks-list.component.ts</context> <context context-type="sourcefile">src/app/components/blocks-list/blocks-list.component.ts</context>
<context context-type="linenumber">71</context> <context context-type="linenumber">71</context>
@ -4180,6 +4195,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.bitcoin.blocks" datatype="html"> <trans-unit id="meta.description.bitcoin.blocks" datatype="html">
<source>See the most recent Bitcoin<x id="PH" equiv-text="seoDescriptionNetwork(this.stateService.network)"/> blocks along with basic stats such as block height, block reward, block size, and more.</source> <source>See the most recent Bitcoin<x id="PH" equiv-text="seoDescriptionNetwork(this.stateService.network)"/> blocks along with basic stats such as block height, block reward, block size, and more.</source>
<target>En güncel Bitcoin <x id="PH" equiv-text="seoDescriptionNetwork(this.stateService.network)"/> blokları için blok yüksekliği, blok büyüklüğü vb temel dataları gör.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/blocks-list/blocks-list.component.ts</context> <context context-type="sourcefile">src/app/components/blocks-list/blocks-list.component.ts</context>
<context context-type="linenumber">73</context> <context context-type="linenumber">73</context>
@ -5162,6 +5178,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.bitcoin.graphs.hashrate" datatype="html"> <trans-unit id="meta.description.bitcoin.graphs.hashrate" datatype="html">
<source>See hashrate and difficulty for the Bitcoin<x id="PH" equiv-text="seoDescriptionNetwork(this.network)"/> network visualized over time.</source> <source>See hashrate and difficulty for the Bitcoin<x id="PH" equiv-text="seoDescriptionNetwork(this.network)"/> network visualized over time.</source>
<target>Bitcoin ağı <x id="PH" equiv-text="seoDescriptionNetwork(this.network)"/> için hashrate ve zorluk seviyelerinin değişimini zaman içinde gör.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/hashrate-chart/hashrate-chart.component.ts</context> <context context-type="sourcefile">src/app/components/hashrate-chart/hashrate-chart.component.ts</context>
<context context-type="linenumber">76</context> <context context-type="linenumber">76</context>
@ -5189,6 +5206,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.descriptions.bitcoin.graphs.hashrate-pools" datatype="html"> <trans-unit id="meta.descriptions.bitcoin.graphs.hashrate-pools" datatype="html">
<source>See Bitcoin mining pool dominance visualized over time: see how top mining pools' share of total hashrate has fluctuated over time.</source> <source>See Bitcoin mining pool dominance visualized over time: see how top mining pools' share of total hashrate has fluctuated over time.</source>
<target>Madencilik havuzu dominasyonunu değişimini zaman içinde gör : en büyük madencilik havuzlarının toplam havuzdan aldığı payın değişimini incele.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts</context> <context context-type="sourcefile">src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts</context>
<context context-type="linenumber">75</context> <context context-type="linenumber">75</context>
@ -5311,6 +5329,7 @@
</trans-unit> </trans-unit>
<trans-unit id="506d3b3e461d170c39745288b9ea96b9ac9b7f78" datatype="html"> <trans-unit id="506d3b3e461d170c39745288b9ea96b9ac9b7f78" datatype="html">
<source>Total amount of BTC held in non-dust Federation UTXOs that have expired timelocks</source> <source>Total amount of BTC held in non-dust Federation UTXOs that have expired timelocks</source>
<target>Dust-dışı Federasyon UTXO'larındaki zaman kilidi bitmiş toplam BTC miktarını gör.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/liquid-reserves-audit/expired-utxos-stats/expired-utxos-stats.component.html</context> <context context-type="sourcefile">src/app/components/liquid-reserves-audit/expired-utxos-stats/expired-utxos-stats.component.html</context>
<context context-type="linenumber">5</context> <context context-type="linenumber">5</context>
@ -5510,6 +5529,7 @@
</trans-unit> </trans-unit>
<trans-unit id="3669efae1ff592688b4df067abf0a272e90af226" datatype="html"> <trans-unit id="3669efae1ff592688b4df067abf0a272e90af226" datatype="html">
<source>Fund / Redemption Tx</source> <source>Fund / Redemption Tx</source>
<target>Fon/ Amortisman İşlemi</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.html</context> <context context-type="sourcefile">src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.html</context>
<context context-type="linenumber">15</context> <context context-type="linenumber">15</context>
@ -5581,6 +5601,7 @@
</trans-unit> </trans-unit>
<trans-unit id="52b32e9a8be459e6539a9b9214c2a17b23206a6c" datatype="html"> <trans-unit id="52b32e9a8be459e6539a9b9214c2a17b23206a6c" datatype="html">
<source>Number of times that the Federation's BTC holdings fall below 95% of the total L-BTC supply</source> <source>Number of times that the Federation's BTC holdings fall below 95% of the total L-BTC supply</source>
<target>Federasyonun tuttuğu BTC miktarının toplam L-BTC'nin %95'inin altına düşme sayısı</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component.html</context> <context context-type="sourcefile">src/app/components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component.html</context>
<context context-type="linenumber">6</context> <context context-type="linenumber">6</context>
@ -5698,6 +5719,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.mempool-block" datatype="html"> <trans-unit id="meta.description.mempool-block" datatype="html">
<source>See stats for <x id="PH" equiv-text="this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'"/><x id="PH_1" equiv-text="seoDescriptionNetwork(this.stateService.network)"/> transactions in the mempool: fee range, aggregate size, and more. Mempool blocks are updated in real-time as the network receives new transactions.</source> <source>See stats for <x id="PH" equiv-text="this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'"/><x id="PH_1" equiv-text="seoDescriptionNetwork(this.stateService.network)"/> transactions in the mempool: fee range, aggregate size, and more. Mempool blocks are updated in real-time as the network receives new transactions.</source>
<target>İşlemler <x id="PH" equiv-text="this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'"/><x id="PH_1" equiv-text="seoDescriptionNetwork(this.stateService.network)"/> için mempool istatistiklerini göster: ücret aralığı, toplam büyüklük, ve fazlasını gör. Mempool blokları, ağa yeni işlem geldiğinde anlık olarak güncellenir. </target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/mempool-block/mempool-block.component.ts</context> <context context-type="sourcefile">src/app/components/mempool-block/mempool-block.component.ts</context>
<context context-type="linenumber">62</context> <context context-type="linenumber">62</context>
@ -5793,6 +5815,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.mining.dashboard" datatype="html"> <trans-unit id="meta.description.mining.dashboard" datatype="html">
<source>Get real-time Bitcoin mining stats like hashrate, difficulty adjustment, block rewards, pool dominance, and more.</source> <source>Get real-time Bitcoin mining stats like hashrate, difficulty adjustment, block rewards, pool dominance, and more.</source>
<target>Anlık olarak hashrate, zorluk seviyesi, blok ödülleri, havuz dominasyonu vb madencilik istatistiklerini görüntüle. </target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/mining-dashboard/mining-dashboard.component.ts</context> <context context-type="sourcefile">src/app/components/mining-dashboard/mining-dashboard.component.ts</context>
<context context-type="linenumber">30</context> <context context-type="linenumber">30</context>
@ -6071,6 +6094,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.mining.pool" datatype="html"> <trans-unit id="meta.description.mining.pool" datatype="html">
<source>See mining pool stats for <x id="PH" equiv-text="poolStats.pool.name"/>: most recent mined blocks, hashrate over time, total block reward to date, known coinbase addresses, and more.</source> <source>See mining pool stats for <x id="PH" equiv-text="poolStats.pool.name"/>: most recent mined blocks, hashrate over time, total block reward to date, known coinbase addresses, and more.</source>
<target>Madencilik havuzu istatistiklerini <x id="PH" equiv-text="poolStats.pool.name"/>: en son bulunan bloklar, hashrate'in zaman içindeki değişimi, bugüne kadarki toplam ödül miktarı, bilinen Coinbase adresleri vb gör.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool/pool-preview.component.ts</context> <context context-type="sourcefile">src/app/components/pool/pool-preview.component.ts</context>
<context context-type="linenumber">86</context> <context context-type="linenumber">86</context>
@ -6305,6 +6329,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.rbf-list" datatype="html"> <trans-unit id="meta.description.rbf-list" datatype="html">
<source>See the most recent RBF replacements on the Bitcoin<x id="PH" equiv-text="seoDescriptionNetwork(this.stateService.network)"/> network, updated in real-time.</source> <source>See the most recent RBF replacements on the Bitcoin<x id="PH" equiv-text="seoDescriptionNetwork(this.stateService.network)"/> network, updated in real-time.</source>
<target>Bitcoin <x id="PH" equiv-text="seoDescriptionNetwork(this.stateService.network)"/> ağı üzerindeki en yeni RBF değişimlerini gerçek zamanlı olarak görüntüle.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/rbf-list/rbf-list.component.ts</context> <context context-type="sourcefile">src/app/components/rbf-list/rbf-list.component.ts</context>
<context context-type="linenumber">62</context> <context context-type="linenumber">62</context>
@ -6618,6 +6643,7 @@
</trans-unit> </trans-unit>
<trans-unit id="68d44b7bd049ae93c2bc15973eb5266aec64693e" datatype="html"> <trans-unit id="68d44b7bd049ae93c2bc15973eb5266aec64693e" datatype="html">
<source>Cap outliers</source> <source>Cap outliers</source>
<target>Sınır dışı değerler</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/statistics/statistics.component.html</context> <context context-type="sourcefile">src/app/components/statistics/statistics.component.html</context>
<context context-type="linenumber">121</context> <context context-type="linenumber">121</context>
@ -6626,6 +6652,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.bitcoin.graphs.mempool" datatype="html"> <trans-unit id="meta.description.bitcoin.graphs.mempool" datatype="html">
<source>See mempool size (in MvB) and transactions per second (in vB/s) visualized over time.</source> <source>See mempool size (in MvB) and transactions per second (in vB/s) visualized over time.</source>
<target>Mempool büyüklüğünün (MvB olarak) ve saniyedeki işlem sayısının (vB/s) zaman içindeki değişimini görselleştir.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/statistics/statistics.component.ts</context> <context context-type="sourcefile">src/app/components/statistics/statistics.component.ts</context>
<context context-type="linenumber">66</context> <context context-type="linenumber">66</context>
@ -6633,6 +6660,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.tv" datatype="html"> <trans-unit id="meta.description.tv" datatype="html">
<source>See Bitcoin blocks and mempool congestion in real-time in a simplified format perfect for a TV.</source> <source>See Bitcoin blocks and mempool congestion in real-time in a simplified format perfect for a TV.</source>
<target>Bitcoin bloklarını ve mempool yoğunluğunu televizyon formatına uygun olarak doğru zamanlı gör</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/television/television.component.ts</context> <context context-type="sourcefile">src/app/components/television/television.component.ts</context>
<context context-type="linenumber">40</context> <context context-type="linenumber">40</context>
@ -6667,6 +6695,7 @@
</trans-unit> </trans-unit>
<trans-unit id="48e4b0c012de5020053ecb26e9ac0d35a1f60688" datatype="html"> <trans-unit id="48e4b0c012de5020053ecb26e9ac0d35a1f60688" datatype="html">
<source>Comma-separated list of raw transactions</source> <source>Comma-separated list of raw transactions</source>
<target>Raw-işlem datalarının virgül ile ayrık gösterimi</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/test-transactions/test-transactions.component.html</context> <context context-type="sourcefile">src/app/components/test-transactions/test-transactions.component.html</context>
<context context-type="linenumber">7</context> <context context-type="linenumber">7</context>
@ -7113,6 +7142,7 @@
</trans-unit> </trans-unit>
<trans-unit id="52a68ca949dfcdeaaea81bec4d597256b8ad42b5" datatype="html"> <trans-unit id="52a68ca949dfcdeaaea81bec4d597256b8ad42b5" datatype="html">
<source>Waiting for your transaction to appear in the mempool</source> <source>Waiting for your transaction to appear in the mempool</source>
<target>İşleminizin mempool'da gözükemsini bekliyoruz.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/tracker/tracker.component.html</context> <context context-type="sourcefile">src/app/components/tracker/tracker.component.html</context>
<context context-type="linenumber">150</context> <context context-type="linenumber">150</context>
@ -7121,6 +7151,7 @@
</trans-unit> </trans-unit>
<trans-unit id="5ad21d21f3e26ddfe0abeed499db5d5c0bd0e325" datatype="html"> <trans-unit id="5ad21d21f3e26ddfe0abeed499db5d5c0bd0e325" datatype="html">
<source>Your transaction is in the mempool, but it will not be confirmed for some time.</source> <source>Your transaction is in the mempool, but it will not be confirmed for some time.</source>
<target>İşleminiz mempool'da yalnız yakın zamanda onaylanması beklenmiyor.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/tracker/tracker.component.html</context> <context context-type="sourcefile">src/app/components/tracker/tracker.component.html</context>
<context context-type="linenumber">156</context> <context context-type="linenumber">156</context>
@ -7129,6 +7160,7 @@
</trans-unit> </trans-unit>
<trans-unit id="809118722b27889f5424609d1779f356bcef2cc2" datatype="html"> <trans-unit id="809118722b27889f5424609d1779f356bcef2cc2" datatype="html">
<source>Your transaction is near the top of the mempool, and is expected to confirm soon.</source> <source>Your transaction is near the top of the mempool, and is expected to confirm soon.</source>
<target>İşleminizin mempool'un üst kademesinde, yakında onaylanması bekleniyor.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/tracker/tracker.component.html</context> <context context-type="sourcefile">src/app/components/tracker/tracker.component.html</context>
<context context-type="linenumber">162</context> <context context-type="linenumber">162</context>
@ -7137,6 +7169,7 @@
</trans-unit> </trans-unit>
<trans-unit id="ee76deb7716e90b79e557394b1d256079b7ec24e" datatype="html"> <trans-unit id="ee76deb7716e90b79e557394b1d256079b7ec24e" datatype="html">
<source>Your transaction is expected to confirm in the next block</source> <source>Your transaction is expected to confirm in the next block</source>
<target>İşleminizin bir sonraki blokta onaylanması bekleniyor.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/tracker/tracker.component.html</context> <context context-type="sourcefile">src/app/components/tracker/tracker.component.html</context>
<context context-type="linenumber">168</context> <context context-type="linenumber">168</context>
@ -7188,6 +7221,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.bitcoin.transaction" datatype="html"> <trans-unit id="meta.description.bitcoin.transaction" datatype="html">
<source>Get real-time status, addresses, fees, script info, and more for <x id="PH" equiv-text="network"/><x id="PH_1" equiv-text="seoDescription"/> transaction with txid <x id="PH_2" equiv-text="this.txId"/>.</source> <source>Get real-time status, addresses, fees, script info, and more for <x id="PH" equiv-text="network"/><x id="PH_1" equiv-text="seoDescription"/> transaction with txid <x id="PH_2" equiv-text="this.txId"/>.</source>
<target>İşlemler <x id="PH" equiv-text="network"/><x id="PH_1" equiv-text="seoDescription"/> ve işlem id'si <x id="PH_2" equiv-text="this.txId"/> için anlık durum, adresler, ücretler, script vb bilgileri çek.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/tracker/tracker.component.ts</context> <context context-type="sourcefile">src/app/components/tracker/tracker.component.ts</context>
<context context-type="linenumber">413</context> <context context-type="linenumber">413</context>
@ -7923,6 +7957,7 @@
</trans-unit> </trans-unit>
<trans-unit id="999bb1a0150c2815a6b4dd64a1850e763603e525" datatype="html"> <trans-unit id="999bb1a0150c2815a6b4dd64a1850e763603e525" datatype="html">
<source><x id="START_PARAGRAPH" ctype="x-p" equiv-text="For any such requ"/><x id="START_BOLD_TEXT" ctype="x-b" equiv-text="mempool.space mer"/>mempool.space merely provides data about the Bitcoin network.<x id="CLOSE_BOLD_TEXT" ctype="x-b" equiv-text="&lt;/b&gt;"/> It cannot help you with retrieving funds, wallet issues, etc.<x id="CLOSE_PARAGRAPH" ctype="x-p" equiv-text="&lt;/p&gt;"/><x id="START_PARAGRAPH" ctype="x-p" equiv-text="For any such requ"/>For any such requests, you need to get in touch with the entity that helped make the transaction (wallet software, exchange company, etc).<x id="CLOSE_PARAGRAPH" ctype="x-p" equiv-text="&lt;/p&gt;"/></source> <source><x id="START_PARAGRAPH" ctype="x-p" equiv-text="For any such requ"/><x id="START_BOLD_TEXT" ctype="x-b" equiv-text="mempool.space mer"/>mempool.space merely provides data about the Bitcoin network.<x id="CLOSE_BOLD_TEXT" ctype="x-b" equiv-text="&lt;/b&gt;"/> It cannot help you with retrieving funds, wallet issues, etc.<x id="CLOSE_PARAGRAPH" ctype="x-p" equiv-text="&lt;/p&gt;"/><x id="START_PARAGRAPH" ctype="x-p" equiv-text="For any such requ"/>For any such requests, you need to get in touch with the entity that helped make the transaction (wallet software, exchange company, etc).<x id="CLOSE_PARAGRAPH" ctype="x-p" equiv-text="&lt;/p&gt;"/></source>
<target><x id="START_PARAGRAPH" ctype="x-p" equiv-text="For any such requ"/><x id="START_BOLD_TEXT" ctype="x-b" equiv-text="mempool.space mer"/>mempool.space Bitcoin ağı hakkında sadece bilgi sağlar. <x id="CLOSE_BOLD_TEXT" ctype="x-b" equiv-text="&lt;/b&gt;"/>kaybettiğiniz fonları, cüzdanlar ile yaşadığınız sorunları çözmekte yardımcı olamaz. <x id="CLOSE_PARAGRAPH" ctype="x-p" equiv-text="&lt;/p&gt;"/><x id="START_PARAGRAPH" ctype="x-p" equiv-text="For any such requ"/>İşlemler ile ilgili sorun yaşarsanız bu işlemi gerçekleştirdiğiniz entite ile iletişime geçmeniz gerekir. (cüzdan yazılımı, borsa vb)<x id="CLOSE_PARAGRAPH" ctype="x-p" equiv-text="&lt;/p&gt;"/></target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/docs/api-docs/api-docs.component.html</context> <context context-type="sourcefile">src/app/docs/api-docs/api-docs.component.html</context>
<context context-type="linenumber">15,16</context> <context context-type="linenumber">15,16</context>
@ -8025,6 +8060,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.docs.faq" datatype="html"> <trans-unit id="meta.description.docs.faq" datatype="html">
<source>Get answers to common questions like: What is a mempool? Why isn't my transaction confirming? How can I run my own instance of The Mempool Open Source Project? And more.</source> <source>Get answers to common questions like: What is a mempool? Why isn't my transaction confirming? How can I run my own instance of The Mempool Open Source Project? And more.</source>
<target>Mempool nedir, neden işlemim onaylanmıyor, Açık Kaynak Kodlu Mempool projesinin bir kopyasını nasıl çalıştırabilirim? gibi temel sorulara cevaplar bulun.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/docs/docs/docs.component.ts</context> <context context-type="sourcefile">src/app/docs/docs/docs.component.ts</context>
<context context-type="linenumber">47</context> <context context-type="linenumber">47</context>
@ -8072,6 +8108,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.docs.websocket-bitcoin" datatype="html"> <trans-unit id="meta.description.docs.websocket-bitcoin" datatype="html">
<source>Documentation for the mempool.space WebSocket API service: get real-time info on blocks, mempools, transactions, addresses, and more.</source> <source>Documentation for the mempool.space WebSocket API service: get real-time info on blocks, mempools, transactions, addresses, and more.</source>
<target>Mempool.space Websoket API servisi için, bloklardan gerçek-zamanlı bilgi çek, mempoollar, işlemler, adresler vb talepler için dökümantasyon. </target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/docs/docs/docs.component.ts</context> <context context-type="sourcefile">src/app/docs/docs/docs.component.ts</context>
<context context-type="linenumber">63</context> <context context-type="linenumber">63</context>
@ -8087,6 +8124,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.docs.electrumrpc" datatype="html"> <trans-unit id="meta.description.docs.electrumrpc" datatype="html">
<source>Documentation for our Electrum RPC interface: get instant, convenient, and reliable access to an Esplora instance.</source> <source>Documentation for our Electrum RPC interface: get instant, convenient, and reliable access to an Esplora instance.</source>
<target>Electrum RPC için arayüz dökümantasyonu: Esplora'ya anında, kolayca ve emniyetli bir şekilde ulaşın. </target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/docs/docs/docs.component.ts</context> <context context-type="sourcefile">src/app/docs/docs/docs.component.ts</context>
<context context-type="linenumber">68</context> <context context-type="linenumber">68</context>
@ -8403,6 +8441,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.lightning.channel" datatype="html"> <trans-unit id="meta.description.lightning.channel" datatype="html">
<source>Overview for Lightning channel <x id="PH" equiv-text="params.get('short_id')"/>. See channel capacity, the Lightning nodes involved, related on-chain transactions, and more.</source> <source>Overview for Lightning channel <x id="PH" equiv-text="params.get('short_id')"/>. See channel capacity, the Lightning nodes involved, related on-chain transactions, and more.</source>
<target>Lightning Kanalı <x id="PH" equiv-text="params.get('short_id')"/> için genel bakış sağlar. Kanal kapasitesi, bağlantılı Lightning nodeları, alakalı zincir üstü işlemler vb veriler. </target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/lightning/channel/channel-preview.component.ts</context> <context context-type="sourcefile">src/app/lightning/channel/channel-preview.component.ts</context>
<context context-type="linenumber">37</context> <context context-type="linenumber">37</context>
@ -9030,6 +9069,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.lightning.dashboard" datatype="html"> <trans-unit id="meta.description.lightning.dashboard" datatype="html">
<source>Get stats on the Lightning network (aggregate capacity, connectivity, etc), Lightning nodes (channels, liquidity, etc) and Lightning channels (status, fees, etc).</source> <source>Get stats on the Lightning network (aggregate capacity, connectivity, etc), Lightning nodes (channels, liquidity, etc) and Lightning channels (status, fees, etc).</source>
<target>Lightning Network için istatistikleri getir. ( toplam kapasite, bağlantılar vb), Ligthning nodeları (kanallar, likidite) ve Lightning kanalları (durum, ücretler vb) </target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts</context> <context context-type="sourcefile">src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts</context>
<context context-type="linenumber">34</context> <context context-type="linenumber">34</context>
@ -9139,6 +9179,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.lightning.node" datatype="html"> <trans-unit id="meta.description.lightning.node" datatype="html">
<source>Overview for the Lightning network node named <x id="PH" equiv-text="node.alias"/>. See channels, capacity, location, fee stats, and more.</source> <source>Overview for the Lightning network node named <x id="PH" equiv-text="node.alias"/>. See channels, capacity, location, fee stats, and more.</source>
<target><x id="PH" equiv-text="node.alias"/> adındaki Lightning ağı nodu için genel bakış. Kanalları, kapasiteyi, lokasyonu, ücret bilgileri ve daha fazlasını gör. </target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/lightning/node/node-preview.component.ts</context> <context context-type="sourcefile">src/app/lightning/node/node-preview.component.ts</context>
<context context-type="linenumber">52</context> <context context-type="linenumber">52</context>
@ -9338,6 +9379,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.lightning.node-map" datatype="html"> <trans-unit id="meta.description.lightning.node-map" datatype="html">
<source>See the channels of non-Tor Lightning network nodes visualized on a world map. Hover/tap on points on the map for node names and details.</source> <source>See the channels of non-Tor Lightning network nodes visualized on a world map. Hover/tap on points on the map for node names and details.</source>
<target>Tor-dışı Lightning ağı nodelarını dünya haritası üzerinde görselleştir. Haritadaki noktaların üzerinde gezerek node adı ve detayları görebilirsiniz.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts</context> <context context-type="sourcefile">src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts</context>
<context context-type="linenumber">74</context> <context context-type="linenumber">74</context>
@ -9362,6 +9404,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.lightning.node-channel-map" datatype="html"> <trans-unit id="meta.description.lightning.node-channel-map" datatype="html">
<source>See the locations of non-Tor Lightning network nodes visualized on a world map. Hover/tap on points on the map for node names and details.</source> <source>See the locations of non-Tor Lightning network nodes visualized on a world map. Hover/tap on points on the map for node names and details.</source>
<target>Tor-dışı Lightning ağı nodelarını dünya haritası üzerinde görselleştir. Haritadaki noktaların üzerinde gezerek node adı ve detayları görebilirsiniz.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/lightning/nodes-map/nodes-map.component.ts</context> <context context-type="sourcefile">src/app/lightning/nodes-map/nodes-map.component.ts</context>
<context context-type="linenumber">52</context> <context context-type="linenumber">52</context>
@ -9369,6 +9412,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.lightning.nodes-network" datatype="html"> <trans-unit id="meta.description.lightning.nodes-network" datatype="html">
<source>See the number of Lightning network nodes visualized over time by network: clearnet only (IPv4, IPv6), darknet (Tor, I2p, cjdns), and both.</source> <source>See the number of Lightning network nodes visualized over time by network: clearnet only (IPv4, IPv6), darknet (Tor, I2p, cjdns), and both.</source>
<target>Ağ türüne göre Lightning ağı nodelarının zaman içerisindeki değişimini göster. Sadece clearnet (IPv4, IPv6), darknet (Tor, I2p, cjdns) ve iki tür bağlantı için. </target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts</context> <context context-type="sourcefile">src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts</context>
<context context-type="linenumber">74</context> <context context-type="linenumber">74</context>
@ -9437,6 +9481,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.lightning.nodes-country-overview" datatype="html"> <trans-unit id="meta.description.lightning.nodes-country-overview" datatype="html">
<source>See a geographical breakdown of the Lightning network: how many Lightning nodes are hosted in countries around the world, aggregate BTC capacity for each country, and more.</source> <source>See a geographical breakdown of the Lightning network: how many Lightning nodes are hosted in countries around the world, aggregate BTC capacity for each country, and more.</source>
<target>Lightning network ağının coğrafi dağılımını görüntüle. Hangi ülkede kaç tane node bulunuyor, ülkeler için toplam BTC kapasitesi ve dha fazlası.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.ts</context> <context context-type="sourcefile">src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.ts</context>
<context context-type="linenumber">47</context> <context context-type="linenumber">47</context>
@ -9507,6 +9552,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.lightning.nodes-country" datatype="html"> <trans-unit id="meta.description.lightning.nodes-country" datatype="html">
<source>Explore all the Lightning nodes hosted in <x id="PH" equiv-text="response.country.en"/> and see an overview of each node's capacity, number of open channels, and more.</source> <source>Explore all the Lightning nodes hosted in <x id="PH" equiv-text="response.country.en"/> and see an overview of each node's capacity, number of open channels, and more.</source>
<target><x id="PH" equiv-text="response.country.en"/> de çalıştırılan bütün Lightning nodeları içn node kapasitesi, açık node sayısı vb bilgileri incele.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/lightning/nodes-per-country/nodes-per-country.component.ts</context> <context context-type="sourcefile">src/app/lightning/nodes-per-country/nodes-per-country.component.ts</context>
<context context-type="linenumber">44</context> <context context-type="linenumber">44</context>
@ -9589,6 +9635,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.lightning.nodes-per-isp" datatype="html"> <trans-unit id="meta.description.lightning.nodes-per-isp" datatype="html">
<source>Browse the top 100 ISPs hosting Lightning nodes along with stats like total number of nodes per ISP, aggregate BTC capacity per ISP, and more</source> <source>Browse the top 100 ISPs hosting Lightning nodes along with stats like total number of nodes per ISP, aggregate BTC capacity per ISP, and more</source>
<target>En fazla Lightning Node'u barındıran 100 ISP'yi ve onların ISP başı toplam node sayısı, ISP'nin toplam BTC kapasitesi vb verilerini incele.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts</context> <context context-type="sourcefile">src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts</context>
<context context-type="linenumber">54</context> <context context-type="linenumber">54</context>
@ -9651,6 +9698,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.lightning.nodes-isp" datatype="html"> <trans-unit id="meta.description.lightning.nodes-isp" datatype="html">
<source>Browse all Bitcoin Lightning nodes using the <x id="PH" equiv-text="response.isp"/> [AS<x id="PH_1" equiv-text="this.route.snapshot.params.isp"/>] ISP and see aggregate stats like total number of nodes, total capacity, and more for the ISP.</source> <source>Browse all Bitcoin Lightning nodes using the <x id="PH" equiv-text="response.isp"/> [AS<x id="PH_1" equiv-text="this.route.snapshot.params.isp"/>] ISP and see aggregate stats like total number of nodes, total capacity, and more for the ISP.</source>
<target><x id="PH" equiv-text="response.isp"/> ISP [AS<x id="PH_1" equiv-text="this.route.snapshot.params.isp"/>] kulanan bütün Lightning nodelarını ve onların toplam node sayısı, toplam kapasites vb görüntüle. </target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.ts</context> <context context-type="sourcefile">src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.ts</context>
<context context-type="linenumber">45</context> <context context-type="linenumber">45</context>
@ -9706,6 +9754,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.lightning.ranking.oldest" datatype="html"> <trans-unit id="meta.description.lightning.ranking.oldest" datatype="html">
<source>See the oldest nodes on the Lightning network along with their capacity, number of channels, location, etc.</source> <source>See the oldest nodes on the Lightning network along with their capacity, number of channels, location, etc.</source>
<target>Lightning ağındaki en eski nodları ve bu nodeların kanal sayısı, kapasitesi ve lokasyonunu vb dataları görüntüle.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.ts</context> <context context-type="sourcefile">src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.ts</context>
<context context-type="linenumber">28</context> <context context-type="linenumber">28</context>
@ -9713,6 +9762,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.lightning.ranking.liquidity" datatype="html"> <trans-unit id="meta.description.lightning.ranking.liquidity" datatype="html">
<source>See Lightning nodes with the most BTC liquidity deployed along with high-level stats like number of open channels, location, node age, and more.</source> <source>See Lightning nodes with the most BTC liquidity deployed along with high-level stats like number of open channels, location, node age, and more.</source>
<target>Lightning ağındaki en fazla BTC likiditesi olan nodelar için açık kanal sayısı, lokasyon, node yaşı vb dataları gör.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts</context> <context context-type="sourcefile">src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts</context>
<context context-type="linenumber">35</context> <context context-type="linenumber">35</context>
@ -9720,6 +9770,7 @@
</trans-unit> </trans-unit>
<trans-unit id="meta.description.lightning.ranking.channels" datatype="html"> <trans-unit id="meta.description.lightning.ranking.channels" datatype="html">
<source>See Lightning nodes with the most channels open along with high-level stats like total node capacity, node age, and more.</source> <source>See Lightning nodes with the most channels open along with high-level stats like total node capacity, node age, and more.</source>
<target>Lightning nodeları için toplam node kapasitesi, node yaşı vb temel dataları görüntüle.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts</context> <context context-type="sourcefile">src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts</context>
<context context-type="linenumber">39</context> <context context-type="linenumber">39</context>
@ -10093,6 +10144,7 @@
</trans-unit> </trans-unit>
<trans-unit id="ba7f0c6fdfa0ab7afc59e9384bca0265d23fb018" datatype="html"> <trans-unit id="ba7f0c6fdfa0ab7afc59e9384bca0265d23fb018" datatype="html">
<source>Your balance is too low.<x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br/&gt;"/>Please <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;top-up-link&quot; href=&quot;/services/accelerator/overview&quot;&gt;"/>top up your account<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a&gt;"/>.</source> <source>Your balance is too low.<x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br/&gt;"/>Please <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;top-up-link&quot; href=&quot;/services/accelerator/overview&quot;&gt;"/>top up your account<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a&gt;"/>.</source>
<target>Balansınız çok düşük. <x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br/&gt;"/> lütfen <x id="START_LINK" ctype="x-a" equiv-text="&lt;a class=&quot;top-up-link&quot; href=&quot;/services/accelerator/overview&quot;&gt;"/> hesabınıza ekleme yapınız <x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a&gt;"/>.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/shared/components/mempool-error/mempool-error.component.html</context> <context context-type="sourcefile">src/app/shared/components/mempool-error/mempool-error.component.html</context>
<context context-type="linenumber">9</context> <context context-type="linenumber">9</context>

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