Merge branch 'master' into nymkappa/configurable-pool-update
This commit is contained in:
commit
0c87a4e7f6
12
LICENSE
12
LICENSE
@ -1,5 +1,5 @@
|
|||||||
The Mempool Open Source Project®
|
The Mempool Open Source Project®
|
||||||
Copyright (c) 2019-2023 Mempool Space K.K. and other shadowy super-coders
|
Copyright (c) 2019-2024 Mempool Space K.K. and other shadowy super-coders
|
||||||
|
|
||||||
This program is free software; you can redistribute it and/or modify it under
|
This program is free software; you can redistribute it and/or modify it under
|
||||||
the terms of the GNU Affero General Public License as published by the Free
|
the terms of the GNU Affero General Public License as published by the Free
|
||||||
@ -12,10 +12,12 @@ or any other contributor to The Mempool Open Source Project.
|
|||||||
|
|
||||||
The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®,
|
The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®,
|
||||||
Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full
|
Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full
|
||||||
Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square logo,
|
Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square Logo,
|
||||||
the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical
|
the mempool block visualization Logo, the mempool Blocks Logo, the mempool
|
||||||
Logo, and the mempool.space Horizontal logo are registered trademarks or trademarks
|
transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo,
|
||||||
of Mempool Space K.K in Japan, the United States, and/or other countries.
|
the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are
|
||||||
|
registered trademarks or trademarks of Mempool Space K.K in Japan,
|
||||||
|
the United States, and/or other countries.
|
||||||
|
|
||||||
See our full Trademark Policy and Guidelines for more details, published on
|
See our full Trademark Policy and Guidelines for more details, published on
|
||||||
<https://mempool.space/trademark-policy>.
|
<https://mempool.space/trademark-policy>.
|
||||||
|
@ -77,7 +77,7 @@ Query OK, 0 rows affected (0.00 sec)
|
|||||||
|
|
||||||
#### Build
|
#### Build
|
||||||
|
|
||||||
_Make sure to use Node.js 16.10 and npm 7._
|
_Make sure to use Node.js 20.x and npm 9.x or newer_
|
||||||
|
|
||||||
_The build process requires [Rust](https://www.rust-lang.org/tools/install) to be installed._
|
_The build process requires [Rust](https://www.rust-lang.org/tools/install) to be installed._
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
|
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
|
||||||
"POOLS_UPDATE_DELAY": 604800,
|
"POOLS_UPDATE_DELAY": 604800,
|
||||||
"AUDIT": false,
|
"AUDIT": false,
|
||||||
"RUST_GBT": false,
|
"RUST_GBT": true,
|
||||||
"LIMIT_GBT": false,
|
"LIMIT_GBT": false,
|
||||||
"CPFP_INDEXING": false,
|
"CPFP_INDEXING": false,
|
||||||
"DISK_CACHE_BLOCK_INTERVAL": 6,
|
"DISK_CACHE_BLOCK_INTERVAL": 6,
|
||||||
|
201
backend/package-lock.json
generated
201
backend/package-lock.json
generated
@ -1,21 +1,22 @@
|
|||||||
{
|
{
|
||||||
"name": "mempool-backend",
|
"name": "mempool-backend",
|
||||||
"version": "3.0.0-beta",
|
"version": "3.1.0-dev",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mempool-backend",
|
"name": "mempool-backend",
|
||||||
"version": "3.0.0-beta",
|
"version": "3.1.0-dev",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "GNU Affero General Public License v3.0",
|
"license": "GNU Affero General Public License v3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@babel/core": "^7.25.2",
|
||||||
"@mempool/electrum-client": "1.1.9",
|
"@mempool/electrum-client": "1.1.9",
|
||||||
"@types/node": "^18.15.3",
|
"@types/node": "^18.15.3",
|
||||||
"axios": "~1.7.2",
|
"axios": "1.7.2",
|
||||||
"bitcoinjs-lib": "~6.1.3",
|
"bitcoinjs-lib": "~6.1.3",
|
||||||
"crypto-js": "~4.2.0",
|
"crypto-js": "~4.2.0",
|
||||||
"express": "~4.19.2",
|
"express": "~4.21.0",
|
||||||
"maxmind": "~4.3.11",
|
"maxmind": "~4.3.11",
|
||||||
"mysql2": "~3.11.0",
|
"mysql2": "~3.11.0",
|
||||||
"redis": "^4.7.0",
|
"redis": "^4.7.0",
|
||||||
@ -2280,6 +2281,7 @@
|
|||||||
"version": "1.7.2",
|
"version": "1.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
|
||||||
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
|
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.6",
|
"follow-redirects": "^1.15.6",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
@ -2488,9 +2490,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/body-parser": {
|
"node_modules/body-parser": {
|
||||||
"version": "1.20.2",
|
"version": "1.20.3",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||||
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
|
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bytes": "3.1.2",
|
"bytes": "3.1.2",
|
||||||
"content-type": "~1.0.5",
|
"content-type": "~1.0.5",
|
||||||
@ -2500,7 +2502,7 @@
|
|||||||
"http-errors": "2.0.0",
|
"http-errors": "2.0.0",
|
||||||
"iconv-lite": "0.4.24",
|
"iconv-lite": "0.4.24",
|
||||||
"on-finished": "2.4.1",
|
"on-finished": "2.4.1",
|
||||||
"qs": "6.11.0",
|
"qs": "6.13.0",
|
||||||
"raw-body": "2.5.2",
|
"raw-body": "2.5.2",
|
||||||
"type-is": "~1.6.18",
|
"type-is": "~1.6.18",
|
||||||
"unpipe": "1.0.0"
|
"unpipe": "1.0.0"
|
||||||
@ -3029,9 +3031,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/encodeurl": {
|
"node_modules/encodeurl": {
|
||||||
"version": "1.0.2",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
|
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
@ -3459,36 +3461,36 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "4.19.2",
|
"version": "4.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
|
||||||
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
|
"integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "~1.3.8",
|
"accepts": "~1.3.8",
|
||||||
"array-flatten": "1.1.1",
|
"array-flatten": "1.1.1",
|
||||||
"body-parser": "1.20.2",
|
"body-parser": "1.20.3",
|
||||||
"content-disposition": "0.5.4",
|
"content-disposition": "0.5.4",
|
||||||
"content-type": "~1.0.4",
|
"content-type": "~1.0.4",
|
||||||
"cookie": "0.6.0",
|
"cookie": "0.6.0",
|
||||||
"cookie-signature": "1.0.6",
|
"cookie-signature": "1.0.6",
|
||||||
"debug": "2.6.9",
|
"debug": "2.6.9",
|
||||||
"depd": "2.0.0",
|
"depd": "2.0.0",
|
||||||
"encodeurl": "~1.0.2",
|
"encodeurl": "~2.0.0",
|
||||||
"escape-html": "~1.0.3",
|
"escape-html": "~1.0.3",
|
||||||
"etag": "~1.8.1",
|
"etag": "~1.8.1",
|
||||||
"finalhandler": "1.2.0",
|
"finalhandler": "1.3.1",
|
||||||
"fresh": "0.5.2",
|
"fresh": "0.5.2",
|
||||||
"http-errors": "2.0.0",
|
"http-errors": "2.0.0",
|
||||||
"merge-descriptors": "1.0.1",
|
"merge-descriptors": "1.0.3",
|
||||||
"methods": "~1.1.2",
|
"methods": "~1.1.2",
|
||||||
"on-finished": "2.4.1",
|
"on-finished": "2.4.1",
|
||||||
"parseurl": "~1.3.3",
|
"parseurl": "~1.3.3",
|
||||||
"path-to-regexp": "0.1.7",
|
"path-to-regexp": "0.1.10",
|
||||||
"proxy-addr": "~2.0.7",
|
"proxy-addr": "~2.0.7",
|
||||||
"qs": "6.11.0",
|
"qs": "6.13.0",
|
||||||
"range-parser": "~1.2.1",
|
"range-parser": "~1.2.1",
|
||||||
"safe-buffer": "5.2.1",
|
"safe-buffer": "5.2.1",
|
||||||
"send": "0.18.0",
|
"send": "0.19.0",
|
||||||
"serve-static": "1.15.0",
|
"serve-static": "1.16.2",
|
||||||
"setprototypeof": "1.2.0",
|
"setprototypeof": "1.2.0",
|
||||||
"statuses": "2.0.1",
|
"statuses": "2.0.1",
|
||||||
"type-is": "~1.6.18",
|
"type-is": "~1.6.18",
|
||||||
@ -3601,12 +3603,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/finalhandler": {
|
"node_modules/finalhandler": {
|
||||||
"version": "1.2.0",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
||||||
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
|
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "2.6.9",
|
"debug": "2.6.9",
|
||||||
"encodeurl": "~1.0.2",
|
"encodeurl": "~2.0.0",
|
||||||
"escape-html": "~1.0.3",
|
"escape-html": "~1.0.3",
|
||||||
"on-finished": "2.4.1",
|
"on-finished": "2.4.1",
|
||||||
"parseurl": "~1.3.3",
|
"parseurl": "~1.3.3",
|
||||||
@ -6050,9 +6052,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/merge-descriptors": {
|
"node_modules/merge-descriptors": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||||
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
|
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/merge-stream": {
|
"node_modules/merge-stream": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@ -6266,9 +6271,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/object-inspect": {
|
"node_modules/object-inspect": {
|
||||||
"version": "1.13.1",
|
"version": "1.13.2",
|
||||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
|
||||||
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
|
"integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
@ -6436,9 +6444,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/path-to-regexp": {
|
"node_modules/path-to-regexp": {
|
||||||
"version": "0.1.7",
|
"version": "0.1.10",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
|
||||||
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
|
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
|
||||||
},
|
},
|
||||||
"node_modules/path-type": {
|
"node_modules/path-type": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
@ -6646,11 +6654,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.11.0",
|
"version": "6.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"side-channel": "^1.0.4"
|
"side-channel": "^1.0.6"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
@ -6871,9 +6879,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/send": {
|
"node_modules/send": {
|
||||||
"version": "0.18.0",
|
"version": "0.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
|
||||||
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
|
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "2.6.9",
|
"debug": "2.6.9",
|
||||||
"depd": "2.0.0",
|
"depd": "2.0.0",
|
||||||
@ -6906,6 +6914,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||||
},
|
},
|
||||||
|
"node_modules/send/node_modules/encodeurl": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/send/node_modules/ms": {
|
"node_modules/send/node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@ -6917,14 +6933,14 @@
|
|||||||
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
|
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
|
||||||
},
|
},
|
||||||
"node_modules/serve-static": {
|
"node_modules/serve-static": {
|
||||||
"version": "1.15.0",
|
"version": "1.16.2",
|
||||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
|
||||||
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
|
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"encodeurl": "~1.0.2",
|
"encodeurl": "~2.0.0",
|
||||||
"escape-html": "~1.0.3",
|
"escape-html": "~1.0.3",
|
||||||
"parseurl": "~1.3.3",
|
"parseurl": "~1.3.3",
|
||||||
"send": "0.18.0"
|
"send": "0.19.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
@ -9603,9 +9619,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"body-parser": {
|
"body-parser": {
|
||||||
"version": "1.20.2",
|
"version": "1.20.3",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||||
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
|
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"bytes": "3.1.2",
|
"bytes": "3.1.2",
|
||||||
"content-type": "~1.0.5",
|
"content-type": "~1.0.5",
|
||||||
@ -9615,7 +9631,7 @@
|
|||||||
"http-errors": "2.0.0",
|
"http-errors": "2.0.0",
|
||||||
"iconv-lite": "0.4.24",
|
"iconv-lite": "0.4.24",
|
||||||
"on-finished": "2.4.1",
|
"on-finished": "2.4.1",
|
||||||
"qs": "6.11.0",
|
"qs": "6.13.0",
|
||||||
"raw-body": "2.5.2",
|
"raw-body": "2.5.2",
|
||||||
"type-is": "~1.6.18",
|
"type-is": "~1.6.18",
|
||||||
"unpipe": "1.0.0"
|
"unpipe": "1.0.0"
|
||||||
@ -9996,9 +10012,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"encodeurl": {
|
"encodeurl": {
|
||||||
"version": "1.0.2",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
|
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="
|
||||||
},
|
},
|
||||||
"error-ex": {
|
"error-ex": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
@ -10303,36 +10319,36 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"express": {
|
"express": {
|
||||||
"version": "4.19.2",
|
"version": "4.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
|
||||||
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
|
"integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"accepts": "~1.3.8",
|
"accepts": "~1.3.8",
|
||||||
"array-flatten": "1.1.1",
|
"array-flatten": "1.1.1",
|
||||||
"body-parser": "1.20.2",
|
"body-parser": "1.20.3",
|
||||||
"content-disposition": "0.5.4",
|
"content-disposition": "0.5.4",
|
||||||
"content-type": "~1.0.4",
|
"content-type": "~1.0.4",
|
||||||
"cookie": "0.6.0",
|
"cookie": "0.6.0",
|
||||||
"cookie-signature": "1.0.6",
|
"cookie-signature": "1.0.6",
|
||||||
"debug": "2.6.9",
|
"debug": "2.6.9",
|
||||||
"depd": "2.0.0",
|
"depd": "2.0.0",
|
||||||
"encodeurl": "~1.0.2",
|
"encodeurl": "~2.0.0",
|
||||||
"escape-html": "~1.0.3",
|
"escape-html": "~1.0.3",
|
||||||
"etag": "~1.8.1",
|
"etag": "~1.8.1",
|
||||||
"finalhandler": "1.2.0",
|
"finalhandler": "1.3.1",
|
||||||
"fresh": "0.5.2",
|
"fresh": "0.5.2",
|
||||||
"http-errors": "2.0.0",
|
"http-errors": "2.0.0",
|
||||||
"merge-descriptors": "1.0.1",
|
"merge-descriptors": "1.0.3",
|
||||||
"methods": "~1.1.2",
|
"methods": "~1.1.2",
|
||||||
"on-finished": "2.4.1",
|
"on-finished": "2.4.1",
|
||||||
"parseurl": "~1.3.3",
|
"parseurl": "~1.3.3",
|
||||||
"path-to-regexp": "0.1.7",
|
"path-to-regexp": "0.1.10",
|
||||||
"proxy-addr": "~2.0.7",
|
"proxy-addr": "~2.0.7",
|
||||||
"qs": "6.11.0",
|
"qs": "6.13.0",
|
||||||
"range-parser": "~1.2.1",
|
"range-parser": "~1.2.1",
|
||||||
"safe-buffer": "5.2.1",
|
"safe-buffer": "5.2.1",
|
||||||
"send": "0.18.0",
|
"send": "0.19.0",
|
||||||
"serve-static": "1.15.0",
|
"serve-static": "1.16.2",
|
||||||
"setprototypeof": "1.2.0",
|
"setprototypeof": "1.2.0",
|
||||||
"statuses": "2.0.1",
|
"statuses": "2.0.1",
|
||||||
"type-is": "~1.6.18",
|
"type-is": "~1.6.18",
|
||||||
@ -10434,12 +10450,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"finalhandler": {
|
"finalhandler": {
|
||||||
"version": "1.2.0",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
||||||
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
|
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"debug": "2.6.9",
|
"debug": "2.6.9",
|
||||||
"encodeurl": "~1.0.2",
|
"encodeurl": "~2.0.0",
|
||||||
"escape-html": "~1.0.3",
|
"escape-html": "~1.0.3",
|
||||||
"on-finished": "2.4.1",
|
"on-finished": "2.4.1",
|
||||||
"parseurl": "~1.3.3",
|
"parseurl": "~1.3.3",
|
||||||
@ -12236,9 +12252,9 @@
|
|||||||
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="
|
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="
|
||||||
},
|
},
|
||||||
"merge-descriptors": {
|
"merge-descriptors": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||||
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
|
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="
|
||||||
},
|
},
|
||||||
"merge-stream": {
|
"merge-stream": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@ -12401,9 +12417,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"object-inspect": {
|
"object-inspect": {
|
||||||
"version": "1.13.1",
|
"version": "1.13.2",
|
||||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
|
||||||
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ=="
|
"integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g=="
|
||||||
},
|
},
|
||||||
"on-finished": {
|
"on-finished": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
@ -12520,9 +12536,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"path-to-regexp": {
|
"path-to-regexp": {
|
||||||
"version": "0.1.7",
|
"version": "0.1.10",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
|
||||||
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
|
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
|
||||||
},
|
},
|
||||||
"path-type": {
|
"path-type": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
@ -12664,11 +12680,11 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"qs": {
|
"qs": {
|
||||||
"version": "6.11.0",
|
"version": "6.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"side-channel": "^1.0.4"
|
"side-channel": "^1.0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"queue-microtask": {
|
"queue-microtask": {
|
||||||
@ -12802,9 +12818,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"send": {
|
"send": {
|
||||||
"version": "0.18.0",
|
"version": "0.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
|
||||||
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
|
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"debug": "2.6.9",
|
"debug": "2.6.9",
|
||||||
"depd": "2.0.0",
|
"depd": "2.0.0",
|
||||||
@ -12836,6 +12852,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"encodeurl": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
|
||||||
|
},
|
||||||
"ms": {
|
"ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@ -12849,14 +12870,14 @@
|
|||||||
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
|
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
|
||||||
},
|
},
|
||||||
"serve-static": {
|
"serve-static": {
|
||||||
"version": "1.15.0",
|
"version": "1.16.2",
|
||||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
|
||||||
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
|
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"encodeurl": "~1.0.2",
|
"encodeurl": "~2.0.0",
|
||||||
"escape-html": "~1.0.3",
|
"escape-html": "~1.0.3",
|
||||||
"parseurl": "~1.3.3",
|
"parseurl": "~1.3.3",
|
||||||
"send": "0.18.0"
|
"send": "0.19.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"set-function-length": {
|
"set-function-length": {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mempool-backend",
|
"name": "mempool-backend",
|
||||||
"version": "3.0.0-beta",
|
"version": "3.1.0-dev",
|
||||||
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
||||||
"license": "GNU Affero General Public License v3.0",
|
"license": "GNU Affero General Public License v3.0",
|
||||||
"homepage": "https://mempool.space",
|
"homepage": "https://mempool.space",
|
||||||
@ -42,10 +42,10 @@
|
|||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.25.2",
|
||||||
"@mempool/electrum-client": "1.1.9",
|
"@mempool/electrum-client": "1.1.9",
|
||||||
"@types/node": "^18.15.3",
|
"@types/node": "^18.15.3",
|
||||||
"axios": "~1.7.2",
|
"axios": "1.7.2",
|
||||||
"bitcoinjs-lib": "~6.1.3",
|
"bitcoinjs-lib": "~6.1.3",
|
||||||
"crypto-js": "~4.2.0",
|
"crypto-js": "~4.2.0",
|
||||||
"express": "~4.19.2",
|
"express": "~4.21.0",
|
||||||
"maxmind": "~4.3.11",
|
"maxmind": "~4.3.11",
|
||||||
"mysql2": "~3.11.0",
|
"mysql2": "~3.11.0",
|
||||||
"rust-gbt": "file:./rust-gbt",
|
"rust-gbt": "file:./rust-gbt",
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Common } from '../../api/common';
|
import { Common } from '../../api/common';
|
||||||
import { MempoolTransactionExtended } from '../../mempool.interfaces';
|
import { MempoolTransactionExtended, TransactionExtended } from '../../mempool.interfaces';
|
||||||
|
|
||||||
const randomTransactions = require('./test-data/transactions-random.json');
|
const randomTransactions = require('./test-data/transactions-random.json');
|
||||||
const replacedTransactions = require('./test-data/transactions-replaced.json');
|
const replacedTransactions = require('./test-data/transactions-replaced.json');
|
||||||
@ -10,14 +10,14 @@ describe('Common', () => {
|
|||||||
describe('RBF', () => {
|
describe('RBF', () => {
|
||||||
const newTransactions = rbfTransactions.concat(randomTransactions);
|
const newTransactions = rbfTransactions.concat(randomTransactions);
|
||||||
test('should detect RBF transactions with fast method', () => {
|
test('should detect RBF transactions with fast method', () => {
|
||||||
const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions);
|
const result: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = Common.findRbfTransactions(newTransactions, replacedTransactions);
|
||||||
expect(Object.values(result).length).toEqual(2);
|
expect(Object.values(result).length).toEqual(2);
|
||||||
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
|
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
|
||||||
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
|
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should detect RBF transactions with scalable method', () => {
|
test('should detect RBF transactions with scalable method', () => {
|
||||||
const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions, true);
|
const result: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = Common.findRbfTransactions(newTransactions, replacedTransactions, true);
|
||||||
expect(Object.values(result).length).toEqual(2);
|
expect(Object.values(result).length).toEqual(2);
|
||||||
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
|
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
|
||||||
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
|
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
|
||||||
|
@ -43,7 +43,7 @@ describe('Mempool Backend Config', () => {
|
|||||||
POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json',
|
POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json',
|
||||||
POOLS_UPDATE_DELAY: 604800,
|
POOLS_UPDATE_DELAY: 604800,
|
||||||
AUDIT: false,
|
AUDIT: false,
|
||||||
RUST_GBT: false,
|
RUST_GBT: true,
|
||||||
LIMIT_GBT: false,
|
LIMIT_GBT: false,
|
||||||
CPFP_INDEXING: false,
|
CPFP_INDEXING: false,
|
||||||
MAX_BLOCKS_BULK_QUERY: 0,
|
MAX_BLOCKS_BULK_QUERY: 0,
|
||||||
|
@ -2,6 +2,7 @@ import config from '../config';
|
|||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import { MempoolTransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
|
import { MempoolTransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
|
||||||
import rbfCache from './rbf-cache';
|
import rbfCache from './rbf-cache';
|
||||||
|
import transactionUtils from './transaction-utils';
|
||||||
|
|
||||||
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
|
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
|
||||||
|
|
||||||
@ -15,7 +16,8 @@ class Audit {
|
|||||||
const matches: string[] = []; // present in both mined block and template
|
const matches: string[] = []; // present in both mined block and template
|
||||||
const added: string[] = []; // present in mined block, not in template
|
const added: string[] = []; // present in mined block, not in template
|
||||||
const unseen: string[] = []; // present in the mined block, not in our mempool
|
const unseen: string[] = []; // present in the mined block, not in our mempool
|
||||||
const prioritized: string[] = []; // higher in the block than would be expected by in-band feerate alone
|
let prioritized: string[] = []; // higher in the block than would be expected by in-band feerate alone
|
||||||
|
let deprioritized: string[] = []; // lower in the block than would be expected by in-band feerate alone
|
||||||
const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
|
const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
|
||||||
const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
|
const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
|
||||||
const accelerated: string[] = []; // prioritized by the mempool accelerator
|
const accelerated: string[] = []; // prioritized by the mempool accelerator
|
||||||
@ -133,23 +135,7 @@ class Audit {
|
|||||||
totalWeight += tx.weight;
|
totalWeight += tx.weight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
({ prioritized, deprioritized } = transactionUtils.identifyPrioritizedTransactions(transactions, 'effectiveFeePerVsize'));
|
||||||
// identify "prioritized" transactions
|
|
||||||
let lastEffectiveRate = 0;
|
|
||||||
// Iterate over the mined template from bottom to top (excluding the coinbase)
|
|
||||||
// Transactions should appear in ascending order of mining priority.
|
|
||||||
for (let i = transactions.length - 1; i > 0; i--) {
|
|
||||||
const blockTx = transactions[i];
|
|
||||||
// If a tx has a lower in-band effective fee rate than the previous tx,
|
|
||||||
// it must have been prioritized out-of-band (in order to have a higher mining priority)
|
|
||||||
// so exclude from the analysis.
|
|
||||||
if ((blockTx.effectiveFeePerVsize || 0) < lastEffectiveRate) {
|
|
||||||
prioritized.push(blockTx.txid);
|
|
||||||
// accelerated txs may or may not have their prioritized fee rate applied, so don't use them as a reference
|
|
||||||
} else if (!isAccelerated[blockTx.txid]) {
|
|
||||||
lastEffectiveRate = blockTx.effectiveFeePerVsize || 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// transactions missing from near the end of our template are probably not being censored
|
// transactions missing from near the end of our template are probably not being censored
|
||||||
let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight);
|
let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight);
|
||||||
|
@ -323,6 +323,7 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
'witness_v1_taproot': 'v1_p2tr',
|
'witness_v1_taproot': 'v1_p2tr',
|
||||||
'nonstandard': 'nonstandard',
|
'nonstandard': 'nonstandard',
|
||||||
'multisig': 'multisig',
|
'multisig': 'multisig',
|
||||||
|
'anchor': 'anchor',
|
||||||
'nulldata': 'op_return'
|
'nulldata': 'op_return'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ import difficultyAdjustment from '../difficulty-adjustment';
|
|||||||
import transactionRepository from '../../repositories/TransactionRepository';
|
import transactionRepository from '../../repositories/TransactionRepository';
|
||||||
import rbfCache from '../rbf-cache';
|
import rbfCache from '../rbf-cache';
|
||||||
import { calculateMempoolTxCpfp } from '../cpfp';
|
import { calculateMempoolTxCpfp } from '../cpfp';
|
||||||
|
import { handleError } from '../../utils/api';
|
||||||
|
|
||||||
class BitcoinRoutes {
|
class BitcoinRoutes {
|
||||||
public initRoutes(app: Application) {
|
public initRoutes(app: Application) {
|
||||||
@ -86,7 +87,7 @@ class BitcoinRoutes {
|
|||||||
res.set('Content-Type', 'application/json');
|
res.set('Content-Type', 'application/json');
|
||||||
res.send(result);
|
res.send(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,13 +106,13 @@ class BitcoinRoutes {
|
|||||||
const result = mempoolBlocks.getMempoolBlocks();
|
const result = mempoolBlocks.getMempoolBlocks();
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getTransactionTimes(req: Request, res: Response) {
|
private getTransactionTimes(req: Request, res: Response) {
|
||||||
if (!Array.isArray(req.query.txId)) {
|
if (!Array.isArray(req.query.txId)) {
|
||||||
res.status(500).send('Not an array');
|
handleError(req, res, 500, 'Not an array');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const txIds: string[] = [];
|
const txIds: string[] = [];
|
||||||
@ -128,12 +129,12 @@ class BitcoinRoutes {
|
|||||||
private async $getBatchedOutspends(req: Request, res: Response): Promise<IEsploraApi.Outspend[][] | void> {
|
private async $getBatchedOutspends(req: Request, res: Response): Promise<IEsploraApi.Outspend[][] | void> {
|
||||||
const txids_csv = req.query.txids;
|
const txids_csv = req.query.txids;
|
||||||
if (!txids_csv || typeof txids_csv !== 'string') {
|
if (!txids_csv || typeof txids_csv !== 'string') {
|
||||||
res.status(500).send('Invalid txids format');
|
handleError(req, res, 500, 'Invalid txids format');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const txids = txids_csv.split(',');
|
const txids = txids_csv.split(',');
|
||||||
if (txids.length > 50) {
|
if (txids.length > 50) {
|
||||||
res.status(400).send('Too many txids requested');
|
handleError(req, res, 400, 'Too many txids requested');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,13 +142,13 @@ class BitcoinRoutes {
|
|||||||
const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids);
|
const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids);
|
||||||
res.json(batchedOutspends);
|
res.json(batchedOutspends);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async $getCpfpInfo(req: Request, res: Response) {
|
private async $getCpfpInfo(req: Request, res: Response) {
|
||||||
if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
|
if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
|
||||||
res.status(501).send(`Invalid transaction ID.`);
|
handleError(req, res, 501, `Invalid transaction ID.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,7 +181,7 @@ class BitcoinRoutes {
|
|||||||
try {
|
try {
|
||||||
cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
|
cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send('failed to get CPFP info');
|
handleError(req, res, 500, 'failed to get CPFP info');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -209,7 +210,7 @@ class BitcoinRoutes {
|
|||||||
if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
|
if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
|
||||||
statusCode = 404;
|
statusCode = 404;
|
||||||
}
|
}
|
||||||
res.status(statusCode).send(e instanceof Error ? e.message : e);
|
handleError(req, res, statusCode, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,7 +224,7 @@ class BitcoinRoutes {
|
|||||||
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
|
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
|
||||||
statusCode = 404;
|
statusCode = 404;
|
||||||
}
|
}
|
||||||
res.status(statusCode).send(e instanceof Error ? e.message : e);
|
handleError(req, res, statusCode, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -284,13 +285,13 @@ class BitcoinRoutes {
|
|||||||
// Not modified
|
// Not modified
|
||||||
// 422 Unprocessable Entity
|
// 422 Unprocessable Entity
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422
|
||||||
res.status(422).send(`Psbt had no missing nonWitnessUtxos.`);
|
handleError(req, res, 422, `Psbt had no missing nonWitnessUtxos.`);
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e instanceof Error && new RegExp(notFoundError).test(e.message)) {
|
if (e instanceof Error && new RegExp(notFoundError).test(e.message)) {
|
||||||
res.status(404).send(e.message);
|
handleError(req, res, 404, e.message);
|
||||||
} else {
|
} else {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -304,7 +305,7 @@ class BitcoinRoutes {
|
|||||||
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
|
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
|
||||||
statusCode = 404;
|
statusCode = 404;
|
||||||
}
|
}
|
||||||
res.status(statusCode).send(e instanceof Error ? e.message : e);
|
handleError(req, res, statusCode, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -314,7 +315,7 @@ class BitcoinRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
|
||||||
res.json(transactions);
|
res.json(transactions);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -336,7 +337,7 @@ class BitcoinRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString());
|
||||||
res.json(block);
|
res.json(block);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -346,7 +347,7 @@ class BitcoinRoutes {
|
|||||||
res.setHeader('content-type', 'text/plain');
|
res.setHeader('content-type', 'text/plain');
|
||||||
res.send(blockHeader);
|
res.send(blockHeader);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -357,10 +358,11 @@ class BitcoinRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
|
||||||
res.json(auditSummary);
|
res.json(auditSummary);
|
||||||
} else {
|
} else {
|
||||||
return res.status(404).send(`audit not available`);
|
handleError(req, res, 404, `audit not available`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -371,7 +373,8 @@ class BitcoinRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
|
||||||
res.json(auditSummary);
|
res.json(auditSummary);
|
||||||
} else {
|
} else {
|
||||||
return res.status(404).send(`transaction audit not available`);
|
handleError(req, res, 404, `transaction audit not available`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
@ -388,42 +391,49 @@ class BitcoinRoutes {
|
|||||||
return await this.getLegacyBlocks(req, res);
|
return await this.getLegacyBlocks(req, res);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getBlocksByBulk(req: Request, res: Response) {
|
private async getBlocksByBulk(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { // Liquid - Not implemented
|
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { // Liquid - Not implemented
|
||||||
return res.status(404).send(`This API is only available for Bitcoin networks`);
|
handleError(req, res, 404, `This API is only available for Bitcoin networks`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (config.MEMPOOL.MAX_BLOCKS_BULK_QUERY <= 0) {
|
if (config.MEMPOOL.MAX_BLOCKS_BULK_QUERY <= 0) {
|
||||||
return res.status(404).send(`This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`);
|
handleError(req, res, 404, `This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (!Common.indexingEnabled()) {
|
if (!Common.indexingEnabled()) {
|
||||||
return res.status(404).send(`Indexing is required for this API`);
|
handleError(req, res, 404, `Indexing is required for this API`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const from = parseInt(req.params.from, 10);
|
const from = parseInt(req.params.from, 10);
|
||||||
if (!req.params.from || from < 0) {
|
if (!req.params.from || from < 0) {
|
||||||
return res.status(400).send(`Parameter 'from' must be a block height (integer)`);
|
handleError(req, res, 400, `Parameter 'from' must be a block height (integer)`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const to = req.params.to === undefined ? await bitcoinApi.$getBlockHeightTip() : parseInt(req.params.to, 10);
|
const to = req.params.to === undefined ? await bitcoinApi.$getBlockHeightTip() : parseInt(req.params.to, 10);
|
||||||
if (to < 0) {
|
if (to < 0) {
|
||||||
return res.status(400).send(`Parameter 'to' must be a block height (integer)`);
|
handleError(req, res, 400, `Parameter 'to' must be a block height (integer)`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (from > to) {
|
if (from > to) {
|
||||||
return res.status(400).send(`Parameter 'to' must be a higher block height than 'from'`);
|
handleError(req, res, 400, `Parameter 'to' must be a higher block height than 'from'`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if ((to - from + 1) > config.MEMPOOL.MAX_BLOCKS_BULK_QUERY) {
|
if ((to - from + 1) > config.MEMPOOL.MAX_BLOCKS_BULK_QUERY) {
|
||||||
return res.status(400).send(`You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`);
|
handleError(req, res, 400, `You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(await blocks.$getBlocksBetweenHeight(from, to));
|
res.json(await blocks.$getBlocksBetweenHeight(from, to));
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -458,7 +468,7 @@ class BitcoinRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(returnBlocks);
|
res.json(returnBlocks);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -483,7 +493,7 @@ class BitcoinRoutes {
|
|||||||
res.json(transactions);
|
res.json(transactions);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100);
|
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100);
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -492,13 +502,13 @@ class BitcoinRoutes {
|
|||||||
const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10));
|
const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10));
|
||||||
res.send(blockHash);
|
res.send(blockHash);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAddress(req: Request, res: Response) {
|
private async getAddress(req: Request, res: Response) {
|
||||||
if (config.MEMPOOL.BACKEND === 'none') {
|
if (config.MEMPOOL.BACKEND === 'none') {
|
||||||
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
|
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -507,15 +517,16 @@ class BitcoinRoutes {
|
|||||||
res.json(addressData);
|
res.json(addressData);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
||||||
return res.status(413).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 413, e instanceof Error ? e.message : e);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAddressTransactions(req: Request, res: Response): Promise<void> {
|
private async getAddressTransactions(req: Request, res: Response): Promise<void> {
|
||||||
if (config.MEMPOOL.BACKEND === 'none') {
|
if (config.MEMPOOL.BACKEND === 'none') {
|
||||||
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
|
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -528,23 +539,23 @@ class BitcoinRoutes {
|
|||||||
res.json(transactions);
|
res.json(transactions);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
||||||
res.status(413).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 413, e instanceof Error ? e.message : e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAddressTransactionSummary(req: Request, res: Response): Promise<void> {
|
private async getAddressTransactionSummary(req: Request, res: Response): Promise<void> {
|
||||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||||
res.status(405).send('Address summary lookups require mempool/electrs backend.');
|
handleError(req, res, 405, 'Address summary lookups require mempool/electrs backend.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getScriptHash(req: Request, res: Response) {
|
private async getScriptHash(req: Request, res: Response) {
|
||||||
if (config.MEMPOOL.BACKEND === 'none') {
|
if (config.MEMPOOL.BACKEND === 'none') {
|
||||||
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
|
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -555,15 +566,16 @@ class BitcoinRoutes {
|
|||||||
res.json(addressData);
|
res.json(addressData);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
||||||
return res.status(413).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 413, e instanceof Error ? e.message : e);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getScriptHashTransactions(req: Request, res: Response): Promise<void> {
|
private async getScriptHashTransactions(req: Request, res: Response): Promise<void> {
|
||||||
if (config.MEMPOOL.BACKEND === 'none') {
|
if (config.MEMPOOL.BACKEND === 'none') {
|
||||||
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
|
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -578,16 +590,16 @@ class BitcoinRoutes {
|
|||||||
res.json(transactions);
|
res.json(transactions);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
||||||
res.status(413).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 413, e instanceof Error ? e.message : e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getScriptHashTransactionSummary(req: Request, res: Response): Promise<void> {
|
private async getScriptHashTransactionSummary(req: Request, res: Response): Promise<void> {
|
||||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||||
res.status(405).send('Scripthash summary lookups require mempool/electrs backend.');
|
handleError(req, res, 405, 'Scripthash summary lookups require mempool/electrs backend.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -597,7 +609,7 @@ class BitcoinRoutes {
|
|||||||
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
|
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
|
||||||
res.send(blockHash);
|
res.send(blockHash);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -624,7 +636,7 @@ class BitcoinRoutes {
|
|||||||
const rawMempool = await bitcoinApi.$getRawMempool();
|
const rawMempool = await bitcoinApi.$getRawMempool();
|
||||||
res.send(rawMempool);
|
res.send(rawMempool);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -632,12 +644,13 @@ class BitcoinRoutes {
|
|||||||
try {
|
try {
|
||||||
const result = blocks.getCurrentBlockHeight();
|
const result = blocks.getCurrentBlockHeight();
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return res.status(503).send(`Service Temporarily Unavailable`);
|
handleError(req, res, 503, `Service Temporarily Unavailable`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
res.setHeader('content-type', 'text/plain');
|
res.setHeader('content-type', 'text/plain');
|
||||||
res.send(result.toString());
|
res.send(result.toString());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -647,7 +660,7 @@ class BitcoinRoutes {
|
|||||||
res.setHeader('content-type', 'text/plain');
|
res.setHeader('content-type', 'text/plain');
|
||||||
res.send(result);
|
res.send(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -657,7 +670,7 @@ class BitcoinRoutes {
|
|||||||
res.setHeader('content-type', 'application/octet-stream');
|
res.setHeader('content-type', 'application/octet-stream');
|
||||||
res.send(result);
|
res.send(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -666,7 +679,7 @@ class BitcoinRoutes {
|
|||||||
const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
|
const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -675,7 +688,7 @@ class BitcoinRoutes {
|
|||||||
const result = await bitcoinClient.validateAddress(req.params.address);
|
const result = await bitcoinClient.validateAddress(req.params.address);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -688,7 +701,7 @@ class BitcoinRoutes {
|
|||||||
replaces
|
replaces
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -697,7 +710,7 @@ class BitcoinRoutes {
|
|||||||
const result = rbfCache.getRbfTrees(false);
|
const result = rbfCache.getRbfTrees(false);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -706,7 +719,7 @@ class BitcoinRoutes {
|
|||||||
const result = rbfCache.getRbfTrees(true);
|
const result = rbfCache.getRbfTrees(true);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -719,7 +732,7 @@ class BitcoinRoutes {
|
|||||||
res.status(204).send();
|
res.status(204).send();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -728,7 +741,7 @@ class BitcoinRoutes {
|
|||||||
const result = await bitcoinApi.$getOutspends(req.params.txId);
|
const result = await bitcoinApi.$getOutspends(req.params.txId);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -738,10 +751,10 @@ class BitcoinRoutes {
|
|||||||
if (da) {
|
if (da) {
|
||||||
res.json(da);
|
res.json(da);
|
||||||
} else {
|
} else {
|
||||||
res.status(503).send(`Service Temporarily Unavailable`);
|
handleError(req, res, 503, `Service Temporarily Unavailable`);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -752,7 +765,7 @@ class BitcoinRoutes {
|
|||||||
const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx);
|
const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx);
|
||||||
res.send(txIdResult);
|
res.send(txIdResult);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
|
handleError(req, res, 400, e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
|
||||||
: (e.message || 'Error'));
|
: (e.message || 'Error'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -764,7 +777,7 @@ class BitcoinRoutes {
|
|||||||
const txIdResult = await bitcoinClient.sendRawTransaction(txHex);
|
const txIdResult = await bitcoinClient.sendRawTransaction(txHex);
|
||||||
res.send(txIdResult);
|
res.send(txIdResult);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
|
handleError(req, res, 400, e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
|
||||||
: (e.message || 'Error'));
|
: (e.message || 'Error'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -776,8 +789,7 @@ class BitcoinRoutes {
|
|||||||
const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate);
|
const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate);
|
||||||
res.send(result);
|
res.send(result);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
res.setHeader('content-type', 'text/plain');
|
handleError(req, res, 400, e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
|
||||||
res.status(400).send(e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
|
|
||||||
: (e.message || 'Error'));
|
: (e.message || 'Error'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,7 @@ import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp';
|
|||||||
import mempool from './mempool';
|
import mempool from './mempool';
|
||||||
import CpfpRepository from '../repositories/CpfpRepository';
|
import CpfpRepository from '../repositories/CpfpRepository';
|
||||||
import accelerationApi from './services/acceleration';
|
import accelerationApi from './services/acceleration';
|
||||||
|
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
|
||||||
|
|
||||||
class Blocks {
|
class Blocks {
|
||||||
private blocks: BlockExtended[] = [];
|
private blocks: BlockExtended[] = [];
|
||||||
@ -219,10 +220,10 @@ class Blocks {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public summarizeBlockTransactions(hash: string, transactions: TransactionExtended[]): BlockSummary {
|
public summarizeBlockTransactions(hash: string, height: number, transactions: TransactionExtended[]): BlockSummary {
|
||||||
return {
|
return {
|
||||||
id: hash,
|
id: hash,
|
||||||
transactions: Common.classifyTransactions(transactions),
|
transactions: Common.classifyTransactions(transactions, height),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -342,7 +343,12 @@ class Blocks {
|
|||||||
id: pool.uniqueId,
|
id: pool.uniqueId,
|
||||||
name: pool.name,
|
name: pool.name,
|
||||||
slug: pool.slug,
|
slug: pool.slug,
|
||||||
|
minerNames: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (extras.pool.name === 'OCEAN') {
|
||||||
|
extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extras.matchRate = null;
|
extras.matchRate = null;
|
||||||
@ -616,7 +622,7 @@ class Blocks {
|
|||||||
// add CPFP
|
// add CPFP
|
||||||
const cpfpSummary = calculateGoodBlockCpfp(height, txs, []);
|
const cpfpSummary = calculateGoodBlockCpfp(height, txs, []);
|
||||||
// classify
|
// classify
|
||||||
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
|
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions);
|
||||||
await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 2);
|
await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 2);
|
||||||
if (unclassifiedBlocks[height].version < 2 && targetSummaryVersion === 2) {
|
if (unclassifiedBlocks[height].version < 2 && targetSummaryVersion === 2) {
|
||||||
const cpfpClusters = await CpfpRepository.$getClustersAt(height);
|
const cpfpClusters = await CpfpRepository.$getClustersAt(height);
|
||||||
@ -653,7 +659,7 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
const cpfpSummary = calculateGoodBlockCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as MempoolTransactionExtended[], []);
|
const cpfpSummary = calculateGoodBlockCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as MempoolTransactionExtended[], []);
|
||||||
// classify
|
// classify
|
||||||
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
|
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions);
|
||||||
const classifiedTxMap: { [txid: string]: TransactionClassified } = {};
|
const classifiedTxMap: { [txid: string]: TransactionClassified } = {};
|
||||||
for (const tx of classifiedTxs) {
|
for (const tx of classifiedTxs) {
|
||||||
classifiedTxMap[tx.txid] = tx;
|
classifiedTxMap[tx.txid] = tx;
|
||||||
@ -912,7 +918,7 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
const cpfpSummary: CpfpSummary = calculateGoodBlockCpfp(block.height, transactions, accelerations.map(a => ({ txid: a.txid, max_bid: a.feeDelta })));
|
const cpfpSummary: CpfpSummary = calculateGoodBlockCpfp(block.height, transactions, accelerations.map(a => ({ txid: a.txid, max_bid: a.feeDelta })));
|
||||||
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
|
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
|
||||||
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions);
|
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, block.height, cpfpSummary.transactions);
|
||||||
this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`);
|
this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`);
|
||||||
|
|
||||||
if (Common.indexingEnabled()) {
|
if (Common.indexingEnabled()) {
|
||||||
@ -1169,7 +1175,7 @@ class Blocks {
|
|||||||
transactions: cpfpSummary.transactions.map(tx => {
|
transactions: cpfpSummary.transactions.map(tx => {
|
||||||
let flags: number = 0;
|
let flags: number = 0;
|
||||||
try {
|
try {
|
||||||
flags = Common.getTransactionFlags(tx);
|
flags = Common.getTransactionFlags(tx, height);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e));
|
logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
@ -1188,7 +1194,7 @@ class Blocks {
|
|||||||
} else {
|
} else {
|
||||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||||
const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
|
const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||||
summary = this.summarizeBlockTransactions(hash, txs);
|
summary = this.summarizeBlockTransactions(hash, height || 0, txs);
|
||||||
summaryVersion = 1;
|
summaryVersion = 1;
|
||||||
} else {
|
} else {
|
||||||
// Call Core RPC
|
// Call Core RPC
|
||||||
@ -1324,7 +1330,7 @@ class Blocks {
|
|||||||
let summaryVersion = 0;
|
let summaryVersion = 0;
|
||||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||||
const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx));
|
const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||||
summary = this.summarizeBlockTransactions(cleanBlock.hash, txs);
|
summary = this.summarizeBlockTransactions(cleanBlock.hash, cleanBlock.height, txs);
|
||||||
summaryVersion = 1;
|
summaryVersion = 1;
|
||||||
} else {
|
} else {
|
||||||
// Call Core RPC
|
// Call Core RPC
|
||||||
|
@ -10,7 +10,6 @@ import logger from '../logger';
|
|||||||
import { getVarIntLength, opcodes, parseMultisigScript } from '../utils/bitcoin-script';
|
import { getVarIntLength, opcodes, parseMultisigScript } from '../utils/bitcoin-script';
|
||||||
|
|
||||||
// Bitcoin Core default policy settings
|
// Bitcoin Core default policy settings
|
||||||
const TX_MAX_STANDARD_VERSION = 2;
|
|
||||||
const MAX_STANDARD_TX_WEIGHT = 400_000;
|
const MAX_STANDARD_TX_WEIGHT = 400_000;
|
||||||
const MAX_BLOCK_SIGOPS_COST = 80_000;
|
const MAX_BLOCK_SIGOPS_COST = 80_000;
|
||||||
const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5);
|
const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5);
|
||||||
@ -80,8 +79,8 @@ export class Common {
|
|||||||
return arr;
|
return arr;
|
||||||
}
|
}
|
||||||
|
|
||||||
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: MempoolTransactionExtended[] } {
|
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} {
|
||||||
const matches: { [txid: string]: MempoolTransactionExtended[] } = {};
|
const matches: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = {};
|
||||||
|
|
||||||
// For small N, a naive nested loop is extremely fast, but it doesn't scale
|
// For small N, a naive nested loop is extremely fast, but it doesn't scale
|
||||||
if (added.length < 1000 && deleted.length < 50 && !forceScalable) {
|
if (added.length < 1000 && deleted.length < 50 && !forceScalable) {
|
||||||
@ -96,7 +95,7 @@ export class Common {
|
|||||||
addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
|
addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
|
||||||
});
|
});
|
||||||
if (foundMatches?.length) {
|
if (foundMatches?.length) {
|
||||||
matches[addedTx.txid] = [...new Set(foundMatches)];
|
matches[addedTx.txid] = { replaced: [...new Set(foundMatches)], replacedBy: addedTx };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -124,7 +123,7 @@ export class Common {
|
|||||||
foundMatches.add(deletedTx);
|
foundMatches.add(deletedTx);
|
||||||
}
|
}
|
||||||
if (foundMatches.size) {
|
if (foundMatches.size) {
|
||||||
matches[addedTx.txid] = [...foundMatches];
|
matches[addedTx.txid] = { replaced: [...foundMatches], replacedBy: addedTx };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,17 +138,17 @@ export class Common {
|
|||||||
const replaced: Set<MempoolTransactionExtended> = new Set();
|
const replaced: Set<MempoolTransactionExtended> = new Set();
|
||||||
for (let i = 0; i < tx.vin.length; i++) {
|
for (let i = 0; i < tx.vin.length; i++) {
|
||||||
const vin = tx.vin[i];
|
const vin = tx.vin[i];
|
||||||
const match = spendMap.get(`${vin.txid}:${vin.vout}`);
|
const key = `${vin.txid}:${vin.vout}`;
|
||||||
|
const match = spendMap.get(key);
|
||||||
if (match && match.txid !== tx.txid) {
|
if (match && match.txid !== tx.txid) {
|
||||||
replaced.add(match);
|
replaced.add(match);
|
||||||
// remove this tx from the spendMap
|
// remove this tx from the spendMap
|
||||||
// prevents the same tx being replaced more than once
|
// prevents the same tx being replaced more than once
|
||||||
for (const replacedVin of match.vin) {
|
for (const replacedVin of match.vin) {
|
||||||
const key = `${replacedVin.txid}:${replacedVin.vout}`;
|
const replacedKey = `${replacedVin.txid}:${replacedVin.vout}`;
|
||||||
spendMap.delete(key);
|
spendMap.delete(replacedKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const key = `${vin.txid}:${vin.vout}`;
|
|
||||||
spendMap.delete(key);
|
spendMap.delete(key);
|
||||||
}
|
}
|
||||||
if (replaced.size) {
|
if (replaced.size) {
|
||||||
@ -200,10 +199,13 @@ export class Common {
|
|||||||
*
|
*
|
||||||
* returns true early if any standardness rule is violated, otherwise false
|
* returns true early if any standardness rule is violated, otherwise false
|
||||||
* (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced)
|
* (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced)
|
||||||
|
*
|
||||||
|
* As standardness rules change, we'll need to apply the rules in force *at the time* to older blocks.
|
||||||
|
* For now, just pull out individual rules into versioned functions where necessary.
|
||||||
*/
|
*/
|
||||||
static isNonStandard(tx: TransactionExtended): boolean {
|
static isNonStandard(tx: TransactionExtended, height?: number): boolean {
|
||||||
// version
|
// version
|
||||||
if (tx.version > TX_MAX_STANDARD_VERSION) {
|
if (this.isNonStandardVersion(tx, height)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,6 +252,8 @@ export class Common {
|
|||||||
}
|
}
|
||||||
} else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
|
} else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
|
||||||
return true;
|
return true;
|
||||||
|
} else if (this.isNonStandardAnchor(tx, height)) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
// TODO: bad-witness-nonstandard
|
// TODO: bad-witness-nonstandard
|
||||||
}
|
}
|
||||||
@ -335,6 +339,49 @@ export class Common {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Individual versioned standardness rules
|
||||||
|
|
||||||
|
static V3_STANDARDNESS_ACTIVATION_HEIGHT = {
|
||||||
|
'testnet4': 42_000,
|
||||||
|
'testnet': 2_900_000,
|
||||||
|
'signet': 211_000,
|
||||||
|
'': 863_500,
|
||||||
|
};
|
||||||
|
static isNonStandardVersion(tx: TransactionExtended, height?: number): boolean {
|
||||||
|
let TX_MAX_STANDARD_VERSION = 3;
|
||||||
|
if (
|
||||||
|
height != null
|
||||||
|
&& this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
|
||||||
|
&& height <= this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
|
||||||
|
) {
|
||||||
|
// V3 transactions were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
|
||||||
|
TX_MAX_STANDARD_VERSION = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tx.version > TX_MAX_STANDARD_VERSION) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT = {
|
||||||
|
'testnet4': 42_000,
|
||||||
|
'testnet': 2_900_000,
|
||||||
|
'signet': 211_000,
|
||||||
|
'': 863_500,
|
||||||
|
};
|
||||||
|
static isNonStandardAnchor(tx: TransactionExtended, height?: number): boolean {
|
||||||
|
if (
|
||||||
|
height != null
|
||||||
|
&& this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
|
||||||
|
&& height <= this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
|
||||||
|
) {
|
||||||
|
// anchor outputs were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
static getNonWitnessSize(tx: TransactionExtended): number {
|
static getNonWitnessSize(tx: TransactionExtended): number {
|
||||||
let weight = tx.weight;
|
let weight = tx.weight;
|
||||||
let hasWitness = false;
|
let hasWitness = false;
|
||||||
@ -415,7 +462,7 @@ export class Common {
|
|||||||
return flags;
|
return flags;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getTransactionFlags(tx: TransactionExtended): number {
|
static getTransactionFlags(tx: TransactionExtended, height?: number): number {
|
||||||
let flags = tx.flags ? BigInt(tx.flags) : 0n;
|
let flags = tx.flags ? BigInt(tx.flags) : 0n;
|
||||||
|
|
||||||
// Update variable flags (CPFP, RBF)
|
// Update variable flags (CPFP, RBF)
|
||||||
@ -564,17 +611,17 @@ export class Common {
|
|||||||
flags |= TransactionFlags.batch_payout;
|
flags |= TransactionFlags.batch_payout;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isNonStandard(tx)) {
|
if (this.isNonStandard(tx, height)) {
|
||||||
flags |= TransactionFlags.nonstandard;
|
flags |= TransactionFlags.nonstandard;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Number(flags);
|
return Number(flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
static classifyTransaction(tx: TransactionExtended): TransactionClassified {
|
static classifyTransaction(tx: TransactionExtended, height?: number): TransactionClassified {
|
||||||
let flags = 0;
|
let flags = 0;
|
||||||
try {
|
try {
|
||||||
flags = Common.getTransactionFlags(tx);
|
flags = Common.getTransactionFlags(tx, height);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn('Failed to add classification flags to transaction: ' + (e instanceof Error ? e.message : e));
|
logger.warn('Failed to add classification flags to transaction: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
@ -585,8 +632,8 @@ export class Common {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static classifyTransactions(txs: TransactionExtended[]): TransactionClassified[] {
|
static classifyTransactions(txs: TransactionExtended[], height?: number): TransactionClassified[] {
|
||||||
return txs.map(Common.classifyTransaction);
|
return txs.map(tx => Common.classifyTransaction(tx, height));
|
||||||
}
|
}
|
||||||
|
|
||||||
static stripTransaction(tx: TransactionExtended): TransactionStripped {
|
static stripTransaction(tx: TransactionExtended): TransactionStripped {
|
||||||
|
@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
|
|||||||
import { RowDataPacket } from 'mysql2';
|
import { RowDataPacket } from 'mysql2';
|
||||||
|
|
||||||
class DatabaseMigration {
|
class DatabaseMigration {
|
||||||
private static currentVersion = 81;
|
private static currentVersion = 82;
|
||||||
private queryTimeout = 3600_000;
|
private queryTimeout = 3600_000;
|
||||||
private statisticsAddedIndexed = false;
|
private statisticsAddedIndexed = false;
|
||||||
private uniqueLogs: string[] = [];
|
private uniqueLogs: string[] = [];
|
||||||
@ -700,6 +700,11 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"');
|
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"');
|
||||||
await this.updateToSchemaVersion(81);
|
await this.updateToSchemaVersion(81);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 82 && isBitcoin === true && config.MEMPOOL.NETWORK === 'mainnet') {
|
||||||
|
await this.$fixBadV1AuditBlocks();
|
||||||
|
await this.updateToSchemaVersion(82);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1314,6 +1319,28 @@ class DatabaseMigration {
|
|||||||
logger.warn(`Failed to migrate cpfp transaction data`);
|
logger.warn(`Failed to migrate cpfp transaction data`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async $fixBadV1AuditBlocks(): Promise<void> {
|
||||||
|
const badBlocks = [
|
||||||
|
'000000000000000000011ad49227fc8c9ba0ca96ad2ebce41a862f9a244478dc',
|
||||||
|
'000000000000000000010ac1f68b3080153f2826ffddc87ceffdd68ed97d6960',
|
||||||
|
'000000000000000000024cbdafeb2660ae8bd2947d166e7fe15d1689e86b2cf7',
|
||||||
|
'00000000000000000002e1dbfbf6ae057f331992a058b822644b368034f87286',
|
||||||
|
'0000000000000000000019973b2778f08ad6d21e083302ff0833d17066921ebb',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const hash of badBlocks) {
|
||||||
|
try {
|
||||||
|
await this.$executeQuery(`
|
||||||
|
UPDATE blocks_audits
|
||||||
|
SET prioritized_txs = '[]'
|
||||||
|
WHERE hash = '${hash}'
|
||||||
|
`, true);
|
||||||
|
} catch (e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new DatabaseMigration();
|
export default new DatabaseMigration();
|
||||||
|
@ -257,6 +257,7 @@ class DiskCache {
|
|||||||
trees: rbfData.rbf.trees,
|
trees: rbfData.rbf.trees,
|
||||||
expiring: rbfData.rbf.expiring.map(([txid, value]) => ({ key: txid, value })),
|
expiring: rbfData.rbf.expiring.map(([txid, value]) => ({ key: txid, value })),
|
||||||
mempool: memPool.getMempool(),
|
mempool: memPool.getMempool(),
|
||||||
|
spendMap: memPool.getSpendMap(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import { Application, Request, Response } from 'express';
|
import { Application, Request, Response } from 'express';
|
||||||
import channelsApi from './channels.api';
|
import channelsApi from './channels.api';
|
||||||
|
import { handleError } from '../../utils/api';
|
||||||
|
|
||||||
class ChannelsRoutes {
|
class ChannelsRoutes {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
@ -22,7 +23,7 @@ class ChannelsRoutes {
|
|||||||
const channels = await channelsApi.$searchChannelsById(req.params.search);
|
const channels = await channelsApi.$searchChannelsById(req.params.search);
|
||||||
res.json(channels);
|
res.json(channels);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,7 +39,7 @@ class ChannelsRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(channel);
|
res.json(channel);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,11 +54,11 @@ class ChannelsRoutes {
|
|||||||
const status: string = typeof req.query.status === 'string' ? req.query.status : '';
|
const status: string = typeof req.query.status === 'string' ? req.query.status : '';
|
||||||
|
|
||||||
if (index < -1) {
|
if (index < -1) {
|
||||||
res.status(400).send('Invalid index');
|
handleError(req, res, 400, 'Invalid index');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (['open', 'active', 'closed'].includes(status) === false) {
|
if (['open', 'active', 'closed'].includes(status) === false) {
|
||||||
res.status(400).send('Invalid status');
|
handleError(req, res, 400, 'Invalid status');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,14 +70,14 @@ class ChannelsRoutes {
|
|||||||
res.header('X-Total-Count', channelsCount.toString());
|
res.header('X-Total-Count', channelsCount.toString());
|
||||||
res.json(channels);
|
res.json(channels);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async $getChannelsByTransactionIds(req: Request, res: Response): Promise<void> {
|
private async $getChannelsByTransactionIds(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
if (!Array.isArray(req.query.txId)) {
|
if (!Array.isArray(req.query.txId)) {
|
||||||
res.status(400).send('Not an array');
|
handleError(req, res, 400, 'Not an array');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const txIds: string[] = [];
|
const txIds: string[] = [];
|
||||||
@ -107,7 +108,7 @@ class ChannelsRoutes {
|
|||||||
|
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,7 +120,7 @@ class ChannelsRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(channels);
|
res.json(channels);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,7 +133,7 @@ class ChannelsRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(channels);
|
res.json(channels);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,8 @@ import { Application, Request, Response } from 'express';
|
|||||||
import nodesApi from './nodes.api';
|
import nodesApi from './nodes.api';
|
||||||
import channelsApi from './channels.api';
|
import channelsApi from './channels.api';
|
||||||
import statisticsApi from './statistics.api';
|
import statisticsApi from './statistics.api';
|
||||||
|
import { handleError } from '../../utils/api';
|
||||||
|
|
||||||
class GeneralLightningRoutes {
|
class GeneralLightningRoutes {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
@ -27,7 +29,7 @@ class GeneralLightningRoutes {
|
|||||||
channels: channels,
|
channels: channels,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,7 +43,7 @@ class GeneralLightningRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(statistics);
|
res.json(statistics);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,7 +52,7 @@ class GeneralLightningRoutes {
|
|||||||
const statistics = await statisticsApi.$getLatestStatistics();
|
const statistics = await statisticsApi.$getLatestStatistics();
|
||||||
res.json(statistics);
|
res.json(statistics);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import { Application, Request, Response } from 'express';
|
|||||||
import nodesApi from './nodes.api';
|
import nodesApi from './nodes.api';
|
||||||
import DB from '../../database';
|
import DB from '../../database';
|
||||||
import { INodesRanking } from '../../mempool.interfaces';
|
import { INodesRanking } from '../../mempool.interfaces';
|
||||||
|
import { handleError } from '../../utils/api';
|
||||||
|
|
||||||
class NodesRoutes {
|
class NodesRoutes {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
@ -31,7 +32,7 @@ class NodesRoutes {
|
|||||||
const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search);
|
const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search);
|
||||||
res.json(nodes);
|
res.json(nodes);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,7 +188,7 @@ class NodesRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(nodes);
|
res.json(nodes);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,7 +196,7 @@ class NodesRoutes {
|
|||||||
try {
|
try {
|
||||||
const node = await nodesApi.$getNode(req.params.public_key);
|
const node = await nodesApi.$getNode(req.params.public_key);
|
||||||
if (!node) {
|
if (!node) {
|
||||||
res.status(404).send('Node not found');
|
handleError(req, res, 404, 'Node not found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.header('Pragma', 'public');
|
res.header('Pragma', 'public');
|
||||||
@ -203,7 +204,7 @@ class NodesRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(node);
|
res.json(node);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,7 +216,7 @@ class NodesRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(statistics);
|
res.json(statistics);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,7 +224,7 @@ class NodesRoutes {
|
|||||||
try {
|
try {
|
||||||
const node = await nodesApi.$getFeeHistogram(req.params.public_key);
|
const node = await nodesApi.$getFeeHistogram(req.params.public_key);
|
||||||
if (!node) {
|
if (!node) {
|
||||||
res.status(404).send('Node not found');
|
handleError(req, res, 404, 'Node not found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.header('Pragma', 'public');
|
res.header('Pragma', 'public');
|
||||||
@ -231,7 +232,7 @@ class NodesRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(node);
|
res.json(node);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,7 +248,7 @@ class NodesRoutes {
|
|||||||
topByChannels: topChannelsNodes,
|
topByChannels: topChannelsNodes,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -259,7 +260,7 @@ class NodesRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(topCapacityNodes);
|
res.json(topCapacityNodes);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,7 +272,7 @@ class NodesRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(topCapacityNodes);
|
res.json(topCapacityNodes);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -283,7 +284,7 @@ class NodesRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(topCapacityNodes);
|
res.json(topCapacityNodes);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,7 +296,7 @@ class NodesRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
|
||||||
res.json(nodesPerAs);
|
res.json(nodesPerAs);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -307,7 +308,7 @@ class NodesRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
|
||||||
res.json(worldNodes);
|
res.json(worldNodes);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -322,7 +323,7 @@ class NodesRoutes {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (country.length === 0) {
|
if (country.length === 0) {
|
||||||
res.status(404).send(`This country does not exist or does not host any lightning nodes on clearnet`);
|
handleError(req, res, 404, `This country does not exist or does not host any lightning nodes on clearnet`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -335,7 +336,7 @@ class NodesRoutes {
|
|||||||
nodes: nodes,
|
nodes: nodes,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -349,7 +350,7 @@ class NodesRoutes {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (isp.length === 0) {
|
if (isp.length === 0) {
|
||||||
res.status(404).send(`This ISP does not exist or does not host any lightning nodes on clearnet`);
|
handleError(req, res, 404, `This ISP does not exist or does not host any lightning nodes on clearnet`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -362,7 +363,7 @@ class NodesRoutes {
|
|||||||
nodes: nodes,
|
nodes: nodes,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -374,7 +375,7 @@ class NodesRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
|
||||||
res.json(nodesPerAs);
|
res.json(nodesPerAs);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import { Application, Request, Response } from 'express';
|
|||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import elementsParser from './elements-parser';
|
import elementsParser from './elements-parser';
|
||||||
import icons from './icons';
|
import icons from './icons';
|
||||||
|
import { handleError } from '../../utils/api';
|
||||||
|
|
||||||
class LiquidRoutes {
|
class LiquidRoutes {
|
||||||
public initRoutes(app: Application) {
|
public initRoutes(app: Application) {
|
||||||
@ -42,7 +43,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('content-length', result.length);
|
res.setHeader('content-length', result.length);
|
||||||
res.send(result);
|
res.send(result);
|
||||||
} else {
|
} else {
|
||||||
res.status(404).send('Asset icon not found');
|
handleError(req, res, 404, 'Asset icon not found');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,7 +52,7 @@ class LiquidRoutes {
|
|||||||
if (result) {
|
if (result) {
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} else {
|
} else {
|
||||||
res.status(404).send('Asset icons not found');
|
handleError(req, res, 404, 'Asset icons not found');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,7 +83,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
|
||||||
res.json(pegs);
|
res.json(pegs);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,7 +95,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
|
||||||
res.json(reserves);
|
res.json(reserves);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,7 +107,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
res.json(currentSupply);
|
res.json(currentSupply);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,7 +119,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
res.json(currentReserves);
|
res.json(currentReserves);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,7 +131,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
res.json(auditStatus);
|
res.json(auditStatus);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,7 +143,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
res.json(federationAddresses);
|
res.json(federationAddresses);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,7 +155,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
res.json(federationAddresses);
|
res.json(federationAddresses);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,7 +167,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
res.json(federationUtxos);
|
res.json(federationUtxos);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,7 +179,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
res.json(expiredUtxos);
|
res.json(expiredUtxos);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,7 +191,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
res.json(federationUtxos);
|
res.json(federationUtxos);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,7 +203,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
res.json(emergencySpentUtxos);
|
res.json(emergencySpentUtxos);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,7 +215,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
res.json(emergencySpentUtxos);
|
res.json(emergencySpentUtxos);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,7 +227,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
res.json(recentPegs);
|
res.json(recentPegs);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -238,7 +239,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
res.json(pegsVolume);
|
res.json(pegsVolume);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,7 +251,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
res.json(pegsCount);
|
res.json(pegsCount);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -369,7 +369,7 @@ class MempoolBlocks {
|
|||||||
const lastBlockIndex = blocks.length - 1;
|
const lastBlockIndex = blocks.length - 1;
|
||||||
let hasBlockStack = blocks.length >= 8;
|
let hasBlockStack = blocks.length >= 8;
|
||||||
let stackWeight;
|
let stackWeight;
|
||||||
let feeStatsCalculator: OnlineFeeStatsCalculator | void;
|
let feeStatsCalculator: OnlineFeeStatsCalculator | null = null;
|
||||||
if (hasBlockStack) {
|
if (hasBlockStack) {
|
||||||
if (blockWeights && blockWeights[7] !== null) {
|
if (blockWeights && blockWeights[7] !== null) {
|
||||||
stackWeight = blockWeights[7];
|
stackWeight = blockWeights[7];
|
||||||
@ -380,28 +380,36 @@ class MempoolBlocks {
|
|||||||
feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5, [10, 20, 30, 40, 50, 60, 70, 80, 90]);
|
feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5, [10, 20, 30, 40, 50, 60, 70, 80, 90]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ancestors: Ancestor[] = [];
|
||||||
|
const descendants: Ancestor[] = [];
|
||||||
|
let ancestor: MempoolTransactionExtended
|
||||||
for (const cluster of clusters) {
|
for (const cluster of clusters) {
|
||||||
for (const memberTxid of cluster) {
|
for (const memberTxid of cluster) {
|
||||||
const mempoolTx = mempool[memberTxid];
|
const mempoolTx = mempool[memberTxid];
|
||||||
if (mempoolTx) {
|
if (mempoolTx) {
|
||||||
const ancestors: Ancestor[] = [];
|
// ugly micro-optimization to avoid allocating new arrays
|
||||||
const descendants: Ancestor[] = [];
|
ancestors.length = 0;
|
||||||
|
descendants.length = 0;
|
||||||
let matched = false;
|
let matched = false;
|
||||||
cluster.forEach(txid => {
|
cluster.forEach(txid => {
|
||||||
|
ancestor = mempool[txid];
|
||||||
if (txid === memberTxid) {
|
if (txid === memberTxid) {
|
||||||
matched = true;
|
matched = true;
|
||||||
} else {
|
} else {
|
||||||
if (!mempool[txid]) {
|
if (!ancestor) {
|
||||||
console.log('txid missing from mempool! ', txid, candidates?.txs[txid]);
|
console.log('txid missing from mempool! ', txid, candidates?.txs[txid]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const relative = {
|
const relative = {
|
||||||
txid: txid,
|
txid: txid,
|
||||||
fee: mempool[txid].fee,
|
fee: ancestor.fee,
|
||||||
weight: (mempool[txid].adjustedVsize * 4),
|
weight: (ancestor.adjustedVsize * 4),
|
||||||
};
|
};
|
||||||
if (matched) {
|
if (matched) {
|
||||||
descendants.push(relative);
|
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 {
|
} else {
|
||||||
ancestors.push(relative);
|
ancestors.push(relative);
|
||||||
}
|
}
|
||||||
@ -410,7 +418,20 @@ class MempoolBlocks {
|
|||||||
if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) {
|
if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) {
|
||||||
mempoolTx.cpfpDirty = true;
|
mempoolTx.cpfpDirty = true;
|
||||||
}
|
}
|
||||||
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;
|
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
|
||||||
// update this thread's mempool with the results
|
// update this thread's mempool with the results
|
||||||
let mempoolTx: MempoolTransactionExtended;
|
let mempoolTx: MempoolTransactionExtended;
|
||||||
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 totalSize = 0;
|
||||||
let totalVsize = 0;
|
let totalVsize = 0;
|
||||||
let totalWeight = 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) {
|
if (txid) {
|
||||||
mempoolTx = mempool[txid];
|
mempoolTx = mempool[txid];
|
||||||
// save position in projected blocks
|
// save position in projected blocks
|
||||||
@ -445,30 +470,37 @@ class MempoolBlocks {
|
|||||||
vsize: totalVsize + (mempoolTx.vsize / 2),
|
vsize: totalVsize + (mempoolTx.vsize / 2),
|
||||||
};
|
};
|
||||||
|
|
||||||
const acceleration = accelerations[txid];
|
if (txid in accelerations) {
|
||||||
if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
|
acceleration = accelerations[txid];
|
||||||
if (!mempoolTx.acceleration) {
|
if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
|
||||||
mempoolTx.cpfpDirty = true;
|
if (!mempoolTx.acceleration) {
|
||||||
}
|
mempoolTx.cpfpDirty = true;
|
||||||
mempoolTx.acceleration = true;
|
}
|
||||||
mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools;
|
mempoolTx.acceleration = true;
|
||||||
mempoolTx.acceleratedAt = acceleration?.added;
|
mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools;
|
||||||
mempoolTx.feeDelta = acceleration?.feeDelta;
|
mempoolTx.acceleratedAt = acceleration?.added;
|
||||||
for (const ancestor of mempoolTx.ancestors || []) {
|
mempoolTx.feeDelta = acceleration?.feeDelta;
|
||||||
if (!mempool[ancestor.txid].acceleration) {
|
for (const ancestor of mempoolTx.ancestors || []) {
|
||||||
mempool[ancestor.txid].cpfpDirty = true;
|
if (!mempool[ancestor.txid].acceleration) {
|
||||||
|
mempool[ancestor.txid].cpfpDirty = true;
|
||||||
|
}
|
||||||
|
mempool[ancestor.txid].acceleration = true;
|
||||||
|
mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy;
|
||||||
|
mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt;
|
||||||
|
mempool[ancestor.txid].feeDelta = mempoolTx.feeDelta;
|
||||||
|
isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (mempoolTx.acceleration) {
|
||||||
|
mempoolTx.cpfpDirty = true;
|
||||||
|
delete mempoolTx.acceleration;
|
||||||
}
|
}
|
||||||
mempool[ancestor.txid].acceleration = true;
|
|
||||||
mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy;
|
|
||||||
mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt;
|
|
||||||
mempool[ancestor.txid].feeDelta = mempoolTx.feeDelta;
|
|
||||||
isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (mempoolTx.acceleration) {
|
if (mempoolTx.acceleration) {
|
||||||
mempoolTx.cpfpDirty = true;
|
mempoolTx.cpfpDirty = true;
|
||||||
|
delete mempoolTx.acceleration;
|
||||||
}
|
}
|
||||||
delete mempoolTx.acceleration;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// online calculation of stack-of-blocks fee stats
|
// online calculation of stack-of-blocks fee stats
|
||||||
@ -486,7 +518,7 @@ class MempoolBlocks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this.dataToMempoolBlocks(
|
mempoolBlocks[blockIndex] = this.dataToMempoolBlocks(
|
||||||
block,
|
block,
|
||||||
transactions,
|
transactions,
|
||||||
totalSize,
|
totalSize,
|
||||||
@ -494,7 +526,7 @@ class MempoolBlocks {
|
|||||||
totalFees,
|
totalFees,
|
||||||
(hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) ? feeStatsCalculator.getRawFeeStats() : undefined,
|
(hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) ? feeStatsCalculator.getRawFeeStats() : undefined,
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
if (saveResults) {
|
if (saveResults) {
|
||||||
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks);
|
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks);
|
||||||
|
@ -19,12 +19,13 @@ class Mempool {
|
|||||||
private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {};
|
private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {};
|
||||||
private mempoolCandidates: { [txid: string ]: boolean } = {};
|
private mempoolCandidates: { [txid: string ]: boolean } = {};
|
||||||
private spendMap = new Map<string, MempoolTransactionExtended>();
|
private spendMap = new Map<string, MempoolTransactionExtended>();
|
||||||
|
private recentlyDeleted: MempoolTransactionExtended[][] = []; // buffer of transactions deleted in recent mempool updates
|
||||||
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
|
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
|
||||||
maxmempool: 300000000, mempoolminfee: Common.isLiquid() ? 0.00000100 : 0.00001000, minrelaytxfee: Common.isLiquid() ? 0.00000100 : 0.00001000 };
|
maxmempool: 300000000, mempoolminfee: Common.isLiquid() ? 0.00000100 : 0.00001000, minrelaytxfee: Common.isLiquid() ? 0.00000100 : 0.00001000 };
|
||||||
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
|
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
|
||||||
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void) | undefined;
|
deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void) | undefined;
|
||||||
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[],
|
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[],
|
||||||
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], candidates?: GbtCandidates) => Promise<void>) | undefined;
|
deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[], candidates?: GbtCandidates) => Promise<void>) | undefined;
|
||||||
|
|
||||||
private accelerations: { [txId: string]: Acceleration } = {};
|
private accelerations: { [txId: string]: Acceleration } = {};
|
||||||
private accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {};
|
private accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {};
|
||||||
@ -74,12 +75,12 @@ class Mempool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
|
public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
|
||||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void): void {
|
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void): void {
|
||||||
this.mempoolChangedCallback = fn;
|
this.mempoolChangedCallback = fn;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number,
|
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number,
|
||||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[],
|
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[],
|
||||||
candidates?: GbtCandidates) => Promise<void>): void {
|
candidates?: GbtCandidates) => Promise<void>): void {
|
||||||
this.$asyncMempoolChangedCallback = fn;
|
this.$asyncMempoolChangedCallback = fn;
|
||||||
}
|
}
|
||||||
@ -362,12 +363,15 @@ class Mempool {
|
|||||||
|
|
||||||
const candidatesChanged = candidates?.added?.length || candidates?.removed?.length;
|
const candidatesChanged = candidates?.added?.length || candidates?.removed?.length;
|
||||||
|
|
||||||
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
this.recentlyDeleted.unshift(deletedTransactions);
|
||||||
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta);
|
this.recentlyDeleted.length = Math.min(this.recentlyDeleted.length, 10); // truncate to the last 10 mempool updates
|
||||||
|
|
||||||
|
if (this.mempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length)) {
|
||||||
|
this.mempoolChangedCallback(this.mempoolCache, newTransactions, this.recentlyDeleted, accelerationDelta);
|
||||||
}
|
}
|
||||||
if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length || candidatesChanged)) {
|
if (this.$asyncMempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length || candidatesChanged)) {
|
||||||
this.updateTimerProgress(timer, 'running async mempool callback');
|
this.updateTimerProgress(timer, 'running async mempool callback');
|
||||||
await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, deletedTransactions, accelerationDelta, candidates);
|
await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, this.recentlyDeleted, accelerationDelta, candidates);
|
||||||
this.updateTimerProgress(timer, 'completed async mempool callback');
|
this.updateTimerProgress(timer, 'completed async mempool callback');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -541,16 +545,7 @@ class Mempool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public handleRbfTransactions(rbfTransactions: { [txid: string]: MempoolTransactionExtended[]; }): void {
|
public handleRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void {
|
||||||
for (const rbfTransaction in rbfTransactions) {
|
|
||||||
if (this.mempoolCache[rbfTransaction] && rbfTransactions[rbfTransaction]?.length) {
|
|
||||||
// Store replaced transactions
|
|
||||||
rbfCache.add(rbfTransactions[rbfTransaction], this.mempoolCache[rbfTransaction]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public handleMinedRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void {
|
|
||||||
for (const rbfTransaction in rbfTransactions) {
|
for (const rbfTransaction in rbfTransactions) {
|
||||||
if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) {
|
if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) {
|
||||||
// Store replaced transactions
|
// Store replaced transactions
|
||||||
|
@ -10,6 +10,7 @@ import mining from "./mining";
|
|||||||
import PricesRepository from '../../repositories/PricesRepository';
|
import PricesRepository from '../../repositories/PricesRepository';
|
||||||
import AccelerationRepository from '../../repositories/AccelerationRepository';
|
import AccelerationRepository from '../../repositories/AccelerationRepository';
|
||||||
import accelerationApi from '../services/acceleration';
|
import accelerationApi from '../services/acceleration';
|
||||||
|
import { handleError } from '../../utils/api';
|
||||||
|
|
||||||
class MiningRoutes {
|
class MiningRoutes {
|
||||||
public initRoutes(app: Application) {
|
public initRoutes(app: Application) {
|
||||||
@ -53,7 +54,7 @@ class MiningRoutes {
|
|||||||
res.header('Cache-control', 'public');
|
res.header('Cache-control', 'public');
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
||||||
if (['testnet', 'signet', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) {
|
if (['testnet', 'signet', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) {
|
||||||
res.status(400).send('Prices are not available on testnets.');
|
handleError(req, res, 400, 'Prices are not available on testnets.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const timestamp = parseInt(req.query.timestamp as string, 10) || 0;
|
const timestamp = parseInt(req.query.timestamp as string, 10) || 0;
|
||||||
@ -71,7 +72,7 @@ class MiningRoutes {
|
|||||||
}
|
}
|
||||||
res.status(200).send(response);
|
res.status(200).send(response);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,9 +85,9 @@ class MiningRoutes {
|
|||||||
res.json(stats);
|
res.json(stats);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
|
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
|
||||||
res.status(404).send(e.message);
|
handleError(req, res, 404, e.message);
|
||||||
} else {
|
} else {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -103,9 +104,9 @@ class MiningRoutes {
|
|||||||
res.json(poolBlocks);
|
res.json(poolBlocks);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
|
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
|
||||||
res.status(404).send(e.message);
|
handleError(req, res, 404, e.message);
|
||||||
} else {
|
} else {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -129,7 +130,7 @@ class MiningRoutes {
|
|||||||
res.json(pools);
|
res.json(pools);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,7 +144,7 @@ class MiningRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(stats);
|
res.json(stats);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,7 +158,7 @@ class MiningRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
||||||
res.json(hashrates);
|
res.json(hashrates);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,9 +173,9 @@ class MiningRoutes {
|
|||||||
res.json(hashrates);
|
res.json(hashrates);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
|
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
|
||||||
res.status(404).send(e.message);
|
handleError(req, res, 404, e.message);
|
||||||
} else {
|
} else {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -203,7 +204,7 @@ class MiningRoutes {
|
|||||||
currentDifficulty: currentDifficulty,
|
currentDifficulty: currentDifficulty,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,7 +218,7 @@ class MiningRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(blockFees);
|
res.json(blockFees);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,7 +236,7 @@ class MiningRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(blockFees);
|
res.json(blockFees);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -249,7 +250,7 @@ class MiningRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(blockRewards);
|
res.json(blockRewards);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,7 +264,7 @@ class MiningRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(blockFeeRates);
|
res.json(blockFeeRates);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,7 +282,7 @@ class MiningRoutes {
|
|||||||
weights: blockWeights
|
weights: blockWeights
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -293,7 +294,7 @@ class MiningRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
||||||
res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment]));
|
res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment]));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -317,7 +318,7 @@ class MiningRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate]));
|
res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate]));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -326,7 +327,7 @@ class MiningRoutes {
|
|||||||
const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash);
|
const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash);
|
||||||
|
|
||||||
if (!audit) {
|
if (!audit) {
|
||||||
res.status(204).send(`This block has not been audited.`);
|
handleError(req, res, 204, `This block has not been audited.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -335,7 +336,7 @@ class MiningRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
|
||||||
res.json(audit);
|
res.json(audit);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -358,7 +359,7 @@ class MiningRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -371,7 +372,7 @@ class MiningRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15));
|
res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -384,7 +385,7 @@ class MiningRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
|
||||||
res.json(audit || 'null');
|
res.json(audit || 'null');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -394,12 +395,12 @@ class MiningRoutes {
|
|||||||
res.header('Cache-control', 'public');
|
res.header('Cache-control', 'public');
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
||||||
res.status(400).send('Acceleration data is not available.');
|
handleError(req, res, 400, 'Acceleration data is not available.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug));
|
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -409,13 +410,13 @@ class MiningRoutes {
|
|||||||
res.header('Cache-control', 'public');
|
res.header('Cache-control', 'public');
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
|
||||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
||||||
res.status(400).send('Acceleration data is not available.');
|
handleError(req, res, 400, 'Acceleration data is not available.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
|
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
|
||||||
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height));
|
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -425,12 +426,12 @@ class MiningRoutes {
|
|||||||
res.header('Cache-control', 'public');
|
res.header('Cache-control', 'public');
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
||||||
res.status(400).send('Acceleration data is not available.');
|
handleError(req, res, 400, 'Acceleration data is not available.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval));
|
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -440,12 +441,12 @@ class MiningRoutes {
|
|||||||
res.header('Cache-control', 'public');
|
res.header('Cache-control', 'public');
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
||||||
res.status(400).send('Acceleration data is not available.');
|
handleError(req, res, 400, 'Acceleration data is not available.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.status(200).send(await AccelerationRepository.$getAccelerationTotals(<string>req.query.pool, <string>req.query.interval));
|
res.status(200).send(await AccelerationRepository.$getAccelerationTotals(<string>req.query.pool, <string>req.query.interval));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -455,12 +456,12 @@ class MiningRoutes {
|
|||||||
res.header('Cache-control', 'public');
|
res.header('Cache-control', 'public');
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
||||||
res.status(400).send('Acceleration data is not available.');
|
handleError(req, res, 400, 'Acceleration data is not available.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.status(200).send(accelerationApi.accelerations || []);
|
res.status(200).send(accelerationApi.accelerations || []);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -472,7 +473,7 @@ class MiningRoutes {
|
|||||||
accelerationApi.accelerationRequested(req.params.txid);
|
accelerationApi.accelerationRequested(req.params.txid);
|
||||||
res.status(200).send();
|
res.status(200).send();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,22 @@ interface CacheEvent {
|
|||||||
value?: any,
|
value?: any,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton for tracking RBF trees
|
||||||
|
*
|
||||||
|
* Maintains a set of RBF trees, where each tree represents a sequence of
|
||||||
|
* consecutive RBF replacements.
|
||||||
|
*
|
||||||
|
* Trees are identified by the txid of the root transaction.
|
||||||
|
*
|
||||||
|
* To maintain consistency, the following invariants must be upheld:
|
||||||
|
* - Symmetry: replacedBy(A) = B <=> A in replaces(B)
|
||||||
|
* - Unique id: treeMap(treeMap(X)) = treeMap(X)
|
||||||
|
* - Unique tree: A in replaces(B) => treeMap(A) == treeMap(B)
|
||||||
|
* - Existence: X in treeMap => treeMap(X) in rbfTrees
|
||||||
|
* - Completeness: X in replacedBy => X in treeMap, Y in replaces => Y in treeMap
|
||||||
|
*/
|
||||||
|
|
||||||
class RbfCache {
|
class RbfCache {
|
||||||
private replacedBy: Map<string, string> = new Map();
|
private replacedBy: Map<string, string> = new Map();
|
||||||
private replaces: Map<string, string[]> = new Map();
|
private replaces: Map<string, string[]> = new Map();
|
||||||
@ -61,6 +77,10 @@ class RbfCache {
|
|||||||
setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
|
setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Low level cache operations
|
||||||
|
*/
|
||||||
|
|
||||||
private addTx(txid: string, tx: MempoolTransactionExtended): void {
|
private addTx(txid: string, tx: MempoolTransactionExtended): void {
|
||||||
this.txs.set(txid, tx);
|
this.txs.set(txid, tx);
|
||||||
this.cacheQueue.push({ op: CacheOp.Add, type: 'tx', txid });
|
this.cacheQueue.push({ op: CacheOp.Add, type: 'tx', txid });
|
||||||
@ -92,6 +112,12 @@ class RbfCache {
|
|||||||
this.cacheQueue.push({ op: CacheOp.Remove, type: 'exp', txid });
|
this.cacheQueue.push({ op: CacheOp.Remove, type: 'exp', txid });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic data structure operations
|
||||||
|
* must uphold tree invariants
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
|
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
|
||||||
if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
|
if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
|
||||||
return;
|
return;
|
||||||
@ -114,6 +140,10 @@ class RbfCache {
|
|||||||
if (!replacedTx.rbf) {
|
if (!replacedTx.rbf) {
|
||||||
txFullRbf = true;
|
txFullRbf = true;
|
||||||
}
|
}
|
||||||
|
if (this.replacedBy.has(replacedTx.txid)) {
|
||||||
|
// should never happen
|
||||||
|
continue;
|
||||||
|
}
|
||||||
this.replacedBy.set(replacedTx.txid, newTx.txid);
|
this.replacedBy.set(replacedTx.txid, newTx.txid);
|
||||||
if (this.treeMap.has(replacedTx.txid)) {
|
if (this.treeMap.has(replacedTx.txid)) {
|
||||||
const treeId = this.treeMap.get(replacedTx.txid);
|
const treeId = this.treeMap.get(replacedTx.txid);
|
||||||
@ -140,18 +170,47 @@ class RbfCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
newTx.fullRbf = txFullRbf;
|
newTx.fullRbf = txFullRbf;
|
||||||
const treeId = replacedTrees[0].tx.txid;
|
|
||||||
const newTree = {
|
const newTree = {
|
||||||
tx: newTx,
|
tx: newTx,
|
||||||
time: newTime,
|
time: newTime,
|
||||||
fullRbf: treeFullRbf,
|
fullRbf: treeFullRbf,
|
||||||
replaces: replacedTrees
|
replaces: replacedTrees
|
||||||
};
|
};
|
||||||
this.addTree(treeId, newTree);
|
this.addTree(newTree.tx.txid, newTree);
|
||||||
this.updateTreeMap(treeId, newTree);
|
this.updateTreeMap(newTree.tx.txid, newTree);
|
||||||
this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid));
|
this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public mined(txid): void {
|
||||||
|
if (!this.txs.has(txid)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const treeId = this.treeMap.get(txid);
|
||||||
|
if (treeId && this.rbfTrees.has(treeId)) {
|
||||||
|
const tree = this.rbfTrees.get(treeId);
|
||||||
|
if (tree) {
|
||||||
|
this.setTreeMined(tree, txid);
|
||||||
|
tree.mined = true;
|
||||||
|
this.dirtyTrees.add(treeId);
|
||||||
|
this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.evict(txid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// flag a transaction as removed from the mempool
|
||||||
|
public evict(txid: string, fast: boolean = false): void {
|
||||||
|
this.evictionCount++;
|
||||||
|
if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) {
|
||||||
|
const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours
|
||||||
|
this.addExpiration(txid, expiryTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read-only public interface
|
||||||
|
*/
|
||||||
|
|
||||||
public has(txId: string): boolean {
|
public has(txId: string): boolean {
|
||||||
return this.txs.has(txId);
|
return this.txs.has(txId);
|
||||||
}
|
}
|
||||||
@ -232,32 +291,6 @@ class RbfCache {
|
|||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public mined(txid): void {
|
|
||||||
if (!this.txs.has(txid)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const treeId = this.treeMap.get(txid);
|
|
||||||
if (treeId && this.rbfTrees.has(treeId)) {
|
|
||||||
const tree = this.rbfTrees.get(treeId);
|
|
||||||
if (tree) {
|
|
||||||
this.setTreeMined(tree, txid);
|
|
||||||
tree.mined = true;
|
|
||||||
this.dirtyTrees.add(treeId);
|
|
||||||
this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.evict(txid);
|
|
||||||
}
|
|
||||||
|
|
||||||
// flag a transaction as removed from the mempool
|
|
||||||
public evict(txid: string, fast: boolean = false): void {
|
|
||||||
this.evictionCount++;
|
|
||||||
if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) {
|
|
||||||
const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours
|
|
||||||
this.addExpiration(txid, expiryTime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// is the transaction involved in a full rbf replacement?
|
// is the transaction involved in a full rbf replacement?
|
||||||
public isFullRbf(txid: string): boolean {
|
public isFullRbf(txid: string): boolean {
|
||||||
const treeId = this.treeMap.get(txid);
|
const treeId = this.treeMap.get(txid);
|
||||||
@ -271,6 +304,10 @@ class RbfCache {
|
|||||||
return tree?.fullRbf;
|
return tree?.fullRbf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache maintenance & utility functions
|
||||||
|
*/
|
||||||
|
|
||||||
private cleanup(): void {
|
private cleanup(): void {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
for (const txid of this.expiring.keys()) {
|
for (const txid of this.expiring.keys()) {
|
||||||
@ -299,10 +336,6 @@ class RbfCache {
|
|||||||
for (const tx of (replaces || [])) {
|
for (const tx of (replaces || [])) {
|
||||||
// recursively remove prior versions from the cache
|
// recursively remove prior versions from the cache
|
||||||
this.replacedBy.delete(tx);
|
this.replacedBy.delete(tx);
|
||||||
// if this is the id of a tree, remove that too
|
|
||||||
if (this.treeMap.get(tx) === tx) {
|
|
||||||
this.removeTree(tx);
|
|
||||||
}
|
|
||||||
this.remove(tx);
|
this.remove(tx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -370,14 +403,21 @@ class RbfCache {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async load({ txs, trees, expiring, mempool }): Promise<void> {
|
public async load({ txs, trees, expiring, mempool, spendMap }): Promise<void> {
|
||||||
try {
|
try {
|
||||||
txs.forEach(txEntry => {
|
txs.forEach(txEntry => {
|
||||||
this.txs.set(txEntry.value.txid, txEntry.value);
|
this.txs.set(txEntry.value.txid, txEntry.value);
|
||||||
});
|
});
|
||||||
this.staleCount = 0;
|
this.staleCount = 0;
|
||||||
for (const deflatedTree of trees) {
|
for (const deflatedTree of trees.sort((a, b) => Object.keys(b).length - Object.keys(a).length)) {
|
||||||
await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
|
const tree = await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
|
||||||
|
if (tree) {
|
||||||
|
this.addTree(tree.tx.txid, tree);
|
||||||
|
this.updateTreeMap(tree.tx.txid, tree);
|
||||||
|
if (tree.mined) {
|
||||||
|
this.evict(tree.tx.txid);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
expiring.forEach(expiringEntry => {
|
expiring.forEach(expiringEntry => {
|
||||||
if (this.txs.has(expiringEntry.key)) {
|
if (this.txs.has(expiringEntry.key)) {
|
||||||
@ -385,6 +425,31 @@ class RbfCache {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.staleCount = 0;
|
this.staleCount = 0;
|
||||||
|
|
||||||
|
// connect cached trees to current mempool transactions
|
||||||
|
const conflicts: Record<string, { replacedBy: MempoolTransactionExtended, replaces: Set<MempoolTransactionExtended> }> = {};
|
||||||
|
for (const tree of this.rbfTrees.values()) {
|
||||||
|
const tx = this.getTx(tree.tx.txid);
|
||||||
|
if (!tx || tree.mined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const vin of tx.vin) {
|
||||||
|
const conflict = spendMap.get(`${vin.txid}:${vin.vout}`);
|
||||||
|
if (conflict && conflict.txid !== tx.txid) {
|
||||||
|
if (!conflicts[conflict.txid]) {
|
||||||
|
conflicts[conflict.txid] = {
|
||||||
|
replacedBy: conflict,
|
||||||
|
replaces: new Set(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
conflicts[conflict.txid].replaces.add(tx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const { replacedBy, replaces } of Object.values(conflicts)) {
|
||||||
|
this.add([...replaces.values()], replacedBy);
|
||||||
|
}
|
||||||
|
|
||||||
await this.checkTrees();
|
await this.checkTrees();
|
||||||
logger.debug(`loaded ${txs.length} txs, ${trees.length} trees into rbf cache, ${expiring.length} due to expire, ${this.staleCount} were stale`);
|
logger.debug(`loaded ${txs.length} txs, ${trees.length} trees into rbf cache, ${expiring.length} due to expire, ${this.staleCount} were stale`);
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
@ -426,6 +491,12 @@ class RbfCache {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if this tx is already in the cache, return early
|
||||||
|
if (this.treeMap.has(txid)) {
|
||||||
|
this.removeTree(deflated.key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// recursively reconstruct child trees
|
// recursively reconstruct child trees
|
||||||
for (const childId of treeInfo.replaces) {
|
for (const childId of treeInfo.replaces) {
|
||||||
const replaced = await this.importTree(mempool, root, childId, deflated, txs, mined);
|
const replaced = await this.importTree(mempool, root, childId, deflated, txs, mined);
|
||||||
@ -457,10 +528,6 @@ class RbfCache {
|
|||||||
fullRbf: treeInfo.fullRbf,
|
fullRbf: treeInfo.fullRbf,
|
||||||
replaces,
|
replaces,
|
||||||
};
|
};
|
||||||
this.treeMap.set(txid, root);
|
|
||||||
if (root === txid) {
|
|
||||||
this.addTree(root, tree);
|
|
||||||
}
|
|
||||||
return tree;
|
return tree;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -511,6 +578,7 @@ class RbfCache {
|
|||||||
processTxs(txs);
|
processTxs(txs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// evict missing transactions
|
||||||
for (const txid of txids) {
|
for (const txid of txids) {
|
||||||
if (!found[txid]) {
|
if (!found[txid]) {
|
||||||
this.evict(txid, false);
|
this.evict(txid, false);
|
||||||
|
@ -365,6 +365,7 @@ class RedisCache {
|
|||||||
trees: rbfTrees.map(loadedTree => { loadedTree.value.key = loadedTree.key; return loadedTree.value; }),
|
trees: rbfTrees.map(loadedTree => { loadedTree.value.key = loadedTree.key; return loadedTree.value; }),
|
||||||
expiring: rbfExpirations,
|
expiring: rbfExpirations,
|
||||||
mempool: memPool.getMempool(),
|
mempool: memPool.getMempool(),
|
||||||
|
spendMap: memPool.getSpendMap(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 adjustedVsize = Math.max(fractionalVsize, sigops * 5); // adjusted vsize = Max(weight, sigops * bytes_per_sigop) / witness_scale_factor
|
||||||
const feePerVbytes = (transaction.fee || 0) / fractionalVsize;
|
const feePerVbytes = (transaction.fee || 0) / fractionalVsize;
|
||||||
const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize;
|
const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize;
|
||||||
|
const effectiveFeePerVsize = transaction['effectiveFeePerVsize'] || adjustedFeePerVsize || feePerVbytes;
|
||||||
const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, {
|
const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, {
|
||||||
order: this.txidToOrdering(transaction.txid),
|
order: this.txidToOrdering(transaction.txid),
|
||||||
vsize,
|
vsize,
|
||||||
@ -128,7 +129,7 @@ class TransactionUtils {
|
|||||||
sigops,
|
sigops,
|
||||||
feePerVsize: feePerVbytes,
|
feePerVsize: feePerVbytes,
|
||||||
adjustedFeePerVsize: adjustedFeePerVsize,
|
adjustedFeePerVsize: adjustedFeePerVsize,
|
||||||
effectiveFeePerVsize: adjustedFeePerVsize,
|
effectiveFeePerVsize: effectiveFeePerVsize,
|
||||||
});
|
});
|
||||||
if (!transactionExtended?.status?.confirmed && !transactionExtended.firstSeen) {
|
if (!transactionExtended?.status?.confirmed && !transactionExtended.firstSeen) {
|
||||||
transactionExtended.firstSeen = Math.round((Date.now() / 1000));
|
transactionExtended.firstSeen = Math.round((Date.now() / 1000));
|
||||||
@ -338,6 +339,87 @@ class TransactionUtils {
|
|||||||
const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2;
|
const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2;
|
||||||
return witness[positionOfScript];
|
return witness[positionOfScript];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// calculate the most parsimonious set of prioritizations given a list of block transactions
|
||||||
|
// (i.e. the most likely prioritizations and deprioritizations)
|
||||||
|
public identifyPrioritizedTransactions(transactions: any[], rateKey: string): { prioritized: string[], deprioritized: string[] } {
|
||||||
|
// find the longest increasing subsequence of transactions
|
||||||
|
// (adapted from https://en.wikipedia.org/wiki/Longest_increasing_subsequence#Efficient_algorithms)
|
||||||
|
// should be O(n log n)
|
||||||
|
const X = transactions.slice(1).reverse().map((tx) => ({ txid: tx.txid, rate: tx[rateKey] })); // standard block order is by *decreasing* effective fee rate, but we want to iterate in increasing order (and skip the coinbase)
|
||||||
|
if (X.length < 2) {
|
||||||
|
return { prioritized: [], deprioritized: [] };
|
||||||
|
}
|
||||||
|
const N = X.length;
|
||||||
|
const P: number[] = new Array(N);
|
||||||
|
const M: number[] = new Array(N + 1);
|
||||||
|
M[0] = -1; // undefined so can be set to any value
|
||||||
|
|
||||||
|
let L = 0;
|
||||||
|
for (let i = 0; i < N; i++) {
|
||||||
|
// Binary search for the smallest positive l ≤ L
|
||||||
|
// such that X[M[l]].effectiveFeePerVsize > X[i].effectiveFeePerVsize
|
||||||
|
let lo = 1;
|
||||||
|
let hi = L + 1;
|
||||||
|
while (lo < hi) {
|
||||||
|
const mid = lo + Math.floor((hi - lo) / 2); // lo <= mid < hi
|
||||||
|
if (X[M[mid]].rate > X[i].rate) {
|
||||||
|
hi = mid;
|
||||||
|
} else { // if X[M[mid]].effectiveFeePerVsize < X[i].effectiveFeePerVsize
|
||||||
|
lo = mid + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// After searching, lo == hi is 1 greater than the
|
||||||
|
// length of the longest prefix of X[i]
|
||||||
|
const newL = lo;
|
||||||
|
|
||||||
|
// The predecessor of X[i] is the last index of
|
||||||
|
// the subsequence of length newL-1
|
||||||
|
P[i] = M[newL - 1];
|
||||||
|
M[newL] = i;
|
||||||
|
|
||||||
|
if (newL > L) {
|
||||||
|
// If we found a subsequence longer than any we've
|
||||||
|
// found yet, update L
|
||||||
|
L = newL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconstruct the longest increasing subsequence
|
||||||
|
// It consists of the values of X at the L indices:
|
||||||
|
// ..., P[P[M[L]]], P[M[L]], M[L]
|
||||||
|
const LIS: any[] = new Array(L);
|
||||||
|
let k = M[L];
|
||||||
|
for (let j = L - 1; j >= 0; j--) {
|
||||||
|
LIS[j] = X[k];
|
||||||
|
k = P[k];
|
||||||
|
}
|
||||||
|
|
||||||
|
const lisMap = new Map<string, number>();
|
||||||
|
LIS.forEach((tx, index) => lisMap.set(tx.txid, index));
|
||||||
|
|
||||||
|
const prioritized: string[] = [];
|
||||||
|
const deprioritized: string[] = [];
|
||||||
|
|
||||||
|
let lastRate = X[0].rate;
|
||||||
|
|
||||||
|
for (const tx of X) {
|
||||||
|
if (lisMap.has(tx.txid)) {
|
||||||
|
lastRate = tx.rate;
|
||||||
|
} else {
|
||||||
|
if (Math.abs(tx.rate - lastRate) < 0.1) {
|
||||||
|
// skip if the rate is almost the same as the previous transaction
|
||||||
|
} else if (tx.rate <= lastRate) {
|
||||||
|
prioritized.push(tx.txid);
|
||||||
|
} else {
|
||||||
|
deprioritized.push(tx.txid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { prioritized, deprioritized };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new TransactionUtils();
|
export default new TransactionUtils();
|
||||||
|
@ -520,8 +520,17 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param newMempool
|
||||||
|
* @param mempoolSize
|
||||||
|
* @param newTransactions array of transactions added this mempool update.
|
||||||
|
* @param recentlyDeletedTransactions array of arrays of transactions removed in the last N mempool updates, most recent first.
|
||||||
|
* @param accelerationDelta
|
||||||
|
* @param candidates
|
||||||
|
*/
|
||||||
async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number,
|
async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number,
|
||||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[],
|
newTransactions: MempoolTransactionExtended[], recentlyDeletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[],
|
||||||
candidates?: GbtCandidates): Promise<void> {
|
candidates?: GbtCandidates): Promise<void> {
|
||||||
if (!this.webSocketServers.length) {
|
if (!this.webSocketServers.length) {
|
||||||
throw new Error('No WebSocket.Server have been set');
|
throw new Error('No WebSocket.Server have been set');
|
||||||
@ -529,6 +538,8 @@ class WebsocketHandler {
|
|||||||
|
|
||||||
this.printLogs();
|
this.printLogs();
|
||||||
|
|
||||||
|
const deletedTransactions = recentlyDeletedTransactions.length ? recentlyDeletedTransactions[0] : [];
|
||||||
|
|
||||||
const transactionIds = (memPool.limitGBT && candidates) ? Object.keys(candidates?.txs || {}) : Object.keys(newMempool);
|
const transactionIds = (memPool.limitGBT && candidates) ? Object.keys(candidates?.txs || {}) : Object.keys(newMempool);
|
||||||
let added = newTransactions;
|
let added = newTransactions;
|
||||||
let removed = deletedTransactions;
|
let removed = deletedTransactions;
|
||||||
@ -547,7 +558,7 @@ class WebsocketHandler {
|
|||||||
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
|
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
|
||||||
const mempoolInfo = memPool.getMempoolInfo();
|
const mempoolInfo = memPool.getMempoolInfo();
|
||||||
const vBytesPerSecond = memPool.getVBytesPerSecond();
|
const vBytesPerSecond = memPool.getVBytesPerSecond();
|
||||||
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
|
const rbfTransactions = Common.findRbfTransactions(newTransactions, recentlyDeletedTransactions.flat());
|
||||||
const da = difficultyAdjustment.getDifficultyAdjustment();
|
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||||
const accelerations = memPool.getAccelerations();
|
const accelerations = memPool.getAccelerations();
|
||||||
memPool.handleRbfTransactions(rbfTransactions);
|
memPool.handleRbfTransactions(rbfTransactions);
|
||||||
@ -578,7 +589,7 @@ class WebsocketHandler {
|
|||||||
const replacedTransactions: { replaced: string, by: TransactionExtended }[] = [];
|
const replacedTransactions: { replaced: string, by: TransactionExtended }[] = [];
|
||||||
for (const tx of newTransactions) {
|
for (const tx of newTransactions) {
|
||||||
if (rbfTransactions[tx.txid]) {
|
if (rbfTransactions[tx.txid]) {
|
||||||
for (const replaced of rbfTransactions[tx.txid]) {
|
for (const replaced of rbfTransactions[tx.txid].replaced) {
|
||||||
replacedTransactions.push({ replaced: replaced.txid, by: tx });
|
replacedTransactions.push({ replaced: replaced.txid, by: tx });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -947,7 +958,7 @@ class WebsocketHandler {
|
|||||||
await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions));
|
await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions));
|
||||||
|
|
||||||
const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
|
const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
|
||||||
memPool.handleMinedRbfTransactions(rbfTransactions);
|
memPool.handleRbfTransactions(rbfTransactions);
|
||||||
memPool.removeFromSpendMap(transactions);
|
memPool.removeFromSpendMap(transactions);
|
||||||
|
|
||||||
if (config.MEMPOOL.AUDIT && memPool.isInSync()) {
|
if (config.MEMPOOL.AUDIT && memPool.isInSync()) {
|
||||||
|
@ -195,7 +195,7 @@ const defaults: IConfig = {
|
|||||||
'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
|
'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
|
||||||
'POOLS_UPDATE_DELAY': 604800, // in seconds, default is one week
|
'POOLS_UPDATE_DELAY': 604800, // in seconds, default is one week
|
||||||
'AUDIT': false,
|
'AUDIT': false,
|
||||||
'RUST_GBT': false,
|
'RUST_GBT': true,
|
||||||
'LIMIT_GBT': false,
|
'LIMIT_GBT': false,
|
||||||
'CPFP_INDEXING': false,
|
'CPFP_INDEXING': false,
|
||||||
'MAX_BLOCKS_BULK_QUERY': 0,
|
'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`
|
id: number; // Note - This is the `unique_id`, not to mix with the auto increment `id`
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
minerNames: string[] | null;
|
||||||
};
|
};
|
||||||
avgFee: number;
|
avgFee: number;
|
||||||
avgFeeRate: number;
|
avgFeeRate: number;
|
||||||
|
@ -14,6 +14,7 @@ import chainTips from '../api/chain-tips';
|
|||||||
import blocks from '../api/blocks';
|
import blocks from '../api/blocks';
|
||||||
import BlocksAuditsRepository from './BlocksAuditsRepository';
|
import BlocksAuditsRepository from './BlocksAuditsRepository';
|
||||||
import transactionUtils from '../api/transaction-utils';
|
import transactionUtils from '../api/transaction-utils';
|
||||||
|
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
|
||||||
|
|
||||||
interface DatabaseBlock {
|
interface DatabaseBlock {
|
||||||
id: string;
|
id: string;
|
||||||
@ -1054,6 +1055,7 @@ class BlocksRepository {
|
|||||||
id: dbBlk.poolId,
|
id: dbBlk.poolId,
|
||||||
name: dbBlk.poolName,
|
name: dbBlk.poolName,
|
||||||
slug: dbBlk.poolSlug,
|
slug: dbBlk.poolSlug,
|
||||||
|
minerNames: null,
|
||||||
};
|
};
|
||||||
extras.avgFee = dbBlk.avgFee;
|
extras.avgFee = dbBlk.avgFee;
|
||||||
extras.avgFeeRate = dbBlk.avgFeeRate;
|
extras.avgFeeRate = dbBlk.avgFeeRate;
|
||||||
@ -1106,7 +1108,7 @@ class BlocksRepository {
|
|||||||
let summaryVersion = 0;
|
let summaryVersion = 0;
|
||||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||||
const txs = (await bitcoinApi.$getTxsForBlock(dbBlk.id)).map(tx => transactionUtils.extendTransaction(tx));
|
const txs = (await bitcoinApi.$getTxsForBlock(dbBlk.id)).map(tx => transactionUtils.extendTransaction(tx));
|
||||||
summary = blocks.summarizeBlockTransactions(dbBlk.id, txs);
|
summary = blocks.summarizeBlockTransactions(dbBlk.id, dbBlk.height, txs);
|
||||||
summaryVersion = 1;
|
summaryVersion = 1;
|
||||||
} else {
|
} else {
|
||||||
// Call Core RPC
|
// Call Core RPC
|
||||||
@ -1123,6 +1125,10 @@ class BlocksRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (extras.pool.name === 'OCEAN') {
|
||||||
|
extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw);
|
||||||
|
}
|
||||||
|
|
||||||
blk.extras = <BlockExtension>extras;
|
blk.extras = <BlockExtension>extras;
|
||||||
return <BlockExtended>blk;
|
return <BlockExtended>blk;
|
||||||
}
|
}
|
||||||
|
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) {
|
if (!opN) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!opN.startsWith('OP_PUSHNUM_')) {
|
if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10);
|
const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10);
|
||||||
@ -178,7 +178,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
|
|||||||
if (!opM) {
|
if (!opM) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!opM.startsWith('OP_PUSHNUM_')) {
|
if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10);
|
const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10);
|
||||||
@ -201,3 +201,27 @@ export function getVarIntLength(n: number): number {
|
|||||||
return 9;
|
return 9;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Extracts miner names from a DATUM coinbase transaction */
|
||||||
|
export function parseDATUMTemplateCreator(coinbaseRaw: string): string[] | null {
|
||||||
|
let bytes: number[] = [];
|
||||||
|
for (let c = 0; c < coinbaseRaw.length; c += 2) {
|
||||||
|
bytes.push(parseInt(coinbaseRaw.slice(c, c + 2), 16));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip block height
|
||||||
|
let tagLengthByte = 1 + bytes[0];
|
||||||
|
|
||||||
|
let tagsLength = bytes[tagLengthByte];
|
||||||
|
if (tagsLength == 0x4c) {
|
||||||
|
tagLengthByte += 1;
|
||||||
|
tagsLength = bytes[tagLengthByte];
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagStart = tagLengthByte + 1;
|
||||||
|
const tags = bytes.slice(tagStart, tagStart + tagsLength);
|
||||||
|
let tagString = String.fromCharCode(...tags);
|
||||||
|
tagString = tagString.replace('\x00', '');
|
||||||
|
|
||||||
|
return tagString.split('\x0f').map((name) => name.replace(/[^a-zA-Z0-9 ]/g, ''));
|
||||||
|
}
|
@ -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_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_POOLS_UPDATE_DELAY__=${MEMPOOL_POOLS_UPDATE_DELAY:=604800}
|
||||||
__MEMPOOL_AUDIT__=${MEMPOOL_AUDIT:=false}
|
__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_LIMIT_GBT__=${MEMPOOL_LIMIT_GBT:=false}
|
||||||
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
|
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
|
||||||
__MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0}
|
__MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0}
|
||||||
|
@ -33,7 +33,7 @@ $ npm run config:defaults:liquid
|
|||||||
|
|
||||||
### 3. Run the Frontend
|
### 3. Run the Frontend
|
||||||
|
|
||||||
_Make sure to use Node.js 16.10 and npm 7._
|
_Make sure to use Node.js 20.x and npm 9.x or newer._
|
||||||
|
|
||||||
Install project dependencies and run the frontend server:
|
Install project dependencies and run the frontend server:
|
||||||
|
|
||||||
@ -70,7 +70,7 @@ Set up the [Mempool backend](../backend/) first, if you haven't already.
|
|||||||
|
|
||||||
### 1. Build the Frontend
|
### 1. Build the Frontend
|
||||||
|
|
||||||
_Make sure to use Node.js 16.10 and npm 7._
|
_Make sure to use Node.js 20.x and npm 9.x or newer._
|
||||||
|
|
||||||
Build the frontend:
|
Build the frontend:
|
||||||
|
|
||||||
|
@ -54,6 +54,10 @@
|
|||||||
"translation": "src/locale/messages.fr.xlf",
|
"translation": "src/locale/messages.fr.xlf",
|
||||||
"baseHref": "/fr/"
|
"baseHref": "/fr/"
|
||||||
},
|
},
|
||||||
|
"hr": {
|
||||||
|
"translation": "src/locale/messages.hr.xlf",
|
||||||
|
"baseHref": "/hr/"
|
||||||
|
},
|
||||||
"ja": {
|
"ja": {
|
||||||
"translation": "src/locale/messages.ja.xlf",
|
"translation": "src/locale/messages.ja.xlf",
|
||||||
"baseHref": "/ja/"
|
"baseHref": "/ja/"
|
||||||
|
@ -750,7 +750,7 @@
|
|||||||
},
|
},
|
||||||
"backendInfo": {
|
"backendInfo": {
|
||||||
"hostname": "node205.tk7.mempool.space",
|
"hostname": "node205.tk7.mempool.space",
|
||||||
"version": "3.0.0-beta",
|
"version": "3.1.0-dev",
|
||||||
"gitCommit": "abbc8a134",
|
"gitCommit": "abbc8a134",
|
||||||
"lightning": false
|
"lightning": false
|
||||||
},
|
},
|
||||||
|
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",
|
"name": "mempool-frontend",
|
||||||
"version": "3.0.0-beta",
|
"version": "3.1.0-dev",
|
||||||
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
||||||
"license": "GNU Affero General Public License v3.0",
|
"license": "GNU Affero General Public License v3.0",
|
||||||
"homepage": "https://mempool.space",
|
"homepage": "https://mempool.space",
|
||||||
@ -92,10 +92,10 @@
|
|||||||
"ngx-infinite-scroll": "^17.0.0",
|
"ngx-infinite-scroll": "^17.0.0",
|
||||||
"qrcode": "1.5.1",
|
"qrcode": "1.5.1",
|
||||||
"rxjs": "~7.8.1",
|
"rxjs": "~7.8.1",
|
||||||
"esbuild": "^0.23.0",
|
"esbuild": "^0.24.0",
|
||||||
"tinyify": "^4.0.0",
|
"tinyify": "^4.0.0",
|
||||||
"tlite": "^0.1.9",
|
"tlite": "^0.1.9",
|
||||||
"tslib": "~2.6.0",
|
"tslib": "~2.7.0",
|
||||||
"zone.js": "~0.14.4"
|
"zone.js": "~0.14.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -115,7 +115,7 @@
|
|||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@cypress/schematic": "^2.5.0",
|
"@cypress/schematic": "^2.5.0",
|
||||||
"@types/cypress": "^1.1.3",
|
"@types/cypress": "^1.1.3",
|
||||||
"cypress": "^13.13.0",
|
"cypress": "^13.15.0",
|
||||||
"cypress-fail-on-console-error": "~5.1.0",
|
"cypress-fail-on-console-error": "~5.1.0",
|
||||||
"cypress-wait-until": "^2.0.1",
|
"cypress-wait-until": "^2.0.1",
|
||||||
"mock-socket": "~9.3.1",
|
"mock-socket": "~9.3.1",
|
||||||
|
@ -21,6 +21,7 @@ import { StorageService } from './services/storage.service';
|
|||||||
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
|
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
|
||||||
import { LanguageService } from './services/language.service';
|
import { LanguageService } from './services/language.service';
|
||||||
import { ThemeService } from './services/theme.service';
|
import { ThemeService } from './services/theme.service';
|
||||||
|
import { TimeService } from './services/time.service';
|
||||||
import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe';
|
import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe';
|
||||||
import { FiatCurrencyPipe } from './shared/pipes/fiat-currency.pipe';
|
import { FiatCurrencyPipe } from './shared/pipes/fiat-currency.pipe';
|
||||||
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
|
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
|
||||||
@ -42,6 +43,7 @@ const providers = [
|
|||||||
EnterpriseService,
|
EnterpriseService,
|
||||||
LanguageService,
|
LanguageService,
|
||||||
ThemeService,
|
ThemeService,
|
||||||
|
TimeService,
|
||||||
ShortenStringPipe,
|
ShortenStringPipe,
|
||||||
FiatShortenerPipe,
|
FiatShortenerPipe,
|
||||||
FiatCurrencyPipe,
|
FiatCurrencyPipe,
|
||||||
|
@ -135,7 +135,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const opN = ops.pop();
|
const opN = ops.pop();
|
||||||
if (!opN.startsWith('OP_PUSHNUM_')) {
|
if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const n = parseInt(opN.match(/[0-9]+/)[0], 10);
|
const n = parseInt(opN.match(/[0-9]+/)[0], 10);
|
||||||
@ -152,7 +152,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const opM = ops.pop();
|
const opM = ops.pop();
|
||||||
if (!opM.startsWith('OP_PUSHNUM_')) {
|
if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const m = parseInt(opM.match(/[0-9]+/)[0], 10);
|
const m = parseInt(opM.match(/[0-9]+/)[0], 10);
|
||||||
|
@ -53,7 +53,7 @@
|
|||||||
<span>Spiral</span>
|
<span>Spiral</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://foundrydigital.com/" target="_blank" title="Foundry">
|
<a href="https://foundrydigital.com/" target="_blank" title="Foundry">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" style="zoom: 1;" width="32" height="76" viewBox="0 0 32 76">
|
<svg xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" style="zoom: 1;" width="32" height="90" viewBox="0 -5 32 90" class="image">
|
||||||
<defs>
|
<defs>
|
||||||
<style>
|
<style>
|
||||||
.d {
|
.d {
|
||||||
@ -125,17 +125,14 @@
|
|||||||
<span>Blockstream</span>
|
<span>Blockstream</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://unchained.com/" target="_blank" title="Unchained">
|
<a href="https://unchained.com/" target="_blank" title="Unchained">
|
||||||
<svg id="Layer_1" width="78" height="78" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 156.68 156.68"><defs><style>.cls-unchained-1{fill:#fff;}</style></defs><path class="cls-unchained-1" d="m78.34,0C35.07,0,0,35.07,0,78.34s35.07,78.34,78.34,78.34,78.34-35.07,78.34-78.34S121.6,0,78.34,0ZM20.23,109.5c-4.99-9.28-7.81-19.89-7.81-31.16C12.42,41.93,41.93,12.42,78.34,12.42c33.15,0,60.58,24.46,65.23,56.32h-37.48c-45.29,0-71.19,20.05-85.85,40.76Zm58.11,34.76c-12.42,0-24.04-3.44-33.96-9.41,3.94-8.85,9.11-18.7,15.84-28.9,20.99-31.8,52.2-31.19,76.49-31.19h7.45c.06,1.18.1,2.38.1,3.58,0,36.41-29.51,65.92-65.92,65.92Z"/><path class="cls-unchained-1" d="m91.98,42.4l-3.62-1.18c-3.94-1.29-7.03-4.38-8.32-8.32l-1.18-3.63c-.13-.39-.68-.39-.81,0l-1.18,3.63c-1.29,3.94-4.38,7.03-8.32,8.32l-3.62,1.18c-.39.13-.39.68,0,.81l3.62,1.18c3.94,1.29,7.03,4.38,8.32,8.32l1.18,3.63c.13.39.68.39.81,0l1.18-3.63c1.29-3.94,4.38-7.03,8.32-8.32l3.62-1.18c.39-.13.39-.68,0-.81Z"/></svg>
|
<svg id="Layer_1" width="78" height="78" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 156.68 156.68" class="image">
|
||||||
|
<defs><style>.cls-unchained-1{fill:#fff;}</style></defs><path class="cls-unchained-1" d="m78.34,0C35.07,0,0,35.07,0,78.34s35.07,78.34,78.34,78.34,78.34-35.07,78.34-78.34S121.6,0,78.34,0ZM20.23,109.5c-4.99-9.28-7.81-19.89-7.81-31.16C12.42,41.93,41.93,12.42,78.34,12.42c33.15,0,60.58,24.46,65.23,56.32h-37.48c-45.29,0-71.19,20.05-85.85,40.76Zm58.11,34.76c-12.42,0-24.04-3.44-33.96-9.41,3.94-8.85,9.11-18.7,15.84-28.9,20.99-31.8,52.2-31.19,76.49-31.19h7.45c.06,1.18.1,2.38.1,3.58,0,36.41-29.51,65.92-65.92,65.92Z"/><path class="cls-unchained-1" d="m91.98,42.4l-3.62-1.18c-3.94-1.29-7.03-4.38-8.32-8.32l-1.18-3.63c-.13-.39-.68-.39-.81,0l-1.18,3.63c-1.29,3.94-4.38,7.03-8.32,8.32l-3.62,1.18c-.39.13-.39.68,0,.81l3.62,1.18c3.94,1.29,7.03,4.38,8.32,8.32l1.18,3.63c.13.39.68.39.81,0l1.18-3.63c1.29-3.94,4.38-7.03,8.32-8.32l3.62-1.18c.39-.13.39-.68,0-.81Z"/>
|
||||||
|
</svg>
|
||||||
<span>Unchained</span>
|
<span>Unchained</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://gemini.com/" target="_blank" title="Gemini">
|
<a href="https://bitkey.world/" target="_blank" title="Bitkey">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360" class="image">
|
<img class="image" src="/resources/profile/bitkey.svg" />
|
||||||
<rect style="fill: black" width="360" height="360" />
|
<span>Bitkey</span>
|
||||||
<g transform="matrix(0.62 0 0 0.62 180 180)">
|
|
||||||
<path style="fill: rgb(0,220,250)" transform=" translate(-162, -162)" d="M 211.74 0 C 154.74 0 106.35 43.84 100.25 100.25 C 43.84 106.35 1.4210854715202004e-14 154.76 1.4210854715202004e-14 211.74 C 0.044122601308501076 273.7212006364817 50.27879936351834 323.95587739869154 112.26 324 C 169.26 324 217.84 280.15999999999997 223.75 223.75 C 280.15999999999997 217.65 324 169.24 324 112.26 C 323.95587739869154 50.278799363518324 273.72120063648174 0.04412260130848722 211.74 -1.4210854715202004e-14 z M 297.74 124.84 C 291.9644950552469 162.621439649343 262.2969457716857 192.26062994820046 224.51 198 L 224.51 124.84 z M 26.3 199.16 C 31.986912917108594 161.30935034910615 61.653433460549415 131.56986937804106 99.48999999999998 125.78999999999999 L 99.49 199 L 26.3 199 z M 198.21 224.51 C 191.87736076583954 267.0991541201681 155.312384597087 298.62923417787493 112.255 298.62923417787493 C 69.19761540291302 298.62923417787493 32.63263923416048 267.0991541201682 26.3 224.51 z M 199.16 124.83999999999999 L 199.16 199 L 124.84 199 L 124.84 124.84 z M 297.7 99.48999999999998 L 125.78999999999999 99.48999999999998 C 132.12263923416046 56.90084587983182 168.687615402913 25.37076582212505 211.745 25.37076582212505 C 254.80238459708698 25.37076582212505 291.3673607658395 56.900845879831834 297.7 99.49 z" stroke-linecap="round" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
<span>Gemini</span>
|
|
||||||
</a>
|
</a>
|
||||||
<a href="https://bullbitcoin.com/" target="_blank" title="Bull Bitcoin">
|
<a href="https://bullbitcoin.com/" target="_blank" title="Bull Bitcoin">
|
||||||
<svg aria-hidden="true" class="image" viewBox="0 -5 40 40" xmlns="http://www.w3.org/2000/svg">
|
<svg aria-hidden="true" class="image" viewBox="0 -5 40 40" xmlns="http://www.w3.org/2000/svg">
|
||||||
@ -150,7 +147,7 @@
|
|||||||
<span>Bull Bitcoin</span>
|
<span>Bull Bitcoin</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://exodus.com/" target="_blank" title="Exodus">
|
<a href="https://exodus.com/" target="_blank" title="Exodus">
|
||||||
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg" class="image">
|
||||||
<circle cx="250" cy="250" r="250" fill="#1F2033"/>
|
<circle cx="250" cy="250" r="250" fill="#1F2033"/>
|
||||||
<g clip-path="url(#clip0_2_14)">
|
<g clip-path="url(#clip0_2_14)">
|
||||||
<path d="M411.042 178.303L271.79 87V138.048L361.121 196.097L350.612 229.351H271.79V271.648H350.612L361.121 304.903L271.79 362.952V414L411.042 322.989L388.271 250.646L411.042 178.303Z" fill="url(#paint0_linear_2_14)"/>
|
<path d="M411.042 178.303L271.79 87V138.048L361.121 196.097L350.612 229.351H271.79V271.648H350.612L361.121 304.903L271.79 362.952V414L411.042 322.989L388.271 250.646L411.042 178.303Z" fill="url(#paint0_linear_2_14)"/>
|
||||||
@ -191,6 +188,19 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<span>Exodus</span>
|
<span>Exodus</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://gemini.com/" target="_blank" title="Gemini">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360" class="image">
|
||||||
|
<rect style="fill: black" width="360" height="360" />
|
||||||
|
<g transform="matrix(0.62 0 0 0.62 180 180)">
|
||||||
|
<path style="fill: rgb(0,220,250)" transform=" translate(-162, -162)" d="M 211.74 0 C 154.74 0 106.35 43.84 100.25 100.25 C 43.84 106.35 1.4210854715202004e-14 154.76 1.4210854715202004e-14 211.74 C 0.044122601308501076 273.7212006364817 50.27879936351834 323.95587739869154 112.26 324 C 169.26 324 217.84 280.15999999999997 223.75 223.75 C 280.15999999999997 217.65 324 169.24 324 112.26 C 323.95587739869154 50.278799363518324 273.72120063648174 0.04412260130848722 211.74 -1.4210854715202004e-14 z M 297.74 124.84 C 291.9644950552469 162.621439649343 262.2969457716857 192.26062994820046 224.51 198 L 224.51 124.84 z M 26.3 199.16 C 31.986912917108594 161.30935034910615 61.653433460549415 131.56986937804106 99.48999999999998 125.78999999999999 L 99.49 199 L 26.3 199 z M 198.21 224.51 C 191.87736076583954 267.0991541201681 155.312384597087 298.62923417787493 112.255 298.62923417787493 C 69.19761540291302 298.62923417787493 32.63263923416048 267.0991541201682 26.3 224.51 z M 199.16 124.83999999999999 L 199.16 199 L 124.84 199 L 124.84 124.84 z M 297.7 99.48999999999998 L 125.78999999999999 99.48999999999998 C 132.12263923416046 56.90084587983182 168.687615402913 25.37076582212505 211.745 25.37076582212505 C 254.80238459708698 25.37076582212505 291.3673607658395 56.900845879831834 297.7 99.49 z" stroke-linecap="round" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<span>Gemini</span>
|
||||||
|
</a>
|
||||||
|
<a href="https://leather.io/" target="_blank" title="Leather">
|
||||||
|
<img class="image" src="/resources/profile/leather.svg" />
|
||||||
|
<span>Leather</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -435,7 +445,7 @@
|
|||||||
Trademark Notice<br>
|
Trademark Notice<br>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
The Mempool Open Source Project®, 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>
|
||||||
<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>.
|
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 {
|
.image.not-rounded {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.intro {
|
.intro {
|
||||||
@ -158,9 +156,8 @@
|
|||||||
margin: 40px 29px 10px;
|
margin: 40px 29px 10px;
|
||||||
&.image.coldcard {
|
&.image.coldcard {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
width: auto;
|
height: auto;
|
||||||
max-height: 50px;
|
margin: 20px 29px 20px;
|
||||||
margin: 40px 29px 14px 29px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -254,3 +251,12 @@
|
|||||||
width: 64px;
|
width: 64px;
|
||||||
height: 64px;
|
height: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.enterprise-sponsor {
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
}
|
@ -75,6 +75,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
@Output() changeMode = new EventEmitter<boolean>();
|
@Output() changeMode = new EventEmitter<boolean>();
|
||||||
|
|
||||||
calculating = true;
|
calculating = true;
|
||||||
|
processing = false;
|
||||||
selectedOption: 'wait' | 'accel';
|
selectedOption: 'wait' | 'accel';
|
||||||
cantPayReason = '';
|
cantPayReason = '';
|
||||||
quoteError = ''; // error fetching estimate or initial data
|
quoteError = ''; // error fetching estimate or initial data
|
||||||
@ -196,9 +197,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
if (changes.scrollEvent && this.scrollEvent) {
|
if (changes.scrollEvent && this.scrollEvent) {
|
||||||
this.scrollToElement('acceleratePreviewAnchor', 'start');
|
this.scrollToElement('acceleratePreviewAnchor', 'start');
|
||||||
}
|
}
|
||||||
if (changes.accelerating) {
|
if (changes.accelerating && this.accelerating) {
|
||||||
if ((this.step === 'processing' || this.step === 'paid') && this.accelerating) {
|
if (this.step === 'processing' || this.step === 'paid') {
|
||||||
this.moveToStep('success');
|
this.moveToStep('success');
|
||||||
|
} else { // Edge case where the transaction gets accelerated by someone else or on another session
|
||||||
|
this.closeModal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -378,9 +381,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
* Account-based acceleration request
|
* Account-based acceleration request
|
||||||
*/
|
*/
|
||||||
accelerateWithMempoolAccount(): void {
|
accelerateWithMempoolAccount(): void {
|
||||||
if (!this.canPay || this.calculating) {
|
if (!this.canPay || this.calculating || this.processing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.processing = true;
|
||||||
if (this.accelerationSubscription) {
|
if (this.accelerationSubscription) {
|
||||||
this.accelerationSubscription.unsubscribe();
|
this.accelerationSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
@ -390,6 +394,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.accelerationUUID
|
this.accelerationUUID
|
||||||
).subscribe({
|
).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
|
this.processing = false;
|
||||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||||
this.audioService.playSound('ascend-chime-cartoon');
|
this.audioService.playSound('ascend-chime-cartoon');
|
||||||
this.showSuccess = true;
|
this.showSuccess = true;
|
||||||
@ -397,6 +402,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.moveToStep('paid');
|
this.moveToStep('paid');
|
||||||
},
|
},
|
||||||
error: (response) => {
|
error: (response) => {
|
||||||
|
this.processing = false;
|
||||||
this.accelerateError = response.error;
|
this.accelerateError = response.error;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -466,10 +472,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
* APPLE PAY
|
* APPLE PAY
|
||||||
*/
|
*/
|
||||||
async requestApplePayPayment(): Promise<void> {
|
async requestApplePayPayment(): Promise<void> {
|
||||||
|
if (this.processing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.conversionsSubscription) {
|
if (this.conversionsSubscription) {
|
||||||
this.conversionsSubscription.unsubscribe();
|
this.conversionsSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.processing = true;
|
||||||
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||||
async (conversions) => {
|
async (conversions) => {
|
||||||
this.conversions = conversions;
|
this.conversions = conversions;
|
||||||
@ -494,6 +504,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
console.error(`Unable to find apple pay button id='apple-pay-button'`);
|
console.error(`Unable to find apple pay button id='apple-pay-button'`);
|
||||||
// Try again
|
// Try again
|
||||||
setTimeout(this.requestApplePayPayment.bind(this), 500);
|
setTimeout(this.requestApplePayPayment.bind(this), 500);
|
||||||
|
this.processing = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.loadingApplePay = false;
|
this.loadingApplePay = false;
|
||||||
@ -505,6 +516,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
||||||
console.error(`Cannot retreive payment card details`);
|
console.error(`Cannot retreive payment card details`);
|
||||||
this.accelerateError = 'apple_pay_no_card_details';
|
this.accelerateError = 'apple_pay_no_card_details';
|
||||||
|
this.processing = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
|
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
|
||||||
@ -516,6 +528,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.accelerationUUID
|
this.accelerationUUID
|
||||||
).subscribe({
|
).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
|
this.processing = false;
|
||||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||||
this.audioService.playSound('ascend-chime-cartoon');
|
this.audioService.playSound('ascend-chime-cartoon');
|
||||||
if (this.applePay) {
|
if (this.applePay) {
|
||||||
@ -526,6 +539,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
},
|
},
|
||||||
error: (response) => {
|
error: (response) => {
|
||||||
|
this.processing = false;
|
||||||
this.accelerateError = response.error;
|
this.accelerateError = response.error;
|
||||||
if (!(response.status === 403 && response.error === 'not_available')) {
|
if (!(response.status === 403 && response.error === 'not_available')) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -537,6 +551,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
this.processing = false;
|
||||||
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
|
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
|
||||||
if (tokenResult.errors) {
|
if (tokenResult.errors) {
|
||||||
errorMessage += ` and errors: ${JSON.stringify(
|
errorMessage += ` and errors: ${JSON.stringify(
|
||||||
@ -547,6 +562,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
this.processing = false;
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -557,10 +573,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
* GOOGLE PAY
|
* GOOGLE PAY
|
||||||
*/
|
*/
|
||||||
async requestGooglePayPayment(): Promise<void> {
|
async requestGooglePayPayment(): Promise<void> {
|
||||||
|
if (this.processing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.conversionsSubscription) {
|
if (this.conversionsSubscription) {
|
||||||
this.conversionsSubscription.unsubscribe();
|
this.conversionsSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.processing = true;
|
||||||
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||||
async (conversions) => {
|
async (conversions) => {
|
||||||
this.conversions = conversions;
|
this.conversions = conversions;
|
||||||
@ -595,6 +615,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
||||||
console.error(`Cannot retreive payment card details`);
|
console.error(`Cannot retreive payment card details`);
|
||||||
this.accelerateError = 'apple_pay_no_card_details';
|
this.accelerateError = 'apple_pay_no_card_details';
|
||||||
|
this.processing = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
|
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
|
||||||
@ -606,6 +627,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.accelerationUUID
|
this.accelerationUUID
|
||||||
).subscribe({
|
).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
|
this.processing = false;
|
||||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||||
this.audioService.playSound('ascend-chime-cartoon');
|
this.audioService.playSound('ascend-chime-cartoon');
|
||||||
if (this.googlePay) {
|
if (this.googlePay) {
|
||||||
@ -616,6 +638,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
},
|
},
|
||||||
error: (response) => {
|
error: (response) => {
|
||||||
|
this.processing = false;
|
||||||
this.accelerateError = response.error;
|
this.accelerateError = response.error;
|
||||||
if (!(response.status === 403 && response.error === 'not_available')) {
|
if (!(response.status === 403 && response.error === 'not_available')) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -627,6 +650,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
this.processing = false;
|
||||||
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
|
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
|
||||||
if (tokenResult.errors) {
|
if (tokenResult.errors) {
|
||||||
errorMessage += ` and errors: ${JSON.stringify(
|
errorMessage += ` and errors: ${JSON.stringify(
|
||||||
@ -644,10 +668,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
* CASHAPP
|
* CASHAPP
|
||||||
*/
|
*/
|
||||||
async requestCashAppPayment(): Promise<void> {
|
async requestCashAppPayment(): Promise<void> {
|
||||||
|
if (this.processing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.conversionsSubscription) {
|
if (this.conversionsSubscription) {
|
||||||
this.conversionsSubscription.unsubscribe();
|
this.conversionsSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.processing = true;
|
||||||
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||||
async (conversions) => {
|
async (conversions) => {
|
||||||
this.conversions = conversions;
|
this.conversions = conversions;
|
||||||
@ -678,6 +706,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.cashAppPay.addEventListener('ontokenization', event => {
|
this.cashAppPay.addEventListener('ontokenization', event => {
|
||||||
const { tokenResult, error } = event.detail;
|
const { tokenResult, error } = event.detail;
|
||||||
if (error) {
|
if (error) {
|
||||||
|
this.processing = false;
|
||||||
this.accelerateError = error;
|
this.accelerateError = error;
|
||||||
} else if (tokenResult.status === 'OK') {
|
} else if (tokenResult.status === 'OK') {
|
||||||
this.servicesApiService.accelerateWithCashApp$(
|
this.servicesApiService.accelerateWithCashApp$(
|
||||||
@ -688,6 +717,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.accelerationUUID
|
this.accelerationUUID
|
||||||
).subscribe({
|
).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
|
this.processing = false;
|
||||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||||
this.audioService.playSound('ascend-chime-cartoon');
|
this.audioService.playSound('ascend-chime-cartoon');
|
||||||
if (this.cashAppPay) {
|
if (this.cashAppPay) {
|
||||||
@ -702,6 +732,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
},
|
},
|
||||||
error: (response) => {
|
error: (response) => {
|
||||||
|
this.processing = false;
|
||||||
this.accelerateError = response.error;
|
this.accelerateError = response.error;
|
||||||
if (!(response.status === 403 && response.error === 'not_available')) {
|
if (!(response.status === 403 && response.error === 'not_available')) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<span class="fee">{{ bar.class === 'tx' ? '' : '+' }}{{ bar.fee | number }} <span class="symbol" i18n="shared.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 class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,14 +21,14 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr *ngIf="accelerationInfo.fee">
|
<tr *ngIf="accelerationInfo.fee">
|
||||||
<td class="label" i18n="transaction.fee|Transaction fee">Fee</td>
|
<td class="label" i18n="transaction.fee|Transaction fee">Fee</td>
|
||||||
<td class="value">{{ accelerationInfo.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
|
<td class="value">{{ accelerationInfo.fee | number }} <span class="symbol" i18n="shared.sats">sats</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngIf="accelerationInfo.bidBoost >= 0 || accelerationInfo.feeDelta">
|
<tr *ngIf="accelerationInfo.bidBoost >= 0 || accelerationInfo.feeDelta">
|
||||||
<td class="label" i18n="transaction.out-of-band-fees">Out-of-band fees</td>
|
<td class="label" i18n="transaction.out-of-band-fees">Out-of-band fees</td>
|
||||||
@if (accelerationInfo.status === 'accelerated') {
|
@if (accelerationInfo.status === 'accelerated') {
|
||||||
<td class="value oobFees">{{ accelerationInfo.feeDelta | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
|
<td class="value oobFees">{{ accelerationInfo.feeDelta | number }} <span class="symbol" i18n="shared.sats">sats</span></td>
|
||||||
} @else {
|
} @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>
|
||||||
<tr *ngIf="accelerationInfo.fee && accelerationInfo.weight">
|
<tr *ngIf="accelerationInfo.fee && accelerationInfo.weight">
|
||||||
@ -47,13 +47,14 @@
|
|||||||
<tr *ngIf="['accelerated', 'mined'].includes(accelerationInfo.status) && hasPoolsData()">
|
<tr *ngIf="['accelerated', 'mined'].includes(accelerationInfo.status) && hasPoolsData()">
|
||||||
<td class="label" i18n="transaction.accelerated-by-hashrate|Accelerated to hashrate">Accelerated by</td>
|
<td class="label" i18n="transaction.accelerated-by-hashrate|Accelerated to hashrate">Accelerated by</td>
|
||||||
<td class="value" *ngIf="accelerationInfo.pools">
|
<td class="value" *ngIf="accelerationInfo.pools">
|
||||||
<ng-container *ngFor="let pool of accelerationInfo.pools">
|
<ng-container *ngFor="let pool of accelerationInfo.pools; let i = index;">
|
||||||
<img *ngIf="accelerationInfo.poolsData[pool]"
|
<img *ngIf="accelerationInfo.poolsData[pool]"
|
||||||
class="pool-logo"
|
class="pool-logo"
|
||||||
[style.opacity]="accelerationInfo?.minedByPoolUniqueId && pool !== accelerationInfo?.minedByPoolUniqueId ? '0.3' : '1'"
|
[style.opacity]="accelerationInfo?.minedByPoolUniqueId && pool !== accelerationInfo?.minedByPoolUniqueId ? '0.3' : '1'"
|
||||||
[src]="'/resources/mining-pools/' + accelerationInfo.poolsData[pool].slug + '.svg'"
|
[src]="'/resources/mining-pools/' + accelerationInfo.poolsData[pool].slug + '.svg'"
|
||||||
onError="this.src = '/resources/mining-pools/default.svg'"
|
onError="this.src = '/resources/mining-pools/default.svg'"
|
||||||
[alt]="'Logo of ' + pool.name + ' mining pool'">
|
[alt]="'Logo of ' + pool.name + ' mining pool'">
|
||||||
|
<br *ngIf="i % 6 === 5">
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
|
|
||||||
.label {
|
.label {
|
||||||
padding-right: 30px;
|
padding-right: 30px;
|
||||||
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pool-logo {
|
.pool-logo {
|
||||||
@ -30,7 +31,8 @@
|
|||||||
height: 22px;
|
height: 22px;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -1px;
|
top: -1px;
|
||||||
margin-right: 3px;
|
margin-right: 4px;
|
||||||
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.oobFees {
|
.oobFees {
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
<div class="node-spacer"></div>
|
<div class="node-spacer"></div>
|
||||||
<div class="interval">
|
<div class="interval">
|
||||||
<div class="interval-time">
|
<div class="interval-time">
|
||||||
<app-time [time]="acceleratedAt - transactionTime"></app-time>
|
<app-time [time]="firstSeenToAccelerated"></app-time>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="node-spacer"></div>
|
<div class="node-spacer"></div>
|
||||||
@ -46,7 +46,7 @@
|
|||||||
<div class="interval-time">
|
<div class="interval-time">
|
||||||
@if (tx.status.confirmed) {
|
@if (tx.status.confirmed) {
|
||||||
<div class="interval-time">
|
<div class="interval-time">
|
||||||
<app-time [time]="tx.status.block_time - acceleratedAt"></app-time>
|
<app-time [time]="acceleratedToMined"></app-time>
|
||||||
</div>
|
</div>
|
||||||
} @else if (standardETA && !tx.status.confirmed) {
|
} @else if (standardETA && !tx.status.confirmed) {
|
||||||
<!-- ~<app-time [time]="standardETA / 1000 - now"></app-time> -->
|
<!-- ~<app-time [time]="standardETA / 1000 - now"></app-time> -->
|
||||||
|
@ -24,6 +24,8 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
|
|||||||
accelerateRatio: number;
|
accelerateRatio: number;
|
||||||
useAbsoluteTime: boolean = false;
|
useAbsoluteTime: boolean = false;
|
||||||
interval: number;
|
interval: number;
|
||||||
|
firstSeenToAccelerated: number;
|
||||||
|
acceleratedToMined: number;
|
||||||
|
|
||||||
tooltipPosition = null;
|
tooltipPosition = null;
|
||||||
hoverInfo: any = null;
|
hoverInfo: any = null;
|
||||||
@ -35,8 +37,6 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.acceleratedAt = this.tx.acceleratedAt ?? new Date().getTime() / 1000;
|
this.acceleratedAt = this.tx.acceleratedAt ?? new Date().getTime() / 1000;
|
||||||
this.now = Math.floor(new Date().getTime() / 1000);
|
|
||||||
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
|
|
||||||
|
|
||||||
this.miningService.getPools().subscribe(pools => {
|
this.miningService.getPools().subscribe(pools => {
|
||||||
for (const pool of pools) {
|
for (const pool of pools) {
|
||||||
@ -44,10 +44,8 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.interval = window.setInterval(() => {
|
this.updateTimes();
|
||||||
this.now = Math.floor(new Date().getTime() / 1000);
|
this.interval = window.setInterval(this.updateTimes.bind(this), 60000);
|
||||||
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
|
|
||||||
}, 60000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges(changes): void {
|
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 {
|
ngOnDestroy(): void {
|
||||||
clearInterval(this.interval);
|
clearInterval(this.interval);
|
||||||
}
|
}
|
||||||
|
@ -264,7 +264,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
|
|||||||
type: 'bar',
|
type: 'bar',
|
||||||
barWidth: '90%',
|
barWidth: '90%',
|
||||||
large: true,
|
large: true,
|
||||||
barMinHeight: 1,
|
barMinHeight: 3,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
dataZoom: (this.widget || data.length === 0 )? undefined : [{
|
dataZoom: (this.widget || data.length === 0 )? undefined : [{
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
<app-fee-rate [fee]="acceleration.effectiveFee" [weight]="acceleration.effectiveVsize * 4"></app-fee-rate>
|
<app-fee-rate [fee]="acceleration.effectiveFee" [weight]="acceleration.effectiveVsize * 4"></app-fee-rate>
|
||||||
</td>
|
</td>
|
||||||
<td class="bid text-right">
|
<td class="bid text-right">
|
||||||
{{ (acceleration.feeDelta) | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
|
{{ (acceleration.feeDelta) | number }} <span class="symbol" i18n="shared.sats">sats</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="time text-right">
|
<td class="time text-right">
|
||||||
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
|
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
|
||||||
@ -41,7 +41,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="!pending">
|
<ng-container *ngIf="!pending">
|
||||||
<td *ngIf="acceleration.boost != null" class="fee text-right">
|
<td *ngIf="acceleration.boost != null" class="fee text-right">
|
||||||
{{ acceleration.boost | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
|
{{ acceleration.boost | number }} <span class="symbol" i18n="shared.sats">sats</span>
|
||||||
</td>
|
</td>
|
||||||
<td *ngIf="acceleration.boost == null" class="fee text-right">
|
<td *ngIf="acceleration.boost == null" class="fee text-right">
|
||||||
~
|
~
|
||||||
@ -64,7 +64,7 @@
|
|||||||
<span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="accelerator.pending">Pending</span>
|
<span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="accelerator.pending">Pending</span>
|
||||||
<span *ngIf="acceleration.status.includes('completed') && acceleration.minedByPoolUniqueId && pools[acceleration.minedByPoolUniqueId]" class="badge badge-success"><ng-container i18n="accelerator.completed">Completed</ng-container><span *ngIf="acceleration.status === 'completed_provisional'"> ⌛</span></span>
|
<span *ngIf="acceleration.status.includes('completed') && acceleration.minedByPoolUniqueId && pools[acceleration.minedByPoolUniqueId]" class="badge badge-success"><ng-container i18n="accelerator.completed">Completed</ng-container><span *ngIf="acceleration.status === 'completed_provisional'"> ⌛</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('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>
|
||||||
<td class="date text-right" *ngIf="!this.widget">
|
<td class="date text-right" *ngIf="!this.widget">
|
||||||
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
|
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy, Inject, LOCALE_ID } from '@angular/core';
|
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy, Inject, LOCALE_ID } from '@angular/core';
|
||||||
import { BehaviorSubject, Observable, Subscription, catchError, filter, of, switchMap, tap, throttleTime } from 'rxjs';
|
import { BehaviorSubject, Observable, Subscription, catchError, combineLatest, filter, of, switchMap, tap, throttleTime, timer } from 'rxjs';
|
||||||
import { Acceleration, BlockExtended, SinglePoolStats } from '../../../interfaces/node-api.interface';
|
import { Acceleration, BlockExtended, SinglePoolStats } from '../../../interfaces/node-api.interface';
|
||||||
import { StateService } from '../../../services/state.service';
|
import { StateService } from '../../../services/state.service';
|
||||||
import { WebsocketService } from '../../../services/websocket.service';
|
import { WebsocketService } from '../../../services/websocket.service';
|
||||||
@ -61,8 +61,11 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
|
|||||||
this.websocketService.want(['blocks']);
|
this.websocketService.want(['blocks']);
|
||||||
this.seoService.setTitle($localize`:@@02573b6980a2d611b4361a2595a4447e390058cd:Accelerations`);
|
this.seoService.setTitle($localize`:@@02573b6980a2d611b4361a2595a4447e390058cd:Accelerations`);
|
||||||
|
|
||||||
this.paramSubscription = this.route.params.pipe(
|
this.paramSubscription = combineLatest([
|
||||||
tap(params => {
|
this.route.params,
|
||||||
|
timer(0),
|
||||||
|
]).pipe(
|
||||||
|
tap(([params]) => {
|
||||||
this.page = +params['page'] || 1;
|
this.page = +params['page'] || 1;
|
||||||
this.pageSubject.next(this.page);
|
this.pageSubject.next(this.page);
|
||||||
})
|
})
|
||||||
|
@ -10,10 +10,10 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="field-value" [class]="chartPositionLeft ? 'chart-left' : ''">
|
<td class="field-value" [class]="chartPositionLeft ? 'chart-left' : ''">
|
||||||
<div class="effective-fee-container">
|
<div class="effective-fee-container">
|
||||||
@if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize)) {
|
@if (accelerationInfo?.acceleratedFeeRate && (!effectiveFeeRate || accelerationInfo.acceleratedFeeRate >= effectiveFeeRate)) {
|
||||||
<app-fee-rate class="oobFees" [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
|
<app-fee-rate class="oobFees" [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
|
||||||
} @else {
|
} @else {
|
||||||
<app-fee-rate class="oobFees" [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
|
<app-fee-rate class="oobFees" [fee]="effectiveFeeRate"></app-fee-rate>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</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 { Transaction } from '../../../interfaces/electrs.interface';
|
||||||
import { Acceleration, SinglePoolStats } from '../../../interfaces/node-api.interface';
|
import { Acceleration, SinglePoolStats } from '../../../interfaces/node-api.interface';
|
||||||
import { EChartsOption, PieSeriesOption } from '../../../graphs/echarts';
|
import { EChartsOption, PieSeriesOption } from '../../../graphs/echarts';
|
||||||
@ -23,7 +23,8 @@ function toRGB({r,g,b}): string {
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class ActiveAccelerationBox implements OnChanges {
|
export class ActiveAccelerationBox implements OnChanges {
|
||||||
@Input() tx: Transaction;
|
@Input() acceleratedBy?: number[];
|
||||||
|
@Input() effectiveFeeRate?: number;
|
||||||
@Input() accelerationInfo: Acceleration;
|
@Input() accelerationInfo: Acceleration;
|
||||||
@Input() miningStats: MiningStats;
|
@Input() miningStats: MiningStats;
|
||||||
@Input() pools: number[];
|
@Input() pools: number[];
|
||||||
@ -41,10 +42,12 @@ export class ActiveAccelerationBox implements OnChanges {
|
|||||||
timespan = '';
|
timespan = '';
|
||||||
chartInstance: any = undefined;
|
chartInstance: any = undefined;
|
||||||
|
|
||||||
constructor() {}
|
constructor(
|
||||||
|
private cd: ChangeDetectorRef,
|
||||||
|
) {}
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
const pools = this.pools || this.accelerationInfo?.pools || this.tx.acceleratedBy;
|
const pools = this.pools || this.accelerationInfo?.pools || this.acceleratedBy;
|
||||||
if (pools && this.miningStats) {
|
if (pools && this.miningStats) {
|
||||||
this.prepareChartOptions(pools);
|
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 acceleratingPools = (poolList || []).filter(id => pools[id]).sort((a,b) => pools[a].lastEstimatedHashrate - pools[b].lastEstimatedHashrate);
|
||||||
const totalAcceleratedHashrate = acceleratingPools.reduce((total, pool) => total + pools[pool].lastEstimatedHashrate, 0);
|
const totalAcceleratedHashrate = acceleratingPools.reduce((total, pool) => total + pools[pool].lastEstimatedHashrate, 0);
|
||||||
const lightenStep = acceleratingPools.length ? (0.48 / acceleratingPools.length) : 0;
|
// Find the first pool with at least 1% of the total network hashrate
|
||||||
|
const firstSignificantPool = acceleratingPools.findIndex(pool => pools[pool].lastEstimatedHashrate > this.miningStats.lastEstimatedHashrate / 100);
|
||||||
|
const numSignificantPools = acceleratingPools.length - firstSignificantPool;
|
||||||
acceleratingPools.forEach((poolId, index) => {
|
acceleratingPools.forEach((poolId, index) => {
|
||||||
const pool = pools[poolId];
|
const pool = pools[poolId];
|
||||||
const poolShare = ((pool.lastEstimatedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1);
|
const poolShare = ((pool.lastEstimatedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1);
|
||||||
data.push(getDataItem(
|
data.push(getDataItem(
|
||||||
pool.lastEstimatedHashrate,
|
pool.lastEstimatedHashrate,
|
||||||
toRGB(lighten({ r: 147, g: 57, b: 244 }, index * lightenStep)),
|
index >= firstSignificantPool
|
||||||
|
? toRGB(lighten({ r: 147, g: 57, b: 244 }, 1 - (index - firstSignificantPool) / (numSignificantPools - 1)))
|
||||||
|
: 'white',
|
||||||
`<b style="color: white">${pool.name} (${poolShare}%)</b>`,
|
`<b style="color: white">${pool.name} (${poolShare}%)</b>`,
|
||||||
true,
|
true,
|
||||||
) as PieSeriesOption);
|
) as PieSeriesOption);
|
||||||
@ -128,6 +135,7 @@ export class ActiveAccelerationBox implements OnChanges {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
this.cd.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
onChartInit(ec) {
|
onChartInit(ec) {
|
||||||
|
@ -55,7 +55,7 @@ export class AddressLabelsComponent implements OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleVin() {
|
handleVin() {
|
||||||
const address = new AddressTypeInfo(this.network || 'mainnet', this.vin.prevout?.scriptpubkey_address, this.vin.prevout?.scriptpubkey_type as AddressType, [this.vin])
|
const address = new AddressTypeInfo(this.network || 'mainnet', this.vin.prevout?.scriptpubkey_address, this.vin.prevout?.scriptpubkey_type as AddressType, [this.vin]);
|
||||||
if (address?.scripts.size) {
|
if (address?.scripts.size) {
|
||||||
const script = address?.scripts.values().next().value;
|
const script = address?.scripts.values().next().value;
|
||||||
if (script.template?.label) {
|
if (script.template?.label) {
|
||||||
|
@ -94,6 +94,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="(stateService.backend$ | async) === 'esplora' && address && utxos && utxos.length > 2">
|
||||||
|
<br>
|
||||||
|
<div class="title-tx">
|
||||||
|
<h2 class="text-left" i18n="address.unspent-outputs">Unspent Outputs</h2>
|
||||||
|
</div>
|
||||||
|
<div class="box">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md">
|
||||||
|
<app-utxo-graph [utxos]="utxos" left="80" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
<div class="title-tx">
|
<div class="title-tx">
|
||||||
<h2 class="text-left">
|
<h2 class="text-left">
|
||||||
|
@ -2,12 +2,12 @@ import { Component, OnInit, OnDestroy, HostListener } from '@angular/core';
|
|||||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||||
import { switchMap, filter, catchError, map, tap } from 'rxjs/operators';
|
import { switchMap, filter, catchError, map, tap } from 'rxjs/operators';
|
||||||
import { Address, ChainStats, Transaction, Vin } from '../../interfaces/electrs.interface';
|
import { Address, ChainStats, Transaction, Utxo, Vin } from '../../interfaces/electrs.interface';
|
||||||
import { WebsocketService } from '../../services/websocket.service';
|
import { WebsocketService } from '../../services/websocket.service';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { AudioService } from '../../services/audio.service';
|
import { AudioService } from '../../services/audio.service';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '../../services/api.service';
|
||||||
import { of, merge, Subscription, Observable } from 'rxjs';
|
import { of, merge, Subscription, Observable, forkJoin } from 'rxjs';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '../../services/seo.service';
|
||||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||||
import { AddressInformation } from '../../interfaces/node-api.interface';
|
import { AddressInformation } from '../../interfaces/node-api.interface';
|
||||||
@ -104,6 +104,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
addressString: string;
|
addressString: string;
|
||||||
isLoadingAddress = true;
|
isLoadingAddress = true;
|
||||||
transactions: Transaction[];
|
transactions: Transaction[];
|
||||||
|
utxos: Utxo[];
|
||||||
isLoadingTransactions = true;
|
isLoadingTransactions = true;
|
||||||
retryLoadMore = false;
|
retryLoadMore = false;
|
||||||
error: any;
|
error: any;
|
||||||
@ -159,6 +160,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
this.address = null;
|
this.address = null;
|
||||||
this.isLoadingTransactions = true;
|
this.isLoadingTransactions = true;
|
||||||
this.transactions = null;
|
this.transactions = null;
|
||||||
|
this.utxos = null;
|
||||||
this.addressInfo = null;
|
this.addressInfo = null;
|
||||||
this.exampleChannel = null;
|
this.exampleChannel = null;
|
||||||
document.body.scrollTo(0, 0);
|
document.body.scrollTo(0, 0);
|
||||||
@ -212,11 +214,23 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
this.updateChainStats();
|
this.updateChainStats();
|
||||||
this.isLoadingAddress = false;
|
this.isLoadingAddress = false;
|
||||||
this.isLoadingTransactions = true;
|
this.isLoadingTransactions = true;
|
||||||
return address.is_pubkey
|
const utxoCount = this.chainStats.utxos + this.mempoolStats.utxos;
|
||||||
|
return forkJoin([
|
||||||
|
address.is_pubkey
|
||||||
? this.electrsApiService.getScriptHashTransactions$((address.address.length === 66 ? '21' : '41') + address.address + 'ac')
|
? this.electrsApiService.getScriptHashTransactions$((address.address.length === 66 ? '21' : '41') + address.address + 'ac')
|
||||||
: this.electrsApiService.getAddressTransactions$(address.address);
|
: this.electrsApiService.getAddressTransactions$(address.address),
|
||||||
|
(utxoCount > 2 && utxoCount <= 500 ? (address.is_pubkey
|
||||||
|
? this.electrsApiService.getScriptHashUtxos$((address.address.length === 66 ? '21' : '41') + address.address + 'ac')
|
||||||
|
: this.electrsApiService.getAddressUtxos$(address.address)) : of(null)).pipe(
|
||||||
|
catchError(() => {
|
||||||
|
return of(null);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
]);
|
||||||
}),
|
}),
|
||||||
switchMap((transactions) => {
|
switchMap(([transactions, utxos]) => {
|
||||||
|
this.utxos = utxos;
|
||||||
|
|
||||||
this.tempTransactions = transactions;
|
this.tempTransactions = transactions;
|
||||||
if (transactions.length) {
|
if (transactions.length) {
|
||||||
this.lastTransactionTxId = transactions[transactions.length - 1].txid;
|
this.lastTransactionTxId = transactions[transactions.length - 1].txid;
|
||||||
@ -309,6 +323,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
this.transactions = this.transactions.slice();
|
this.transactions = this.transactions.slice();
|
||||||
this.mempoolStats.removeTx(transaction);
|
this.mempoolStats.removeTx(transaction);
|
||||||
this.audioService.playSound('magic');
|
this.audioService.playSound('magic');
|
||||||
|
this.confirmTransaction(tx);
|
||||||
} else {
|
} else {
|
||||||
if (this.addTransaction(transaction, false)) {
|
if (this.addTransaction(transaction, false)) {
|
||||||
this.audioService.playSound('magic');
|
this.audioService.playSound('magic');
|
||||||
@ -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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -346,9 +386,65 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
this.transactions.splice(index, 1);
|
this.transactions.splice(index, 1);
|
||||||
this.transactions = this.transactions.slice();
|
this.transactions = this.transactions.slice();
|
||||||
|
|
||||||
|
// update utxos in-place
|
||||||
|
if (this.utxos != null) {
|
||||||
|
let utxosChanged = false;
|
||||||
|
for (const vin of transaction.vin) {
|
||||||
|
if (vin.prevout?.scriptpubkey_address === this.address.address) {
|
||||||
|
this.utxos.push({
|
||||||
|
txid: vin.txid,
|
||||||
|
vout: vin.vout,
|
||||||
|
value: vin.prevout.value,
|
||||||
|
status: { confirmed: true }, // Assuming the input was confirmed
|
||||||
|
});
|
||||||
|
utxosChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [index, vout] of transaction.vout.entries()) {
|
||||||
|
if (vout.scriptpubkey_address === this.address.address) {
|
||||||
|
const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === transaction.txid && utxo.vout === index);
|
||||||
|
if (utxoIndex !== -1) {
|
||||||
|
this.utxos.splice(utxoIndex, 1);
|
||||||
|
utxosChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (utxosChanged) {
|
||||||
|
this.utxos = this.utxos.slice();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
confirmTransaction(transaction: Transaction): void {
|
||||||
|
// update utxos in-place
|
||||||
|
if (this.utxos != null) {
|
||||||
|
let utxosChanged = false;
|
||||||
|
for (const vin of transaction.vin) {
|
||||||
|
if (vin.prevout?.scriptpubkey_address === this.address.address) {
|
||||||
|
const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === vin.txid && utxo.vout === vin.vout);
|
||||||
|
if (utxoIndex !== -1) {
|
||||||
|
this.utxos[utxoIndex].status = JSON.parse(JSON.stringify(transaction.status));
|
||||||
|
utxosChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [index, vout] of transaction.vout.entries()) {
|
||||||
|
if (vout.scriptpubkey_address === this.address.address) {
|
||||||
|
const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === transaction.txid && utxo.vout === index);
|
||||||
|
if (utxoIndex !== -1) {
|
||||||
|
this.utxos[utxoIndex].status = JSON.parse(JSON.stringify(transaction.status));
|
||||||
|
utxosChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (utxosChanged) {
|
||||||
|
this.utxos = this.utxos.slice();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
loadMore(): void {
|
loadMore(): void {
|
||||||
if (this.isLoadingTransactions || this.fullyLoaded) {
|
if (this.isLoadingTransactions || this.fullyLoaded) {
|
||||||
return;
|
return;
|
||||||
|
@ -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') {
|
@if (digitsInfo === '1.8-8') {
|
||||||
‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | number }}
|
‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | number }}
|
||||||
} @else {
|
} @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">
|
<span class="symbol">
|
||||||
<ng-container *ngTemplateOutlet="prefix"></ng-container>sats
|
<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
|
// initialize the scene without any entry transition
|
||||||
setup(transactions: TransactionStripped[]): void {
|
setup(transactions: TransactionStripped[], sort: boolean = false): void {
|
||||||
const filtersAvailable = transactions.reduce((flagSet, tx) => flagSet || tx.flags > 0, false);
|
const filtersAvailable = transactions.reduce((flagSet, tx) => flagSet || tx.flags > 0, false);
|
||||||
if (filtersAvailable !== this.filtersAvailable) {
|
if (filtersAvailable !== this.filtersAvailable) {
|
||||||
this.setFilterFlags();
|
this.setFilterFlags();
|
||||||
@ -206,7 +206,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
this.filtersAvailable = filtersAvailable;
|
this.filtersAvailable = filtersAvailable;
|
||||||
if (this.scene) {
|
if (this.scene) {
|
||||||
this.clearUpdateQueue();
|
this.clearUpdateQueue();
|
||||||
this.scene.setup(transactions);
|
this.scene.setup(transactions, sort);
|
||||||
this.readyNextFrame = true;
|
this.readyNextFrame = true;
|
||||||
this.start();
|
this.start();
|
||||||
this.updateSearchHighlight();
|
this.updateSearchHighlight();
|
||||||
|
@ -88,16 +88,19 @@ export default class BlockScene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// set up the scene with an initial set of transactions, without any transition animation
|
// set up the scene with an initial set of transactions, without any transition animation
|
||||||
setup(txs: TransactionStripped[]) {
|
setup(txs: TransactionStripped[], sort: boolean = false) {
|
||||||
// clean up any old transactions
|
// clean up any old transactions
|
||||||
Object.values(this.txs).forEach(tx => {
|
Object.values(this.txs).forEach(tx => {
|
||||||
tx.destroy();
|
tx.destroy();
|
||||||
delete this.txs[tx.txid];
|
delete this.txs[tx.txid];
|
||||||
});
|
});
|
||||||
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
|
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
|
||||||
txs.forEach(tx => {
|
let txViews = txs.map(tx => new TxView(tx, this));
|
||||||
const txView = new TxView(tx, this);
|
if (sort) {
|
||||||
this.txs[tx.txid] = txView;
|
txViews = txViews.sort(feeRateDescending);
|
||||||
|
}
|
||||||
|
txViews.forEach(txView => {
|
||||||
|
this.txs[txView.txid] = txView;
|
||||||
this.place(txView);
|
this.place(txView);
|
||||||
this.saveGridToScreenPosition(txView);
|
this.saveGridToScreenPosition(txView);
|
||||||
this.applyTxUpdate(txView, {
|
this.applyTxUpdate(txView, {
|
||||||
|
@ -33,7 +33,7 @@ export default class TxView implements TransactionStripped {
|
|||||||
flags: number;
|
flags: number;
|
||||||
bigintFlags?: bigint | null = 0b00000100_00000000_00000000_00000000n;
|
bigintFlags?: bigint | null = 0b00000100_00000000_00000000_00000000n;
|
||||||
time?: number;
|
time?: number;
|
||||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
|
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'added_deprioritized' | 'deprioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
|
||||||
context?: 'projected' | 'actual';
|
context?: 'projected' | 'actual';
|
||||||
scene?: BlockScene;
|
scene?: BlockScene;
|
||||||
|
|
||||||
|
@ -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 {
|
export function desaturate(color: Color, amount: number): Color {
|
||||||
const gray = (color.r + color.g + color.b) / 6;
|
const gray = (color.r + color.g + color.b) / 6;
|
||||||
return {
|
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 {
|
export function setOpacity(color: Color, opacity: number): Color {
|
||||||
return {
|
return {
|
||||||
...color,
|
...color,
|
||||||
@ -142,6 +155,10 @@ export function defaultColorFunction(
|
|||||||
return auditColors.added_prioritized;
|
return auditColors.added_prioritized;
|
||||||
case 'prioritized':
|
case 'prioritized':
|
||||||
return auditColors.prioritized;
|
return auditColors.prioritized;
|
||||||
|
case 'added_deprioritized':
|
||||||
|
return auditColors.added_prioritized;
|
||||||
|
case 'deprioritized':
|
||||||
|
return auditColors.prioritized;
|
||||||
case 'selected':
|
case 'selected':
|
||||||
return colors.marginal[levelIndex] || colors.marginal[defaultMempoolFeeColors.length - 1];
|
return colors.marginal[levelIndex] || colors.marginal[defaultMempoolFeeColors.length - 1];
|
||||||
case 'accelerated':
|
case 'accelerated':
|
||||||
|
@ -40,7 +40,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="label" i18n="transaction.fee|Transaction fee">Fee</td>
|
<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>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="label" i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
|
<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" i18n="tx-features.tag.added|Added">Added</span>
|
||||||
<span class="badge badge-warning ml-1" i18n="tx-features.tag.prioritized|Prioritized">Prioritized</span>
|
<span class="badge badge-warning ml-1" i18n="tx-features.tag.prioritized|Prioritized">Prioritized</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
<span *ngSwitchCase="'deprioritized'" class="badge badge-warning" i18n="tx-features.tag.prioritized|Deprioritized">Deprioritized</span>
|
||||||
|
<ng-container *ngSwitchCase="'added_deprioritized'">
|
||||||
|
<span class="badge badge-warning" i18n="tx-features.tag.added|Added">Added</span>
|
||||||
|
<span class="badge badge-warning ml-1" i18n="tx-features.tag.prioritized|Deprioritized">Deprioritized</span>
|
||||||
|
</ng-container>
|
||||||
<span *ngSwitchCase="'selected'" class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span>
|
<span *ngSwitchCase="'selected'" class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span>
|
||||||
<span *ngSwitchCase="'rbf'" class="badge badge-warning" i18n="tx-features.tag.conflict|Conflict">Conflict</span>
|
<span *ngSwitchCase="'rbf'" class="badge badge-warning" i18n="tx-features.tag.conflict|Conflict">Conflict</span>
|
||||||
<span *ngSwitchCase="'accelerated'" class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span>
|
<span *ngSwitchCase="'accelerated'" class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span>
|
||||||
|
@ -53,6 +53,13 @@
|
|||||||
<td i18n="block.miner">Miner</td>
|
<td i18n="block.miner">Miner</td>
|
||||||
<td *ngIf="stateService.env.MINING_DASHBOARD">
|
<td *ngIf="stateService.env.MINING_DASHBOARD">
|
||||||
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge" style="color: #FFF;padding:0;">
|
<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'">
|
<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 }}
|
{{ block.extras.pool.name }}
|
||||||
</a>
|
</a>
|
||||||
@ -60,8 +67,15 @@
|
|||||||
<td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">
|
<td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">
|
||||||
<span [attr.data-cy]="'block-details-miner-badge'" placement="bottom" class="badge"
|
<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'">
|
[class]="!block?.extras.pool.name || block?.extras.pool.slug === 'unknown' ? 'badge-secondary' : 'badge-primary'">
|
||||||
{{ block?.extras.pool.name }}
|
<span class="miner-name" *ngIf="block.extras.pool.minerNames?.length > 1 && block.extras.pool.minerNames[1] != ''">
|
||||||
</span>
|
@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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -137,7 +137,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
|
|||||||
})
|
})
|
||||||
),
|
),
|
||||||
this.stateService.env.ACCELERATOR === true && block.height > 819500
|
this.stateService.env.ACCELERATOR === true && block.height > 819500
|
||||||
? this.servicesApiService.getAccelerationHistory$({ blockHeight: block.height })
|
? this.servicesApiService.getAllAccelerationHistory$({ blockHeight: block.height })
|
||||||
.pipe(catchError(() => {
|
.pipe(catchError(() => {
|
||||||
return of([]);
|
return of([]);
|
||||||
}))
|
}))
|
||||||
|
@ -66,10 +66,10 @@
|
|||||||
[class.badge-success]="blockAudit?.matchRate >= 99"
|
[class.badge-success]="blockAudit?.matchRate >= 99"
|
||||||
[class.badge-warning]="blockAudit?.matchRate >= 75 && blockAudit?.matchRate < 99"
|
[class.badge-warning]="blockAudit?.matchRate >= 75 && blockAudit?.matchRate < 99"
|
||||||
[class.badge-danger]="blockAudit?.matchRate < 75"
|
[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>
|
>{{ blockAudit?.matchRate }}%</span>
|
||||||
<ng-template #nullHealth>
|
<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>
|
<span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
@ -182,6 +182,13 @@
|
|||||||
<td i18n="block.miner">Miner</td>
|
<td i18n="block.miner">Miner</td>
|
||||||
<td *ngIf="stateService.env.MINING_DASHBOARD">
|
<td *ngIf="stateService.env.MINING_DASHBOARD">
|
||||||
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge" style="color: #FFF;padding:0;">
|
<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'">
|
<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 }}
|
{{ block.extras.pool.name }}
|
||||||
</a>
|
</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 {
|
.row {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
|
@ -17,6 +17,7 @@ import { PriceService, Price } from '../../services/price.service';
|
|||||||
import { CacheService } from '../../services/cache.service';
|
import { CacheService } from '../../services/cache.service';
|
||||||
import { ServicesApiServices } from '../../services/services-api.service';
|
import { ServicesApiServices } from '../../services/services-api.service';
|
||||||
import { PreloadService } from '../../services/preload.service';
|
import { PreloadService } from '../../services/preload.service';
|
||||||
|
import { identifyPrioritizedTransactions } from '../../shared/transaction.utils';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-block',
|
selector: 'app-block',
|
||||||
@ -318,7 +319,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
this.accelerationsSubscription = this.block$.pipe(
|
this.accelerationsSubscription = this.block$.pipe(
|
||||||
switchMap((block) => {
|
switchMap((block) => {
|
||||||
return this.stateService.env.ACCELERATOR === true && block.height > 819500
|
return this.stateService.env.ACCELERATOR === true && block.height > 819500
|
||||||
? this.servicesApiService.getAccelerationHistory$({ blockHeight: block.height })
|
? this.servicesApiService.getAllAccelerationHistory$({ blockHeight: block.height })
|
||||||
.pipe(catchError(() => {
|
.pipe(catchError(() => {
|
||||||
return of([]);
|
return of([]);
|
||||||
}))
|
}))
|
||||||
@ -326,7 +327,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
})
|
})
|
||||||
).subscribe((accelerations) => {
|
).subscribe((accelerations) => {
|
||||||
this.accelerations = accelerations;
|
this.accelerations = accelerations;
|
||||||
if (accelerations.length) {
|
if (accelerations.length && this.strippedTransactions) { // Don't call setupBlockAudit if we don't have transactions yet; it will be called later in overviewSubscription
|
||||||
this.setupBlockAudit();
|
this.setupBlockAudit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -524,6 +525,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
const isUnseen = {};
|
const isUnseen = {};
|
||||||
const isAdded = {};
|
const isAdded = {};
|
||||||
const isPrioritized = {};
|
const isPrioritized = {};
|
||||||
|
const isDeprioritized = {};
|
||||||
const isCensored = {};
|
const isCensored = {};
|
||||||
const isMissing = {};
|
const isMissing = {};
|
||||||
const isSelected = {};
|
const isSelected = {};
|
||||||
@ -535,6 +537,17 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
this.numUnexpected = 0;
|
this.numUnexpected = 0;
|
||||||
|
|
||||||
if (blockAudit?.template) {
|
if (blockAudit?.template) {
|
||||||
|
// augment with locally calculated *de*prioritized transactions if possible
|
||||||
|
const { prioritized, deprioritized } = identifyPrioritizedTransactions(transactions);
|
||||||
|
// but if the local calculation produces returns unexpected results, don't use it
|
||||||
|
let useLocalDeprioritized = deprioritized.length < (transactions.length * 0.1);
|
||||||
|
for (const tx of prioritized) {
|
||||||
|
if (!isPrioritized[tx] && !isAccelerated[tx]) {
|
||||||
|
useLocalDeprioritized = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const tx of blockAudit.template) {
|
for (const tx of blockAudit.template) {
|
||||||
inTemplate[tx.txid] = true;
|
inTemplate[tx.txid] = true;
|
||||||
if (tx.acc) {
|
if (tx.acc) {
|
||||||
@ -550,9 +563,14 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
for (const txid of blockAudit.addedTxs) {
|
for (const txid of blockAudit.addedTxs) {
|
||||||
isAdded[txid] = true;
|
isAdded[txid] = true;
|
||||||
}
|
}
|
||||||
for (const txid of blockAudit.prioritizedTxs || []) {
|
for (const txid of blockAudit.prioritizedTxs) {
|
||||||
isPrioritized[txid] = true;
|
isPrioritized[txid] = true;
|
||||||
}
|
}
|
||||||
|
if (useLocalDeprioritized) {
|
||||||
|
for (const txid of deprioritized || []) {
|
||||||
|
isDeprioritized[txid] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
for (const txid of blockAudit.missingTxs) {
|
for (const txid of blockAudit.missingTxs) {
|
||||||
isCensored[txid] = true;
|
isCensored[txid] = true;
|
||||||
}
|
}
|
||||||
@ -608,6 +626,12 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
} else {
|
} else {
|
||||||
tx.status = 'prioritized';
|
tx.status = 'prioritized';
|
||||||
}
|
}
|
||||||
|
} else if (isDeprioritized[tx.txid]) {
|
||||||
|
if (isAdded[tx.txid] || (blockAudit.version > 0 && isUnseen[tx.txid])) {
|
||||||
|
tx.status = 'added_deprioritized';
|
||||||
|
} else {
|
||||||
|
tx.status = 'deprioritized';
|
||||||
|
}
|
||||||
} else if (isAdded[tx.txid] && (blockAudit.version === 0 || isUnseen[tx.txid])) {
|
} else if (isAdded[tx.txid] && (blockAudit.version === 0 || isUnseen[tx.txid])) {
|
||||||
tx.status = 'added';
|
tx.status = 'added';
|
||||||
} else if (inTemplate[tx.txid]) {
|
} else if (inTemplate[tx.txid]) {
|
||||||
|
@ -60,9 +60,14 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<div class="animated" *ngIf="block.extras?.pool != undefined && showPools">
|
<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]">
|
<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]">
|
||||||
<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'">
|
<ng-container *ngIf="block.extras.pool.minerNames?.length > 1 && block.extras.pool.minerNames[1] != ''; else centralisedPool">
|
||||||
{{ block.extras.pool.name}}
|
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -19,6 +19,38 @@
|
|||||||
pointer-events: none;
|
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 {
|
.mined-block {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
@ -155,9 +187,16 @@
|
|||||||
|
|
||||||
.badge {
|
.badge {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 15px;
|
top: 19px;
|
||||||
z-index: 101;
|
z-index: 101;
|
||||||
color: #FFF;
|
color: #FFF;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 145px;
|
||||||
|
|
||||||
|
&.miner-name {
|
||||||
|
max-width: 125px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pool-logo {
|
.pool-logo {
|
||||||
@ -168,6 +207,10 @@
|
|||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pool-logo.faded {
|
||||||
|
filter: grayscale(100%) brightness(1.5);
|
||||||
|
}
|
||||||
|
|
||||||
.animated {
|
.animated {
|
||||||
transition: all 0.15s ease-in-out;
|
transition: all 0.15s ease-in-out;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
<app-indexing-progress *ngIf="!widget"></app-indexing-progress>
|
<app-indexing-progress *ngIf="!widget"></app-indexing-progress>
|
||||||
|
|
||||||
<div class="container-xl" style="min-height: 335px" [ngClass]="{'widget': widget, 'full-height': !widget, 'legacy': !isMempoolModule}">
|
<div class="container-xl" style="min-height: 335px" [ngClass]="{'widget': widget, 'full-height': !widget, 'legacy': !isMempoolModule}">
|
||||||
<h1 *ngIf="!widget" class="float-left" i18n="master-page.blocks">Blocks</h1>
|
<div *ngIf="!widget" class="float-left" style="display: flex; width: 100%; align-items: center;">
|
||||||
<div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div>
|
<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>
|
<div class="clearfix"></div>
|
||||||
|
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
.spinner-border {
|
.spinner-border {
|
||||||
height: 25px;
|
height: 25px;
|
||||||
width: 25px;
|
width: 25px;
|
||||||
margin-top: 13px;
|
margin-top: -10px;
|
||||||
|
margin-left: -13px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container-xl {
|
.container-xl {
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
<div class="input-group-prepend">
|
<div class="input-group-prepend">
|
||||||
<span class="input-group-text">{{ currency$ | async }}</span>
|
<span class="input-group-text">{{ currency$ | async }}</span>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" class="form-control" formControlName="fiat" (input)="transformInput('fiat')" (click)="selectAll($event)">
|
<input type="text" inputmode="numeric" class="form-control" formControlName="fiat" (input)="transformInput('fiat')" (click)="selectAll($event)">
|
||||||
<app-clipboard [button]="true" [text]="form.get('fiat').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
|
<app-clipboard [button]="true" [text]="form.get('fiat').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -20,7 +20,7 @@
|
|||||||
<div class="input-group-prepend">
|
<div class="input-group-prepend">
|
||||||
<span class="input-group-text">BTC</span>
|
<span class="input-group-text">BTC</span>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" class="form-control" formControlName="bitcoin" (input)="transformInput('bitcoin')" (click)="selectAll($event)">
|
<input type="text" inputmode="numeric" class="form-control" formControlName="bitcoin" (input)="transformInput('bitcoin')" (click)="selectAll($event)">
|
||||||
<app-clipboard [button]="true" [text]="form.get('bitcoin').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
|
<app-clipboard [button]="true" [text]="form.get('bitcoin').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -28,7 +28,7 @@
|
|||||||
<div class="input-group-prepend">
|
<div class="input-group-prepend">
|
||||||
<span class="input-group-text" i18n="shared.sats">sats</span>
|
<span class="input-group-text" i18n="shared.sats">sats</span>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" class="form-control" formControlName="satoshis" (input)="transformInput('satoshis')" (click)="selectAll($event)">
|
<input type="text" inputmode="numeric" class="form-control" formControlName="satoshis" (input)="transformInput('satoshis')" (click)="selectAll($event)">
|
||||||
<app-clipboard [button]="true" [text]="form.get('satoshis').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
|
<app-clipboard [button]="true" [text]="form.get('satoshis').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -77,7 +77,7 @@ export class DifficultyMiningComponent implements OnInit {
|
|||||||
base: `${da.progressPercent.toFixed(2)}%`,
|
base: `${da.progressPercent.toFixed(2)}%`,
|
||||||
change: da.difficultyChange,
|
change: da.difficultyChange,
|
||||||
progress: da.progressPercent,
|
progress: da.progressPercent,
|
||||||
remainingBlocks: da.remainingBlocks - 1,
|
remainingBlocks: da.remainingBlocks,
|
||||||
colorAdjustments,
|
colorAdjustments,
|
||||||
colorPreviousAdjustments,
|
colorPreviousAdjustments,
|
||||||
newDifficultyHeight: da.nextRetargetHeight,
|
newDifficultyHeight: da.nextRetargetHeight,
|
||||||
|
@ -153,8 +153,8 @@ export class DifficultyComponent implements OnInit {
|
|||||||
base: `${da.progressPercent.toFixed(2)}%`,
|
base: `${da.progressPercent.toFixed(2)}%`,
|
||||||
change: da.difficultyChange,
|
change: da.difficultyChange,
|
||||||
progress: da.progressPercent,
|
progress: da.progressPercent,
|
||||||
minedBlocks: this.currentIndex + 1,
|
minedBlocks: this.currentIndex,
|
||||||
remainingBlocks: da.remainingBlocks - 1,
|
remainingBlocks: da.remainingBlocks,
|
||||||
expectedBlocks: Math.floor(da.expectedBlocks),
|
expectedBlocks: Math.floor(da.expectedBlocks),
|
||||||
colorAdjustments,
|
colorAdjustments,
|
||||||
colorPreviousAdjustments,
|
colorPreviousAdjustments,
|
||||||
|
@ -36,6 +36,13 @@
|
|||||||
<app-twitter-login customClass="btn btn-sm" width="180px" redirectTo="/testnet4/faucet" buttonString="Link your Twitter"></app-twitter-login>
|
<app-twitter-login customClass="btn btn-sm" width="180px" redirectTo="/testnet4/faucet" buttonString="Link your Twitter"></app-twitter-login>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@else if (error === 'account_limited') {
|
||||||
|
<div class="alert alert-mempool d-block text-center w-100">
|
||||||
|
<div class="d-inline align-middle">
|
||||||
|
<span class="mb-2 mr-2">Your Twitter account does not allow you to access the faucet</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@else if (error) {
|
@else if (error) {
|
||||||
<!-- User can request -->
|
<!-- User can request -->
|
||||||
<app-mempool-error class="w-100" [error]="error"></app-mempool-error>
|
<app-mempool-error class="w-100" [error]="error"></app-mempool-error>
|
||||||
|
@ -85,7 +85,6 @@
|
|||||||
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-home" *ngIf="network.val === '' && stateService.env.ACCELERATOR">
|
<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()">
|
<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>
|
<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>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-pools" *ngIf="stateService.env.MINING_DASHBOARD">
|
<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;
|
lastBlockHeight: number;
|
||||||
blockIndex: number;
|
blockIndex: number;
|
||||||
isLoading$ = new BehaviorSubject<boolean>(true);
|
isLoading$ = new BehaviorSubject<boolean>(false);
|
||||||
timeLtrSubscription: Subscription;
|
timeLtrSubscription: Subscription;
|
||||||
timeLtr: boolean;
|
timeLtr: boolean;
|
||||||
chainDirection: string = 'right';
|
chainDirection: string = 'right';
|
||||||
@ -95,6 +95,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.updateBlock({
|
this.updateBlock({
|
||||||
|
block: this.blockIndex,
|
||||||
removed,
|
removed,
|
||||||
changed,
|
changed,
|
||||||
added
|
added
|
||||||
@ -110,8 +111,11 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
|
|||||||
if (this.blockGraph) {
|
if (this.blockGraph) {
|
||||||
this.blockGraph.clear(changes.index.currentValue > changes.index.previousValue ? this.chainDirection : this.poolDirection);
|
this.blockGraph.clear(changes.index.currentValue > changes.index.previousValue ? this.chainDirection : this.poolDirection);
|
||||||
}
|
}
|
||||||
this.isLoading$.next(true);
|
if (!this.websocketService.startTrackMempoolBlock(changes.index.currentValue) && this.stateService.mempoolBlockState && this.stateService.mempoolBlockState.block === changes.index.currentValue) {
|
||||||
this.websocketService.startTrackMempoolBlock(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);
|
this.isLoading$.next(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resumeBlock(transactionsStripped: TransactionStripped[]): void {
|
||||||
|
if (this.blockGraph) {
|
||||||
|
this.firstLoad = false;
|
||||||
|
this.blockGraph.setup(transactionsStripped, true);
|
||||||
|
this.blockIndex = this.index;
|
||||||
|
this.isLoading$.next(false);
|
||||||
|
} else {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.resumeBlock(transactionsStripped);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onTxClick(event: { tx: TransactionStripped, keyModifier: boolean }): void {
|
onTxClick(event: { tx: TransactionStripped, keyModifier: boolean }): void {
|
||||||
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.tx.txid}`);
|
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.tx.txid}`);
|
||||||
if (!event.keyModifier) {
|
if (!event.keyModifier) {
|
||||||
|
@ -71,7 +71,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
this.mempoolBlockTransactions$ = this.stateService.liveMempoolBlockTransactions$.pipe(map(txMap => Object.values(txMap)));
|
this.mempoolBlockTransactions$ = this.stateService.liveMempoolBlockTransactions$.pipe(map(({transactions}) => Object.values(transactions)));
|
||||||
|
|
||||||
this.network$ = this.stateService.networkChanged$;
|
this.network$ = this.stateService.networkChanged$;
|
||||||
}
|
}
|
||||||
|
@ -213,7 +213,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
if (state.mempoolPosition) {
|
if (state.mempoolPosition) {
|
||||||
this.txPosition = state.mempoolPosition;
|
this.txPosition = state.mempoolPosition;
|
||||||
if (this.txPosition.accelerated && !oldTxPosition.accelerated) {
|
if (this.txPosition.accelerated && !oldTxPosition?.accelerated) {
|
||||||
this.acceleratingArrow = true;
|
this.acceleratingArrow = true;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.acceleratingArrow = false;
|
this.acceleratingArrow = false;
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
|
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
|
||||||
<td>{{ 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>
|
||||||
<tr *only-vsize>
|
<tr *only-vsize>
|
||||||
<td class="td-width" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
|
<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">
|
<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>
|
<form [formGroup]="testTxsForm" (submit)="testTxsForm.valid && testTxs()" novalidate>
|
||||||
<label for="maxfeerate" i18n="test.tx.raw-hex">Raw hex</label>
|
<label for="maxfeerate" i18n="test.tx.raw-hex">Raw hex</label>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnChanges } from '@angular/core';
|
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnChanges } from '@angular/core';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { dates } from '../../shared/i18n/dates';
|
import { TimeService } from '../../services/time.service';
|
||||||
import { DatePipe } from '@angular/common';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-time',
|
selector: 'app-time',
|
||||||
@ -12,19 +11,9 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
interval: number;
|
interval: number;
|
||||||
text: string;
|
text: string;
|
||||||
tooltip: string;
|
tooltip: string;
|
||||||
precisionThresholds = {
|
|
||||||
year: 100,
|
|
||||||
month: 18,
|
|
||||||
week: 12,
|
|
||||||
day: 31,
|
|
||||||
hour: 48,
|
|
||||||
minute: 90,
|
|
||||||
second: 90
|
|
||||||
};
|
|
||||||
intervals = {};
|
|
||||||
|
|
||||||
@Input() time: number;
|
@Input() time: number;
|
||||||
@Input() dateString: number;
|
@Input() dateString: string;
|
||||||
@Input() kind: 'plain' | 'since' | 'until' | 'span' | 'before' | 'within' = 'plain';
|
@Input() kind: 'plain' | 'since' | 'until' | 'span' | 'before' | 'within' = 'plain';
|
||||||
@Input() fastRender = false;
|
@Input() fastRender = false;
|
||||||
@Input() fixedRender = false;
|
@Input() fixedRender = false;
|
||||||
@ -40,37 +29,26 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
constructor(
|
constructor(
|
||||||
private ref: ChangeDetectorRef,
|
private ref: ChangeDetectorRef,
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
private datePipe: DatePipe,
|
private timeService: TimeService,
|
||||||
) {
|
) {}
|
||||||
this.intervals = {
|
|
||||||
year: 31536000,
|
|
||||||
month: 2592000,
|
|
||||||
week: 604800,
|
|
||||||
day: 86400,
|
|
||||||
hour: 3600,
|
|
||||||
minute: 60,
|
|
||||||
second: 1
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
this.calculateTime();
|
||||||
if(this.fixedRender){
|
if(this.fixedRender){
|
||||||
this.text = this.calculate();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.stateService.isBrowser) {
|
if (!this.stateService.isBrowser) {
|
||||||
this.text = this.calculate();
|
|
||||||
this.ref.markForCheck();
|
this.ref.markForCheck();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.interval = window.setInterval(() => {
|
this.interval = window.setInterval(() => {
|
||||||
this.text = this.calculate();
|
this.calculateTime();
|
||||||
this.ref.markForCheck();
|
this.ref.markForCheck();
|
||||||
}, 1000 * (this.fastRender ? 1 : 60));
|
}, 1000 * (this.fastRender ? 1 : 60));
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges() {
|
ngOnChanges() {
|
||||||
this.text = this.calculate();
|
this.calculateTime();
|
||||||
this.ref.markForCheck();
|
this.ref.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,224 +56,21 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
clearInterval(this.interval);
|
clearInterval(this.interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
calculate() {
|
calculateTime(): void {
|
||||||
if (this.time == null) {
|
const { text, tooltip } = this.timeService.calculate(
|
||||||
return;
|
this.time,
|
||||||
}
|
this.kind,
|
||||||
|
this.relative,
|
||||||
let seconds: number;
|
this.precision,
|
||||||
switch (this.kind) {
|
this.minUnit,
|
||||||
case 'since':
|
this.showTooltip,
|
||||||
seconds = Math.floor((+new Date() - +new Date(this.dateString || this.time * 1000)) / 1000);
|
this.units,
|
||||||
this.tooltip = this.datePipe.transform(new Date(this.dateString || this.time * 1000), 'yyyy-MM-dd HH:mm');
|
this.dateString,
|
||||||
break;
|
this.lowercaseStart,
|
||||||
case 'until':
|
this.numUnits,
|
||||||
case 'within':
|
this.fractionDigits,
|
||||||
seconds = (+new Date(this.time) - +new Date()) / 1000;
|
);
|
||||||
this.tooltip = this.datePipe.transform(new Date(this.time), 'yyyy-MM-dd HH:mm');
|
this.text = text;
|
||||||
break;
|
this.tooltip = tooltip;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@
|
|||||||
<div class="blockchain-wrapper" [style]="{ height: blockchainHeight * 1.16 + 'px' }">
|
<div class="blockchain-wrapper" [style]="{ height: blockchainHeight * 1.16 + 'px' }">
|
||||||
<app-clockchain [height]="blockchainHeight" [width]="blockchainWidth" mode="none"></app-clockchain>
|
<app-clockchain [height]="blockchainHeight" [width]="blockchainWidth" mode="none"></app-clockchain>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel">
|
<div class="panel" *ngIf="!error || waitingForTransaction">
|
||||||
@if (replaced) {
|
@if (replaced) {
|
||||||
<div class="alert-replaced" role="alert">
|
<div class="alert-replaced" role="alert">
|
||||||
<span i18n="transaction.rbf.replacement|RBF replacement">This transaction has been replaced by:</span>
|
<span i18n="transaction.rbf.replacement|RBF replacement">This transaction has been replaced by:</span>
|
||||||
@ -65,23 +65,25 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field narrower">
|
@if (!replaced) {
|
||||||
<div class="label" i18n="transaction.eta|Transaction ETA">ETA</div>
|
<div class="field narrower">
|
||||||
<div class="value">
|
<div class="label" i18n="transaction.eta|Transaction ETA">ETA</div>
|
||||||
<ng-container *ngIf="(ETA$ | async) as eta; else etaSkeleton">
|
<div class="value">
|
||||||
<span class="justify-content-end d-flex align-items-center">
|
<ng-container *ngIf="(ETA$ | async) as eta; else etaSkeleton">
|
||||||
@if (eta.blocks >= 7) {
|
<span class="justify-content-end d-flex align-items-center">
|
||||||
<span i18n="transaction.eta.not-any-time-soon|Transaction ETA mot any time soon">Not any time soon</span>
|
@if (eta.blocks >= 7) {
|
||||||
} @else {
|
<span i18n="transaction.eta.not-any-time-soon|Transaction ETA mot any time soon">Not any time soon</span>
|
||||||
<app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time>
|
} @else {
|
||||||
}
|
<app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time>
|
||||||
</span>
|
}
|
||||||
</ng-container>
|
</span>
|
||||||
<ng-template #etaSkeleton>
|
</ng-container>
|
||||||
<span class="skeleton-loader" style="max-width: 200px;"></span>
|
<ng-template #etaSkeleton>
|
||||||
</ng-template>
|
<span class="skeleton-loader" style="max-width: 200px;"></span>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
} @else if (tx && tx.status?.confirmed) {
|
} @else if (tx && tx.status?.confirmed) {
|
||||||
<div class="field narrower mt-2">
|
<div class="field narrower mt-2">
|
||||||
<div class="label" i18n="transaction.confirmed-at">Confirmed at</div>
|
<div class="label" i18n="transaction.confirmed-at">Confirmed at</div>
|
||||||
@ -111,7 +113,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bottom-panel">
|
<div class="bottom-panel" *ngIf="!error || waitingForTransaction">
|
||||||
@if (isLoading) {
|
@if (isLoading) {
|
||||||
<div class="progress-icon">
|
<div class="progress-icon">
|
||||||
<div class="spinner-border text-light" style="width: 1em; height: 1em"></div>
|
<div class="spinner-border text-light" style="width: 1em; height: 1em"></div>
|
||||||
@ -185,6 +187,12 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="bottom-panel" *ngIf="error && !waitingForTransaction">
|
||||||
|
<app-http-error [error]="error">
|
||||||
|
<span i18n="transaction.error.loading-transaction-data">Error loading transaction data.</span>
|
||||||
|
</app-http-error>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="footer-link"
|
<div class="footer-link"
|
||||||
[routerLink]="['/tx' | relativeUrl, tx?.txid || txId]"
|
[routerLink]="['/tx' | relativeUrl, tx?.txid || txId]"
|
||||||
[queryParams]="{ mode: 'details' }"
|
[queryParams]="{ mode: 'details' }"
|
||||||
|
@ -286,14 +286,14 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
|||||||
this.accelerationInfo = null;
|
this.accelerationInfo = null;
|
||||||
}),
|
}),
|
||||||
switchMap((blockHash: string) => {
|
switchMap((blockHash: string) => {
|
||||||
return this.servicesApiService.getAccelerationHistory$({ blockHash });
|
return this.servicesApiService.getAllAccelerationHistory$({ blockHash }, null, this.txId);
|
||||||
}),
|
}),
|
||||||
catchError(() => {
|
catchError(() => {
|
||||||
return of(null);
|
return of(null);
|
||||||
})
|
})
|
||||||
).subscribe((accelerationHistory) => {
|
).subscribe((accelerationHistory) => {
|
||||||
for (const acceleration of accelerationHistory) {
|
for (const acceleration of accelerationHistory) {
|
||||||
if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional')) {
|
if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional') && acceleration.pools.includes(acceleration.minedByPoolUniqueId)) {
|
||||||
const boostCost = acceleration.boostCost || acceleration.bidBoost;
|
const boostCost = acceleration.boostCost || acceleration.bidBoost;
|
||||||
acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
|
acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
|
||||||
acceleration.boost = boostCost;
|
acceleration.boost = boostCost;
|
||||||
@ -747,7 +747,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
checkAccelerationEligibility() {
|
checkAccelerationEligibility() {
|
||||||
if (this.tx) {
|
if (this.tx) {
|
||||||
this.tx.flags = getTransactionFlags(this.tx);
|
this.tx.flags = getTransactionFlags(this.tx, null, null, this.tx.status?.block_time, this.stateService.network);
|
||||||
const replaceableInputs = (this.tx.flags & (TransactionFlags.sighash_none | TransactionFlags.sighash_acp)) > 0n;
|
const replaceableInputs = (this.tx.flags & (TransactionFlags.sighash_none | TransactionFlags.sighash_acp)) > 0n;
|
||||||
const highSigop = (this.tx.sigops * 20) > this.tx.weight;
|
const highSigop = (this.tx.sigops * 20) > this.tx.weight;
|
||||||
this.eligibleForAcceleration = !replaceableInputs && !highSigop;
|
this.eligibleForAcceleration = !replaceableInputs && !highSigop;
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
<div *ngIf="officialMempoolSpace">
|
<div *ngIf="officialMempoolSpace">
|
||||||
<h2>Trademark Policy and Guidelines</h2>
|
<h2>Trademark Policy and Guidelines</h2>
|
||||||
<h5>The Mempool Open Source Project ®</h5>
|
<h5>The Mempool Open Source Project ®</h5>
|
||||||
<h6>Updated: July 3, 2024</h6>
|
<h6>Updated: August 19, 2024</h6>
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<div class="text-left">
|
<div class="text-left">
|
||||||
@ -95,16 +95,31 @@
|
|||||||
<p>The mempool Square Logo</p>
|
<p>The mempool Square Logo</p>
|
||||||
<br><br>
|
<br><br>
|
||||||
|
|
||||||
<app-svg-images name="accelerator" height="76px"></app-svg-images>
|
<app-svg-images name="accelerator" style="width: 500px; max-width: 80%"></app-svg-images>
|
||||||
<br><br>
|
<br><br>
|
||||||
<p>The Mempool Accelerator Logo</p>
|
<p>The Mempool Accelerator Logo</p>
|
||||||
<br><br>
|
<br><br>
|
||||||
|
|
||||||
|
<img src="/resources/mempool-research.png" style="width: 500px; max-width: 80%">
|
||||||
|
<br><br>
|
||||||
|
<p>The mempool research Logo</p>
|
||||||
|
<br><br>
|
||||||
|
|
||||||
<app-svg-images name="goggles" height="96px"></app-svg-images>
|
<app-svg-images name="goggles" height="96px"></app-svg-images>
|
||||||
<br><br>
|
<br><br>
|
||||||
<p>The Mempool Goggles Logo</p>
|
<p>The Mempool Goggles Logo</p>
|
||||||
<br><br>
|
<br><br>
|
||||||
|
|
||||||
|
<img src="/resources/mempool-transaction.png" style="width: 500px; max-width: 80%">
|
||||||
|
<br><br>
|
||||||
|
<p>The mempool transaction Logo</p>
|
||||||
|
<br><br>
|
||||||
|
|
||||||
|
<img src="/resources/mempool-block-visualization.png" style="width: 500px; max-width: 80%">
|
||||||
|
<br><br>
|
||||||
|
<p>The mempool block visualization Logo</p>
|
||||||
|
<br><br>
|
||||||
|
|
||||||
<img src="/resources/mempool-blocks-2-3-logo.jpeg" style="width: 500px; max-width: 80%">
|
<img src="/resources/mempool-blocks-2-3-logo.jpeg" style="width: 500px; max-width: 80%">
|
||||||
<br><br>
|
<br><br>
|
||||||
<p>The mempool Blocks Logo</p>
|
<p>The mempool Blocks Logo</p>
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</span>
|
</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-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>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
@ -551,23 +551,23 @@
|
|||||||
<td class="td-width align-items-center align-middle" i18n="transaction.eta|Transaction ETA">ETA</td>
|
<td class="td-width align-items-center align-middle" i18n="transaction.eta|Transaction ETA">ETA</td>
|
||||||
<td>
|
<td>
|
||||||
<ng-container *ngIf="(ETA$ | async) as eta; else etaSkeleton">
|
<ng-container *ngIf="(ETA$ | async) as eta; else etaSkeleton">
|
||||||
@if (eta.blocks >= 7) {
|
@if (network === 'liquid' || network === 'liquidtestnet') {
|
||||||
<span [class]="(!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) ? 'etaDeepMempool justify-content-end align-items-center' : ''">
|
|
||||||
<span i18n="transaction.eta.not-any-time-soon|Transaction ETA mot any time soon">Not any time soon</span>
|
|
||||||
@if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) {
|
|
||||||
<a class="btn btn-sm accelerateDeepMempool btn-small-height float-right" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
} @else if (network === 'liquid' || network === 'liquidtestnet') {
|
|
||||||
<app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time>
|
<app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time>
|
||||||
} @else {
|
} @else {
|
||||||
<span [class]="(!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) ? 'etaDeepMempool justify-content-end align-items-center' : ''">
|
<span [class]="(!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && notAcceleratedOnLoad) ? 'etaDeepMempool d-flex justify-content-between' : ''">
|
||||||
<app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time>
|
@if (eta.blocks >= 7) {
|
||||||
@if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) {
|
<span i18n="transaction.eta.not-any-time-soon|Transaction ETA mot any time soon">Not any time soon</span>
|
||||||
<a class="btn btn-sm accelerateDeepMempool btn-small-height float-right" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
|
} @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>
|
</span>
|
||||||
}
|
}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@ -606,16 +606,11 @@
|
|||||||
@if (!isLoadingTx) {
|
@if (!isLoadingTx) {
|
||||||
<tr>
|
<tr>
|
||||||
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
|
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
|
||||||
<td class="text-wrap">{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
|
<td class="text-wrap">{{ tx.fee | number }} <span class="symbol" i18n="shared.sats">sats</span>
|
||||||
@if (accelerationInfo?.bidBoost) {
|
@if (accelerationInfo?.bidBoost ?? tx.feeDelta > 0) {
|
||||||
<span class="oobFees" i18n-ngbTooltip="Acceleration Fees" ngbTooltip="Acceleration fees paid out-of-band"> +{{ accelerationInfo.bidBoost | number }} </span><span class="symbol" i18n="shared.sat|sat">sat</span>
|
<span class="oobFees" i18n-ngbTooltip="Acceleration Fees" ngbTooltip="Acceleration fees paid out-of-band"> +{{ accelerationInfo?.bidBoost ?? tx.feeDelta | number }} </span><span class="symbol" i18n="shared.sats">sats</span>
|
||||||
<span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee + accelerationInfo.bidBoost"></app-fiat></span>
|
|
||||||
} @else if (tx.feeDelta && !accelerationInfo) {
|
|
||||||
<span class="oobFees" i18n-ngbTooltip="Acceleration Fees" ngbTooltip="Acceleration fees paid out-of-band"> +{{ tx.feeDelta | number }} </span><span class="symbol" i18n="shared.sat|sat">sat</span>
|
|
||||||
<span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee + tx.feeDelta"></app-fiat></span>
|
|
||||||
} @else {
|
|
||||||
<span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee"></app-fiat></span>
|
|
||||||
}
|
}
|
||||||
|
<span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee + ((accelerationInfo?.bidBoost ?? tx.feeDelta) || 0)"></app-fiat></span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
} @else {
|
} @else {
|
||||||
@ -675,7 +670,7 @@
|
|||||||
<ng-template #acceleratingRow>
|
<ng-template #acceleratingRow>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="2" colspan="2" style="padding: 0;">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr></tr>
|
<tr></tr>
|
||||||
@ -689,8 +684,15 @@
|
|||||||
@if (pool) {
|
@if (pool) {
|
||||||
<td class="wrap-cell">
|
<td class="wrap-cell">
|
||||||
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, pool.slug]" class="badge" style="color: #FFF;padding:0;">
|
<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'">
|
<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>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
} @else {
|
} @else {
|
||||||
|
@ -60,6 +60,19 @@
|
|||||||
top: -1px;
|
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 {
|
.badge.badge-accelerated {
|
||||||
background-color: var(--tertiary);
|
background-color: var(--tertiary);
|
||||||
color: white;
|
color: white;
|
||||||
@ -287,37 +300,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.accelerate {
|
.accelerate {
|
||||||
display: flex !important;
|
@media (min-width: 850px) {
|
||||||
align-self: auto;
|
margin-left: auto;
|
||||||
margin-left: auto;
|
|
||||||
background-color: var(--tertiary);
|
|
||||||
@media (max-width: 849px) {
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.etaDeepMempool {
|
.etaDeepMempool {
|
||||||
justify-content: flex-end;
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-content: center;
|
|
||||||
@media (max-width: 995px) {
|
|
||||||
justify-content: left !important;
|
|
||||||
}
|
|
||||||
@media (max-width: 849px) {
|
@media (max-width: 849px) {
|
||||||
justify-content: right !important;
|
justify-content: right !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.accelerateDeepMempool {
|
.accelerateDeepMempool {
|
||||||
align-self: auto;
|
|
||||||
margin-left: auto;
|
|
||||||
background-color: var(--tertiary);
|
background-color: var(--tertiary);
|
||||||
@media (max-width: 995px) {
|
margin-left: 5px;
|
||||||
margin-left: 0px;
|
|
||||||
}
|
|
||||||
@media (max-width: 849px) {
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.goggles-icon {
|
.goggles-icon {
|
||||||
@ -336,3 +333,8 @@
|
|||||||
.oobFees {
|
.oobFees {
|
||||||
color: #905cf4;
|
color: #905cf4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
@ -42,6 +42,7 @@ interface Pool {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
minerNames: string[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TxAuditStatus {
|
export interface TxAuditStatus {
|
||||||
@ -139,6 +140,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
firstLoad = true;
|
firstLoad = true;
|
||||||
waitingForAccelerationInfo: boolean = false;
|
waitingForAccelerationInfo: boolean = false;
|
||||||
isLoadingFirstSeen = false;
|
isLoadingFirstSeen = false;
|
||||||
|
notAcceleratedOnLoad: boolean = null;
|
||||||
|
|
||||||
featuresEnabled: boolean;
|
featuresEnabled: boolean;
|
||||||
segwitEnabled: boolean;
|
segwitEnabled: boolean;
|
||||||
@ -191,7 +193,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.hideAccelerationSummary = this.stateService.isMempoolSpaceBuild ? this.storageService.getValue('hide-accelerator-pref') == 'true' : true;
|
this.hideAccelerationSummary = this.stateService.isMempoolSpaceBuild ? this.storageService.getValue('hide-accelerator-pref') == 'true' : true;
|
||||||
|
|
||||||
if (!this.stateService.isLiquid()) {
|
if (!this.stateService.isLiquid()) {
|
||||||
this.miningService.getMiningStats('1w').subscribe(stats => {
|
this.miningService.getMiningStats('1m').subscribe(stats => {
|
||||||
this.miningStats = stats;
|
this.miningStats = stats;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -343,7 +345,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.setIsAccelerated();
|
this.setIsAccelerated();
|
||||||
}),
|
}),
|
||||||
switchMap((blockHeight: number) => {
|
switchMap((blockHeight: number) => {
|
||||||
return this.servicesApiService.getAccelerationHistory$({ blockHeight }).pipe(
|
return this.servicesApiService.getAllAccelerationHistory$({ blockHeight }, null, this.txId).pipe(
|
||||||
switchMap((accelerationHistory: Acceleration[]) => {
|
switchMap((accelerationHistory: Acceleration[]) => {
|
||||||
if (this.tx.acceleration && !accelerationHistory.length) { // If the just mined transaction was accelerated, but services backend did not return any acceleration data, retry
|
if (this.tx.acceleration && !accelerationHistory.length) { // If the just mined transaction was accelerated, but services backend did not return any acceleration data, retry
|
||||||
return throwError('retry');
|
return throwError('retry');
|
||||||
@ -358,12 +360,18 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}),
|
}),
|
||||||
).subscribe((accelerationHistory) => {
|
).subscribe((accelerationHistory) => {
|
||||||
for (const acceleration of accelerationHistory) {
|
for (const acceleration of accelerationHistory) {
|
||||||
if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional')) {
|
if (acceleration.txid === this.txId) {
|
||||||
const boostCost = acceleration.boostCost || acceleration.bidBoost;
|
if (acceleration.status === 'completed' || acceleration.status === 'completed_provisional') {
|
||||||
acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
|
if (acceleration.pools.includes(acceleration.minedByPoolUniqueId)) {
|
||||||
acceleration.boost = boostCost;
|
const boostCost = acceleration.boostCost || acceleration.bidBoost;
|
||||||
this.tx.acceleratedAt = acceleration.added;
|
acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
|
||||||
this.accelerationInfo = acceleration;
|
acceleration.boost = boostCost;
|
||||||
|
this.tx.acceleratedAt = acceleration.added;
|
||||||
|
this.accelerationInfo = acceleration;
|
||||||
|
} else {
|
||||||
|
this.tx.feeDelta = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
this.waitingForAccelerationInfo = false;
|
this.waitingForAccelerationInfo = false;
|
||||||
this.setIsAccelerated();
|
this.setIsAccelerated();
|
||||||
}
|
}
|
||||||
@ -484,7 +492,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
if (this.stateService.network === '') {
|
if (this.stateService.network === '') {
|
||||||
if (!this.mempoolPosition.accelerated) {
|
if (!this.mempoolPosition.accelerated) {
|
||||||
if (!this.accelerationFlowCompleted && !this.hideAccelerationSummary && !this.showAccelerationSummary) {
|
if (!this.accelerationFlowCompleted && !this.hideAccelerationSummary && !this.showAccelerationSummary) {
|
||||||
this.miningService.getMiningStats('1w').subscribe(stats => {
|
this.miningService.getMiningStats('1m').subscribe(stats => {
|
||||||
this.miningStats = stats;
|
this.miningStats = stats;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -843,6 +851,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.setIsAccelerated(firstCpfp);
|
this.setIsAccelerated(firstCpfp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.notAcceleratedOnLoad === null) {
|
||||||
|
this.notAcceleratedOnLoad = !this.isAcceleration;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.isAcceleration && this.fragmentParams.has('accelerate')) {
|
if (!this.isAcceleration && this.fragmentParams.has('accelerate')) {
|
||||||
this.forceAccelerationSummary = true;
|
this.forceAccelerationSummary = true;
|
||||||
}
|
}
|
||||||
@ -895,7 +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.segwitEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'segwit');
|
||||||
this.taprootEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'taproot');
|
this.taprootEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'taproot');
|
||||||
this.rbfEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'rbf');
|
this.rbfEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'rbf');
|
||||||
this.tx.flags = getTransactionFlags(this.tx);
|
this.tx.flags = getTransactionFlags(this.tx, null, null, this.tx.status?.block_time, this.stateService.network);
|
||||||
this.filters = this.tx.flags ? toFilters(this.tx.flags).filter(f => f.txPage) : [];
|
this.filters = this.tx.flags ? toFilters(this.tx.flags).filter(f => f.txPage) : [];
|
||||||
this.checkAccelerationEligibility();
|
this.checkAccelerationEligibility();
|
||||||
} else {
|
} else {
|
||||||
@ -960,6 +972,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.filters = [];
|
this.filters = [];
|
||||||
this.showCpfpDetails = false;
|
this.showCpfpDetails = false;
|
||||||
this.showAccelerationDetails = false;
|
this.showAccelerationDetails = false;
|
||||||
|
this.accelerationFlowCompleted = false;
|
||||||
this.accelerationInfo = null;
|
this.accelerationInfo = null;
|
||||||
this.cashappEligible = false;
|
this.cashappEligible = false;
|
||||||
this.txInBlockIndex = null;
|
this.txInBlockIndex = null;
|
||||||
@ -1077,6 +1090,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
(!this.hideAccelerationSummary && !this.accelerationFlowCompleted)
|
(!this.hideAccelerationSummary && !this.accelerationFlowCompleted)
|
||||||
|| this.forceAccelerationSummary
|
|| this.forceAccelerationSummary
|
||||||
)
|
)
|
||||||
|
&& this.notAcceleratedOnLoad // avoid briefly showing accelerator checkout on already accelerated txs
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -321,7 +321,7 @@
|
|||||||
<div class="float-left mt-2-5" *ngIf="!transactionPage && !tx.vin[0].is_coinbase && tx.fee !== -1">
|
<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>
|
<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"
|
<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>
|
||||||
<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>
|
<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>
|
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>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>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>blockHash</code>: a block hash</a>
|
||||||
<li><code>blockHeight</code>: a block height</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>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>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>`
|
</ul></p>`
|
||||||
},
|
},
|
||||||
urlString: "/v1/services/accelerator/accelerations/history",
|
urlString: "/v1/services/accelerator/accelerations/history",
|
||||||
@ -9187,21 +9189,22 @@ export const restApiDocsData = [
|
|||||||
headers: '',
|
headers: '',
|
||||||
response: `[
|
response: `[
|
||||||
{
|
{
|
||||||
"txid": "d7e1796d8eb4a09d4e6c174e36cfd852f1e6e6c9f7df4496339933cd32cbdd1d",
|
"txid": "f829900985aad885c13fb90555d27514b05a338202c7ef5d694f4813ad474487",
|
||||||
"status": "completed",
|
"status": "completed_provisional",
|
||||||
"added": 1707421053,
|
"added": 1728111527,
|
||||||
"lastUpdated": 1719134667,
|
"lastUpdated": 1728112113,
|
||||||
"effectiveFee": 146,
|
"effectiveFee": 1385,
|
||||||
"effectiveVsize": 141,
|
"effectiveVsize": 276,
|
||||||
"feeDelta": 14000,
|
"feeDelta": 3000,
|
||||||
"blockHash": "00000000000000000000482f0746d62141694b9210a813b97eb8445780a32003",
|
"blockHash": "00000000000000000000cde89e34036ece454ca2d07ddd7f71ab46307ca87423",
|
||||||
"blockHeight": 829559,
|
"blockHeight": 864248,
|
||||||
"bidBoost": 3239,
|
"bidBoost": 65,
|
||||||
"boostVersion": "v1",
|
"boostVersion": "v2",
|
||||||
"pools": [
|
"pools": [
|
||||||
111
|
111,
|
||||||
|
115,
|
||||||
],
|
],
|
||||||
"minedByPoolUniqueId": 111
|
"minedByPoolUniqueId": 115
|
||||||
}
|
}
|
||||||
]`,
|
]`,
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// Import tree-shakeable echarts
|
// Import tree-shakeable echarts
|
||||||
import * as echarts from 'echarts/core';
|
import * as echarts from 'echarts/core';
|
||||||
import { LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart } from 'echarts/charts';
|
import { LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart, CustomChart } from 'echarts/charts';
|
||||||
import { TitleComponent, TooltipComponent, GridComponent, LegendComponent, GeoComponent, DataZoomComponent, VisualMapComponent, MarkLineComponent } from 'echarts/components';
|
import { TitleComponent, TooltipComponent, GridComponent, LegendComponent, GeoComponent, DataZoomComponent, VisualMapComponent, MarkLineComponent } from 'echarts/components';
|
||||||
import { SVGRenderer, CanvasRenderer } from 'echarts/renderers';
|
import { SVGRenderer, CanvasRenderer } from 'echarts/renderers';
|
||||||
// Typescript interfaces
|
// Typescript interfaces
|
||||||
@ -12,6 +12,7 @@ echarts.use([
|
|||||||
TitleComponent, TooltipComponent, GridComponent,
|
TitleComponent, TooltipComponent, GridComponent,
|
||||||
LegendComponent, GeoComponent, DataZoomComponent,
|
LegendComponent, GeoComponent, DataZoomComponent,
|
||||||
VisualMapComponent, MarkLineComponent,
|
VisualMapComponent, MarkLineComponent,
|
||||||
LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart
|
LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart,
|
||||||
|
CustomChart,
|
||||||
]);
|
]);
|
||||||
export { echarts, EChartsOption, TreemapSeriesOption, LineSeriesOption, PieSeriesOption };
|
export { echarts, EChartsOption, TreemapSeriesOption, LineSeriesOption, PieSeriesOption };
|
@ -36,6 +36,7 @@ import { HashrateChartPoolsComponent } from '../components/hashrates-chart-pools
|
|||||||
import { BlockHealthGraphComponent } from '../components/block-health-graph/block-health-graph.component';
|
import { BlockHealthGraphComponent } from '../components/block-health-graph/block-health-graph.component';
|
||||||
import { AddressComponent } from '../components/address/address.component';
|
import { AddressComponent } from '../components/address/address.component';
|
||||||
import { AddressGraphComponent } from '../components/address-graph/address-graph.component';
|
import { AddressGraphComponent } from '../components/address-graph/address-graph.component';
|
||||||
|
import { UtxoGraphComponent } from '../components/utxo-graph/utxo-graph.component';
|
||||||
import { ActiveAccelerationBox } from '../components/acceleration/active-acceleration-box/active-acceleration-box.component';
|
import { ActiveAccelerationBox } from '../components/acceleration/active-acceleration-box/active-acceleration-box.component';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
@ -76,6 +77,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
HashrateChartPoolsComponent,
|
HashrateChartPoolsComponent,
|
||||||
BlockHealthGraphComponent,
|
BlockHealthGraphComponent,
|
||||||
AddressGraphComponent,
|
AddressGraphComponent,
|
||||||
|
UtxoGraphComponent,
|
||||||
ActiveAccelerationBox,
|
ActiveAccelerationBox,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -233,3 +233,10 @@ interface AssetStats {
|
|||||||
peg_out_amount: number;
|
peg_out_amount: number;
|
||||||
burn_count: number;
|
burn_count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Utxo {
|
||||||
|
txid: string;
|
||||||
|
vout: number;
|
||||||
|
value: number;
|
||||||
|
status: Status;
|
||||||
|
}
|
@ -203,6 +203,7 @@ export interface BlockExtension {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
minerNames: string[] | null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,7 +240,7 @@ export interface TransactionStripped {
|
|||||||
acc?: boolean;
|
acc?: boolean;
|
||||||
flags?: number | null;
|
flags?: number | null;
|
||||||
time?: number;
|
time?: number;
|
||||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
|
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'added_deprioritized' | 'deprioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
|
||||||
context?: 'projected' | 'actual';
|
context?: 'projected' | 'actual';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user