mirror of
https://github.com/mempool/mempool.git
synced 2025-02-23 14:40:38 +01:00
Merge branch 'master' into nymkappa/configurable-pool-update
This commit is contained in:
commit
0c87a4e7f6
133 changed files with 3223 additions and 1644 deletions
12
LICENSE
12
LICENSE
|
@ -1,5 +1,5 @@
|
|||
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
|
||||
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®,
|
||||
Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full
|
||||
Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square logo,
|
||||
the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical
|
||||
Logo, and the mempool.space Horizontal logo are registered trademarks or trademarks
|
||||
of Mempool Space K.K in Japan, the United States, and/or other countries.
|
||||
Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square Logo,
|
||||
the mempool block visualization Logo, the mempool Blocks Logo, the mempool
|
||||
transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo,
|
||||
the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are
|
||||
registered trademarks or trademarks of Mempool Space K.K in Japan,
|
||||
the United States, and/or other countries.
|
||||
|
||||
See our full Trademark Policy and Guidelines for more details, published on
|
||||
<https://mempool.space/trademark-policy>.
|
||||
|
|
|
@ -77,7 +77,7 @@ Query OK, 0 rows affected (0.00 sec)
|
|||
|
||||
#### 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._
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
|
||||
"POOLS_UPDATE_DELAY": 604800,
|
||||
"AUDIT": false,
|
||||
"RUST_GBT": false,
|
||||
"RUST_GBT": true,
|
||||
"LIMIT_GBT": false,
|
||||
"CPFP_INDEXING": false,
|
||||
"DISK_CACHE_BLOCK_INTERVAL": 6,
|
||||
|
|
201
backend/package-lock.json
generated
201
backend/package-lock.json
generated
|
@ -1,21 +1,22 @@
|
|||
{
|
||||
"name": "mempool-backend",
|
||||
"version": "3.0.0-beta",
|
||||
"version": "3.1.0-dev",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mempool-backend",
|
||||
"version": "3.0.0-beta",
|
||||
"version": "3.1.0-dev",
|
||||
"hasInstallScript": true,
|
||||
"license": "GNU Affero General Public License v3.0",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
"@mempool/electrum-client": "1.1.9",
|
||||
"@types/node": "^18.15.3",
|
||||
"axios": "~1.7.2",
|
||||
"axios": "1.7.2",
|
||||
"bitcoinjs-lib": "~6.1.3",
|
||||
"crypto-js": "~4.2.0",
|
||||
"express": "~4.19.2",
|
||||
"express": "~4.21.0",
|
||||
"maxmind": "~4.3.11",
|
||||
"mysql2": "~3.11.0",
|
||||
"redis": "^4.7.0",
|
||||
|
@ -2280,6 +2281,7 @@
|
|||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
|
||||
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
|
@ -2488,9 +2490,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
|
||||
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
|
||||
"version": "1.20.3",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"content-type": "~1.0.5",
|
||||
|
@ -2500,7 +2502,7 @@
|
|||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "2.4.1",
|
||||
"qs": "6.11.0",
|
||||
"qs": "6.13.0",
|
||||
"raw-body": "2.5.2",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "1.0.0"
|
||||
|
@ -3029,9 +3031,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"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==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
|
@ -3459,36 +3461,36 @@
|
|||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
|
||||
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
|
||||
"integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.20.2",
|
||||
"body-parser": "1.20.3",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.6.0",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "1.2.0",
|
||||
"finalhandler": "1.3.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"merge-descriptors": "1.0.1",
|
||||
"merge-descriptors": "1.0.3",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.7",
|
||||
"path-to-regexp": "0.1.10",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "6.11.0",
|
||||
"qs": "6.13.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "0.18.0",
|
||||
"serve-static": "1.15.0",
|
||||
"send": "0.19.0",
|
||||
"serve-static": "1.16.2",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"type-is": "~1.6.18",
|
||||
|
@ -3601,12 +3603,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
|
||||
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
||||
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
|
@ -6050,9 +6052,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
||||
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-stream": {
|
||||
"version": "2.0.0",
|
||||
|
@ -6266,9 +6271,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
|
||||
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
|
||||
"integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
|
@ -6436,9 +6444,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
|
||||
"version": "0.1.10",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
|
||||
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
|
||||
},
|
||||
"node_modules/path-type": {
|
||||
"version": "4.0.0",
|
||||
|
@ -6646,11 +6654,11 @@
|
|||
]
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.11.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.4"
|
||||
"side-channel": "^1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
|
@ -6871,9 +6879,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
|
||||
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
|
||||
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
|
@ -6906,6 +6914,14 @@
|
|||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||
},
|
||||
"node_modules/send/node_modules/encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/send/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
|
@ -6917,14 +6933,14 @@
|
|||
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
|
||||
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
|
||||
"version": "1.16.2",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
|
||||
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
|
||||
"dependencies": {
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "0.18.0"
|
||||
"send": "0.19.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
|
@ -9603,9 +9619,9 @@
|
|||
}
|
||||
},
|
||||
"body-parser": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
|
||||
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
|
||||
"version": "1.20.3",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
|
||||
"requires": {
|
||||
"bytes": "3.1.2",
|
||||
"content-type": "~1.0.5",
|
||||
|
@ -9615,7 +9631,7 @@
|
|||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "2.4.1",
|
||||
"qs": "6.11.0",
|
||||
"qs": "6.13.0",
|
||||
"raw-body": "2.5.2",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "1.0.0"
|
||||
|
@ -9996,9 +10012,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="
|
||||
},
|
||||
"error-ex": {
|
||||
"version": "1.3.2",
|
||||
|
@ -10303,36 +10319,36 @@
|
|||
}
|
||||
},
|
||||
"express": {
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
|
||||
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
|
||||
"integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
|
||||
"requires": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.20.2",
|
||||
"body-parser": "1.20.3",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.6.0",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "1.2.0",
|
||||
"finalhandler": "1.3.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"merge-descriptors": "1.0.1",
|
||||
"merge-descriptors": "1.0.3",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.7",
|
||||
"path-to-regexp": "0.1.10",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "6.11.0",
|
||||
"qs": "6.13.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "0.18.0",
|
||||
"serve-static": "1.15.0",
|
||||
"send": "0.19.0",
|
||||
"serve-static": "1.16.2",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"type-is": "~1.6.18",
|
||||
|
@ -10434,12 +10450,12 @@
|
|||
}
|
||||
},
|
||||
"finalhandler": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
|
||||
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
||||
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
|
||||
"requires": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
|
@ -12236,9 +12252,9 @@
|
|||
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="
|
||||
},
|
||||
"merge-descriptors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
||||
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="
|
||||
},
|
||||
"merge-stream": {
|
||||
"version": "2.0.0",
|
||||
|
@ -12401,9 +12417,9 @@
|
|||
}
|
||||
},
|
||||
"object-inspect": {
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
|
||||
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ=="
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
|
||||
"integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g=="
|
||||
},
|
||||
"on-finished": {
|
||||
"version": "2.4.1",
|
||||
|
@ -12520,9 +12536,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"path-to-regexp": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
|
||||
"version": "0.1.10",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
|
||||
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
|
||||
},
|
||||
"path-type": {
|
||||
"version": "4.0.0",
|
||||
|
@ -12664,11 +12680,11 @@
|
|||
"dev": true
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.11.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||
"requires": {
|
||||
"side-channel": "^1.0.4"
|
||||
"side-channel": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"queue-microtask": {
|
||||
|
@ -12802,9 +12818,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"send": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
|
||||
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
|
||||
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
|
||||
"requires": {
|
||||
"debug": "2.6.9",
|
||||
"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": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
|
@ -12849,14 +12870,14 @@
|
|||
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
|
||||
},
|
||||
"serve-static": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
|
||||
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
|
||||
"version": "1.16.2",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
|
||||
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
|
||||
"requires": {
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "0.18.0"
|
||||
"send": "0.19.0"
|
||||
}
|
||||
},
|
||||
"set-function-length": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "mempool-backend",
|
||||
"version": "3.0.0-beta",
|
||||
"version": "3.1.0-dev",
|
||||
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
||||
"license": "GNU Affero General Public License v3.0",
|
||||
"homepage": "https://mempool.space",
|
||||
|
@ -42,10 +42,10 @@
|
|||
"@babel/core": "^7.25.2",
|
||||
"@mempool/electrum-client": "1.1.9",
|
||||
"@types/node": "^18.15.3",
|
||||
"axios": "~1.7.2",
|
||||
"axios": "1.7.2",
|
||||
"bitcoinjs-lib": "~6.1.3",
|
||||
"crypto-js": "~4.2.0",
|
||||
"express": "~4.19.2",
|
||||
"express": "~4.21.0",
|
||||
"maxmind": "~4.3.11",
|
||||
"mysql2": "~3.11.0",
|
||||
"rust-gbt": "file:./rust-gbt",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 replacedTransactions = require('./test-data/transactions-replaced.json');
|
||||
|
@ -10,14 +10,14 @@ describe('Common', () => {
|
|||
describe('RBF', () => {
|
||||
const newTransactions = rbfTransactions.concat(randomTransactions);
|
||||
test('should detect RBF transactions with fast method', () => {
|
||||
const result: { [txid: string]: 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(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
|
||||
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
|
||||
});
|
||||
|
||||
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(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
|
||||
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
|
||||
|
|
|
@ -43,7 +43,7 @@ describe('Mempool Backend Config', () => {
|
|||
POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json',
|
||||
POOLS_UPDATE_DELAY: 604800,
|
||||
AUDIT: false,
|
||||
RUST_GBT: false,
|
||||
RUST_GBT: true,
|
||||
LIMIT_GBT: false,
|
||||
CPFP_INDEXING: false,
|
||||
MAX_BLOCKS_BULK_QUERY: 0,
|
||||
|
|
|
@ -2,6 +2,7 @@ import config from '../config';
|
|||
import logger from '../logger';
|
||||
import { MempoolTransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
|
||||
import rbfCache from './rbf-cache';
|
||||
import transactionUtils from './transaction-utils';
|
||||
|
||||
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
|
||||
|
||||
|
@ -15,7 +16,8 @@ class Audit {
|
|||
const matches: string[] = []; // present in both mined block and template
|
||||
const added: string[] = []; // present in mined block, not in template
|
||||
const unseen: string[] = []; // present in the mined block, not in our mempool
|
||||
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 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
|
||||
|
@ -133,23 +135,7 @@ class Audit {
|
|||
totalWeight += tx.weight;
|
||||
}
|
||||
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
({ prioritized, deprioritized } = transactionUtils.identifyPrioritizedTransactions(transactions, 'effectiveFeePerVsize'));
|
||||
|
||||
// transactions missing from near the end of our template are probably not being censored
|
||||
let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight);
|
||||
|
|
|
@ -323,6 +323,7 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||
'witness_v1_taproot': 'v1_p2tr',
|
||||
'nonstandard': 'nonstandard',
|
||||
'multisig': 'multisig',
|
||||
'anchor': 'anchor',
|
||||
'nulldata': 'op_return'
|
||||
};
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import difficultyAdjustment from '../difficulty-adjustment';
|
|||
import transactionRepository from '../../repositories/TransactionRepository';
|
||||
import rbfCache from '../rbf-cache';
|
||||
import { calculateMempoolTxCpfp } from '../cpfp';
|
||||
import { handleError } from '../../utils/api';
|
||||
|
||||
class BitcoinRoutes {
|
||||
public initRoutes(app: Application) {
|
||||
|
@ -86,7 +87,7 @@ class BitcoinRoutes {
|
|||
res.set('Content-Type', 'application/json');
|
||||
res.send(result);
|
||||
} 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();
|
||||
res.json(result);
|
||||
} 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) {
|
||||
if (!Array.isArray(req.query.txId)) {
|
||||
res.status(500).send('Not an array');
|
||||
handleError(req, res, 500, 'Not an array');
|
||||
return;
|
||||
}
|
||||
const txIds: string[] = [];
|
||||
|
@ -128,12 +129,12 @@ class BitcoinRoutes {
|
|||
private async $getBatchedOutspends(req: Request, res: Response): Promise<IEsploraApi.Outspend[][] | void> {
|
||||
const txids_csv = req.query.txids;
|
||||
if (!txids_csv || typeof txids_csv !== 'string') {
|
||||
res.status(500).send('Invalid txids format');
|
||||
handleError(req, res, 500, 'Invalid txids format');
|
||||
return;
|
||||
}
|
||||
const txids = txids_csv.split(',');
|
||||
if (txids.length > 50) {
|
||||
res.status(400).send('Too many txids requested');
|
||||
handleError(req, res, 400, 'Too many txids requested');
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -141,13 +142,13 @@ class BitcoinRoutes {
|
|||
const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids);
|
||||
res.json(batchedOutspends);
|
||||
} 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) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -180,7 +181,7 @@ class BitcoinRoutes {
|
|||
try {
|
||||
cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
|
||||
} catch (e) {
|
||||
res.status(500).send('failed to get CPFP info');
|
||||
handleError(req, res, 500, 'failed to get CPFP info');
|
||||
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) {
|
||||
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) {
|
||||
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
|
||||
// 422 Unprocessable Entity
|
||||
// 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) {
|
||||
if (e instanceof Error && new RegExp(notFoundError).test(e.message)) {
|
||||
res.status(404).send(e.message);
|
||||
handleError(req, res, 404, e.message);
|
||||
} 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) {
|
||||
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.json(transactions);
|
||||
} 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.json(block);
|
||||
} 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.send(blockHeader);
|
||||
} 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.json(auditSummary);
|
||||
} else {
|
||||
return res.status(404).send(`audit not available`);
|
||||
handleError(req, res, 404, `audit not available`);
|
||||
return;
|
||||
}
|
||||
} 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.json(auditSummary);
|
||||
} else {
|
||||
return res.status(404).send(`transaction audit not available`);
|
||||
handleError(req, res, 404, `transaction audit not available`);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
|
@ -388,42 +391,49 @@ class BitcoinRoutes {
|
|||
return await this.getLegacyBlocks(req, res);
|
||||
}
|
||||
} 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) {
|
||||
try {
|
||||
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) {
|
||||
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()) {
|
||||
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);
|
||||
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);
|
||||
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) {
|
||||
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) {
|
||||
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.json(await blocks.$getBlocksBetweenHeight(from, to));
|
||||
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -458,10 +468,10 @@ class BitcoinRoutes {
|
|||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(returnBlocks);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async getBlockTransactions(req: Request, res: Response) {
|
||||
try {
|
||||
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0);
|
||||
|
@ -483,7 +493,7 @@ class BitcoinRoutes {
|
|||
res.json(transactions);
|
||||
} catch (e) {
|
||||
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));
|
||||
res.send(blockHash);
|
||||
} 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) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -507,15 +517,16 @@ class BitcoinRoutes {
|
|||
res.json(addressData);
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
||||
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> {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -528,23 +539,23 @@ class BitcoinRoutes {
|
|||
res.json(transactions);
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
||||
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 getAddressTransactionSummary(req: Request, res: Response): Promise<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private async getScriptHash(req: Request, res: Response) {
|
||||
if (config.MEMPOOL.BACKEND === 'none') {
|
||||
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
|
||||
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -555,15 +566,16 @@ class BitcoinRoutes {
|
|||
res.json(addressData);
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
||||
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> {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -578,16 +590,16 @@ class BitcoinRoutes {
|
|||
res.json(transactions);
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
||||
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 getScriptHashTransactionSummary(req: Request, res: Response): Promise<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -597,7 +609,7 @@ class BitcoinRoutes {
|
|||
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
|
||||
res.send(blockHash);
|
||||
} 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();
|
||||
res.send(rawMempool);
|
||||
} 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 {
|
||||
const result = blocks.getCurrentBlockHeight();
|
||||
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.send(result.toString());
|
||||
} 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.send(result);
|
||||
} 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.send(result);
|
||||
} 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);
|
||||
res.json(result);
|
||||
} 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);
|
||||
res.json(result);
|
||||
} 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
|
||||
});
|
||||
} 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);
|
||||
res.json(result);
|
||||
} 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);
|
||||
res.json(result);
|
||||
} 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();
|
||||
}
|
||||
} 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);
|
||||
res.json(result);
|
||||
} 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) {
|
||||
res.json(da);
|
||||
} else {
|
||||
res.status(503).send(`Service Temporarily Unavailable`);
|
||||
handleError(req, res, 503, `Service Temporarily Unavailable`);
|
||||
}
|
||||
} 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);
|
||||
res.send(txIdResult);
|
||||
} 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'));
|
||||
}
|
||||
}
|
||||
|
@ -764,7 +777,7 @@ class BitcoinRoutes {
|
|||
const txIdResult = await bitcoinClient.sendRawTransaction(txHex);
|
||||
res.send(txIdResult);
|
||||
} 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'));
|
||||
}
|
||||
}
|
||||
|
@ -776,8 +789,7 @@ class BitcoinRoutes {
|
|||
const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate);
|
||||
res.send(result);
|
||||
} catch (e: any) {
|
||||
res.setHeader('content-type', 'text/plain');
|
||||
res.status(400).send(e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
|
||||
handleError(req, res, 400, e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
|
||||
: (e.message || 'Error'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp';
|
|||
import mempool from './mempool';
|
||||
import CpfpRepository from '../repositories/CpfpRepository';
|
||||
import accelerationApi from './services/acceleration';
|
||||
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
|
||||
|
||||
class Blocks {
|
||||
private blocks: BlockExtended[] = [];
|
||||
|
@ -219,10 +220,10 @@ class Blocks {
|
|||
};
|
||||
}
|
||||
|
||||
public summarizeBlockTransactions(hash: string, transactions: TransactionExtended[]): BlockSummary {
|
||||
public summarizeBlockTransactions(hash: string, height: number, transactions: TransactionExtended[]): BlockSummary {
|
||||
return {
|
||||
id: hash,
|
||||
transactions: Common.classifyTransactions(transactions),
|
||||
transactions: Common.classifyTransactions(transactions, height),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -342,7 +343,12 @@ class Blocks {
|
|||
id: pool.uniqueId,
|
||||
name: pool.name,
|
||||
slug: pool.slug,
|
||||
minerNames: null,
|
||||
};
|
||||
|
||||
if (extras.pool.name === 'OCEAN') {
|
||||
extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw);
|
||||
}
|
||||
}
|
||||
|
||||
extras.matchRate = null;
|
||||
|
@ -616,7 +622,7 @@ class Blocks {
|
|||
// add CPFP
|
||||
const cpfpSummary = calculateGoodBlockCpfp(height, txs, []);
|
||||
// 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);
|
||||
if (unclassifiedBlocks[height].version < 2 && targetSummaryVersion === 2) {
|
||||
const cpfpClusters = await CpfpRepository.$getClustersAt(height);
|
||||
|
@ -653,7 +659,7 @@ class Blocks {
|
|||
}
|
||||
const cpfpSummary = calculateGoodBlockCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as MempoolTransactionExtended[], []);
|
||||
// classify
|
||||
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
|
||||
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions);
|
||||
const classifiedTxMap: { [txid: string]: TransactionClassified } = {};
|
||||
for (const tx of classifiedTxs) {
|
||||
classifiedTxMap[tx.txid] = tx;
|
||||
|
@ -912,7 +918,7 @@ class Blocks {
|
|||
}
|
||||
const cpfpSummary: CpfpSummary = calculateGoodBlockCpfp(block.height, transactions, accelerations.map(a => ({ txid: a.txid, max_bid: a.feeDelta })));
|
||||
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
|
||||
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions);
|
||||
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, block.height, cpfpSummary.transactions);
|
||||
this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`);
|
||||
|
||||
if (Common.indexingEnabled()) {
|
||||
|
@ -1169,7 +1175,7 @@ class Blocks {
|
|||
transactions: cpfpSummary.transactions.map(tx => {
|
||||
let flags: number = 0;
|
||||
try {
|
||||
flags = Common.getTransactionFlags(tx);
|
||||
flags = Common.getTransactionFlags(tx, height);
|
||||
} catch (e) {
|
||||
logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
|
@ -1188,7 +1194,7 @@ class Blocks {
|
|||
} else {
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
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;
|
||||
} else {
|
||||
// Call Core RPC
|
||||
|
@ -1324,7 +1330,7 @@ class Blocks {
|
|||
let summaryVersion = 0;
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||
summary = this.summarizeBlockTransactions(cleanBlock.hash, txs);
|
||||
summary = this.summarizeBlockTransactions(cleanBlock.hash, cleanBlock.height, txs);
|
||||
summaryVersion = 1;
|
||||
} else {
|
||||
// Call Core RPC
|
||||
|
|
|
@ -10,7 +10,6 @@ import logger from '../logger';
|
|||
import { getVarIntLength, opcodes, parseMultisigScript } from '../utils/bitcoin-script';
|
||||
|
||||
// Bitcoin Core default policy settings
|
||||
const TX_MAX_STANDARD_VERSION = 2;
|
||||
const MAX_STANDARD_TX_WEIGHT = 400_000;
|
||||
const MAX_BLOCK_SIGOPS_COST = 80_000;
|
||||
const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5);
|
||||
|
@ -80,8 +79,8 @@ export class Common {
|
|||
return arr;
|
||||
}
|
||||
|
||||
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: MempoolTransactionExtended[] } {
|
||||
const matches: { [txid: string]: MempoolTransactionExtended[] } = {};
|
||||
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} {
|
||||
const matches: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = {};
|
||||
|
||||
// For small N, a naive nested loop is extremely fast, but it doesn't scale
|
||||
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));
|
||||
});
|
||||
if (foundMatches?.length) {
|
||||
matches[addedTx.txid] = [...new Set(foundMatches)];
|
||||
matches[addedTx.txid] = { replaced: [...new Set(foundMatches)], replacedBy: addedTx };
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
@ -124,7 +123,7 @@ export class Common {
|
|||
foundMatches.add(deletedTx);
|
||||
}
|
||||
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();
|
||||
for (let i = 0; i < tx.vin.length; 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) {
|
||||
replaced.add(match);
|
||||
// remove this tx from the spendMap
|
||||
// prevents the same tx being replaced more than once
|
||||
for (const replacedVin of match.vin) {
|
||||
const key = `${replacedVin.txid}:${replacedVin.vout}`;
|
||||
spendMap.delete(key);
|
||||
const replacedKey = `${replacedVin.txid}:${replacedVin.vout}`;
|
||||
spendMap.delete(replacedKey);
|
||||
}
|
||||
}
|
||||
const key = `${vin.txid}:${vin.vout}`;
|
||||
spendMap.delete(key);
|
||||
}
|
||||
if (replaced.size) {
|
||||
|
@ -200,10 +199,13 @@ export class Common {
|
|||
*
|
||||
* returns true early if any standardness rule is violated, otherwise false
|
||||
* (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced)
|
||||
*
|
||||
* As standardness rules change, we'll need to apply the rules in force *at the time* to older blocks.
|
||||
* For now, just pull out individual rules into versioned functions where necessary.
|
||||
*/
|
||||
static isNonStandard(tx: TransactionExtended): boolean {
|
||||
static isNonStandard(tx: TransactionExtended, height?: number): boolean {
|
||||
// version
|
||||
if (tx.version > TX_MAX_STANDARD_VERSION) {
|
||||
if (this.isNonStandardVersion(tx, height)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -250,6 +252,8 @@ export class Common {
|
|||
}
|
||||
} else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
|
||||
return true;
|
||||
} else if (this.isNonStandardAnchor(tx, height)) {
|
||||
return true;
|
||||
}
|
||||
// TODO: bad-witness-nonstandard
|
||||
}
|
||||
|
@ -335,6 +339,49 @@ export class Common {
|
|||
return false;
|
||||
}
|
||||
|
||||
// Individual versioned standardness rules
|
||||
|
||||
static V3_STANDARDNESS_ACTIVATION_HEIGHT = {
|
||||
'testnet4': 42_000,
|
||||
'testnet': 2_900_000,
|
||||
'signet': 211_000,
|
||||
'': 863_500,
|
||||
};
|
||||
static isNonStandardVersion(tx: TransactionExtended, height?: number): boolean {
|
||||
let TX_MAX_STANDARD_VERSION = 3;
|
||||
if (
|
||||
height != null
|
||||
&& this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
|
||||
&& height <= this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
|
||||
) {
|
||||
// V3 transactions were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
|
||||
TX_MAX_STANDARD_VERSION = 2;
|
||||
}
|
||||
|
||||
if (tx.version > TX_MAX_STANDARD_VERSION) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT = {
|
||||
'testnet4': 42_000,
|
||||
'testnet': 2_900_000,
|
||||
'signet': 211_000,
|
||||
'': 863_500,
|
||||
};
|
||||
static isNonStandardAnchor(tx: TransactionExtended, height?: number): boolean {
|
||||
if (
|
||||
height != null
|
||||
&& this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
|
||||
&& height <= this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
|
||||
) {
|
||||
// anchor outputs were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static getNonWitnessSize(tx: TransactionExtended): number {
|
||||
let weight = tx.weight;
|
||||
let hasWitness = false;
|
||||
|
@ -415,7 +462,7 @@ export class Common {
|
|||
return flags;
|
||||
}
|
||||
|
||||
static getTransactionFlags(tx: TransactionExtended): number {
|
||||
static getTransactionFlags(tx: TransactionExtended, height?: number): number {
|
||||
let flags = tx.flags ? BigInt(tx.flags) : 0n;
|
||||
|
||||
// Update variable flags (CPFP, RBF)
|
||||
|
@ -548,7 +595,7 @@ export class Common {
|
|||
if (hasFakePubkey) {
|
||||
flags |= TransactionFlags.fake_pubkey;
|
||||
}
|
||||
|
||||
|
||||
// fast but bad heuristic to detect possible coinjoins
|
||||
// (at least 5 inputs and 5 outputs, less than half of which are unique amounts, with no address reuse)
|
||||
const addressReuse = Object.keys(reusedOutputAddresses).reduce((acc, key) => Math.max(acc, (reusedInputAddresses[key] || 0) + (reusedOutputAddresses[key] || 0)), 0) > 1;
|
||||
|
@ -564,17 +611,17 @@ export class Common {
|
|||
flags |= TransactionFlags.batch_payout;
|
||||
}
|
||||
|
||||
if (this.isNonStandard(tx)) {
|
||||
if (this.isNonStandard(tx, height)) {
|
||||
flags |= TransactionFlags.nonstandard;
|
||||
}
|
||||
|
||||
return Number(flags);
|
||||
}
|
||||
|
||||
static classifyTransaction(tx: TransactionExtended): TransactionClassified {
|
||||
static classifyTransaction(tx: TransactionExtended, height?: number): TransactionClassified {
|
||||
let flags = 0;
|
||||
try {
|
||||
flags = Common.getTransactionFlags(tx);
|
||||
flags = Common.getTransactionFlags(tx, height);
|
||||
} catch (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[] {
|
||||
return txs.map(Common.classifyTransaction);
|
||||
static classifyTransactions(txs: TransactionExtended[], height?: number): TransactionClassified[] {
|
||||
return txs.map(tx => Common.classifyTransaction(tx, height));
|
||||
}
|
||||
|
||||
static stripTransaction(tx: TransactionExtended): TransactionStripped {
|
||||
|
|
|
@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
|
|||
import { RowDataPacket } from 'mysql2';
|
||||
|
||||
class DatabaseMigration {
|
||||
private static currentVersion = 81;
|
||||
private static currentVersion = 82;
|
||||
private queryTimeout = 3600_000;
|
||||
private statisticsAddedIndexed = false;
|
||||
private uniqueLogs: string[] = [];
|
||||
|
@ -700,6 +700,11 @@ class DatabaseMigration {
|
|||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"');
|
||||
await this.updateToSchemaVersion(81);
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 82 && isBitcoin === true && config.MEMPOOL.NETWORK === 'mainnet') {
|
||||
await this.$fixBadV1AuditBlocks();
|
||||
await this.updateToSchemaVersion(82);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1314,6 +1319,28 @@ class DatabaseMigration {
|
|||
logger.warn(`Failed to migrate cpfp transaction data`);
|
||||
}
|
||||
}
|
||||
|
||||
private async $fixBadV1AuditBlocks(): Promise<void> {
|
||||
const badBlocks = [
|
||||
'000000000000000000011ad49227fc8c9ba0ca96ad2ebce41a862f9a244478dc',
|
||||
'000000000000000000010ac1f68b3080153f2826ffddc87ceffdd68ed97d6960',
|
||||
'000000000000000000024cbdafeb2660ae8bd2947d166e7fe15d1689e86b2cf7',
|
||||
'00000000000000000002e1dbfbf6ae057f331992a058b822644b368034f87286',
|
||||
'0000000000000000000019973b2778f08ad6d21e083302ff0833d17066921ebb',
|
||||
];
|
||||
|
||||
for (const hash of badBlocks) {
|
||||
try {
|
||||
await this.$executeQuery(`
|
||||
UPDATE blocks_audits
|
||||
SET prioritized_txs = '[]'
|
||||
WHERE hash = '${hash}'
|
||||
`, true);
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new DatabaseMigration();
|
||||
|
|
|
@ -257,6 +257,7 @@ class DiskCache {
|
|||
trees: rbfData.rbf.trees,
|
||||
expiring: rbfData.rbf.expiring.map(([txid, value]) => ({ key: txid, value })),
|
||||
mempool: memPool.getMempool(),
|
||||
spendMap: memPool.getSpendMap(),
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import config from '../../config';
|
||||
import { Application, Request, Response } from 'express';
|
||||
import channelsApi from './channels.api';
|
||||
import { handleError } from '../../utils/api';
|
||||
|
||||
class ChannelsRoutes {
|
||||
constructor() { }
|
||||
|
@ -22,7 +23,7 @@ class ChannelsRoutes {
|
|||
const channels = await channelsApi.$searchChannelsById(req.params.search);
|
||||
res.json(channels);
|
||||
} 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.json(channel);
|
||||
} 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 : '';
|
||||
|
||||
if (index < -1) {
|
||||
res.status(400).send('Invalid index');
|
||||
handleError(req, res, 400, 'Invalid index');
|
||||
return;
|
||||
}
|
||||
if (['open', 'active', 'closed'].includes(status) === false) {
|
||||
res.status(400).send('Invalid status');
|
||||
handleError(req, res, 400, 'Invalid status');
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -69,14 +70,14 @@ class ChannelsRoutes {
|
|||
res.header('X-Total-Count', channelsCount.toString());
|
||||
res.json(channels);
|
||||
} 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> {
|
||||
try {
|
||||
if (!Array.isArray(req.query.txId)) {
|
||||
res.status(400).send('Not an array');
|
||||
handleError(req, res, 400, 'Not an array');
|
||||
return;
|
||||
}
|
||||
const txIds: string[] = [];
|
||||
|
@ -107,7 +108,7 @@ class ChannelsRoutes {
|
|||
|
||||
res.json(result);
|
||||
} 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.json(channels);
|
||||
} 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.json(channels);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@ import { Application, Request, Response } from 'express';
|
|||
import nodesApi from './nodes.api';
|
||||
import channelsApi from './channels.api';
|
||||
import statisticsApi from './statistics.api';
|
||||
import { handleError } from '../../utils/api';
|
||||
|
||||
class GeneralLightningRoutes {
|
||||
constructor() { }
|
||||
|
||||
|
@ -27,7 +29,7 @@ class GeneralLightningRoutes {
|
|||
channels: channels,
|
||||
});
|
||||
} 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.json(statistics);
|
||||
} 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();
|
||||
res.json(statistics);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { Application, Request, Response } from 'express';
|
|||
import nodesApi from './nodes.api';
|
||||
import DB from '../../database';
|
||||
import { INodesRanking } from '../../mempool.interfaces';
|
||||
import { handleError } from '../../utils/api';
|
||||
|
||||
class NodesRoutes {
|
||||
constructor() { }
|
||||
|
@ -31,7 +32,7 @@ class NodesRoutes {
|
|||
const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search);
|
||||
res.json(nodes);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -181,13 +182,13 @@ class NodesRoutes {
|
|||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(nodes);
|
||||
} catch (e) {
|
||||
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 {
|
||||
const node = await nodesApi.$getNode(req.params.public_key);
|
||||
if (!node) {
|
||||
res.status(404).send('Node not found');
|
||||
handleError(req, res, 404, 'Node not found');
|
||||
return;
|
||||
}
|
||||
res.header('Pragma', 'public');
|
||||
|
@ -203,7 +204,7 @@ class NodesRoutes {
|
|||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(node);
|
||||
} 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.json(statistics);
|
||||
} 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 {
|
||||
const node = await nodesApi.$getFeeHistogram(req.params.public_key);
|
||||
if (!node) {
|
||||
res.status(404).send('Node not found');
|
||||
handleError(req, res, 404, 'Node not found');
|
||||
return;
|
||||
}
|
||||
res.header('Pragma', 'public');
|
||||
|
@ -231,7 +232,7 @@ class NodesRoutes {
|
|||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(node);
|
||||
} 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,
|
||||
});
|
||||
} 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.json(topCapacityNodes);
|
||||
} 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.json(topCapacityNodes);
|
||||
} 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.json(topCapacityNodes);
|
||||
} 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.json(nodesPerAs);
|
||||
} 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.json(worldNodes);
|
||||
} 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) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -335,7 +336,7 @@ class NodesRoutes {
|
|||
nodes: nodes,
|
||||
});
|
||||
} 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) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -362,7 +363,7 @@ class NodesRoutes {
|
|||
nodes: nodes,
|
||||
});
|
||||
} 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.json(nodesPerAs);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { Application, Request, Response } from 'express';
|
|||
import config from '../../config';
|
||||
import elementsParser from './elements-parser';
|
||||
import icons from './icons';
|
||||
import { handleError } from '../../utils/api';
|
||||
|
||||
class LiquidRoutes {
|
||||
public initRoutes(app: Application) {
|
||||
|
@ -42,7 +43,7 @@ class LiquidRoutes {
|
|||
res.setHeader('content-length', result.length);
|
||||
res.send(result);
|
||||
} 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) {
|
||||
res.json(result);
|
||||
} 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.json(pegs);
|
||||
} 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.json(reserves);
|
||||
} 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.json(currentSupply);
|
||||
} 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.json(currentReserves);
|
||||
} 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.json(auditStatus);
|
||||
} 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.json(federationAddresses);
|
||||
} 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.json(federationAddresses);
|
||||
} 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.json(federationUtxos);
|
||||
} 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.json(expiredUtxos);
|
||||
} 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.json(federationUtxos);
|
||||
} 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.json(emergencySpentUtxos);
|
||||
} 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.json(emergencySpentUtxos);
|
||||
} 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.json(recentPegs);
|
||||
} 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.json(pegsVolume);
|
||||
} 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.json(pegsCount);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -369,7 +369,7 @@ class MempoolBlocks {
|
|||
const lastBlockIndex = blocks.length - 1;
|
||||
let hasBlockStack = blocks.length >= 8;
|
||||
let stackWeight;
|
||||
let feeStatsCalculator: OnlineFeeStatsCalculator | void;
|
||||
let feeStatsCalculator: OnlineFeeStatsCalculator | null = null;
|
||||
if (hasBlockStack) {
|
||||
if (blockWeights && blockWeights[7] !== null) {
|
||||
stackWeight = blockWeights[7];
|
||||
|
@ -380,28 +380,36 @@ class MempoolBlocks {
|
|||
feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5, [10, 20, 30, 40, 50, 60, 70, 80, 90]);
|
||||
}
|
||||
|
||||
const ancestors: Ancestor[] = [];
|
||||
const descendants: Ancestor[] = [];
|
||||
let ancestor: MempoolTransactionExtended
|
||||
for (const cluster of clusters) {
|
||||
for (const memberTxid of cluster) {
|
||||
const mempoolTx = mempool[memberTxid];
|
||||
if (mempoolTx) {
|
||||
const ancestors: Ancestor[] = [];
|
||||
const descendants: Ancestor[] = [];
|
||||
// ugly micro-optimization to avoid allocating new arrays
|
||||
ancestors.length = 0;
|
||||
descendants.length = 0;
|
||||
let matched = false;
|
||||
cluster.forEach(txid => {
|
||||
ancestor = mempool[txid];
|
||||
if (txid === memberTxid) {
|
||||
matched = true;
|
||||
} else {
|
||||
if (!mempool[txid]) {
|
||||
if (!ancestor) {
|
||||
console.log('txid missing from mempool! ', txid, candidates?.txs[txid]);
|
||||
return;
|
||||
}
|
||||
const relative = {
|
||||
txid: txid,
|
||||
fee: mempool[txid].fee,
|
||||
weight: (mempool[txid].adjustedVsize * 4),
|
||||
fee: ancestor.fee,
|
||||
weight: (ancestor.adjustedVsize * 4),
|
||||
};
|
||||
if (matched) {
|
||||
descendants.push(relative);
|
||||
mempoolTx.lastBoosted = Math.max(mempoolTx.lastBoosted || 0, mempool[txid].firstSeen || 0);
|
||||
if (!mempoolTx.lastBoosted || (ancestor.firstSeen && ancestor.firstSeen > mempoolTx.lastBoosted)) {
|
||||
mempoolTx.lastBoosted = ancestor.firstSeen;
|
||||
}
|
||||
} else {
|
||||
ancestors.push(relative);
|
||||
}
|
||||
|
@ -410,7 +418,20 @@ class MempoolBlocks {
|
|||
if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) {
|
||||
mempoolTx.cpfpDirty = true;
|
||||
}
|
||||
Object.assign(mempoolTx, {ancestors, descendants, bestDescendant: null, cpfpChecked: true});
|
||||
// ugly micro-optimization to avoid allocating new arrays or objects
|
||||
if (mempoolTx.ancestors) {
|
||||
mempoolTx.ancestors.length = 0;
|
||||
} else {
|
||||
mempoolTx.ancestors = [];
|
||||
}
|
||||
if (mempoolTx.descendants) {
|
||||
mempoolTx.descendants.length = 0;
|
||||
} else {
|
||||
mempoolTx.descendants = [];
|
||||
}
|
||||
mempoolTx.ancestors.push(...ancestors);
|
||||
mempoolTx.descendants.push(...descendants);
|
||||
mempoolTx.cpfpChecked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -420,7 +441,10 @@ class MempoolBlocks {
|
|||
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
|
||||
// update this thread's mempool with the results
|
||||
let mempoolTx: MempoolTransactionExtended;
|
||||
const mempoolBlocks: MempoolBlockWithTransactions[] = blocks.map((block, blockIndex) => {
|
||||
let acceleration: Acceleration;
|
||||
const mempoolBlocks: MempoolBlockWithTransactions[] = [];
|
||||
for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
|
||||
const block = blocks[blockIndex];
|
||||
let totalSize = 0;
|
||||
let totalVsize = 0;
|
||||
let totalWeight = 0;
|
||||
|
@ -436,7 +460,8 @@ class MempoolBlocks {
|
|||
}
|
||||
}
|
||||
|
||||
for (const txid of block) {
|
||||
for (let i = 0; i < block.length; i++) {
|
||||
const txid = block[i];
|
||||
if (txid) {
|
||||
mempoolTx = mempool[txid];
|
||||
// save position in projected blocks
|
||||
|
@ -445,30 +470,37 @@ class MempoolBlocks {
|
|||
vsize: totalVsize + (mempoolTx.vsize / 2),
|
||||
};
|
||||
|
||||
const acceleration = accelerations[txid];
|
||||
if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
|
||||
if (!mempoolTx.acceleration) {
|
||||
mempoolTx.cpfpDirty = true;
|
||||
}
|
||||
mempoolTx.acceleration = true;
|
||||
mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools;
|
||||
mempoolTx.acceleratedAt = acceleration?.added;
|
||||
mempoolTx.feeDelta = acceleration?.feeDelta;
|
||||
for (const ancestor of mempoolTx.ancestors || []) {
|
||||
if (!mempool[ancestor.txid].acceleration) {
|
||||
mempool[ancestor.txid].cpfpDirty = true;
|
||||
if (txid in accelerations) {
|
||||
acceleration = accelerations[txid];
|
||||
if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
|
||||
if (!mempoolTx.acceleration) {
|
||||
mempoolTx.cpfpDirty = true;
|
||||
}
|
||||
mempoolTx.acceleration = true;
|
||||
mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools;
|
||||
mempoolTx.acceleratedAt = acceleration?.added;
|
||||
mempoolTx.feeDelta = acceleration?.feeDelta;
|
||||
for (const ancestor of mempoolTx.ancestors || []) {
|
||||
if (!mempool[ancestor.txid].acceleration) {
|
||||
mempool[ancestor.txid].cpfpDirty = true;
|
||||
}
|
||||
mempool[ancestor.txid].acceleration = true;
|
||||
mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy;
|
||||
mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt;
|
||||
mempool[ancestor.txid].feeDelta = mempoolTx.feeDelta;
|
||||
isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy;
|
||||
}
|
||||
} else {
|
||||
if (mempoolTx.acceleration) {
|
||||
mempoolTx.cpfpDirty = true;
|
||||
delete mempoolTx.acceleration;
|
||||
}
|
||||
mempool[ancestor.txid].acceleration = true;
|
||||
mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy;
|
||||
mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt;
|
||||
mempool[ancestor.txid].feeDelta = mempoolTx.feeDelta;
|
||||
isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy;
|
||||
}
|
||||
} else {
|
||||
if (mempoolTx.acceleration) {
|
||||
mempoolTx.cpfpDirty = true;
|
||||
delete mempoolTx.acceleration;
|
||||
}
|
||||
delete mempoolTx.acceleration;
|
||||
}
|
||||
|
||||
// online calculation of stack-of-blocks fee stats
|
||||
|
@ -486,7 +518,7 @@ class MempoolBlocks {
|
|||
}
|
||||
}
|
||||
}
|
||||
return this.dataToMempoolBlocks(
|
||||
mempoolBlocks[blockIndex] = this.dataToMempoolBlocks(
|
||||
block,
|
||||
transactions,
|
||||
totalSize,
|
||||
|
@ -494,7 +526,7 @@ class MempoolBlocks {
|
|||
totalFees,
|
||||
(hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) ? feeStatsCalculator.getRawFeeStats() : undefined,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
if (saveResults) {
|
||||
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks);
|
||||
|
|
|
@ -19,12 +19,13 @@ class Mempool {
|
|||
private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {};
|
||||
private mempoolCandidates: { [txid: string ]: boolean } = {};
|
||||
private spendMap = new Map<string, MempoolTransactionExtended>();
|
||||
private recentlyDeleted: MempoolTransactionExtended[][] = []; // buffer of transactions deleted in recent mempool updates
|
||||
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
|
||||
maxmempool: 300000000, mempoolminfee: Common.isLiquid() ? 0.00000100 : 0.00001000, minrelaytxfee: Common.isLiquid() ? 0.00000100 : 0.00001000 };
|
||||
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
|
||||
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void) | undefined;
|
||||
deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void) | undefined;
|
||||
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[],
|
||||
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], candidates?: GbtCandidates) => Promise<void>) | undefined;
|
||||
deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[], candidates?: GbtCandidates) => Promise<void>) | undefined;
|
||||
|
||||
private accelerations: { [txId: string]: Acceleration } = {};
|
||||
private accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {};
|
||||
|
@ -74,12 +75,12 @@ class Mempool {
|
|||
}
|
||||
|
||||
public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
|
||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void): void {
|
||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void): void {
|
||||
this.mempoolChangedCallback = fn;
|
||||
}
|
||||
|
||||
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number,
|
||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[],
|
||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[],
|
||||
candidates?: GbtCandidates) => Promise<void>): void {
|
||||
this.$asyncMempoolChangedCallback = fn;
|
||||
}
|
||||
|
@ -362,12 +363,15 @@ class Mempool {
|
|||
|
||||
const candidatesChanged = candidates?.added?.length || candidates?.removed?.length;
|
||||
|
||||
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
||||
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta);
|
||||
this.recentlyDeleted.unshift(deletedTransactions);
|
||||
this.recentlyDeleted.length = Math.min(this.recentlyDeleted.length, 10); // truncate to the last 10 mempool updates
|
||||
|
||||
if (this.mempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length)) {
|
||||
this.mempoolChangedCallback(this.mempoolCache, newTransactions, this.recentlyDeleted, accelerationDelta);
|
||||
}
|
||||
if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length || candidatesChanged)) {
|
||||
if (this.$asyncMempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length || candidatesChanged)) {
|
||||
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');
|
||||
}
|
||||
|
||||
|
@ -541,16 +545,7 @@ class Mempool {
|
|||
}
|
||||
}
|
||||
|
||||
public handleRbfTransactions(rbfTransactions: { [txid: string]: MempoolTransactionExtended[]; }): void {
|
||||
for (const rbfTransaction in rbfTransactions) {
|
||||
if (this.mempoolCache[rbfTransaction] && rbfTransactions[rbfTransaction]?.length) {
|
||||
// Store replaced transactions
|
||||
rbfCache.add(rbfTransactions[rbfTransaction], this.mempoolCache[rbfTransaction]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public handleMinedRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void {
|
||||
public handleRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void {
|
||||
for (const rbfTransaction in rbfTransactions) {
|
||||
if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) {
|
||||
// Store replaced transactions
|
||||
|
|
|
@ -10,6 +10,7 @@ import mining from "./mining";
|
|||
import PricesRepository from '../../repositories/PricesRepository';
|
||||
import AccelerationRepository from '../../repositories/AccelerationRepository';
|
||||
import accelerationApi from '../services/acceleration';
|
||||
import { handleError } from '../../utils/api';
|
||||
|
||||
class MiningRoutes {
|
||||
public initRoutes(app: Application) {
|
||||
|
@ -53,12 +54,12 @@ class MiningRoutes {
|
|||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
||||
if (['testnet', 'signet', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) {
|
||||
res.status(400).send('Prices are not available on testnets.');
|
||||
handleError(req, res, 400, 'Prices are not available on testnets.');
|
||||
return;
|
||||
}
|
||||
const timestamp = parseInt(req.query.timestamp as string, 10) || 0;
|
||||
const currency = req.query.currency as string;
|
||||
|
||||
|
||||
let response;
|
||||
if (timestamp && currency) {
|
||||
response = await PricesRepository.$getNearestHistoricalPrice(timestamp, currency);
|
||||
|
@ -71,7 +72,7 @@ class MiningRoutes {
|
|||
}
|
||||
res.status(200).send(response);
|
||||
} 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);
|
||||
} catch (e) {
|
||||
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 {
|
||||
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);
|
||||
} catch (e) {
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
} 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.json(stats);
|
||||
} 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.json(hashrates);
|
||||
} 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);
|
||||
} catch (e) {
|
||||
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 {
|
||||
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,
|
||||
});
|
||||
} 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.json(blockFees);
|
||||
} 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.json(blockFees);
|
||||
} 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.json(blockRewards);
|
||||
} 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.json(blockFeeRates);
|
||||
} 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
|
||||
});
|
||||
} 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.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment]));
|
||||
} 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.json(blocksHealth.map(health => [health.time, health.height, health.match_rate]));
|
||||
} 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);
|
||||
|
||||
if (!audit) {
|
||||
res.status(204).send(`This block has not been audited.`);
|
||||
handleError(req, res, 204, `This block has not been audited.`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -335,7 +336,7 @@ class MiningRoutes {
|
|||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
|
||||
res.json(audit);
|
||||
} 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.json(result);
|
||||
} 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.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15));
|
||||
} 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.json(audit || 'null');
|
||||
} 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
||||
res.status(400).send('Acceleration data is not available.');
|
||||
handleError(req, res, 400, 'Acceleration data is not available.');
|
||||
return;
|
||||
}
|
||||
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug));
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -409,13 +410,13 @@ class MiningRoutes {
|
|||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
|
||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
||||
res.status(400).send('Acceleration data is not available.');
|
||||
handleError(req, res, 400, 'Acceleration data is not available.');
|
||||
return;
|
||||
}
|
||||
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
|
||||
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height));
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -425,12 +426,12 @@ class MiningRoutes {
|
|||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
||||
res.status(400).send('Acceleration data is not available.');
|
||||
handleError(req, res, 400, 'Acceleration data is not available.');
|
||||
return;
|
||||
}
|
||||
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval));
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -440,12 +441,12 @@ class MiningRoutes {
|
|||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
||||
res.status(400).send('Acceleration data is not available.');
|
||||
handleError(req, res, 400, 'Acceleration data is not available.');
|
||||
return;
|
||||
}
|
||||
res.status(200).send(await AccelerationRepository.$getAccelerationTotals(<string>req.query.pool, <string>req.query.interval));
|
||||
} 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
||||
res.status(400).send('Acceleration data is not available.');
|
||||
handleError(req, res, 400, 'Acceleration data is not available.');
|
||||
return;
|
||||
}
|
||||
res.status(200).send(accelerationApi.accelerations || []);
|
||||
} 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);
|
||||
res.status(200).send();
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,6 +44,22 @@ interface CacheEvent {
|
|||
value?: any,
|
||||
}
|
||||
|
||||
/**
|
||||
* Singleton for tracking RBF trees
|
||||
*
|
||||
* Maintains a set of RBF trees, where each tree represents a sequence of
|
||||
* consecutive RBF replacements.
|
||||
*
|
||||
* Trees are identified by the txid of the root transaction.
|
||||
*
|
||||
* To maintain consistency, the following invariants must be upheld:
|
||||
* - Symmetry: replacedBy(A) = B <=> A in replaces(B)
|
||||
* - Unique id: treeMap(treeMap(X)) = treeMap(X)
|
||||
* - Unique tree: A in replaces(B) => treeMap(A) == treeMap(B)
|
||||
* - Existence: X in treeMap => treeMap(X) in rbfTrees
|
||||
* - Completeness: X in replacedBy => X in treeMap, Y in replaces => Y in treeMap
|
||||
*/
|
||||
|
||||
class RbfCache {
|
||||
private replacedBy: Map<string, string> = new Map();
|
||||
private replaces: Map<string, string[]> = new Map();
|
||||
|
@ -61,6 +77,10 @@ class RbfCache {
|
|||
setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Low level cache operations
|
||||
*/
|
||||
|
||||
private addTx(txid: string, tx: MempoolTransactionExtended): void {
|
||||
this.txs.set(txid, tx);
|
||||
this.cacheQueue.push({ op: CacheOp.Add, type: 'tx', txid });
|
||||
|
@ -92,6 +112,12 @@ class RbfCache {
|
|||
this.cacheQueue.push({ op: CacheOp.Remove, type: 'exp', txid });
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic data structure operations
|
||||
* must uphold tree invariants
|
||||
*/
|
||||
|
||||
|
||||
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
|
||||
if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
|
||||
return;
|
||||
|
@ -114,6 +140,10 @@ class RbfCache {
|
|||
if (!replacedTx.rbf) {
|
||||
txFullRbf = true;
|
||||
}
|
||||
if (this.replacedBy.has(replacedTx.txid)) {
|
||||
// should never happen
|
||||
continue;
|
||||
}
|
||||
this.replacedBy.set(replacedTx.txid, newTx.txid);
|
||||
if (this.treeMap.has(replacedTx.txid)) {
|
||||
const treeId = this.treeMap.get(replacedTx.txid);
|
||||
|
@ -140,18 +170,47 @@ class RbfCache {
|
|||
}
|
||||
}
|
||||
newTx.fullRbf = txFullRbf;
|
||||
const treeId = replacedTrees[0].tx.txid;
|
||||
const newTree = {
|
||||
tx: newTx,
|
||||
time: newTime,
|
||||
fullRbf: treeFullRbf,
|
||||
replaces: replacedTrees
|
||||
};
|
||||
this.addTree(treeId, newTree);
|
||||
this.updateTreeMap(treeId, newTree);
|
||||
this.addTree(newTree.tx.txid, newTree);
|
||||
this.updateTreeMap(newTree.tx.txid, newTree);
|
||||
this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid));
|
||||
}
|
||||
|
||||
public mined(txid): void {
|
||||
if (!this.txs.has(txid)) {
|
||||
return;
|
||||
}
|
||||
const treeId = this.treeMap.get(txid);
|
||||
if (treeId && this.rbfTrees.has(treeId)) {
|
||||
const tree = this.rbfTrees.get(treeId);
|
||||
if (tree) {
|
||||
this.setTreeMined(tree, txid);
|
||||
tree.mined = true;
|
||||
this.dirtyTrees.add(treeId);
|
||||
this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId });
|
||||
}
|
||||
}
|
||||
this.evict(txid);
|
||||
}
|
||||
|
||||
// flag a transaction as removed from the mempool
|
||||
public evict(txid: string, fast: boolean = false): void {
|
||||
this.evictionCount++;
|
||||
if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) {
|
||||
const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours
|
||||
this.addExpiration(txid, expiryTime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read-only public interface
|
||||
*/
|
||||
|
||||
public has(txId: string): boolean {
|
||||
return this.txs.has(txId);
|
||||
}
|
||||
|
@ -232,32 +291,6 @@ class RbfCache {
|
|||
return changes;
|
||||
}
|
||||
|
||||
public mined(txid): void {
|
||||
if (!this.txs.has(txid)) {
|
||||
return;
|
||||
}
|
||||
const treeId = this.treeMap.get(txid);
|
||||
if (treeId && this.rbfTrees.has(treeId)) {
|
||||
const tree = this.rbfTrees.get(treeId);
|
||||
if (tree) {
|
||||
this.setTreeMined(tree, txid);
|
||||
tree.mined = true;
|
||||
this.dirtyTrees.add(treeId);
|
||||
this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId });
|
||||
}
|
||||
}
|
||||
this.evict(txid);
|
||||
}
|
||||
|
||||
// flag a transaction as removed from the mempool
|
||||
public evict(txid: string, fast: boolean = false): void {
|
||||
this.evictionCount++;
|
||||
if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) {
|
||||
const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours
|
||||
this.addExpiration(txid, expiryTime);
|
||||
}
|
||||
}
|
||||
|
||||
// is the transaction involved in a full rbf replacement?
|
||||
public isFullRbf(txid: string): boolean {
|
||||
const treeId = this.treeMap.get(txid);
|
||||
|
@ -271,6 +304,10 @@ class RbfCache {
|
|||
return tree?.fullRbf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache maintenance & utility functions
|
||||
*/
|
||||
|
||||
private cleanup(): void {
|
||||
const now = Date.now();
|
||||
for (const txid of this.expiring.keys()) {
|
||||
|
@ -299,10 +336,6 @@ class RbfCache {
|
|||
for (const tx of (replaces || [])) {
|
||||
// recursively remove prior versions from the cache
|
||||
this.replacedBy.delete(tx);
|
||||
// if this is the id of a tree, remove that too
|
||||
if (this.treeMap.get(tx) === tx) {
|
||||
this.removeTree(tx);
|
||||
}
|
||||
this.remove(tx);
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
txs.forEach(txEntry => {
|
||||
this.txs.set(txEntry.value.txid, txEntry.value);
|
||||
});
|
||||
this.staleCount = 0;
|
||||
for (const deflatedTree of trees) {
|
||||
await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
|
||||
for (const deflatedTree of trees.sort((a, b) => Object.keys(b).length - Object.keys(a).length)) {
|
||||
const tree = await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
|
||||
if (tree) {
|
||||
this.addTree(tree.tx.txid, tree);
|
||||
this.updateTreeMap(tree.tx.txid, tree);
|
||||
if (tree.mined) {
|
||||
this.evict(tree.tx.txid);
|
||||
}
|
||||
}
|
||||
}
|
||||
expiring.forEach(expiringEntry => {
|
||||
if (this.txs.has(expiringEntry.key)) {
|
||||
|
@ -385,6 +425,31 @@ class RbfCache {
|
|||
}
|
||||
});
|
||||
this.staleCount = 0;
|
||||
|
||||
// connect cached trees to current mempool transactions
|
||||
const conflicts: Record<string, { replacedBy: MempoolTransactionExtended, replaces: Set<MempoolTransactionExtended> }> = {};
|
||||
for (const tree of this.rbfTrees.values()) {
|
||||
const tx = this.getTx(tree.tx.txid);
|
||||
if (!tx || tree.mined) {
|
||||
continue;
|
||||
}
|
||||
for (const vin of tx.vin) {
|
||||
const conflict = spendMap.get(`${vin.txid}:${vin.vout}`);
|
||||
if (conflict && conflict.txid !== tx.txid) {
|
||||
if (!conflicts[conflict.txid]) {
|
||||
conflicts[conflict.txid] = {
|
||||
replacedBy: conflict,
|
||||
replaces: new Set(),
|
||||
};
|
||||
}
|
||||
conflicts[conflict.txid].replaces.add(tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const { replacedBy, replaces } of Object.values(conflicts)) {
|
||||
this.add([...replaces.values()], replacedBy);
|
||||
}
|
||||
|
||||
await this.checkTrees();
|
||||
logger.debug(`loaded ${txs.length} txs, ${trees.length} trees into rbf cache, ${expiring.length} due to expire, ${this.staleCount} were stale`);
|
||||
this.cleanup();
|
||||
|
@ -426,6 +491,12 @@ class RbfCache {
|
|||
return;
|
||||
}
|
||||
|
||||
// if this tx is already in the cache, return early
|
||||
if (this.treeMap.has(txid)) {
|
||||
this.removeTree(deflated.key);
|
||||
return;
|
||||
}
|
||||
|
||||
// recursively reconstruct child trees
|
||||
for (const childId of treeInfo.replaces) {
|
||||
const replaced = await this.importTree(mempool, root, childId, deflated, txs, mined);
|
||||
|
@ -457,10 +528,6 @@ class RbfCache {
|
|||
fullRbf: treeInfo.fullRbf,
|
||||
replaces,
|
||||
};
|
||||
this.treeMap.set(txid, root);
|
||||
if (root === txid) {
|
||||
this.addTree(root, tree);
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
|
@ -511,6 +578,7 @@ class RbfCache {
|
|||
processTxs(txs);
|
||||
}
|
||||
|
||||
// evict missing transactions
|
||||
for (const txid of txids) {
|
||||
if (!found[txid]) {
|
||||
this.evict(txid, false);
|
||||
|
|
|
@ -365,6 +365,7 @@ class RedisCache {
|
|||
trees: rbfTrees.map(loadedTree => { loadedTree.value.key = loadedTree.key; return loadedTree.value; }),
|
||||
expiring: rbfExpirations,
|
||||
mempool: memPool.getMempool(),
|
||||
spendMap: memPool.getSpendMap(),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -121,6 +121,7 @@ class TransactionUtils {
|
|||
const adjustedVsize = Math.max(fractionalVsize, sigops * 5); // adjusted vsize = Max(weight, sigops * bytes_per_sigop) / witness_scale_factor
|
||||
const feePerVbytes = (transaction.fee || 0) / fractionalVsize;
|
||||
const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize;
|
||||
const effectiveFeePerVsize = transaction['effectiveFeePerVsize'] || adjustedFeePerVsize || feePerVbytes;
|
||||
const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, {
|
||||
order: this.txidToOrdering(transaction.txid),
|
||||
vsize,
|
||||
|
@ -128,7 +129,7 @@ class TransactionUtils {
|
|||
sigops,
|
||||
feePerVsize: feePerVbytes,
|
||||
adjustedFeePerVsize: adjustedFeePerVsize,
|
||||
effectiveFeePerVsize: adjustedFeePerVsize,
|
||||
effectiveFeePerVsize: effectiveFeePerVsize,
|
||||
});
|
||||
if (!transactionExtended?.status?.confirmed && !transactionExtended.firstSeen) {
|
||||
transactionExtended.firstSeen = Math.round((Date.now() / 1000));
|
||||
|
@ -338,6 +339,87 @@ class TransactionUtils {
|
|||
const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2;
|
||||
return witness[positionOfScript];
|
||||
}
|
||||
|
||||
// calculate the most parsimonious set of prioritizations given a list of block transactions
|
||||
// (i.e. the most likely prioritizations and deprioritizations)
|
||||
public identifyPrioritizedTransactions(transactions: any[], rateKey: string): { prioritized: string[], deprioritized: string[] } {
|
||||
// find the longest increasing subsequence of transactions
|
||||
// (adapted from https://en.wikipedia.org/wiki/Longest_increasing_subsequence#Efficient_algorithms)
|
||||
// should be O(n log n)
|
||||
const X = transactions.slice(1).reverse().map((tx) => ({ txid: tx.txid, rate: tx[rateKey] })); // standard block order is by *decreasing* effective fee rate, but we want to iterate in increasing order (and skip the coinbase)
|
||||
if (X.length < 2) {
|
||||
return { prioritized: [], deprioritized: [] };
|
||||
}
|
||||
const N = X.length;
|
||||
const P: number[] = new Array(N);
|
||||
const M: number[] = new Array(N + 1);
|
||||
M[0] = -1; // undefined so can be set to any value
|
||||
|
||||
let L = 0;
|
||||
for (let i = 0; i < N; i++) {
|
||||
// Binary search for the smallest positive l ≤ L
|
||||
// such that X[M[l]].effectiveFeePerVsize > X[i].effectiveFeePerVsize
|
||||
let lo = 1;
|
||||
let hi = L + 1;
|
||||
while (lo < hi) {
|
||||
const mid = lo + Math.floor((hi - lo) / 2); // lo <= mid < hi
|
||||
if (X[M[mid]].rate > X[i].rate) {
|
||||
hi = mid;
|
||||
} else { // if X[M[mid]].effectiveFeePerVsize < X[i].effectiveFeePerVsize
|
||||
lo = mid + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// After searching, lo == hi is 1 greater than the
|
||||
// length of the longest prefix of X[i]
|
||||
const newL = lo;
|
||||
|
||||
// The predecessor of X[i] is the last index of
|
||||
// the subsequence of length newL-1
|
||||
P[i] = M[newL - 1];
|
||||
M[newL] = i;
|
||||
|
||||
if (newL > L) {
|
||||
// If we found a subsequence longer than any we've
|
||||
// found yet, update L
|
||||
L = newL;
|
||||
}
|
||||
}
|
||||
|
||||
// Reconstruct the longest increasing subsequence
|
||||
// It consists of the values of X at the L indices:
|
||||
// ..., P[P[M[L]]], P[M[L]], M[L]
|
||||
const LIS: any[] = new Array(L);
|
||||
let k = M[L];
|
||||
for (let j = L - 1; j >= 0; j--) {
|
||||
LIS[j] = X[k];
|
||||
k = P[k];
|
||||
}
|
||||
|
||||
const lisMap = new Map<string, number>();
|
||||
LIS.forEach((tx, index) => lisMap.set(tx.txid, index));
|
||||
|
||||
const prioritized: string[] = [];
|
||||
const deprioritized: string[] = [];
|
||||
|
||||
let lastRate = X[0].rate;
|
||||
|
||||
for (const tx of X) {
|
||||
if (lisMap.has(tx.txid)) {
|
||||
lastRate = tx.rate;
|
||||
} else {
|
||||
if (Math.abs(tx.rate - lastRate) < 0.1) {
|
||||
// skip if the rate is almost the same as the previous transaction
|
||||
} else if (tx.rate <= lastRate) {
|
||||
prioritized.push(tx.txid);
|
||||
} else {
|
||||
deprioritized.push(tx.txid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { prioritized, deprioritized };
|
||||
}
|
||||
}
|
||||
|
||||
export default new TransactionUtils();
|
||||
|
|
|
@ -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,
|
||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[],
|
||||
newTransactions: MempoolTransactionExtended[], recentlyDeletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[],
|
||||
candidates?: GbtCandidates): Promise<void> {
|
||||
if (!this.webSocketServers.length) {
|
||||
throw new Error('No WebSocket.Server have been set');
|
||||
|
@ -529,6 +538,8 @@ class WebsocketHandler {
|
|||
|
||||
this.printLogs();
|
||||
|
||||
const deletedTransactions = recentlyDeletedTransactions.length ? recentlyDeletedTransactions[0] : [];
|
||||
|
||||
const transactionIds = (memPool.limitGBT && candidates) ? Object.keys(candidates?.txs || {}) : Object.keys(newMempool);
|
||||
let added = newTransactions;
|
||||
let removed = deletedTransactions;
|
||||
|
@ -547,7 +558,7 @@ class WebsocketHandler {
|
|||
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
|
||||
const mempoolInfo = memPool.getMempoolInfo();
|
||||
const vBytesPerSecond = memPool.getVBytesPerSecond();
|
||||
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
|
||||
const rbfTransactions = Common.findRbfTransactions(newTransactions, recentlyDeletedTransactions.flat());
|
||||
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||
const accelerations = memPool.getAccelerations();
|
||||
memPool.handleRbfTransactions(rbfTransactions);
|
||||
|
@ -578,7 +589,7 @@ class WebsocketHandler {
|
|||
const replacedTransactions: { replaced: string, by: TransactionExtended }[] = [];
|
||||
for (const tx of newTransactions) {
|
||||
if (rbfTransactions[tx.txid]) {
|
||||
for (const replaced of rbfTransactions[tx.txid]) {
|
||||
for (const replaced of rbfTransactions[tx.txid].replaced) {
|
||||
replacedTransactions.push({ replaced: replaced.txid, by: tx });
|
||||
}
|
||||
}
|
||||
|
@ -947,7 +958,7 @@ class WebsocketHandler {
|
|||
await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions));
|
||||
|
||||
const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
|
||||
memPool.handleMinedRbfTransactions(rbfTransactions);
|
||||
memPool.handleRbfTransactions(rbfTransactions);
|
||||
memPool.removeFromSpendMap(transactions);
|
||||
|
||||
if (config.MEMPOOL.AUDIT && memPool.isInSync()) {
|
||||
|
|
|
@ -195,7 +195,7 @@ const defaults: IConfig = {
|
|||
'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
|
||||
'POOLS_UPDATE_DELAY': 604800, // in seconds, default is one week
|
||||
'AUDIT': false,
|
||||
'RUST_GBT': false,
|
||||
'RUST_GBT': true,
|
||||
'LIMIT_GBT': false,
|
||||
'CPFP_INDEXING': false,
|
||||
'MAX_BLOCKS_BULK_QUERY': 0,
|
||||
|
|
|
@ -299,6 +299,7 @@ export interface BlockExtension {
|
|||
id: number; // Note - This is the `unique_id`, not to mix with the auto increment `id`
|
||||
name: string;
|
||||
slug: string;
|
||||
minerNames: string[] | null;
|
||||
};
|
||||
avgFee: number;
|
||||
avgFeeRate: number;
|
||||
|
|
|
@ -14,6 +14,7 @@ import chainTips from '../api/chain-tips';
|
|||
import blocks from '../api/blocks';
|
||||
import BlocksAuditsRepository from './BlocksAuditsRepository';
|
||||
import transactionUtils from '../api/transaction-utils';
|
||||
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
|
||||
|
||||
interface DatabaseBlock {
|
||||
id: string;
|
||||
|
@ -1054,6 +1055,7 @@ class BlocksRepository {
|
|||
id: dbBlk.poolId,
|
||||
name: dbBlk.poolName,
|
||||
slug: dbBlk.poolSlug,
|
||||
minerNames: null,
|
||||
};
|
||||
extras.avgFee = dbBlk.avgFee;
|
||||
extras.avgFeeRate = dbBlk.avgFeeRate;
|
||||
|
@ -1106,7 +1108,7 @@ class BlocksRepository {
|
|||
let summaryVersion = 0;
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
const txs = (await bitcoinApi.$getTxsForBlock(dbBlk.id)).map(tx => transactionUtils.extendTransaction(tx));
|
||||
summary = blocks.summarizeBlockTransactions(dbBlk.id, txs);
|
||||
summary = blocks.summarizeBlockTransactions(dbBlk.id, dbBlk.height, txs);
|
||||
summaryVersion = 1;
|
||||
} else {
|
||||
// Call Core RPC
|
||||
|
@ -1123,6 +1125,10 @@ class BlocksRepository {
|
|||
}
|
||||
}
|
||||
|
||||
if (extras.pool.name === 'OCEAN') {
|
||||
extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw);
|
||||
}
|
||||
|
||||
blk.extras = <BlockExtension>extras;
|
||||
return <BlockExtended>blk;
|
||||
}
|
||||
|
|
9
backend/src/utils/api.ts
Normal file
9
backend/src/utils/api.ts
Normal 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);
|
||||
}
|
||||
}
|
|
@ -158,7 +158,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
|
|||
if (!opN) {
|
||||
return;
|
||||
}
|
||||
if (!opN.startsWith('OP_PUSHNUM_')) {
|
||||
if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
|
||||
return;
|
||||
}
|
||||
const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10);
|
||||
|
@ -178,7 +178,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
|
|||
if (!opM) {
|
||||
return;
|
||||
}
|
||||
if (!opM.startsWith('OP_PUSHNUM_')) {
|
||||
if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
|
||||
return;
|
||||
}
|
||||
const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10);
|
||||
|
@ -200,4 +200,28 @@ export function getVarIntLength(n: number): number {
|
|||
} else {
|
||||
return 9;
|
||||
}
|
||||
}
|
||||
|
||||
/** Extracts miner names from a DATUM coinbase transaction */
|
||||
export function parseDATUMTemplateCreator(coinbaseRaw: string): string[] | null {
|
||||
let bytes: number[] = [];
|
||||
for (let c = 0; c < coinbaseRaw.length; c += 2) {
|
||||
bytes.push(parseInt(coinbaseRaw.slice(c, c + 2), 16));
|
||||
}
|
||||
|
||||
// Skip block height
|
||||
let tagLengthByte = 1 + bytes[0];
|
||||
|
||||
let tagsLength = bytes[tagLengthByte];
|
||||
if (tagsLength == 0x4c) {
|
||||
tagLengthByte += 1;
|
||||
tagsLength = bytes[tagLengthByte];
|
||||
}
|
||||
|
||||
const tagStart = tagLengthByte + 1;
|
||||
const tags = bytes.slice(tagStart, tagStart + tagsLength);
|
||||
let tagString = String.fromCharCode(...tags);
|
||||
tagString = tagString.replace('\x00', '');
|
||||
|
||||
return tagString.split('\x0f').map((name) => name.replace(/[^a-zA-Z0-9 ]/g, ''));
|
||||
}
|
|
@ -31,7 +31,7 @@ __MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubuserconte
|
|||
__MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master}
|
||||
__MEMPOOL_POOLS_UPDATE_DELAY__=${MEMPOOL_POOLS_UPDATE_DELAY:=604800}
|
||||
__MEMPOOL_AUDIT__=${MEMPOOL_AUDIT:=false}
|
||||
__MEMPOOL_RUST_GBT__=${MEMPOOL_RUST_GBT:=false}
|
||||
__MEMPOOL_RUST_GBT__=${MEMPOOL_RUST_GBT:=true}
|
||||
__MEMPOOL_LIMIT_GBT__=${MEMPOOL_LIMIT_GBT:=false}
|
||||
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
|
||||
__MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0}
|
||||
|
|
|
@ -33,7 +33,7 @@ $ npm run config:defaults:liquid
|
|||
|
||||
### 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:
|
||||
|
||||
|
@ -70,7 +70,7 @@ Set up the [Mempool backend](../backend/) first, if you haven't already.
|
|||
|
||||
### 1. Build the Frontend
|
||||
|
||||
_Make sure to use Node.js 16.10 and npm 7._
|
||||
_Make sure to use Node.js 20.x and npm 9.x or newer._
|
||||
|
||||
Build the frontend:
|
||||
|
||||
|
|
|
@ -54,6 +54,10 @@
|
|||
"translation": "src/locale/messages.fr.xlf",
|
||||
"baseHref": "/fr/"
|
||||
},
|
||||
"hr": {
|
||||
"translation": "src/locale/messages.hr.xlf",
|
||||
"baseHref": "/hr/"
|
||||
},
|
||||
"ja": {
|
||||
"translation": "src/locale/messages.ja.xlf",
|
||||
"baseHref": "/ja/"
|
||||
|
|
|
@ -750,7 +750,7 @@
|
|||
},
|
||||
"backendInfo": {
|
||||
"hostname": "node205.tk7.mempool.space",
|
||||
"version": "3.0.0-beta",
|
||||
"version": "3.1.0-dev",
|
||||
"gitCommit": "abbc8a134",
|
||||
"lightning": false
|
||||
},
|
||||
|
|
1384
frontend/package-lock.json
generated
1384
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "mempool-frontend",
|
||||
"version": "3.0.0-beta",
|
||||
"version": "3.1.0-dev",
|
||||
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
||||
"license": "GNU Affero General Public License v3.0",
|
||||
"homepage": "https://mempool.space",
|
||||
|
@ -92,10 +92,10 @@
|
|||
"ngx-infinite-scroll": "^17.0.0",
|
||||
"qrcode": "1.5.1",
|
||||
"rxjs": "~7.8.1",
|
||||
"esbuild": "^0.23.0",
|
||||
"esbuild": "^0.24.0",
|
||||
"tinyify": "^4.0.0",
|
||||
"tlite": "^0.1.9",
|
||||
"tslib": "~2.6.0",
|
||||
"tslib": "~2.7.0",
|
||||
"zone.js": "~0.14.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -115,7 +115,7 @@
|
|||
"optionalDependencies": {
|
||||
"@cypress/schematic": "^2.5.0",
|
||||
"@types/cypress": "^1.1.3",
|
||||
"cypress": "^13.13.0",
|
||||
"cypress": "^13.15.0",
|
||||
"cypress-fail-on-console-error": "~5.1.0",
|
||||
"cypress-wait-until": "^2.0.1",
|
||||
"mock-socket": "~9.3.1",
|
||||
|
|
|
@ -21,6 +21,7 @@ import { StorageService } from './services/storage.service';
|
|||
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
|
||||
import { LanguageService } from './services/language.service';
|
||||
import { ThemeService } from './services/theme.service';
|
||||
import { TimeService } from './services/time.service';
|
||||
import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe';
|
||||
import { FiatCurrencyPipe } from './shared/pipes/fiat-currency.pipe';
|
||||
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
|
||||
|
@ -42,6 +43,7 @@ const providers = [
|
|||
EnterpriseService,
|
||||
LanguageService,
|
||||
ThemeService,
|
||||
TimeService,
|
||||
ShortenStringPipe,
|
||||
FiatShortenerPipe,
|
||||
FiatCurrencyPipe,
|
||||
|
|
|
@ -135,7 +135,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
|
|||
return;
|
||||
}
|
||||
const opN = ops.pop();
|
||||
if (!opN.startsWith('OP_PUSHNUM_')) {
|
||||
if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
|
||||
return;
|
||||
}
|
||||
const n = parseInt(opN.match(/[0-9]+/)[0], 10);
|
||||
|
@ -152,7 +152,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
|
|||
}
|
||||
}
|
||||
const opM = ops.pop();
|
||||
if (!opM.startsWith('OP_PUSHNUM_')) {
|
||||
if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
|
||||
return;
|
||||
}
|
||||
const m = parseInt(opM.match(/[0-9]+/)[0], 10);
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
<span>Spiral</span>
|
||||
</a>
|
||||
<a href="https://foundrydigital.com/" target="_blank" title="Foundry">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" style="zoom: 1;" width="32" height="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>
|
||||
<style>
|
||||
.d {
|
||||
|
@ -125,17 +125,14 @@
|
|||
<span>Blockstream</span>
|
||||
</a>
|
||||
<a href="https://unchained.com/" target="_blank" title="Unchained">
|
||||
<svg id="Layer_1" width="78" height="78" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 156.68 156.68"><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>
|
||||
</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 href="https://bitkey.world/" target="_blank" title="Bitkey">
|
||||
<img class="image" src="/resources/profile/bitkey.svg" />
|
||||
<span>Bitkey</span>
|
||||
</a>
|
||||
<a href="https://bullbitcoin.com/" target="_blank" title="Bull Bitcoin">
|
||||
<svg aria-hidden="true" class="image" viewBox="0 -5 40 40" xmlns="http://www.w3.org/2000/svg">
|
||||
|
@ -150,7 +147,7 @@
|
|||
<span>Bull Bitcoin</span>
|
||||
</a>
|
||||
<a href="https://exodus.com/" target="_blank" title="Exodus">
|
||||
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<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"/>
|
||||
<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)"/>
|
||||
|
@ -191,6 +188,19 @@
|
|||
</svg>
|
||||
<span>Exodus</span>
|
||||
</a>
|
||||
<a href="https://gemini.com/" target="_blank" title="Gemini">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360" class="image">
|
||||
<rect style="fill: black" width="360" height="360" />
|
||||
<g transform="matrix(0.62 0 0 0.62 180 180)">
|
||||
<path style="fill: rgb(0,220,250)" transform=" translate(-162, -162)" d="M 211.74 0 C 154.74 0 106.35 43.84 100.25 100.25 C 43.84 106.35 1.4210854715202004e-14 154.76 1.4210854715202004e-14 211.74 C 0.044122601308501076 273.7212006364817 50.27879936351834 323.95587739869154 112.26 324 C 169.26 324 217.84 280.15999999999997 223.75 223.75 C 280.15999999999997 217.65 324 169.24 324 112.26 C 323.95587739869154 50.278799363518324 273.72120063648174 0.04412260130848722 211.74 -1.4210854715202004e-14 z M 297.74 124.84 C 291.9644950552469 162.621439649343 262.2969457716857 192.26062994820046 224.51 198 L 224.51 124.84 z M 26.3 199.16 C 31.986912917108594 161.30935034910615 61.653433460549415 131.56986937804106 99.48999999999998 125.78999999999999 L 99.49 199 L 26.3 199 z M 198.21 224.51 C 191.87736076583954 267.0991541201681 155.312384597087 298.62923417787493 112.255 298.62923417787493 C 69.19761540291302 298.62923417787493 32.63263923416048 267.0991541201682 26.3 224.51 z M 199.16 124.83999999999999 L 199.16 199 L 124.84 199 L 124.84 124.84 z M 297.7 99.48999999999998 L 125.78999999999999 99.48999999999998 C 132.12263923416046 56.90084587983182 168.687615402913 25.37076582212505 211.745 25.37076582212505 C 254.80238459708698 25.37076582212505 291.3673607658395 56.900845879831834 297.7 99.49 z" stroke-linecap="round" />
|
||||
</g>
|
||||
</svg>
|
||||
<span>Gemini</span>
|
||||
</a>
|
||||
<a href="https://leather.io/" target="_blank" title="Leather">
|
||||
<img class="image" src="/resources/profile/leather.svg" />
|
||||
<span>Leather</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -435,7 +445,7 @@
|
|||
Trademark Notice<br>
|
||||
</div>
|
||||
<p>
|
||||
The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem®, Mempool Goggles™, the mempool logo, the mempool Square logo, the mempool 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®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem®, Mempool Goggles™, the mempool Logo, the mempool Square Logo, the mempool block visualization Logo, the mempool Blocks Logo, the mempool transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo, the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
|
||||
</p>
|
||||
<p>
|
||||
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on <https://mempool.space/trademark-policy>.
|
||||
|
|
|
@ -13,8 +13,6 @@
|
|||
|
||||
.image.not-rounded {
|
||||
border-radius: 0;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.intro {
|
||||
|
@ -158,9 +156,8 @@
|
|||
margin: 40px 29px 10px;
|
||||
&.image.coldcard {
|
||||
border-radius: 0;
|
||||
width: auto;
|
||||
max-height: 50px;
|
||||
margin: 40px 29px 14px 29px;
|
||||
height: auto;
|
||||
margin: 20px 29px 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -254,3 +251,12 @@
|
|||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.enterprise-sponsor {
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
max-width: 800px;
|
||||
}
|
||||
}
|
|
@ -75,6 +75,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||
@Output() changeMode = new EventEmitter<boolean>();
|
||||
|
||||
calculating = true;
|
||||
processing = false;
|
||||
selectedOption: 'wait' | 'accel';
|
||||
cantPayReason = '';
|
||||
quoteError = ''; // error fetching estimate or initial data
|
||||
|
@ -196,9 +197,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||
if (changes.scrollEvent && this.scrollEvent) {
|
||||
this.scrollToElement('acceleratePreviewAnchor', 'start');
|
||||
}
|
||||
if (changes.accelerating) {
|
||||
if ((this.step === 'processing' || this.step === 'paid') && this.accelerating) {
|
||||
if (changes.accelerating && this.accelerating) {
|
||||
if (this.step === 'processing' || this.step === 'paid') {
|
||||
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
|
||||
*/
|
||||
accelerateWithMempoolAccount(): void {
|
||||
if (!this.canPay || this.calculating) {
|
||||
if (!this.canPay || this.calculating || this.processing) {
|
||||
return;
|
||||
}
|
||||
this.processing = true;
|
||||
if (this.accelerationSubscription) {
|
||||
this.accelerationSubscription.unsubscribe();
|
||||
}
|
||||
|
@ -390,6 +394,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||
this.accelerationUUID
|
||||
).subscribe({
|
||||
next: () => {
|
||||
this.processing = false;
|
||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||
this.audioService.playSound('ascend-chime-cartoon');
|
||||
this.showSuccess = true;
|
||||
|
@ -397,6 +402,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||
this.moveToStep('paid');
|
||||
},
|
||||
error: (response) => {
|
||||
this.processing = false;
|
||||
this.accelerateError = response.error;
|
||||
}
|
||||
});
|
||||
|
@ -466,10 +472,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||
* APPLE PAY
|
||||
*/
|
||||
async requestApplePayPayment(): Promise<void> {
|
||||
if (this.processing) {
|
||||
return;
|
||||
}
|
||||
if (this.conversionsSubscription) {
|
||||
this.conversionsSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
this.processing = true;
|
||||
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||
async (conversions) => {
|
||||
this.conversions = conversions;
|
||||
|
@ -494,6 +504,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||
console.error(`Unable to find apple pay button id='apple-pay-button'`);
|
||||
// Try again
|
||||
setTimeout(this.requestApplePayPayment.bind(this), 500);
|
||||
this.processing = false;
|
||||
return;
|
||||
}
|
||||
this.loadingApplePay = false;
|
||||
|
@ -505,6 +516,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
||||
console.error(`Cannot retreive payment card details`);
|
||||
this.accelerateError = 'apple_pay_no_card_details';
|
||||
this.processing = false;
|
||||
return;
|
||||
}
|
||||
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
|
||||
|
@ -516,6 +528,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||
this.accelerationUUID
|
||||
).subscribe({
|
||||
next: () => {
|
||||
this.processing = false;
|
||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||
this.audioService.playSound('ascend-chime-cartoon');
|
||||
if (this.applePay) {
|
||||
|
@ -526,6 +539,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||
}, 1000);
|
||||
},
|
||||
error: (response) => {
|
||||
this.processing = false;
|
||||
this.accelerateError = response.error;
|
||||
if (!(response.status === 403 && response.error === 'not_available')) {
|
||||
setTimeout(() => {
|
||||
|
@ -537,6 +551,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||
}
|
||||
});
|
||||
} else {
|
||||
this.processing = false;
|
||||
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
|
||||
if (tokenResult.errors) {
|
||||
errorMessage += ` and errors: ${JSON.stringify(
|
||||
|
@ -547,6 +562,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||
}
|
||||
});
|
||||
} catch (e) {
|
||||
this.processing = false;
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
@ -557,10 +573,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||
* GOOGLE PAY
|
||||
*/
|
||||
async requestGooglePayPayment(): Promise<void> {
|
||||
if (this.processing) {
|
||||
return;
|
||||
}
|
||||
if (this.conversionsSubscription) {
|
||||
this.conversionsSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
|
||||
this.processing = true;
|
||||
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||
async (conversions) => {
|
||||
this.conversions = conversions;
|
||||
|
@ -595,6 +615,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
||||
console.error(`Cannot retreive payment card details`);
|
||||
this.accelerateError = 'apple_pay_no_card_details';
|
||||
this.processing = false;
|
||||
return;
|
||||
}
|
||||
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
|
||||
|
@ -606,6 +627,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||
this.accelerationUUID
|
||||
).subscribe({
|
||||
next: () => {
|
||||
this.processing = false;
|
||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||
this.audioService.playSound('ascend-chime-cartoon');
|
||||
if (this.googlePay) {
|
||||
|
@ -616,6 +638,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||
}, 1000);
|
||||
},
|
||||
error: (response) => {
|
||||
this.processing = false;
|
||||
this.accelerateError = response.error;
|
||||
if (!(response.status === 403 && response.error === 'not_available')) {
|
||||
setTimeout(() => {
|
||||
|
@ -627,6 +650,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||
}
|
||||
});
|
||||
} else {
|
||||
this.processing = false;
|
||||
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
|
||||
if (tokenResult.errors) {
|
||||
errorMessage += ` and errors: ${JSON.stringify(
|
||||
|
@ -644,10 +668,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||
* CASHAPP
|
||||
*/
|
||||
async requestCashAppPayment(): Promise<void> {
|
||||
if (this.processing) {
|
||||
return;
|
||||
}
|
||||
if (this.conversionsSubscription) {
|
||||
this.conversionsSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
this.processing = true;
|
||||
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||
async (conversions) => {
|
||||
this.conversions = conversions;
|
||||
|
@ -678,6 +706,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||
this.cashAppPay.addEventListener('ontokenization', event => {
|
||||
const { tokenResult, error } = event.detail;
|
||||
if (error) {
|
||||
this.processing = false;
|
||||
this.accelerateError = error;
|
||||
} else if (tokenResult.status === 'OK') {
|
||||
this.servicesApiService.accelerateWithCashApp$(
|
||||
|
@ -688,6 +717,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||
this.accelerationUUID
|
||||
).subscribe({
|
||||
next: () => {
|
||||
this.processing = false;
|
||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||
this.audioService.playSound('ascend-chime-cartoon');
|
||||
if (this.cashAppPay) {
|
||||
|
@ -702,6 +732,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||
}, 1000);
|
||||
},
|
||||
error: (response) => {
|
||||
this.processing = false;
|
||||
this.accelerateError = response.error;
|
||||
if (!(response.status === 403 && response.error === 'not_available')) {
|
||||
setTimeout(() => {
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
</p>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<span class="fee">{{ bar.class === 'tx' ? '' : '+' }}{{ bar.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></span>
|
||||
<span class="fee">{{ bar.class === 'tx' ? '' : '+' }}{{ bar.fee | number }} <span class="symbol" i18n="shared.sats">sats</span></span>
|
||||
<div class="spacer"></div>
|
||||
<div class="spacer"></div>
|
||||
</div>
|
||||
|
|
|
@ -21,14 +21,14 @@
|
|||
</tr>
|
||||
<tr *ngIf="accelerationInfo.fee">
|
||||
<td class="label" i18n="transaction.fee|Transaction fee">Fee</td>
|
||||
<td class="value">{{ accelerationInfo.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
|
||||
<td class="value">{{ accelerationInfo.fee | number }} <span class="symbol" i18n="shared.sats">sats</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="accelerationInfo.bidBoost >= 0 || accelerationInfo.feeDelta">
|
||||
<td class="label" i18n="transaction.out-of-band-fees">Out-of-band fees</td>
|
||||
@if (accelerationInfo.status === 'accelerated') {
|
||||
<td class="value oobFees">{{ accelerationInfo.feeDelta | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
|
||||
<td class="value oobFees">{{ accelerationInfo.feeDelta | number }} <span class="symbol" i18n="shared.sats">sats</span></td>
|
||||
} @else {
|
||||
<td class="value oobFees">{{ accelerationInfo.bidBoost | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
|
||||
<td class="value oobFees">{{ accelerationInfo.bidBoost | number }} <span class="symbol" i18n="shared.sats">sats</span></td>
|
||||
}
|
||||
</tr>
|
||||
<tr *ngIf="accelerationInfo.fee && accelerationInfo.weight">
|
||||
|
@ -47,13 +47,14 @@
|
|||
<tr *ngIf="['accelerated', 'mined'].includes(accelerationInfo.status) && hasPoolsData()">
|
||||
<td class="label" i18n="transaction.accelerated-by-hashrate|Accelerated to hashrate">Accelerated by</td>
|
||||
<td class="value" *ngIf="accelerationInfo.pools">
|
||||
<ng-container *ngFor="let pool of accelerationInfo.pools">
|
||||
<ng-container *ngFor="let pool of accelerationInfo.pools; let i = index;">
|
||||
<img *ngIf="accelerationInfo.poolsData[pool]"
|
||||
class="pool-logo"
|
||||
[style.opacity]="accelerationInfo?.minedByPoolUniqueId && pool !== accelerationInfo?.minedByPoolUniqueId ? '0.3' : '1'"
|
||||
[src]="'/resources/mining-pools/' + accelerationInfo.poolsData[pool].slug + '.svg'"
|
||||
onError="this.src = '/resources/mining-pools/default.svg'"
|
||||
[alt]="'Logo of ' + pool.name + ' mining pool'">
|
||||
<br *ngIf="i % 6 === 5">
|
||||
</ng-container>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
|
||||
.label {
|
||||
padding-right: 30px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.pool-logo {
|
||||
|
@ -30,7 +31,8 @@
|
|||
height: 22px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
margin-right: 3px;
|
||||
margin-right: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.oobFees {
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
<div class="node-spacer"></div>
|
||||
<div class="interval">
|
||||
<div class="interval-time">
|
||||
<app-time [time]="acceleratedAt - transactionTime"></app-time>
|
||||
<app-time [time]="firstSeenToAccelerated"></app-time>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-spacer"></div>
|
||||
|
@ -46,7 +46,7 @@
|
|||
<div class="interval-time">
|
||||
@if (tx.status.confirmed) {
|
||||
<div class="interval-time">
|
||||
<app-time [time]="tx.status.block_time - acceleratedAt"></app-time>
|
||||
<app-time [time]="acceleratedToMined"></app-time>
|
||||
</div>
|
||||
} @else if (standardETA && !tx.status.confirmed) {
|
||||
<!-- ~<app-time [time]="standardETA / 1000 - now"></app-time> -->
|
||||
|
|
|
@ -24,6 +24,8 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
|
|||
accelerateRatio: number;
|
||||
useAbsoluteTime: boolean = false;
|
||||
interval: number;
|
||||
firstSeenToAccelerated: number;
|
||||
acceleratedToMined: number;
|
||||
|
||||
tooltipPosition = null;
|
||||
hoverInfo: any = null;
|
||||
|
@ -35,8 +37,6 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
|
|||
|
||||
ngOnInit(): void {
|
||||
this.acceleratedAt = this.tx.acceleratedAt ?? new Date().getTime() / 1000;
|
||||
this.now = Math.floor(new Date().getTime() / 1000);
|
||||
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
|
||||
|
||||
this.miningService.getPools().subscribe(pools => {
|
||||
for (const pool of pools) {
|
||||
|
@ -44,10 +44,8 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
|
|||
}
|
||||
});
|
||||
|
||||
this.interval = window.setInterval(() => {
|
||||
this.now = Math.floor(new Date().getTime() / 1000);
|
||||
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
|
||||
}, 60000);
|
||||
this.updateTimes();
|
||||
this.interval = window.setInterval(this.updateTimes.bind(this), 60000);
|
||||
}
|
||||
|
||||
ngOnChanges(changes): void {
|
||||
|
@ -64,6 +62,13 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
|
|||
// }
|
||||
}
|
||||
|
||||
updateTimes(): void {
|
||||
this.now = Math.floor(new Date().getTime() / 1000);
|
||||
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
|
||||
this.firstSeenToAccelerated = Math.max(0, this.acceleratedAt - this.transactionTime);
|
||||
this.acceleratedToMined = Math.max(0, this.tx.status.block_time - this.acceleratedAt);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
|
|
|
@ -264,7 +264,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
|
|||
type: 'bar',
|
||||
barWidth: '90%',
|
||||
large: true,
|
||||
barMinHeight: 1,
|
||||
barMinHeight: 3,
|
||||
},
|
||||
],
|
||||
dataZoom: (this.widget || data.length === 0 )? undefined : [{
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
<app-fee-rate [fee]="acceleration.effectiveFee" [weight]="acceleration.effectiveVsize * 4"></app-fee-rate>
|
||||
</td>
|
||||
<td class="bid text-right">
|
||||
{{ (acceleration.feeDelta) | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
|
||||
{{ (acceleration.feeDelta) | number }} <span class="symbol" i18n="shared.sats">sats</span>
|
||||
</td>
|
||||
<td class="time text-right">
|
||||
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
|
||||
|
@ -41,7 +41,7 @@
|
|||
</ng-container>
|
||||
<ng-container *ngIf="!pending">
|
||||
<td *ngIf="acceleration.boost != null" class="fee text-right">
|
||||
{{ acceleration.boost | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
|
||||
{{ acceleration.boost | number }} <span class="symbol" i18n="shared.sats">sats</span>
|
||||
</td>
|
||||
<td *ngIf="acceleration.boost == null" class="fee text-right">
|
||||
~
|
||||
|
@ -64,7 +64,7 @@
|
|||
<span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="accelerator.pending">Pending</span>
|
||||
<span *ngIf="acceleration.status.includes('completed') && acceleration.minedByPoolUniqueId && pools[acceleration.minedByPoolUniqueId]" class="badge badge-success"><ng-container i18n="accelerator.completed">Completed</ng-container><span *ngIf="acceleration.status === 'completed_provisional'"> ⌛</span></span>
|
||||
<span *ngIf="acceleration.status.includes('completed') && (!acceleration.minedByPoolUniqueId || !pools[acceleration.minedByPoolUniqueId])" class="badge badge-success"><ng-container i18n="transaction.rbf.mined">Mined</ng-container><span *ngIf="acceleration.status === 'completed_provisional'"> ⌛</span></span>
|
||||
<span *ngIf="acceleration.status.includes('failed')" class="badge badge-danger"><ng-container i18n="accelerator.canceled">Failed</ng-container><span *ngIf="acceleration.status === 'failed_provisional'"> ⌛</span></span>
|
||||
<span *ngIf="acceleration.status.includes('failed')" class="badge badge-danger"><ng-container i18n="accelerator.canceled">Canceled</ng-container><span *ngIf="acceleration.status === 'failed_provisional'"> ⌛</span></span>
|
||||
</td>
|
||||
<td class="date text-right" *ngIf="!this.widget">
|
||||
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { StateService } from '../../../services/state.service';
|
||||
import { WebsocketService } from '../../../services/websocket.service';
|
||||
|
@ -61,8 +61,11 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
|
|||
this.websocketService.want(['blocks']);
|
||||
this.seoService.setTitle($localize`:@@02573b6980a2d611b4361a2595a4447e390058cd:Accelerations`);
|
||||
|
||||
this.paramSubscription = this.route.params.pipe(
|
||||
tap(params => {
|
||||
this.paramSubscription = combineLatest([
|
||||
this.route.params,
|
||||
timer(0),
|
||||
]).pipe(
|
||||
tap(([params]) => {
|
||||
this.page = +params['page'] || 1;
|
||||
this.pageSubject.next(this.page);
|
||||
})
|
||||
|
|
|
@ -10,10 +10,10 @@
|
|||
</td>
|
||||
<td class="field-value" [class]="chartPositionLeft ? 'chart-left' : ''">
|
||||
<div class="effective-fee-container">
|
||||
@if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize)) {
|
||||
@if (accelerationInfo?.acceleratedFeeRate && (!effectiveFeeRate || accelerationInfo.acceleratedFeeRate >= effectiveFeeRate)) {
|
||||
<app-fee-rate class="oobFees" [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
|
||||
} @else {
|
||||
<app-fee-rate class="oobFees" [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
|
||||
<app-fee-rate class="oobFees" [fee]="effectiveFeeRate"></app-fee-rate>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, ChangeDetectionStrategy, Input, Output, OnChanges, SimpleChanges, EventEmitter } from '@angular/core';
|
||||
import { Component, ChangeDetectionStrategy, Input, Output, OnChanges, SimpleChanges, EventEmitter, ChangeDetectorRef } from '@angular/core';
|
||||
import { Transaction } from '../../../interfaces/electrs.interface';
|
||||
import { Acceleration, SinglePoolStats } from '../../../interfaces/node-api.interface';
|
||||
import { EChartsOption, PieSeriesOption } from '../../../graphs/echarts';
|
||||
|
@ -23,7 +23,8 @@ function toRGB({r,g,b}): string {
|
|||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ActiveAccelerationBox implements OnChanges {
|
||||
@Input() tx: Transaction;
|
||||
@Input() acceleratedBy?: number[];
|
||||
@Input() effectiveFeeRate?: number;
|
||||
@Input() accelerationInfo: Acceleration;
|
||||
@Input() miningStats: MiningStats;
|
||||
@Input() pools: number[];
|
||||
|
@ -41,10 +42,12 @@ export class ActiveAccelerationBox implements OnChanges {
|
|||
timespan = '';
|
||||
chartInstance: any = undefined;
|
||||
|
||||
constructor() {}
|
||||
constructor(
|
||||
private cd: ChangeDetectorRef,
|
||||
) {}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
const pools = this.pools || this.accelerationInfo?.pools || this.tx.acceleratedBy;
|
||||
const pools = this.pools || this.accelerationInfo?.pools || this.acceleratedBy;
|
||||
if (pools && this.miningStats) {
|
||||
this.prepareChartOptions(pools);
|
||||
}
|
||||
|
@ -67,13 +70,17 @@ export class ActiveAccelerationBox implements OnChanges {
|
|||
|
||||
const acceleratingPools = (poolList || []).filter(id => pools[id]).sort((a,b) => pools[a].lastEstimatedHashrate - pools[b].lastEstimatedHashrate);
|
||||
const totalAcceleratedHashrate = acceleratingPools.reduce((total, pool) => total + pools[pool].lastEstimatedHashrate, 0);
|
||||
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) => {
|
||||
const pool = pools[poolId];
|
||||
const poolShare = ((pool.lastEstimatedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1);
|
||||
data.push(getDataItem(
|
||||
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>`,
|
||||
true,
|
||||
) as PieSeriesOption);
|
||||
|
@ -128,6 +135,7 @@ export class ActiveAccelerationBox implements OnChanges {
|
|||
}
|
||||
]
|
||||
};
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
onChartInit(ec) {
|
||||
|
|
|
@ -55,7 +55,7 @@ export class AddressLabelsComponent implements OnChanges {
|
|||
}
|
||||
|
||||
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) {
|
||||
const script = address?.scripts.values().next().value;
|
||||
if (script.template?.label) {
|
||||
|
|
|
@ -94,6 +94,20 @@
|
|||
</div>
|
||||
</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>
|
||||
<div class="title-tx">
|
||||
<h2 class="text-left">
|
||||
|
|
|
@ -2,12 +2,12 @@ import { Component, OnInit, OnDestroy, HostListener } from '@angular/core';
|
|||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
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 { StateService } from '../../services/state.service';
|
||||
import { AudioService } from '../../services/audio.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 { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
import { AddressInformation } from '../../interfaces/node-api.interface';
|
||||
|
@ -104,6 +104,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||
addressString: string;
|
||||
isLoadingAddress = true;
|
||||
transactions: Transaction[];
|
||||
utxos: Utxo[];
|
||||
isLoadingTransactions = true;
|
||||
retryLoadMore = false;
|
||||
error: any;
|
||||
|
@ -159,6 +160,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||
this.address = null;
|
||||
this.isLoadingTransactions = true;
|
||||
this.transactions = null;
|
||||
this.utxos = null;
|
||||
this.addressInfo = null;
|
||||
this.exampleChannel = null;
|
||||
document.body.scrollTo(0, 0);
|
||||
|
@ -212,11 +214,23 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||
this.updateChainStats();
|
||||
this.isLoadingAddress = false;
|
||||
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.getAddressTransactions$(address.address);
|
||||
: this.electrsApiService.getAddressTransactions$(address.address),
|
||||
(utxoCount > 2 && utxoCount <= 500 ? (address.is_pubkey
|
||||
? this.electrsApiService.getScriptHashUtxos$((address.address.length === 66 ? '21' : '41') + address.address + 'ac')
|
||||
: this.electrsApiService.getAddressUtxos$(address.address)) : of(null)).pipe(
|
||||
catchError(() => {
|
||||
return of(null);
|
||||
})
|
||||
)
|
||||
]);
|
||||
}),
|
||||
switchMap((transactions) => {
|
||||
switchMap(([transactions, utxos]) => {
|
||||
this.utxos = utxos;
|
||||
|
||||
this.tempTransactions = transactions;
|
||||
if (transactions.length) {
|
||||
this.lastTransactionTxId = transactions[transactions.length - 1].txid;
|
||||
|
@ -309,6 +323,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||
this.transactions = this.transactions.slice();
|
||||
this.mempoolStats.removeTx(transaction);
|
||||
this.audioService.playSound('magic');
|
||||
this.confirmTransaction(tx);
|
||||
} else {
|
||||
if (this.addTransaction(transaction, false)) {
|
||||
this.audioService.playSound('magic');
|
||||
|
@ -334,6 +349,31 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
// update utxos in-place
|
||||
if (this.utxos != null) {
|
||||
let utxosChanged = false;
|
||||
for (const vin of transaction.vin) {
|
||||
const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === vin.txid && utxo.vout === vin.vout);
|
||||
if (utxoIndex !== -1) {
|
||||
this.utxos.splice(utxoIndex, 1);
|
||||
utxosChanged = true;
|
||||
}
|
||||
}
|
||||
for (const [index, vout] of transaction.vout.entries()) {
|
||||
if (vout.scriptpubkey_address === this.address.address) {
|
||||
this.utxos.push({
|
||||
txid: transaction.txid,
|
||||
vout: index,
|
||||
value: vout.value,
|
||||
status: JSON.parse(JSON.stringify(transaction.status)),
|
||||
});
|
||||
utxosChanged = true;
|
||||
}
|
||||
}
|
||||
if (utxosChanged) {
|
||||
this.utxos = this.utxos.slice();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -346,9 +386,65 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||
this.transactions.splice(index, 1);
|
||||
this.transactions = this.transactions.slice();
|
||||
|
||||
// update utxos in-place
|
||||
if (this.utxos != null) {
|
||||
let utxosChanged = false;
|
||||
for (const vin of transaction.vin) {
|
||||
if (vin.prevout?.scriptpubkey_address === this.address.address) {
|
||||
this.utxos.push({
|
||||
txid: vin.txid,
|
||||
vout: vin.vout,
|
||||
value: vin.prevout.value,
|
||||
status: { confirmed: true }, // Assuming the input was confirmed
|
||||
});
|
||||
utxosChanged = true;
|
||||
}
|
||||
}
|
||||
for (const [index, vout] of transaction.vout.entries()) {
|
||||
if (vout.scriptpubkey_address === this.address.address) {
|
||||
const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === transaction.txid && utxo.vout === index);
|
||||
if (utxoIndex !== -1) {
|
||||
this.utxos.splice(utxoIndex, 1);
|
||||
utxosChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (utxosChanged) {
|
||||
this.utxos = this.utxos.slice();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
confirmTransaction(transaction: Transaction): void {
|
||||
// update utxos in-place
|
||||
if (this.utxos != null) {
|
||||
let utxosChanged = false;
|
||||
for (const vin of transaction.vin) {
|
||||
if (vin.prevout?.scriptpubkey_address === this.address.address) {
|
||||
const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === vin.txid && utxo.vout === vin.vout);
|
||||
if (utxoIndex !== -1) {
|
||||
this.utxos[utxoIndex].status = JSON.parse(JSON.stringify(transaction.status));
|
||||
utxosChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [index, vout] of transaction.vout.entries()) {
|
||||
if (vout.scriptpubkey_address === this.address.address) {
|
||||
const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === transaction.txid && utxo.vout === index);
|
||||
if (utxoIndex !== -1) {
|
||||
this.utxos[utxoIndex].status = JSON.parse(JSON.stringify(transaction.status));
|
||||
utxosChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (utxosChanged) {
|
||||
this.utxos = this.utxos.slice();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadMore(): void {
|
||||
if (this.isLoadingTransactions || this.fullyLoaded) {
|
||||
return;
|
||||
|
|
|
@ -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.sats">sats</option>
|
||||
<option value="fiat" i18n="shared.fiat|Fiat">Fiat</option>
|
||||
</select>
|
||||
</div>
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -30,7 +30,7 @@
|
|||
@if (digitsInfo === '1.8-8') {
|
||||
‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | number }}
|
||||
} @else {
|
||||
‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | amountShortener : satoshis < 1000 && satoshis > -1000 ? 0 : 1 }}
|
||||
‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | amountShortener : (satoshis < 1000 && satoshis > -1000 ? 0 : 1) : undefined : true }}
|
||||
}
|
||||
<span class="symbol">
|
||||
<ng-container *ngTemplateOutlet="prefix"></ng-container>sats
|
||||
|
|
|
@ -198,7 +198,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||
}
|
||||
|
||||
// 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);
|
||||
if (filtersAvailable !== this.filtersAvailable) {
|
||||
this.setFilterFlags();
|
||||
|
@ -206,7 +206,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||
this.filtersAvailable = filtersAvailable;
|
||||
if (this.scene) {
|
||||
this.clearUpdateQueue();
|
||||
this.scene.setup(transactions);
|
||||
this.scene.setup(transactions, sort);
|
||||
this.readyNextFrame = true;
|
||||
this.start();
|
||||
this.updateSearchHighlight();
|
||||
|
|
|
@ -88,16 +88,19 @@ export default class BlockScene {
|
|||
}
|
||||
|
||||
// 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
|
||||
Object.values(this.txs).forEach(tx => {
|
||||
tx.destroy();
|
||||
delete this.txs[tx.txid];
|
||||
});
|
||||
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
|
||||
txs.forEach(tx => {
|
||||
const txView = new TxView(tx, this);
|
||||
this.txs[tx.txid] = txView;
|
||||
let txViews = txs.map(tx => new TxView(tx, this));
|
||||
if (sort) {
|
||||
txViews = txViews.sort(feeRateDescending);
|
||||
}
|
||||
txViews.forEach(txView => {
|
||||
this.txs[txView.txid] = txView;
|
||||
this.place(txView);
|
||||
this.saveGridToScreenPosition(txView);
|
||||
this.applyTxUpdate(txView, {
|
||||
|
|
|
@ -33,7 +33,7 @@ export default class TxView implements TransactionStripped {
|
|||
flags: number;
|
||||
bigintFlags?: bigint | null = 0b00000100_00000000_00000000_00000000n;
|
||||
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';
|
||||
scene?: BlockScene;
|
||||
|
||||
|
|
|
@ -11,6 +11,10 @@ export function hexToColor(hex: string): Color {
|
|||
};
|
||||
}
|
||||
|
||||
export function colorToHex(color: Color): string {
|
||||
return [color.r, color.g, color.b].map(c => Math.round(c * 255).toString(16)).join('');
|
||||
}
|
||||
|
||||
export function desaturate(color: Color, amount: number): Color {
|
||||
const gray = (color.r + color.g + color.b) / 6;
|
||||
return {
|
||||
|
@ -30,6 +34,15 @@ export function darken(color: Color, amount: number): Color {
|
|||
};
|
||||
}
|
||||
|
||||
export function mix(color1: Color, color2: Color, amount: number): Color {
|
||||
return {
|
||||
r: color1.r * (1 - amount) + color2.r * amount,
|
||||
g: color1.g * (1 - amount) + color2.g * amount,
|
||||
b: color1.b * (1 - amount) + color2.b * amount,
|
||||
a: color1.a * (1 - amount) + color2.a * amount,
|
||||
};
|
||||
}
|
||||
|
||||
export function setOpacity(color: Color, opacity: number): Color {
|
||||
return {
|
||||
...color,
|
||||
|
@ -142,6 +155,10 @@ export function defaultColorFunction(
|
|||
return auditColors.added_prioritized;
|
||||
case 'prioritized':
|
||||
return auditColors.prioritized;
|
||||
case 'added_deprioritized':
|
||||
return auditColors.added_prioritized;
|
||||
case 'deprioritized':
|
||||
return auditColors.prioritized;
|
||||
case 'selected':
|
||||
return colors.marginal[levelIndex] || colors.marginal[defaultMempoolFeeColors.length - 1];
|
||||
case 'accelerated':
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<td class="label" i18n="transaction.fee|Transaction fee">Fee</td>
|
||||
<td class="value">{{ fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [blockConversion]="blockConversion" [value]="fee"></app-fiat></span>
|
||||
<td class="value">{{ fee | number }} <span class="symbol" i18n="shared.sats">sats</span> <span class="fiat"><app-fiat [blockConversion]="blockConversion" [value]="fee"></app-fiat></span>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label" i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
|
||||
|
@ -79,6 +79,11 @@
|
|||
<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>
|
||||
</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="'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>
|
||||
|
|
|
@ -53,6 +53,13 @@
|
|||
<td i18n="block.miner">Miner</td>
|
||||
<td *ngIf="stateService.env.MINING_DASHBOARD">
|
||||
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge" style="color: #FFF;padding:0;">
|
||||
<span class="miner-name" *ngIf="block.extras.pool.minerNames?.length > 1 && block.extras.pool.minerNames[1] != ''">
|
||||
@if (block.extras.pool.minerNames[1].length > 16) {
|
||||
{{ block.extras.pool.minerNames[1].slice(0, 15) }}…
|
||||
} @else {
|
||||
{{ block.extras.pool.minerNames[1] }}
|
||||
}
|
||||
</span>
|
||||
<img class="pool-logo" [src]="'/resources/mining-pools/' + block.extras.pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.name + ' mining pool'">
|
||||
{{ block.extras.pool.name }}
|
||||
</a>
|
||||
|
@ -60,8 +67,15 @@
|
|||
<td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">
|
||||
<span [attr.data-cy]="'block-details-miner-badge'" placement="bottom" class="badge"
|
||||
[class]="!block?.extras.pool.name || block?.extras.pool.slug === 'unknown' ? 'badge-secondary' : 'badge-primary'">
|
||||
{{ block?.extras.pool.name }}
|
||||
</span>
|
||||
<span class="miner-name" *ngIf="block.extras.pool.minerNames?.length > 1 && block.extras.pool.minerNames[1] != ''">
|
||||
@if (block.extras.pool.minerNames[1].length > 16) {
|
||||
{{ block.extras.pool.minerNames[1].slice(0, 15) }}…
|
||||
} @else {
|
||||
{{ block.extras.pool.minerNames[1] }}
|
||||
}
|
||||
</span>
|
||||
{{ block.extras.pool.name }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
|
@ -137,7 +137,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
|
|||
})
|
||||
),
|
||||
this.stateService.env.ACCELERATOR === true && block.height > 819500
|
||||
? this.servicesApiService.getAccelerationHistory$({ blockHeight: block.height })
|
||||
? this.servicesApiService.getAllAccelerationHistory$({ blockHeight: block.height })
|
||||
.pipe(catchError(() => {
|
||||
return of([]);
|
||||
}))
|
||||
|
|
|
@ -66,10 +66,10 @@
|
|||
[class.badge-success]="blockAudit?.matchRate >= 99"
|
||||
[class.badge-warning]="blockAudit?.matchRate >= 75 && blockAudit?.matchRate < 99"
|
||||
[class.badge-danger]="blockAudit?.matchRate < 75"
|
||||
*ngIf="blockAudit?.matchRate != null; else nullHealth"
|
||||
*ngIf="blockAudit?.matchRate != null && blockAudit?.id === block.id; else nullHealth"
|
||||
>{{ blockAudit?.matchRate }}%</span>
|
||||
<ng-template #nullHealth>
|
||||
<ng-container *ngIf="!isLoadingOverview; else loadingHealth">
|
||||
<ng-container *ngIf="!isLoadingOverview && blockAudit?.id === block.id; else loadingHealth">
|
||||
<span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
|
@ -182,6 +182,13 @@
|
|||
<td i18n="block.miner">Miner</td>
|
||||
<td *ngIf="stateService.env.MINING_DASHBOARD">
|
||||
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge" style="color: #FFF;padding:0;">
|
||||
<span class="miner-name" *ngIf="block.extras.pool.minerNames?.length > 1 && block.extras.pool.minerNames[1] != ''">
|
||||
@if (block.extras.pool.minerNames[1].length > 16) {
|
||||
{{ block.extras.pool.minerNames[1].slice(0, 15) }}…
|
||||
} @else {
|
||||
{{ block.extras.pool.minerNames[1] }}
|
||||
}
|
||||
</span>
|
||||
<img class="pool-logo" [src]="'/resources/mining-pools/' + block.extras.pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.name + ' mining pool'">
|
||||
{{ block.extras.pool.name }}
|
||||
</a>
|
||||
|
|
|
@ -81,6 +81,19 @@ h1 {
|
|||
}
|
||||
}
|
||||
|
||||
.miner-name {
|
||||
margin-right: 4px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.pool-logo {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.row {
|
||||
flex-direction: column;
|
||||
@media (min-width: 768px) {
|
||||
|
|
|
@ -17,6 +17,7 @@ import { PriceService, Price } from '../../services/price.service';
|
|||
import { CacheService } from '../../services/cache.service';
|
||||
import { ServicesApiServices } from '../../services/services-api.service';
|
||||
import { PreloadService } from '../../services/preload.service';
|
||||
import { identifyPrioritizedTransactions } from '../../shared/transaction.utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-block',
|
||||
|
@ -318,7 +319,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||
this.accelerationsSubscription = this.block$.pipe(
|
||||
switchMap((block) => {
|
||||
return this.stateService.env.ACCELERATOR === true && block.height > 819500
|
||||
? this.servicesApiService.getAccelerationHistory$({ blockHeight: block.height })
|
||||
? this.servicesApiService.getAllAccelerationHistory$({ blockHeight: block.height })
|
||||
.pipe(catchError(() => {
|
||||
return of([]);
|
||||
}))
|
||||
|
@ -326,7 +327,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||
})
|
||||
).subscribe((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();
|
||||
}
|
||||
});
|
||||
|
@ -524,6 +525,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||
const isUnseen = {};
|
||||
const isAdded = {};
|
||||
const isPrioritized = {};
|
||||
const isDeprioritized = {};
|
||||
const isCensored = {};
|
||||
const isMissing = {};
|
||||
const isSelected = {};
|
||||
|
@ -535,6 +537,17 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||
this.numUnexpected = 0;
|
||||
|
||||
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) {
|
||||
inTemplate[tx.txid] = true;
|
||||
if (tx.acc) {
|
||||
|
@ -550,9 +563,14 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||
for (const txid of blockAudit.addedTxs) {
|
||||
isAdded[txid] = true;
|
||||
}
|
||||
for (const txid of blockAudit.prioritizedTxs || []) {
|
||||
for (const txid of blockAudit.prioritizedTxs) {
|
||||
isPrioritized[txid] = true;
|
||||
}
|
||||
if (useLocalDeprioritized) {
|
||||
for (const txid of deprioritized || []) {
|
||||
isDeprioritized[txid] = true;
|
||||
}
|
||||
}
|
||||
for (const txid of blockAudit.missingTxs) {
|
||||
isCensored[txid] = true;
|
||||
}
|
||||
|
@ -608,6 +626,12 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||
} else {
|
||||
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])) {
|
||||
tx.status = 'added';
|
||||
} else if (inTemplate[tx.txid]) {
|
||||
|
|
|
@ -60,9 +60,14 @@
|
|||
</ng-container>
|
||||
</div>
|
||||
<div class="animated" *ngIf="block.extras?.pool != undefined && showPools">
|
||||
<a [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-pool'" class="badge" [routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
|
||||
<img class="pool-logo" [src]="'/resources/mining-pools/' + block.extras.pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.name + ' mining pool'">
|
||||
{{ block.extras.pool.name}}
|
||||
<a [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-pool'" class="badge" [class.miner-name]="block.extras.pool.minerNames?.length > 1 && block.extras.pool.minerNames[1] != ''" [routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
|
||||
<ng-container *ngIf="block.extras.pool.minerNames?.length > 1 && block.extras.pool.minerNames[1] != ''; else centralisedPool">
|
||||
<img [ngbTooltip]="block.extras.pool.name" class="pool-logo faded" [src]="'/resources/mining-pools/' + block.extras.pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.name + ' mining pool'">
|
||||
{{ block.extras.pool.minerNames[1] }}
|
||||
</ng-container>
|
||||
<ng-template #centralisedPool>
|
||||
<img class="pool-logo" [src]="'/resources/mining-pools/' + block.extras.pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.name + ' mining pool'"> {{ block.extras.pool.name }}
|
||||
</ng-template>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -19,6 +19,38 @@
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
.on-pool-name-text {
|
||||
display: inline-block;
|
||||
padding-top: 2px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
|
||||
.on-pool {
|
||||
align-items: center;
|
||||
background-color: var(--bg);
|
||||
display: inline-block;
|
||||
margin-top: 4px;
|
||||
padding: .25em .4em;
|
||||
border-radius: .25rem;
|
||||
}
|
||||
|
||||
.on-pool-container {
|
||||
align-items: center;
|
||||
position: relative;
|
||||
top: -8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.on-pool-container.selected {
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.pool-container {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.mined-block {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
|
@ -155,9 +187,16 @@
|
|||
|
||||
.badge {
|
||||
position: relative;
|
||||
top: 15px;
|
||||
top: 19px;
|
||||
z-index: 101;
|
||||
color: #FFF;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 145px;
|
||||
|
||||
&.miner-name {
|
||||
max-width: 125px;
|
||||
}
|
||||
}
|
||||
|
||||
.pool-logo {
|
||||
|
@ -168,6 +207,10 @@
|
|||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.pool-logo.faded {
|
||||
filter: grayscale(100%) brightness(1.5);
|
||||
}
|
||||
|
||||
.animated {
|
||||
transition: all 0.15s ease-in-out;
|
||||
white-space: nowrap;
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
<app-indexing-progress *ngIf="!widget"></app-indexing-progress>
|
||||
|
||||
<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 && isLoading" class="spinner-border ml-3" role="status"></div>
|
||||
<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 *ngIf="!widget && isLoading" class="spinner-border" role="status"></div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
.spinner-border {
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
margin-top: 13px;
|
||||
margin-top: -10px;
|
||||
margin-left: -13px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.container-xl {
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">{{ currency$ | async }}</span>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
|
@ -20,7 +20,7 @@
|
|||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">BTC</span>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
|
@ -28,7 +28,7 @@
|
|||
<div class="input-group-prepend">
|
||||
<span class="input-group-text" i18n="shared.sats">sats</span>
|
||||
</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>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -77,7 +77,7 @@ export class DifficultyMiningComponent implements OnInit {
|
|||
base: `${da.progressPercent.toFixed(2)}%`,
|
||||
change: da.difficultyChange,
|
||||
progress: da.progressPercent,
|
||||
remainingBlocks: da.remainingBlocks - 1,
|
||||
remainingBlocks: da.remainingBlocks,
|
||||
colorAdjustments,
|
||||
colorPreviousAdjustments,
|
||||
newDifficultyHeight: da.nextRetargetHeight,
|
||||
|
|
|
@ -153,8 +153,8 @@ export class DifficultyComponent implements OnInit {
|
|||
base: `${da.progressPercent.toFixed(2)}%`,
|
||||
change: da.difficultyChange,
|
||||
progress: da.progressPercent,
|
||||
minedBlocks: this.currentIndex + 1,
|
||||
remainingBlocks: da.remainingBlocks - 1,
|
||||
minedBlocks: this.currentIndex,
|
||||
remainingBlocks: da.remainingBlocks,
|
||||
expectedBlocks: Math.floor(da.expectedBlocks),
|
||||
colorAdjustments,
|
||||
colorPreviousAdjustments,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
</div>
|
||||
|
||||
<div class="faucet-container text-center">
|
||||
|
||||
|
||||
@if (txid) {
|
||||
<div class="alert alert-success w-100 text-truncate">
|
||||
<fa-icon [icon]="['fas', 'circle-check']"></fa-icon>
|
||||
|
@ -36,6 +36,13 @@
|
|||
<app-twitter-login customClass="btn btn-sm" width="180px" redirectTo="/testnet4/faucet" buttonString="Link your Twitter"></app-twitter-login>
|
||||
</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) {
|
||||
<!-- User can request -->
|
||||
<app-mempool-error class="w-100" [error]="error"></app-mempool-error>
|
||||
|
@ -81,7 +88,7 @@
|
|||
}
|
||||
|
||||
<!-- Send back coins -->
|
||||
@if (status?.address) {
|
||||
@if (status?.address) {
|
||||
<div class="mt-4 alert alert-info w-100">If you no longer need your testnet4 coins, please consider <a class="text-primary" [routerLink]="['/address/' | relativeUrl, status.address]"><u>sending them back</u></a> to replenish the faucet.</div>
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ export class FaucetComponent implements OnInit, OnDestroy {
|
|||
error: string = '';
|
||||
user: any = undefined;
|
||||
txid: string = '';
|
||||
|
||||
|
||||
faucetStatusSubscription: Subscription;
|
||||
status: {
|
||||
min: number; // minimum amount to request at once (in sats)
|
||||
|
|
|
@ -85,7 +85,6 @@
|
|||
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-home" *ngIf="network.val === '' && stateService.env.ACCELERATOR">
|
||||
<a class="nav-link" [routerLink]="['/acceleration' | relativeUrl]" (click)="collapse()">
|
||||
<fa-icon [icon]="['fas', 'rocket']" [fixedWidth]="true" i18n-title="master-page.accelerator-dashboard" title="Accelerator Dashboard"></fa-icon>
|
||||
<span class="badge badge-pill badge-warning beta" i18n="beta">beta</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-pools" *ngIf="stateService.env.MINING_DASHBOARD">
|
||||
|
|
|
@ -31,7 +31,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
|
|||
|
||||
lastBlockHeight: number;
|
||||
blockIndex: number;
|
||||
isLoading$ = new BehaviorSubject<boolean>(true);
|
||||
isLoading$ = new BehaviorSubject<boolean>(false);
|
||||
timeLtrSubscription: Subscription;
|
||||
timeLtr: boolean;
|
||||
chainDirection: string = 'right';
|
||||
|
@ -95,6 +95,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
|
|||
}
|
||||
}
|
||||
this.updateBlock({
|
||||
block: this.blockIndex,
|
||||
removed,
|
||||
changed,
|
||||
added
|
||||
|
@ -110,8 +111,11 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
|
|||
if (this.blockGraph) {
|
||||
this.blockGraph.clear(changes.index.currentValue > changes.index.previousValue ? this.chainDirection : this.poolDirection);
|
||||
}
|
||||
this.isLoading$.next(true);
|
||||
this.websocketService.startTrackMempoolBlock(changes.index.currentValue);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -153,6 +157,19 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
|
|||
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 {
|
||||
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.tx.txid}`);
|
||||
if (!event.keyModifier) {
|
||||
|
|
|
@ -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$;
|
||||
}
|
||||
|
|
|
@ -213,7 +213,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||
}
|
||||
if (state.mempoolPosition) {
|
||||
this.txPosition = state.mempoolPosition;
|
||||
if (this.txPosition.accelerated && !oldTxPosition.accelerated) {
|
||||
if (this.txPosition.accelerated && !oldTxPosition?.accelerated) {
|
||||
this.acceleratingArrow = true;
|
||||
setTimeout(() => {
|
||||
this.acceleratingArrow = false;
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
|
||||
<td>{{ rbfInfo.tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
|
||||
<td>{{ rbfInfo.tx.fee | number }} <span class="symbol" i18n="shared.sats">sats</span></td>
|
||||
</tr>
|
||||
<tr *only-vsize>
|
||||
<td class="td-width" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,5 +1,8 @@
|
|||
<div class="container-xl">
|
||||
<h1 class="text-left" i18n="shared.test-transactions|Test Transactions">Test Transactions</h1>
|
||||
<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>
|
||||
<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>
|
||||
<label for="maxfeerate" i18n="test.tx.raw-hex">Raw hex</label>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnChanges } from '@angular/core';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { dates } from '../../shared/i18n/dates';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { TimeService } from '../../services/time.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-time',
|
||||
|
@ -12,19 +11,9 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
|||
interval: number;
|
||||
text: string;
|
||||
tooltip: string;
|
||||
precisionThresholds = {
|
||||
year: 100,
|
||||
month: 18,
|
||||
week: 12,
|
||||
day: 31,
|
||||
hour: 48,
|
||||
minute: 90,
|
||||
second: 90
|
||||
};
|
||||
intervals = {};
|
||||
|
||||
@Input() time: number;
|
||||
@Input() dateString: number;
|
||||
@Input() dateString: string;
|
||||
@Input() kind: 'plain' | 'since' | 'until' | 'span' | 'before' | 'within' = 'plain';
|
||||
@Input() fastRender = false;
|
||||
@Input() fixedRender = false;
|
||||
|
@ -40,37 +29,26 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
|||
constructor(
|
||||
private ref: ChangeDetectorRef,
|
||||
private stateService: StateService,
|
||||
private datePipe: DatePipe,
|
||||
) {
|
||||
this.intervals = {
|
||||
year: 31536000,
|
||||
month: 2592000,
|
||||
week: 604800,
|
||||
day: 86400,
|
||||
hour: 3600,
|
||||
minute: 60,
|
||||
second: 1
|
||||
};
|
||||
}
|
||||
private timeService: TimeService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.calculateTime();
|
||||
if(this.fixedRender){
|
||||
this.text = this.calculate();
|
||||
return;
|
||||
}
|
||||
if (!this.stateService.isBrowser) {
|
||||
this.text = this.calculate();
|
||||
this.ref.markForCheck();
|
||||
return;
|
||||
}
|
||||
this.interval = window.setInterval(() => {
|
||||
this.text = this.calculate();
|
||||
this.calculateTime();
|
||||
this.ref.markForCheck();
|
||||
}, 1000 * (this.fastRender ? 1 : 60));
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
this.text = this.calculate();
|
||||
this.calculateTime();
|
||||
this.ref.markForCheck();
|
||||
}
|
||||
|
||||
|
@ -78,224 +56,21 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
|||
clearInterval(this.interval);
|
||||
}
|
||||
|
||||
calculate() {
|
||||
if (this.time == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let seconds: number;
|
||||
switch (this.kind) {
|
||||
case 'since':
|
||||
seconds = Math.floor((+new Date() - +new Date(this.dateString || this.time * 1000)) / 1000);
|
||||
this.tooltip = this.datePipe.transform(new Date(this.dateString || this.time * 1000), 'yyyy-MM-dd HH:mm');
|
||||
break;
|
||||
case 'until':
|
||||
case 'within':
|
||||
seconds = (+new Date(this.time) - +new Date()) / 1000;
|
||||
this.tooltip = this.datePipe.transform(new Date(this.time), 'yyyy-MM-dd HH:mm');
|
||||
break;
|
||||
default:
|
||||
seconds = Math.floor(this.time);
|
||||
this.tooltip = '';
|
||||
}
|
||||
|
||||
if (!this.showTooltip || this.relative) {
|
||||
this.tooltip = '';
|
||||
}
|
||||
|
||||
if (seconds < 1 && this.kind === 'span') {
|
||||
return $localize`:@@date-base.immediately:Immediately`;
|
||||
} else if (seconds < 60) {
|
||||
if (this.relative || this.kind === 'since') {
|
||||
if (this.lowercaseStart) {
|
||||
return $localize`:@@date-base.just-now:Just now`.charAt(0).toLowerCase() + $localize`:@@date-base.just-now:Just now`.slice(1);
|
||||
}
|
||||
return $localize`:@@date-base.just-now:Just now`;
|
||||
} else if (this.kind === 'until' || this.kind === 'within') {
|
||||
seconds = 60;
|
||||
}
|
||||
}
|
||||
|
||||
let counter: number;
|
||||
const result = [];
|
||||
let usedUnits = 0;
|
||||
for (const [index, unit] of this.units.entries()) {
|
||||
let precisionUnit = this.units[Math.min(this.units.length - 1, index + this.precision)];
|
||||
counter = Math.floor(seconds / this.intervals[unit]);
|
||||
const precisionCounter = Math.round(seconds / this.intervals[precisionUnit]);
|
||||
if (precisionCounter > this.precisionThresholds[precisionUnit]) {
|
||||
precisionUnit = unit;
|
||||
}
|
||||
if (this.units.indexOf(precisionUnit) === this.units.indexOf(this.minUnit)) {
|
||||
counter = Math.max(1, counter);
|
||||
}
|
||||
if (counter > 0) {
|
||||
let rounded;
|
||||
const roundFactor = Math.pow(10,this.fractionDigits || 0);
|
||||
if ((this.kind === 'until' || this.kind === 'within') && usedUnits < this.numUnits) {
|
||||
rounded = Math.floor((seconds / this.intervals[precisionUnit]) * roundFactor) / roundFactor;
|
||||
} else {
|
||||
rounded = Math.round((seconds / this.intervals[precisionUnit]) * roundFactor) / roundFactor;
|
||||
}
|
||||
if ((this.kind !== 'until' && this.kind !== 'within')|| this.numUnits === 1) {
|
||||
return this.formatTime(this.kind, precisionUnit, rounded);
|
||||
} else {
|
||||
if (!usedUnits) {
|
||||
result.push(this.formatTime(this.kind, precisionUnit, rounded));
|
||||
} else {
|
||||
result.push(this.formatTime('', precisionUnit, rounded));
|
||||
}
|
||||
seconds -= (rounded * this.intervals[precisionUnit]);
|
||||
usedUnits++;
|
||||
if (usedUnits >= this.numUnits) {
|
||||
return result.join(', ');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.join(', ');
|
||||
}
|
||||
|
||||
private formatTime(kind, unit, number): string {
|
||||
const dateStrings = dates(number);
|
||||
switch (kind) {
|
||||
case 'since':
|
||||
if (number === 1) {
|
||||
switch (unit) { // singular (1 day)
|
||||
case 'year': return $localize`:@@time-since:${dateStrings.i18nYear}:DATE: ago`; break;
|
||||
case 'month': return $localize`:@@time-since:${dateStrings.i18nMonth}:DATE: ago`; break;
|
||||
case 'week': return $localize`:@@time-since:${dateStrings.i18nWeek}:DATE: ago`; break;
|
||||
case 'day': return $localize`:@@time-since:${dateStrings.i18nDay}:DATE: ago`; break;
|
||||
case 'hour': return $localize`:@@time-since:${dateStrings.i18nHour}:DATE: ago`; break;
|
||||
case 'minute': return $localize`:@@time-since:${dateStrings.i18nMinute}:DATE: ago`; break;
|
||||
case 'second': return $localize`:@@time-since:${dateStrings.i18nSecond}:DATE: ago`; break;
|
||||
}
|
||||
} else {
|
||||
switch (unit) { // plural (2 days)
|
||||
case 'year': return $localize`:@@time-since:${dateStrings.i18nYears}:DATE: ago`; break;
|
||||
case 'month': return $localize`:@@time-since:${dateStrings.i18nMonths}:DATE: ago`; break;
|
||||
case 'week': return $localize`:@@time-since:${dateStrings.i18nWeeks}:DATE: ago`; break;
|
||||
case 'day': return $localize`:@@time-since:${dateStrings.i18nDays}:DATE: ago`; break;
|
||||
case 'hour': return $localize`:@@time-since:${dateStrings.i18nHours}:DATE: ago`; break;
|
||||
case 'minute': return $localize`:@@time-since:${dateStrings.i18nMinutes}:DATE: ago`; break;
|
||||
case 'second': return $localize`:@@time-since:${dateStrings.i18nSeconds}:DATE: ago`; break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'until':
|
||||
if (number === 1) {
|
||||
switch (unit) { // singular (In ~1 day)
|
||||
case 'year': return $localize`:@@time-until:In ~${dateStrings.i18nYear}:DATE:`; break;
|
||||
case 'month': return $localize`:@@time-until:In ~${dateStrings.i18nMonth}:DATE:`; break;
|
||||
case 'week': return $localize`:@@time-until:In ~${dateStrings.i18nWeek}:DATE:`; break;
|
||||
case 'day': return $localize`:@@time-until:In ~${dateStrings.i18nDay}:DATE:`; break;
|
||||
case 'hour': return $localize`:@@time-until:In ~${dateStrings.i18nHour}:DATE:`; break;
|
||||
case 'minute': return $localize`:@@time-until:In ~${dateStrings.i18nMinute}:DATE:`;
|
||||
case 'second': return $localize`:@@time-until:In ~${dateStrings.i18nSecond}:DATE:`;
|
||||
}
|
||||
} else {
|
||||
switch (unit) { // plural (In ~2 days)
|
||||
case 'year': return $localize`:@@time-until:In ~${dateStrings.i18nYears}:DATE:`; break;
|
||||
case 'month': return $localize`:@@time-until:In ~${dateStrings.i18nMonths}:DATE:`; break;
|
||||
case 'week': return $localize`:@@time-until:In ~${dateStrings.i18nWeeks}:DATE:`; break;
|
||||
case 'day': return $localize`:@@time-until:In ~${dateStrings.i18nDays}:DATE:`; break;
|
||||
case 'hour': return $localize`:@@time-until:In ~${dateStrings.i18nHours}:DATE:`; break;
|
||||
case 'minute': return $localize`:@@time-until:In ~${dateStrings.i18nMinutes}:DATE:`; break;
|
||||
case 'second': return $localize`:@@time-until:In ~${dateStrings.i18nSeconds}:DATE:`; break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'within':
|
||||
if (number === 1) {
|
||||
switch (unit) { // singular (In ~1 day)
|
||||
case 'year': return $localize`:@@time-within:within ~${dateStrings.i18nYear}:DATE:`; break;
|
||||
case 'month': return $localize`:@@time-within:within ~${dateStrings.i18nMonth}:DATE:`; break;
|
||||
case 'week': return $localize`:@@time-within:within ~${dateStrings.i18nWeek}:DATE:`; break;
|
||||
case 'day': return $localize`:@@time-within:within ~${dateStrings.i18nDay}:DATE:`; break;
|
||||
case 'hour': return $localize`:@@time-within:within ~${dateStrings.i18nHour}:DATE:`; break;
|
||||
case 'minute': return $localize`:@@time-within:within ~${dateStrings.i18nMinute}:DATE:`;
|
||||
case 'second': return $localize`:@@time-within:within ~${dateStrings.i18nSecond}:DATE:`;
|
||||
}
|
||||
} else {
|
||||
switch (unit) { // plural (In ~2 days)
|
||||
case 'year': return $localize`:@@time-within:within ~${dateStrings.i18nYears}:DATE:`; break;
|
||||
case 'month': return $localize`:@@time-within:within ~${dateStrings.i18nMonths}:DATE:`; break;
|
||||
case 'week': return $localize`:@@time-within:within ~${dateStrings.i18nWeeks}:DATE:`; break;
|
||||
case 'day': return $localize`:@@time-within:within ~${dateStrings.i18nDays}:DATE:`; break;
|
||||
case 'hour': return $localize`:@@time-within:within ~${dateStrings.i18nHours}:DATE:`; break;
|
||||
case 'minute': return $localize`:@@time-within:within ~${dateStrings.i18nMinutes}:DATE:`; break;
|
||||
case 'second': return $localize`:@@time-within:within ~${dateStrings.i18nSeconds}:DATE:`; break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'span':
|
||||
if (number === 1) {
|
||||
switch (unit) { // singular (1 day)
|
||||
case 'year': return $localize`:@@time-span:After ${dateStrings.i18nYear}:DATE:`; break;
|
||||
case 'month': return $localize`:@@time-span:After ${dateStrings.i18nMonth}:DATE:`; break;
|
||||
case 'week': return $localize`:@@time-span:After ${dateStrings.i18nWeek}:DATE:`; break;
|
||||
case 'day': return $localize`:@@time-span:After ${dateStrings.i18nDay}:DATE:`; break;
|
||||
case 'hour': return $localize`:@@time-span:After ${dateStrings.i18nHour}:DATE:`; break;
|
||||
case 'minute': return $localize`:@@time-span:After ${dateStrings.i18nMinute}:DATE:`; break;
|
||||
case 'second': return $localize`:@@time-span:After ${dateStrings.i18nSecond}:DATE:`; break;
|
||||
}
|
||||
} else {
|
||||
switch (unit) { // plural (2 days)
|
||||
case 'year': return $localize`:@@time-span:After ${dateStrings.i18nYears}:DATE:`; break;
|
||||
case 'month': return $localize`:@@time-span:After ${dateStrings.i18nMonths}:DATE:`; break;
|
||||
case 'week': return $localize`:@@time-span:After ${dateStrings.i18nWeeks}:DATE:`; break;
|
||||
case 'day': return $localize`:@@time-span:After ${dateStrings.i18nDays}:DATE:`; break;
|
||||
case 'hour': return $localize`:@@time-span:After ${dateStrings.i18nHours}:DATE:`; break;
|
||||
case 'minute': return $localize`:@@time-span:After ${dateStrings.i18nMinutes}:DATE:`; break;
|
||||
case 'second': return $localize`:@@time-span:After ${dateStrings.i18nSeconds}:DATE:`; break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'before':
|
||||
if (number === 1) {
|
||||
switch (unit) { // singular (1 day)
|
||||
case 'year': return $localize`:@@time-before:${dateStrings.i18nYear}:DATE: before`; break;
|
||||
case 'month': return $localize`:@@time-before:${dateStrings.i18nMonth}:DATE: before`; break;
|
||||
case 'week': return $localize`:@@time-before:${dateStrings.i18nWeek}:DATE: before`; break;
|
||||
case 'day': return $localize`:@@time-before:${dateStrings.i18nDay}:DATE: before`; break;
|
||||
case 'hour': return $localize`:@@time-before:${dateStrings.i18nHour}:DATE: before`; break;
|
||||
case 'minute': return $localize`:@@time-before:${dateStrings.i18nMinute}:DATE: before`; break;
|
||||
case 'second': return $localize`:@@time-before:${dateStrings.i18nSecond}:DATE: before`; break;
|
||||
}
|
||||
} else {
|
||||
switch (unit) { // plural (2 days)
|
||||
case 'year': return $localize`:@@time-before:${dateStrings.i18nYears}:DATE: before`; break;
|
||||
case 'month': return $localize`:@@time-before:${dateStrings.i18nMonths}:DATE: before`; break;
|
||||
case 'week': return $localize`:@@time-before:${dateStrings.i18nWeeks}:DATE: before`; break;
|
||||
case 'day': return $localize`:@@time-before:${dateStrings.i18nDays}:DATE: before`; break;
|
||||
case 'hour': return $localize`:@@time-before:${dateStrings.i18nHours}:DATE: before`; break;
|
||||
case 'minute': return $localize`:@@time-before:${dateStrings.i18nMinutes}:DATE: before`; break;
|
||||
case 'second': return $localize`:@@time-before:${dateStrings.i18nSeconds}:DATE: before`; break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (number === 1) {
|
||||
switch (unit) { // singular (1 day)
|
||||
case 'year': return dateStrings.i18nYear; break;
|
||||
case 'month': return dateStrings.i18nMonth; break;
|
||||
case 'week': return dateStrings.i18nWeek; break;
|
||||
case 'day': return dateStrings.i18nDay; break;
|
||||
case 'hour': return dateStrings.i18nHour; break;
|
||||
case 'minute': return dateStrings.i18nMinute; break;
|
||||
case 'second': return dateStrings.i18nSecond; break;
|
||||
}
|
||||
} else {
|
||||
switch (unit) { // plural (2 days)
|
||||
case 'year': return dateStrings.i18nYears; break;
|
||||
case 'month': return dateStrings.i18nMonths; break;
|
||||
case 'week': return dateStrings.i18nWeeks; break;
|
||||
case 'day': return dateStrings.i18nDays; break;
|
||||
case 'hour': return dateStrings.i18nHours; break;
|
||||
case 'minute': return dateStrings.i18nMinutes; break;
|
||||
case 'second': return dateStrings.i18nSeconds; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
calculateTime(): void {
|
||||
const { text, tooltip } = this.timeService.calculate(
|
||||
this.time,
|
||||
this.kind,
|
||||
this.relative,
|
||||
this.precision,
|
||||
this.minUnit,
|
||||
this.showTooltip,
|
||||
this.units,
|
||||
this.dateString,
|
||||
this.lowercaseStart,
|
||||
this.numUnits,
|
||||
this.fractionDigits,
|
||||
);
|
||||
this.text = text;
|
||||
this.tooltip = tooltip;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
<div class="blockchain-wrapper" [style]="{ height: blockchainHeight * 1.16 + 'px' }">
|
||||
<app-clockchain [height]="blockchainHeight" [width]="blockchainWidth" mode="none"></app-clockchain>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel" *ngIf="!error || waitingForTransaction">
|
||||
@if (replaced) {
|
||||
<div class="alert-replaced" role="alert">
|
||||
<span i18n="transaction.rbf.replacement|RBF replacement">This transaction has been replaced by:</span>
|
||||
|
@ -65,23 +65,25 @@
|
|||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="field narrower">
|
||||
<div class="label" i18n="transaction.eta|Transaction ETA">ETA</div>
|
||||
<div class="value">
|
||||
<ng-container *ngIf="(ETA$ | async) as eta; else etaSkeleton">
|
||||
<span class="justify-content-end d-flex align-items-center">
|
||||
@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>
|
||||
}
|
||||
</span>
|
||||
</ng-container>
|
||||
<ng-template #etaSkeleton>
|
||||
<span class="skeleton-loader" style="max-width: 200px;"></span>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
@if (!replaced) {
|
||||
<div class="field narrower">
|
||||
<div class="label" i18n="transaction.eta|Transaction ETA">ETA</div>
|
||||
<div class="value">
|
||||
<ng-container *ngIf="(ETA$ | async) as eta; else etaSkeleton">
|
||||
<span class="justify-content-end d-flex align-items-center">
|
||||
@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>
|
||||
}
|
||||
</span>
|
||||
</ng-container>
|
||||
<ng-template #etaSkeleton>
|
||||
<span class="skeleton-loader" style="max-width: 200px;"></span>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
} @else if (tx && tx.status?.confirmed) {
|
||||
<div class="field narrower mt-2">
|
||||
<div class="label" i18n="transaction.confirmed-at">Confirmed at</div>
|
||||
|
@ -111,7 +113,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom-panel">
|
||||
<div class="bottom-panel" *ngIf="!error || waitingForTransaction">
|
||||
@if (isLoading) {
|
||||
<div class="progress-icon">
|
||||
<div class="spinner-border text-light" style="width: 1em; height: 1em"></div>
|
||||
|
@ -184,6 +186,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"
|
||||
[routerLink]="['/tx' | relativeUrl, tx?.txid || txId]"
|
||||
|
|
|
@ -286,14 +286,14 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
|||
this.accelerationInfo = null;
|
||||
}),
|
||||
switchMap((blockHash: string) => {
|
||||
return this.servicesApiService.getAccelerationHistory$({ blockHash });
|
||||
return this.servicesApiService.getAllAccelerationHistory$({ blockHash }, null, this.txId);
|
||||
}),
|
||||
catchError(() => {
|
||||
return of(null);
|
||||
})
|
||||
).subscribe((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;
|
||||
acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
|
||||
acceleration.boost = boostCost;
|
||||
|
@ -747,7 +747,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
|||
|
||||
checkAccelerationEligibility() {
|
||||
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 highSigop = (this.tx.sigops * 20) > this.tx.weight;
|
||||
this.eligibleForAcceleration = !replaceableInputs && !highSigop;
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<div *ngIf="officialMempoolSpace">
|
||||
<h2>Trademark Policy and Guidelines</h2>
|
||||
<h5>The Mempool Open Source Project ®</h5>
|
||||
<h6>Updated: July 3, 2024</h6>
|
||||
<h6>Updated: August 19, 2024</h6>
|
||||
<br>
|
||||
|
||||
<div class="text-left">
|
||||
|
@ -95,16 +95,31 @@
|
|||
<p>The mempool Square Logo</p>
|
||||
<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>
|
||||
<p>The Mempool Accelerator Logo</p>
|
||||
<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>
|
||||
<br><br>
|
||||
<p>The Mempool Goggles Logo</p>
|
||||
<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%">
|
||||
<br><br>
|
||||
<p>The mempool Blocks Logo</p>
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
</ng-template>
|
||||
</span>
|
||||
<span class="field col-sm-4 text-center"><ng-container *ngIf="transactionTime > 0">‎{{ transactionTime * 1000 | date:'yyyy-MM-dd HH:mm' }}</ng-container></span>
|
||||
<span class="field col-sm-4 text-right"><span class="label" i18n="transaction.fee|Transaction fee">Fee</span> {{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></span>
|
||||
<span class="field col-sm-4 text-right"><span class="label" i18n="transaction.fee|Transaction fee">Fee</span> {{ tx.fee | number }} <span class="symbol" i18n="shared.sats">sats</span></span>
|
||||
</div>
|
||||
|
||||
|
||||
|
|
|
@ -551,23 +551,23 @@
|
|||
<td class="td-width align-items-center align-middle" i18n="transaction.eta|Transaction ETA">ETA</td>
|
||||
<td>
|
||||
<ng-container *ngIf="(ETA$ | async) as eta; else etaSkeleton">
|
||||
@if (eta.blocks >= 7) {
|
||||
<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') {
|
||||
@if (network === 'liquid' || network === 'liquidtestnet') {
|
||||
<app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time>
|
||||
} @else {
|
||||
<span [class]="(!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) ? 'etaDeepMempool justify-content-end align-items-center' : ''">
|
||||
<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 [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>
|
||||
}
|
||||
@if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && notAcceleratedOnLoad) {
|
||||
<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™ tooltip" ngbTooltip="This transaction cannot be accelerated">
|
||||
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</span>
|
||||
<span class="eta justify-content-end">
|
||||
</span>
|
||||
}
|
||||
</ng-container>
|
||||
|
@ -606,16 +606,11 @@
|
|||
@if (!isLoadingTx) {
|
||||
<tr>
|
||||
<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>
|
||||
@if (accelerationInfo?.bidBoost) {
|
||||
<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="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>
|
||||
}
|
||||
<td class="text-wrap">{{ tx.fee | number }} <span class="symbol" i18n="shared.sats">sats</span>
|
||||
@if (accelerationInfo?.bidBoost ?? tx.feeDelta > 0) {
|
||||
<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.sats">sats</span>
|
||||
}
|
||||
<span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee + ((accelerationInfo?.bidBoost ?? tx.feeDelta) || 0)"></app-fiat></span>
|
||||
</td>
|
||||
</tr>
|
||||
} @else {
|
||||
|
@ -675,7 +670,7 @@
|
|||
<ng-template #acceleratingRow>
|
||||
<tr>
|
||||
<td rowspan="2" colspan="2" style="padding: 0;">
|
||||
<app-active-acceleration-box [tx]="tx" [accelerationInfo]="accelerationInfo" [miningStats]="miningStats" [hasCpfp]="hasCpfp" (toggleCpfp)="showCpfpDetails = !showCpfpDetails" [chartPositionLeft]="isMobile"></app-active-acceleration-box>
|
||||
<app-active-acceleration-box [acceleratedBy]="tx.acceleratedBy" [effectiveFeeRate]="tx.effectiveFeePerVsize" [accelerationInfo]="accelerationInfo" [miningStats]="miningStats" [hasCpfp]="hasCpfp" (toggleCpfp)="showCpfpDetails = !showCpfpDetails" [chartPositionLeft]="isMobile"></app-active-acceleration-box>
|
||||
</td>
|
||||
</tr>
|
||||
<tr></tr>
|
||||
|
@ -689,8 +684,15 @@
|
|||
@if (pool) {
|
||||
<td class="wrap-cell">
|
||||
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, pool.slug]" class="badge" style="color: #FFF;padding:0;">
|
||||
<span class="miner-name" *ngIf="pool.minerNames?.length > 1 && pool.minerNames[1] != ''">
|
||||
@if (pool.minerNames[1].length > 16) {
|
||||
{{ pool.minerNames[1].slice(0, 15) }}…
|
||||
} @else {
|
||||
{{ pool.minerNames[1] }}
|
||||
}
|
||||
</span>
|
||||
<img class="pool-logo" [src]="'/resources/mining-pools/' + pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + pool.name + ' mining pool'">
|
||||
{{ pool.name }}
|
||||
{{ pool.name }}
|
||||
</a>
|
||||
</td>
|
||||
} @else {
|
||||
|
|
|
@ -60,6 +60,19 @@
|
|||
top: -1px;
|
||||
}
|
||||
|
||||
.miner-name {
|
||||
margin-right: 4px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.pool-logo {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.badge.badge-accelerated {
|
||||
background-color: var(--tertiary);
|
||||
color: white;
|
||||
|
@ -287,37 +300,21 @@
|
|||
}
|
||||
|
||||
.accelerate {
|
||||
display: flex !important;
|
||||
align-self: auto;
|
||||
margin-left: auto;
|
||||
background-color: var(--tertiary);
|
||||
@media (max-width: 849px) {
|
||||
margin-left: 5px;
|
||||
}
|
||||
@media (min-width: 850px) {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.etaDeepMempool {
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
align-content: center;
|
||||
@media (max-width: 995px) {
|
||||
justify-content: left !important;
|
||||
}
|
||||
@media (max-width: 849px) {
|
||||
justify-content: right !important;
|
||||
}
|
||||
}
|
||||
|
||||
.accelerateDeepMempool {
|
||||
align-self: auto;
|
||||
margin-left: auto;
|
||||
background-color: var(--tertiary);
|
||||
@media (max-width: 995px) {
|
||||
margin-left: 0px;
|
||||
}
|
||||
@media (max-width: 849px) {
|
||||
margin-left: 5px;
|
||||
}
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.goggles-icon {
|
||||
|
@ -335,4 +332,9 @@
|
|||
|
||||
.oobFees {
|
||||
color: #905cf4;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
|
@ -42,6 +42,7 @@ interface Pool {
|
|||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
minerNames: string[] | null;
|
||||
}
|
||||
|
||||
export interface TxAuditStatus {
|
||||
|
@ -139,6 +140,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
firstLoad = true;
|
||||
waitingForAccelerationInfo: boolean = false;
|
||||
isLoadingFirstSeen = false;
|
||||
notAcceleratedOnLoad: boolean = null;
|
||||
|
||||
featuresEnabled: boolean;
|
||||
segwitEnabled: boolean;
|
||||
|
@ -191,7 +193,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
this.hideAccelerationSummary = this.stateService.isMempoolSpaceBuild ? this.storageService.getValue('hide-accelerator-pref') == 'true' : true;
|
||||
|
||||
if (!this.stateService.isLiquid()) {
|
||||
this.miningService.getMiningStats('1w').subscribe(stats => {
|
||||
this.miningService.getMiningStats('1m').subscribe(stats => {
|
||||
this.miningStats = stats;
|
||||
});
|
||||
}
|
||||
|
@ -343,7 +345,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
this.setIsAccelerated();
|
||||
}),
|
||||
switchMap((blockHeight: number) => {
|
||||
return this.servicesApiService.getAccelerationHistory$({ blockHeight }).pipe(
|
||||
return this.servicesApiService.getAllAccelerationHistory$({ blockHeight }, null, this.txId).pipe(
|
||||
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
|
||||
return throwError('retry');
|
||||
|
@ -358,12 +360,18 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
}),
|
||||
).subscribe((accelerationHistory) => {
|
||||
for (const acceleration of accelerationHistory) {
|
||||
if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional')) {
|
||||
const boostCost = acceleration.boostCost || acceleration.bidBoost;
|
||||
acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
|
||||
acceleration.boost = boostCost;
|
||||
this.tx.acceleratedAt = acceleration.added;
|
||||
this.accelerationInfo = acceleration;
|
||||
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;
|
||||
acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
|
||||
acceleration.boost = boostCost;
|
||||
this.tx.acceleratedAt = acceleration.added;
|
||||
this.accelerationInfo = acceleration;
|
||||
} else {
|
||||
this.tx.feeDelta = undefined;
|
||||
}
|
||||
}
|
||||
this.waitingForAccelerationInfo = false;
|
||||
this.setIsAccelerated();
|
||||
}
|
||||
|
@ -484,7 +492,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
if (this.stateService.network === '') {
|
||||
if (!this.mempoolPosition.accelerated) {
|
||||
if (!this.accelerationFlowCompleted && !this.hideAccelerationSummary && !this.showAccelerationSummary) {
|
||||
this.miningService.getMiningStats('1w').subscribe(stats => {
|
||||
this.miningService.getMiningStats('1m').subscribe(stats => {
|
||||
this.miningStats = stats;
|
||||
});
|
||||
}
|
||||
|
@ -842,6 +850,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
this.tx.feeDelta = cpfpInfo.feeDelta;
|
||||
this.setIsAccelerated(firstCpfp);
|
||||
}
|
||||
|
||||
if (this.notAcceleratedOnLoad === null) {
|
||||
this.notAcceleratedOnLoad = !this.isAcceleration;
|
||||
}
|
||||
|
||||
if (!this.isAcceleration && this.fragmentParams.has('accelerate')) {
|
||||
this.forceAccelerationSummary = true;
|
||||
|
@ -895,7 +907,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.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.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.checkAccelerationEligibility();
|
||||
} else {
|
||||
|
@ -960,6 +972,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
this.filters = [];
|
||||
this.showCpfpDetails = false;
|
||||
this.showAccelerationDetails = false;
|
||||
this.accelerationFlowCompleted = false;
|
||||
this.accelerationInfo = null;
|
||||
this.cashappEligible = false;
|
||||
this.txInBlockIndex = null;
|
||||
|
@ -1077,6 +1090,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
(!this.hideAccelerationSummary && !this.accelerationFlowCompleted)
|
||||
|| this.forceAccelerationSummary
|
||||
)
|
||||
&& this.notAcceleratedOnLoad // avoid briefly showing accelerator checkout on already accelerated txs
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -321,7 +321,7 @@
|
|||
<div class="float-left mt-2-5" *ngIf="!transactionPage && !tx.vin[0].is_coinbase && tx.fee !== -1">
|
||||
<app-fee-rate [fee]="tx.fee" [weight]="tx.weight"></app-fee-rate>
|
||||
<span class="d-none d-sm-inline-block"> – {{ tx.fee | number }} <span class="symbol"
|
||||
i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee"></app-fiat></span></span>
|
||||
i18n="shared.sats">sats</span> <span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee"></app-fiat></span></span>
|
||||
</div>
|
||||
<div class="float-left mt-2-5 grey-info-text" *ngIf="tx.fee === -1" i18n="transactions-list.load-to-reveal-fee-info">Show more inputs to reveal fee data</div>
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
374
frontend/src/app/components/utxo-graph/utxo-graph.component.ts
Normal file
374
frontend/src/app/components/utxo-graph/utxo-graph.component.ts
Normal file
|
@ -0,0 +1,374 @@
|
|||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, NgZone, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
|
||||
import { EChartsOption } from '../../graphs/echarts';
|
||||
import { 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';
|
||||
import { colorToHex, hexToColor, mix } from '../block-overview-graph/utils';
|
||||
import { TimeService } from '../../services/time.service';
|
||||
|
||||
const newColorHex = '1bd8f4';
|
||||
const oldColorHex = '9339f4';
|
||||
const pendingColorHex = 'eba814';
|
||||
const newColor = hexToColor(newColorHex);
|
||||
const oldColor = hexToColor(oldColorHex);
|
||||
|
||||
interface Circle {
|
||||
x: number,
|
||||
y: number,
|
||||
r: number,
|
||||
i: number,
|
||||
}
|
||||
|
||||
interface UtxoCircle extends Circle {
|
||||
utxo: Utxo;
|
||||
}
|
||||
|
||||
function sortedInsert(positions: { c1: Circle, c2: Circle, d: number, p: number, side?: boolean }[], newPosition: { c1: Circle, c2: Circle, d: number, p: number }): void {
|
||||
let left = 0;
|
||||
let right = positions.length;
|
||||
while (left < right) {
|
||||
const mid = Math.floor((left + right) / 2);
|
||||
if (positions[mid].p > newPosition.p) {
|
||||
right = mid;
|
||||
} else {
|
||||
left = mid + 1;
|
||||
}
|
||||
}
|
||||
positions.splice(left, 0, newPosition, {...newPosition, side: true });
|
||||
}
|
||||
@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;
|
||||
lastUpdate: number = 0;
|
||||
updateInterval;
|
||||
|
||||
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,
|
||||
private timeService: TimeService,
|
||||
) {
|
||||
// re-render the chart every 10 seconds, to keep the age colors up to date
|
||||
this.updateInterval = setInterval(() => {
|
||||
if (this.lastUpdate < Date.now() - 10000 && this.utxos) {
|
||||
this.prepareChartOptions(this.utxos);
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.isLoading = true;
|
||||
if (!this.utxos) {
|
||||
return;
|
||||
}
|
||||
if (changes.utxos) {
|
||||
this.prepareChartOptions(this.utxos);
|
||||
}
|
||||
}
|
||||
|
||||
prepareChartOptions(utxos: Utxo[]): void {
|
||||
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 intersection = (c1: Circle, c2: Circle, d: number, r: number, side: boolean): { x: number, y: number} => {
|
||||
const d1 = c1.r + r;
|
||||
const d2 = c2.r + r;
|
||||
const a = (d1 * d1 - d2 * d2 + d * d) / (2 * d);
|
||||
const h = Math.sqrt(d1 * d1 - a * a);
|
||||
const x3 = c1.x + a * (c2.x - c1.x) / d;
|
||||
const y3 = c1.y + a * (c2.y - c1.y) / d;
|
||||
return side
|
||||
? { x: x3 + h * (c2.y - c1.y) / d, y: y3 - h * (c2.x - c1.x) / d }
|
||||
: { x: x3 - h * (c2.y - c1.y) / d, y: y3 + h * (c2.x - c1.x) / d };
|
||||
};
|
||||
|
||||
// ~Linear algorithm to pack circles as tightly as possible without overlaps
|
||||
const placedCircles: UtxoCircle[] = [];
|
||||
const positions: { c1: Circle, c2: Circle, d: number, p: number, side?: boolean }[] = [];
|
||||
// Pack in descending order of value, and limit to the top 500 to preserve performance
|
||||
const sortedUtxos = utxos.sort((a, b) => {
|
||||
if (a.value === b.value) {
|
||||
if (a.status.confirmed && !b.status.confirmed) {
|
||||
return -1;
|
||||
} else if (!a.status.confirmed && b.status.confirmed) {
|
||||
return 1;
|
||||
} else {
|
||||
return a.status.block_height - b.status.block_height;
|
||||
}
|
||||
}
|
||||
return b.value - a.value;
|
||||
}).slice(0, 500);
|
||||
const maxR = Math.sqrt(sortedUtxos.reduce((max, utxo) => Math.max(max, utxo.value), 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, i: index });
|
||||
return;
|
||||
}
|
||||
if (index === 1) {
|
||||
const c = placedCircles[0];
|
||||
placedCircles.push({ x: c.r + r, y: 0, r, utxo, i: index });
|
||||
sortedInsert(positions, { c1: c, c2: placedCircles[1], d: c.r + r, p: 0 });
|
||||
return;
|
||||
}
|
||||
if (index === 2) {
|
||||
const c = placedCircles[0];
|
||||
placedCircles.push({ x: -c.r - r, y: 0, r, utxo, i: index });
|
||||
sortedInsert(positions, { c1: c, c2: placedCircles[2], d: c.r + r, p: 0 });
|
||||
return;
|
||||
}
|
||||
|
||||
// The best position will be touching two other circles
|
||||
// find the closest such position to the center of the graph
|
||||
// where the circle can be placed without overlapping other circles
|
||||
const numCircles = placedCircles.length;
|
||||
let newCircle: UtxoCircle = null;
|
||||
while (positions.length > 0) {
|
||||
const position = positions.shift();
|
||||
// if the circles are too far apart, skip
|
||||
if (position.d > (position.c1.r + position.c2.r + r + r)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { x, y } = intersection(position.c1, position.c2, position.d, r, position.side);
|
||||
if (isNaN(x) || isNaN(y)) {
|
||||
// should never happen
|
||||
continue;
|
||||
}
|
||||
|
||||
// check if the circle would overlap any other circles here
|
||||
let valid = true;
|
||||
const nearbyCircles: { c: UtxoCircle, d: number, s: number }[] = [];
|
||||
for (let k = 0; k < numCircles; k++) {
|
||||
const c = placedCircles[k];
|
||||
if (k === position.c1.i || k === position.c2.i) {
|
||||
nearbyCircles.push({ c, d: c.r + r, s: 0 });
|
||||
continue;
|
||||
}
|
||||
const d = distance(x, y, c.x, c.y);
|
||||
if (d < (r + c.r)) {
|
||||
valid = false;
|
||||
break;
|
||||
} else {
|
||||
nearbyCircles.push({ c, d, s: d - c.r - r });
|
||||
}
|
||||
}
|
||||
if (valid) {
|
||||
newCircle = { x, y, r, utxo, i: index };
|
||||
// add new positions to the candidate list
|
||||
const nearest = nearbyCircles.sort((a, b) => a.s - b.s).slice(0, 5);
|
||||
for (const n of nearest) {
|
||||
if (n.d < (n.c.r + r + maxR + maxR)) {
|
||||
sortedInsert(positions, { c1: newCircle, c2: n.c, d: n.d, p: distance((n.c.x + x) / 2, (n.c.y + y), 0, 0) });
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (newCircle) {
|
||||
placedCircles.push(newCircle);
|
||||
} else {
|
||||
// should never happen
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// 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) => [
|
||||
circle.utxo.txid + circle.utxo.vout,
|
||||
circle.utxo,
|
||||
circle.x,
|
||||
circle.y,
|
||||
circle.r,
|
||||
]);
|
||||
|
||||
this.chartOptions = {
|
||||
series: [{
|
||||
type: 'custom',
|
||||
coordinateSystem: undefined,
|
||||
data: data,
|
||||
encode: {
|
||||
itemName: 0,
|
||||
x: 2,
|
||||
y: 3,
|
||||
r: 4,
|
||||
},
|
||||
renderItem: (params, api) => {
|
||||
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 datum = data[params.dataIndex];
|
||||
const utxo = datum[1] as Utxo;
|
||||
const x = datum[2] as number;
|
||||
const y = datum[3] as number;
|
||||
const r = datum[4] as number;
|
||||
if (r * scale < 2) {
|
||||
// skip items too small to render cleanly
|
||||
return;
|
||||
}
|
||||
|
||||
const valueStr = renderSats(utxo.value, this.stateService.network);
|
||||
const elements: any[] = [
|
||||
{
|
||||
type: 'circle',
|
||||
autoBatch: true,
|
||||
shape: {
|
||||
r: (r * scale) - 1,
|
||||
},
|
||||
style: {
|
||||
fill: '#' + this.getColor(utxo),
|
||||
}
|
||||
},
|
||||
];
|
||||
const labelFontSize = Math.min(36, r * scale * 0.3);
|
||||
if (labelFontSize > 8) {
|
||||
elements.push({
|
||||
type: 'text',
|
||||
style: {
|
||||
text: valueStr,
|
||||
fontSize: labelFontSize,
|
||||
fill: '#fff',
|
||||
align: 'center',
|
||||
verticalAlign: 'middle',
|
||||
},
|
||||
});
|
||||
}
|
||||
return {
|
||||
type: 'group',
|
||||
x: (x * scale) + offsetX,
|
||||
y: (y * scale) + offsetY,
|
||||
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[1] 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}
|
||||
<br>
|
||||
${utxo.status.confirmed ? 'Confirmed ' + this.timeService.calculate(utxo.status.block_time, 'since', true, 1, 'minute').text : 'Pending'}
|
||||
`;
|
||||
},
|
||||
}
|
||||
};
|
||||
this.lastUpdate = Date.now();
|
||||
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
getColor(utxo: Utxo): string {
|
||||
if (utxo.status.confirmed) {
|
||||
const age = Date.now() / 1000 - utxo.status.block_time;
|
||||
const oneHour = 60 * 60;
|
||||
const fourYears = 4 * 365 * 24 * 60 * 60;
|
||||
|
||||
if (age < oneHour) {
|
||||
return newColorHex;
|
||||
} else if (age >= fourYears) {
|
||||
return oldColorHex;
|
||||
} else {
|
||||
// Logarithmic scale between 1 hour and 4 years
|
||||
const logAge = Math.log(age / oneHour);
|
||||
const logMax = Math.log(fourYears / oneHour);
|
||||
const t = logAge / logMax;
|
||||
return colorToHex(mix(newColor, oldColor, t));
|
||||
}
|
||||
} else {
|
||||
return pendingColorHex;
|
||||
}
|
||||
}
|
||||
|
||||
onChartClick(e): void {
|
||||
if (e.data?.[1]?.txid) {
|
||||
this.zone.run(() => {
|
||||
const url = this.relativeUrlPipe.transform(`/tx/${e.data[1].txid}`);
|
||||
if (e.event.event.shiftKey || e.event.event.ctrlKey || e.event.event.metaKey) {
|
||||
window.open(url + '?mode=details#vout=' + e.data[1].vout);
|
||||
} else {
|
||||
this.router.navigate([url], { fragment: `vout=${e.data[1].vout}` });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onChartInit(ec): void {
|
||||
this.chartInstance = ec;
|
||||
this.chartInstance.on('click', 'series', this.onChartClick.bind(this));
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.subscription) {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
clearInterval(this.updateInterval);
|
||||
}
|
||||
|
||||
isMobile(): boolean {
|
||||
return (window.innerWidth <= 767.98);
|
||||
}
|
||||
}
|
|
@ -9163,11 +9163,13 @@ export const restApiDocsData = [
|
|||
Filters can be applied:<ul>
|
||||
<li><code>status</code>: <code>all</code>, <code>requested</code>, <code>accelerating</code>, <code>mined</code>, <code>completed</code>, <code>failed</code></li>
|
||||
<li><code>timeframe</code>: <code>24h</code>, <code>3d</code>, <code>1w</code>, <code>1m</code>, <code>3m</code>, <code>6m</code>, <code>1y</code>, <code>2y</code>, <code>3y</code>, <code>4y</code>, <code>all</code></li>
|
||||
<li><code>poolUniqueId</code>: any id from <a target="_blank" href="https://github.com/mempool/mining-pools/blob/master/pools-v2.json">https://github.com/mempool/mining-pools/blob/master/pools-v2.json</a>. <i>Note: This will return all acceleration requests accepted by the pool but the the listed transactions may have been mined by another pool.</i>
|
||||
<li><code>minedByPoolUniqueId</code>: any id from <a target="_blank" href="https://github.com/mempool/mining-pools/blob/master/pools-v2.json">pools-v2.json</a>
|
||||
<li><code>blockHash</code>: a block hash</a>
|
||||
<li><code>blockHeight</code>: a block height</a>
|
||||
<li><code>page</code>: the requested page number if using pagination <i>(min: 1)</i></a>
|
||||
<li><code>pageLength</code>: the page lenght if using pagination <i>(min: 1, max: 50)</i></a>
|
||||
<li><code>from</code>: unix timestamp (<i>overrides <code>timeframe</code></i>)</a>
|
||||
<li><code>to</code>: unix timestamp (<i>overrides <code>timeframe</code></i>)</a>
|
||||
</ul></p>`
|
||||
},
|
||||
urlString: "/v1/services/accelerator/accelerations/history",
|
||||
|
@ -9187,21 +9189,22 @@ export const restApiDocsData = [
|
|||
headers: '',
|
||||
response: `[
|
||||
{
|
||||
"txid": "d7e1796d8eb4a09d4e6c174e36cfd852f1e6e6c9f7df4496339933cd32cbdd1d",
|
||||
"status": "completed",
|
||||
"added": 1707421053,
|
||||
"lastUpdated": 1719134667,
|
||||
"effectiveFee": 146,
|
||||
"effectiveVsize": 141,
|
||||
"feeDelta": 14000,
|
||||
"blockHash": "00000000000000000000482f0746d62141694b9210a813b97eb8445780a32003",
|
||||
"blockHeight": 829559,
|
||||
"bidBoost": 3239,
|
||||
"boostVersion": "v1",
|
||||
"txid": "f829900985aad885c13fb90555d27514b05a338202c7ef5d694f4813ad474487",
|
||||
"status": "completed_provisional",
|
||||
"added": 1728111527,
|
||||
"lastUpdated": 1728112113,
|
||||
"effectiveFee": 1385,
|
||||
"effectiveVsize": 276,
|
||||
"feeDelta": 3000,
|
||||
"blockHash": "00000000000000000000cde89e34036ece454ca2d07ddd7f71ab46307ca87423",
|
||||
"blockHeight": 864248,
|
||||
"bidBoost": 65,
|
||||
"boostVersion": "v2",
|
||||
"pools": [
|
||||
111
|
||||
111,
|
||||
115,
|
||||
],
|
||||
"minedByPoolUniqueId": 111
|
||||
"minedByPoolUniqueId": 115
|
||||
}
|
||||
]`,
|
||||
},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Import tree-shakeable echarts
|
||||
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 { SVGRenderer, CanvasRenderer } from 'echarts/renderers';
|
||||
// Typescript interfaces
|
||||
|
@ -12,6 +12,7 @@ echarts.use([
|
|||
TitleComponent, TooltipComponent, GridComponent,
|
||||
LegendComponent, GeoComponent, DataZoomComponent,
|
||||
VisualMapComponent, MarkLineComponent,
|
||||
LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart
|
||||
LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart,
|
||||
CustomChart,
|
||||
]);
|
||||
export { echarts, EChartsOption, TreemapSeriesOption, LineSeriesOption, PieSeriesOption };
|
|
@ -36,6 +36,7 @@ import { HashrateChartPoolsComponent } from '../components/hashrates-chart-pools
|
|||
import { BlockHealthGraphComponent } from '../components/block-health-graph/block-health-graph.component';
|
||||
import { AddressComponent } from '../components/address/address.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 { CommonModule } from '@angular/common';
|
||||
|
||||
|
@ -76,6 +77,7 @@ import { CommonModule } from '@angular/common';
|
|||
HashrateChartPoolsComponent,
|
||||
BlockHealthGraphComponent,
|
||||
AddressGraphComponent,
|
||||
UtxoGraphComponent,
|
||||
ActiveAccelerationBox,
|
||||
],
|
||||
imports: [
|
||||
|
|
|
@ -233,3 +233,10 @@ interface AssetStats {
|
|||
peg_out_amount: number;
|
||||
burn_count: number;
|
||||
}
|
||||
|
||||
export interface Utxo {
|
||||
txid: string;
|
||||
vout: number;
|
||||
value: number;
|
||||
status: Status;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue